[
  {
    "path": ".gitignore",
    "content": "# Created by .ignore support plugin (hsz.mobi)\n*.iml\n.idea\ntarget\n*.log\ndependency-reduced-pom.xml"
  },
  {
    "path": ".travis.yml",
    "content": "language: java\n\njdk:\n  - oraclejdk10\n\naddons:\n  postgresql: \"9.5\"\n\nservices:\n  - postgresql\n\nbefore_script:\n  - psql -f $TRAVIS_BUILD_DIR/server/core/src/main/resources/create_schema.sql -U postgres\n\nscript:\n  - mvn clean test\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment include:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at dmitriy@blynk.cc. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]\n\n[homepage]: http://contributor-covenant.org\n[version]: http://contributor-covenant.org/version/1/4/\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "## How to submit a bug report\n\nPlease ensure to specify the following:\n\n* Blynk Server version (e.g. 0.28.0)\n* Contextual information (e.g. what you were trying to achieve)\n* Simplest possible steps to reproduce\n* Anything that might be relevant in your opinion, such as:\n  * JDK/JRE version or the output of `java -version`\n  * Operating system and the output of `uname -a`\n  * Network configuration\n\n\n### Example\n\n```\nBlynk Server version: 0.28.0\n\nContext:\nI encountered an exception which looks suspicious while running my Local Server.\n\nSteps to reproduce:\n1. ...\n2. ...\n3. ...\n4. ...\n\n$ java -version\njava version \"1.8.0_51\"\nJava(TM) SE Runtime Environment (build 1.8.0_51-b13)\nJava HotSpot(TM) 64-Bit Server VM (build 24.51-b03, mixed mode)\n\nOperating system: Ubuntu Linux 14.04 64-bit\n"
  },
  {
    "path": "README.md",
    "content": "# What is Blynk?\n\n### Note that this Blynk Legacy server is now discontinued and unsupported!\nIt does not work with the latest Blynk app, and the Legacy app will be withdrawn from the app/play stores from 30th June 2022.\nPreviously installed apps will continue to work, although later versions of the legacy app do not have the ability to\ncreate new accounts, so the either an earlier version of the app will be needed, or accounst will need to be created manually\nby copying and renaming the default Blynk.cc account.\n\nThe Legacy cloud servers will be decommissioned on 31st December 2022. This will have no impact on local legacy servers, but \nas all support and updates for this legacy local server and the legacy apps apps has already ceased, using a local legacy server\nshould be seen as an interim measure, because the server will eventually become vulnerable when support for Java 11 is withdrawn\nand when the apps no longer run on newer mobile OS releases.\n\nInstalling the iOS app after it is withdrawn from the app store will require jailbraking the Apple mobile device, and Android users\nwill need to source and side-load a copy of the legacy app outside of the playstore.\n\n-----------------------------------------------------------------------------------------------------------------------------------\n\nBlynk is a platform with iOS and Android apps to control Arduino, ESP8266, Raspberry Pi and the likes over the Internet.  \nYou can easily build graphic interfaces for all your projects by simply dragging and dropping widgets.\nIf you need more information, please follow these links:\n* [Blynk site](https://www.blynk.io)\n* [Blynk docs](http://docs.blynk.cc)\n* [Blynk community](https://community.blynk.cc)\n* [Blynk Examples generator](https://examples.blynk.cc)\n* [Facebook](http://www.fb.com/blynkapp)\n* [Twitter](http://twitter.com/blynk_app)\n* [App Store](https://itunes.apple.com/us/app/blynk-control-arduino-raspberry/id808760481?ls=1&mt=8)\n* [Google Play](https://play.google.com/store/apps/details?id=cc.blynk)\n* [Blynk library](https://github.com/blynkkk/blynk-library)\n* [Kickstarter](https://www.kickstarter.com/projects/167134865/blynk-build-an-app-for-your-arduino-project-in-5-m/description)\n\n![Dashboard settings](https://github.com/Peterkn2001/blynk-server/blob/master/docs/overview/dash_settings.png)\n![Widgets Box](https://github.com/Peterkn2001/blynk-server/blob/master/docs/overview/widgets_box.png)\n![Dashboard](https://github.com/Peterkn2001/blynk-server/blob/master/docs/overview/dash.png)\n![Dashboard2](https://github.com/Peterkn2001/blynk-server/blob/master/docs/overview/dash2.png)\n\n# Content \n\n- [Download](#blynk-server)\n- [Requirements](#requirements)\n- [Quick Local Server setup](#quick-local-server-setup)\n- [Enabling mail on Local server](#enabling-mail-on-local-server)\n- [Quick local server setup on Raspberry PI](#quick-local-server-setup-on-raspberry-pi)\n- [Docker container setup](#docker-container-setup)\n- [Enabling server auto restart on unix-like systems](#enabling-server-auto-restart-on-unix-like-systems)\n- [Enabling server auto restart on Windows](#enabling-server-auto-restart-on-windows)\n- [Update instruction for unix-like systems](#update-instruction-for-unix-like-systems)\n- [Update instruction for Windows](#update-instruction-for-windows)\n- [App and sketch changes for Local Server](#app-and-sketch-changes)\n- [Advanced local server setup](#advanced-local-server-setup)\n- [Administration UI](#administration-ui)\n- [HTTP/S RESTful API](#https-restful)\n- [Enabling sms on local server](#enabling-sms-on-local-server)\n- [Enabling raw data storage](#enabling-raw-data-storage)\n- [Automatic Let's Encrypt Certificates](#automatic-lets-encrypt-certificates-generation)\n- [Manual Let's Encrypt SSL/TLS Certificates](#manual-lets-encrypt-ssltls-certificates)\n- [Generate own SSL certificates](#generate-own-ssl-certificates)\n- [Install java for Ubuntu](#install-java-for-ubuntu)\n- [How Blynk Works?](#how-blynk-works)\n- [Blynk Protocol](#blynk-protocol)\n\n# GETTING STARTED\n\n## Blynk server\nBlynk Server is an Open-Source [Netty](https://github.com/netty/netty) based Java server, responsible for forwarding \nmessages between Blynk mobile application and various microcontroller boards and SBCs (i.e. Arduino, Raspberry Pi. etc).\n\n**Download latest server build [here](https://github.com/Peterkn2001/blynk-server/releases).**\n\n[![GitHub version](https://img.shields.io/github/release/Peterkn2001/blynk-server.svg)](https://github.com/Peterkn2001/blynk-server/releases/latest)\n[![GitHub download](https://img.shields.io/github/downloads/Peterkn2001/blynk-server/total.svg)](https://github.com/Peterkn2001/blynk-server/releases/latest)\n\n## Requirements\n- Java 8/11 required (OpenJDK, Oracle) \n- Any OS that can run java \n- At least 30 MB of RAM (could be less with tuning)\n- Open ports 9443 (for app and hardware with ssl), 8080 (for hardware without ssl)\n\n[Ubuntu java installation instruction](#install-java-for-ubuntu).\n\nFor Windows download Java [here](https://www.oracle.com/technetwork/java/javase/downloads/jdk11-downloads-5066655.html) and install. \n\n## Quick local server setup\n\n+ Make sure you are using Java 11\n\n        java -version\n        Output: java version \"11\"\n\n+ Run the server on default 'hardware port 8080' and default 'application port 9443' (SSL port)\n\n        java -jar server-0.41.16.jar -dataFolder /path\n        \nThat's it! \n\n**NOTE: ```/path``` should be real existing path to folder where you want to store all your data.**\n\n+ As an output you should see something like that:\n\n        Blynk Server successfully started.\n        All server output is stored in current folder in 'logs/blynk.log' file.\n        \n### Enabling mail on Local server\n\n### NOTE - From May 30th 2022 Google has stopped allowing less secure applications on personal Gmail accounts, so email from the Blynk local server will not be possible in most cases.\n\nTo enable mail notifications on Local server you need to provide your own mail credentials. Create file `mail.properties` within same folder where `server.jar` is.\nMail properties:\n\n        mail.smtp.auth=true\n        mail.smtp.starttls.enable=true\n        mail.smtp.host=smtp.gmail.com\n        mail.smtp.port=587\n        mail.smtp.username=YOUR_EMAIL_HERE\n        mail.smtp.password=YOUR_EMAIL_PASS_HERE\n        \nFind example [here](https://github.com/Peterkn2001/blynk-server/blob/master/server/notifications/email/src/main/resources/mail.properties).\n\nWARNING : only gmail accounts are allowed.\n\nNOTE : you'll need to setup Gmail to allow less secure applications.\nGo [here](https://www.google.com/settings/security/lesssecureapps) and then click \"Allow less secure apps\".\n\n## Quick local server setup on Raspberry PI\n\n+ Login to Raspberry Pi via ssh;\n+ Install java 8: \n        \n        sudo apt install openjdk-8-jdk openjdk-8-jre\n        \n+ Make sure you are using Java 8\n\n        java -version\n        Output: java version \"1.8\"\n        \n+ Download Blynk server jar file (or manually copy it to Raspberry Pi via ssh and scp command): \n   \n        wget \"https://github.com/Peterkn2001/blynk-server/releases/download/v0.41.16/server-0.41.16-java8.jar\"\n\n+ Run the server on default 'hardware port 8080' and default 'application port 9443' (SSL port)\n\n        java -jar server-0.41.16-java8.jar -dataFolder /home/pi/Blynk\n        \nThat's it! \n\n+ As output you will see something like that:\n\n        Blynk Server successfully started.\n        All server output is stored in current folder in 'logs/blynk.log' file.\n\n## Docker container setup\n\n### Quick Launch\n\n+ Install [Docker](https://docs.docker.com/install/)\n+ Run Docker container\n\n        docker run -p 8080:8080 -p 9443:9443 mpherg/blynk-server\n\n### Quick Launch on Raspberry Pi\n\n+ Install [Docker](https://docs.docker.com/engine/install/debian/)\n+ Run Docker container\n\n        docker run -p 8080:8080 -p 9443:9443 linuxkonsult/rasbian-blynk\n\n### Full customisation\n\n+ Check [README](server/Docker) in docker folder\n\n\n\n\n## Enabling server auto restart on unix-like systems\n        \n+ To enable server auto restart find /etc/rc.local file and add:\n\n        java -jar /home/pi/server-0.41.16-java8.jar -dataFolder /home/pi/Blynk &\n        \n+ Or if the approach above doesn't work, execute \n       \n        crontab -e\n\nadd the following line\n\n        @reboot java -jar /home/pi/server-0.41.16-java8.jar -dataFolder /home/pi/Blynk &\n        \nsave and exit.\n\n## Enabling server auto restart on Windows\n\n+ Create bat file:\n\n        start-blynk.bat\n\n+ Put in it one line: \n\n        java -jar server-0.41.16.jar -dataFolder /home/pi/Blynk\n        \n+ Put bat file to windows startup folder\n\nYou can also use [this](https://github.com/Peterkn2001/blynk-server/tree/master/scripts/win) script to run server.\n\n## Update instruction for unix-like systems\n\n**IMPORTANT**\nServer should be always updated before you update Blynk App. To update your server to a newer version you would need to kill old process and start a new one.\n\n+ Find process id of Blynk server\n\n        ps -aux | grep java\n        \n+ You should see something like that\n \n        username   10539  1.0 12.1 3325808 428948 pts/76 Sl   Jan22   9:11 java -jar server-0.41.16.jar   \n        \n+ Kill the old process\n\n        kill 10539\n        \n10539 - blynk server process id from command output above.\n \n+ Start new server [as usual](#quick-local-server-setup)\n\nAfter this steps you can update Blynk app. Server version downgrade is not supported. \n\n**WARNING!**\nPlease **do not** revert your server to lower versions. You may loose all of your data.\n\n## Update instruction for Windows\n\n+ Open Task Manager;\n\n+ Find Java process;\n\n+ Stop process;\n\n+ Start new server [as usual](#quick-local-server-setup)\n                \n## App and sketch changes\n\n+ Specify custom server path in your application\n\n![Custom server icon](https://github.com/Peterkn2001/blynk-server/blob/master/docs/login.png)\n![Server properties menu](https://github.com/Peterkn2001/blynk-server/blob/master/docs/custom.png)\n\n+ Change your ethernet sketch from\n\n    ```\n    Blynk.begin(auth);\n    ```\n    \n    to\n    \n    ```\n    Blynk.begin(auth, \"your_host\", 8080);\n    ```\n    \n    or to\n    \n    ```\n    Blynk.begin(auth, IPAddress(xxx,xxx,xxx,xxx), 8080);\n    ```\n        \n+ Change your WIFI sketch from\n        \n    ```\n    Blynk.begin(auth, SSID, pass));\n    ```\n   \n    to\n    \n    ```\n    Blynk.begin(auth, SSID, pass, \"your_host\", 8080);\n    ```\n    \n    or to\n    \n    ```\n    Blynk.begin(auth, SSID, pass, IPAddress(XXX,XXX,XXX,XXX), 8080);\n    ```\n        \n+ Change your rasp PI javascript from\n\n    ```\n    var blynk = new Blynk.Blynk(AUTH, options = {connector : new Blynk.TcpClient()});\n    ```\n    \n    to\n    \n    ```\n    var blynk = new Blynk.Blynk(AUTH, options= {addr:\"xxx.xxx.xxx.xxx\", port:8080});\n    ```\n        \n+ or in case of USB when running blynk-ser.sh provide '-s' option with address of your local server\n\n        ./blynk-ser.sh -s you_host_or_IP\n        \n        \n**IMPORTANT** \nBlynk is being constantly developed. Mobile apps and server are updated often. To avoid problems during updates either turn off auto-update for Blynk app, or update both local server and blynk app at same time to avoid possible migration issues.\n\n**IMPORTANT** \nBlynk local server is different from  Blynk Cloud server. They are not related at all. You have to create new account when using Blynk local server.\n\n## Advanced local server setup\nFor more flexibility you can extend server with more options by creating ```server.properties``` file in same folder as ```server.jar```. \nExample could be found [here](https://github.com/Peterkn2001/blynk-server/blob/master/server/core/src/main/resources/server.properties).\nYou could also specify any path to ```server.properties``` file via command line argument ```-serverConfig```. You can \ndo the same with ```mail.properties``` via ```-mailConfig``` and ```sms.properties``` via ```-smsConfig```.\n \nFor example:\n\n    java -jar server-0.41.16-java8.jar -dataFolder /home/pi/Blynk -serverConfig /home/pi/someFolder/server.properties\n\nAvailable server options:\n\n+ Blynk app, https, web sockets, admin port\n        \n        https.port=9443\n\n\n+ Http, hardware and web sockets port\n\n        http.port=8080\n        \n        \n+ For simplicity Blynk already provides server jar with built in SSL certificates, so you have working server out of the box via SSL/TLS sockets. But as certificate and it's private key are in public this is totally not secure. So in order to fix that you need to provide your own certificates. And change below properties with path to your cert. and private key and it's password. See how to generate self-signed certificates [here](#generate-ssl-certificates)\n\n        #points to cert and key that placed in same folder as running jar.\n        \n        server.ssl.cert=./server_embedded.crt\n        server.ssl.key=./server_embedded.pem\n        server.ssl.key.pass=pupkin123\n\n**Note**: if you use Let's Encrypt certificates you'll have to add ```#define BLYNK_SSL_USE_LETSENCRYPT``` before ```#include <BlynkSimpleEsp8266_SSL.h>``` in the Arduino Sketch for your hardware.\n        \n+ User profiles folder. Folder in which all users profiles will be stored. By default System.getProperty(\"java.io.tmpdir\")/blynk used. Will be created if not exists\n\n        data.folder=/tmp/blynk\n        \n\n+ Folder for all application logs. Will be created if it doesn't exist. \".\" is dir from which you are running script.\n\n        logs.folder=./logs\n        \n\n+ Log debug level. Possible values: trace|debug|info|error. Defines how precise logging will be. From left to right -> maximum logging to minimum\n\n        log.level=trace\n        \n\n+ Maximum allowed number of user dashboards.\n\n        user.dashboard.max.limit=100\n        \n\n+ 100 Req/sec rate limit per user. You also may want to extend this limit on [hardware side](https://github.com/blynkkk/blynk-library/blob/f4e132652906d63d683abeed89f5d6ebe369e37a/Blynk/BlynkConfig.h#L42).\n\n        user.message.quota.limit=100\n        \n\n+ this setting defines how often you can send mail/tweet/push or any other notification. Specified in seconds\n        \n        notifications.frequency.user.quota.limit=60\n        \n\n+ Maximum allowed user profile size. In Kb's.\n\n        user.profile.max.size=128\n        \n        \n+ Number of strings to store in terminal widget (terminal history data)\n\n        terminal.strings.pool.size=25\n        \n\n+ Maximum allowed number of notification queue. Queue responsible for processing email, pushes, twits sending. Because of performance issue - those queue is processed in separate thread, this is required due to blocking nature of all above operations. Usually limit shouldn't be reached\n        \n        notifications.queue.limit=5000\n        \n        \n+ Number of threads for performing blocking operations - push, twits, emails, db queries. Recommended to hold this value low unless you have to perform a lot of blocking operations.\n\n        blocking.processor.thread.pool.limit=6\n        \n\n+ Period for flushing all user DB to disk. In millis\n\n        profile.save.worker.period=60000\n\n+ Specifies maximum period of time when hardware socket could be idle. After which socket will be closed due to non activity. In seconds. Leave it empty for infinity timeout\n\n        hard.socket.idle.timeout=15\n        \n+ Mostly required for local servers setup in case user want to log raw data in CSV format. See [raw data] (#raw-data-storage) section for more info.\n        \n        enable.raw.data.store=true\n        \n+ Url for opening admin page. Must start from \"/\". For \"/admin\" url path will look like that \"https://127.0.0.1:9443/admin\". \n\n        admin.rootPath=/admin\n        \n+ Comma separated list of administrator IPs. Allow access to admin UI only for those IPs. You may set it for 0.0.0.0/0 to allow access for all. You may use CIDR notation. For instance, 192.168.0.53/24.\n        \n        allowed.administrator.ips=0.0.0.0/0\n        \n+ Default admin name and password. Will be created on initial server start\n        \n        admin.email=admin@blynk.cc\n        admin.pass=admin\n\n+ Host for reset password redirect and certificate generation. By default current server IP is taken from \"eth\" network interface. Could be replaced with more friendly hostname. It is recommended to override this property with your server IP to avoid possible problems of host resolving.\n        \n        server.host=blynk-cloud.com\n        \n+ Email used for certificate registration, could be omitted in case you already specified it in mail.properties.\n        \n        contact.email=pupkin@gmail.com\n        \n## Administration UI\n\nBlynk server provides administration panel where you can monitor your server. It is accessible at this URL:\n\n        https://your_ip:9443/admin\n        \n![Administration UI](https://github.com/Peterkn2001/blynk-server/blob/master/docs/admin_panel.png)\n              \n**WARNING**\nPlease change default admin password and name right after login to admin page. **THIS IS SECURITY MEASURE**.\n        \n**WARNING**\nDefault ```allowed.administrator.ips``` setting allows access for everyone. In other words, \nadministration page available from any other computer. Please restrict access to it via property ```allowed.administrator.ips```.\n\n### Turn off chrome https warning on localhost\n\n- Paste in chrome \n\n        chrome://flags/#allow-insecure-localhost\n\n- You should see highlighted text saying: \"Allow invalid certificates for resources loaded from localhost\". Click enable.\n        \n## HTTP/S RESTful\nBlynk HTTP/S RESTful API allows to easily read and write values to/from Pins in Blynk apps and Hardware. \nHttp API description could be found [here](http://docs.blynkapi.apiary.io).\n\n### Enabling sms on local server\nTo enable SMS notifications on Local Server you need to provide credentials for SMS gateway (currently Blynk server\nsupports only 1 provider - [Nexmo](https://www.nexmo.com/). You need to create file ```sms.properties``` \nwithin same folder where server.jar is.\n\n        nexmo.api.key=\n        nexmo.api.secret=\n        \nAnd fill in the above properties with the credentials you'll get from Nexmo. (Account -> Settings -> API settings).\nYou can also send SMS over email if your cell provider supports that. See [discussion](http://community.blynk.cc/t/sms-notification-for-important-alert/2542) for more details.\n \n\n## Enabling raw data storage\nBy default raw data storage is disabled (as it consumes disk space a lot). \nWhen you enable it, every ```Blynk.virtualWrite``` command will be saved to DB.\nYou will need to install PostgreSQL Database (**minimum required version is 9.5**) to enable this functionality:\n\n#### 1. Enabling raw data on server\n\nEnable raw data in ```server.properties``` : \n\n        enable.db=true\n        enable.raw.db.data.store=true\n\n#### 2. Install PostgreSQL. Option A\n\n        sudo sh -c 'echo \"deb http://apt.postgresql.org/pub/repos/apt/ `lsb_release -cs`-pgdg main\" >> /etc/apt/sources.list.d/pgdg.list'\n        wget -q https://www.postgresql.org/media/keys/ACCC4CF8.asc -O - | sudo apt-key add -\n        \n        sudo apt-get update\n        sudo apt-get install postgresql postgresql-contrib\n        \n#### 2. Install PostgreSQL.  Option B \n\n        sudo apt-get update\n        apt-get --no-install-recommends install postgresql-9.6 postgresql-contrib-9.6\n\n#### 3. Download Blynk DB script\n\n        wget https://raw.githubusercontent.com/Peterkn2001Peterkn2001/blynk-server/master/server/core/src/main/resources/create_schema.sql\n        wget https://raw.githubusercontent.com/Peterkn2001/blynk-server/master/server/core/src/main/resources/reporting_schema.sql\n\n#### 4. Move create_schema.sql and reporting_schema.sql to temp folder (to avoid permission problems)\n\n        mv create_schema.sql /tmp\n        mv reporting_schema.sql /tmp\n        \nResult:  \n\n        /tmp/create_schema.sql\n        /tmp/reporting_schema.sql\n        \nCopy it to clipboard from your console.\n\n#### 5. Connect to PostgreSQL\n\n        sudo su - postgres\n        psql\n\n#### 6. Create Blynk DB and Reporting DB, test user and tables\n\n        \\i /tmp/create_schema.sql\n        \\i /tmp/reporting_schema.sql\n        \n```/tmp/create_schema.sql``` - is path from step 4.\n        \nYou should see next output:\n\n        postgres=# \\i /tmp/create_schema.sql\n        CREATE DATABASE\n        You are now connected to database \"blynk\" as user \"postgres\".\n        CREATE TABLE\n        CREATE TABLE\n        CREATE TABLE\n        CREATE TABLE\n        CREATE TABLE\n        CREATE TABLE\n        CREATE TABLE\n        CREATE TABLE\n        CREATE TABLE\n        CREATE TABLE\n        CREATE TABLE\n        CREATE ROLE\n        GRANT\n        GRANT\n\n#### Quit\n\n        \\q\n               \nNow start your server and you should see next text in ```postgres.log``` file : \n\n        2017-03-02 16:17:18.367 - DB url : jdbc:postgresql://localhost:5432/blynk?tcpKeepAlive=true&socketTimeout=150\n        2017-03-02 16:17:18.367 - DB user : test\n        2017-03-02 16:17:18.367 - Connecting to DB...\n        2017-03-02 16:17:18.455 - Connected to database successfully.\n        \nWARNING:\nRaw data may consume your disk space very quickly!\n\n### CSV data format\n\nData format is:\n\n        value,timestamp,deviceId\n        \nFor example:\n\n        10,1438022081332,0\n        \nWhere ```10``` - value of pin.\n```1438022081332``` - the difference, measured in milliseconds, between the current time and midnight, January 1, 1970 UTC.\nTo display the date/time in excel you may use formula:\n\n        =((COLUMN/(60*60*24)/1000+25569))\n        \n```0``` - device id\n        \n### Automatic Let's Encrypt certificates generation\n\nLatest Blynk server has super cool feature - automatic Let's Encrypt certificates generation. \nHowever, it has few requirements: \n \n+ Add ```server.host``` property in ```server.properties``` file. \nFor example : \n \n        server.host=myhost.com\n\nIP is not supported, this is the limitation of Let's Encrypt. Also have in mind that ```myhost.com``` \nshould be resolved by public DNS severs.\n        \n+ Add ```contact.email``` property in ```server.properties```. For example : \n \n        contact.email=test@gmail.com\n        \n+ You need to start server on port 80 (requires root or admin rights) or \nmake [port forwarding](#port-forwarding-for-https-api) to default Blynk HTTP port - 8080.\n\nThat's it! Run server as regular and certificates will be generated automatically.\n\n![](https://gifyu.com/images/certs.gif)\n\n### Manual Let's Encrypt SSL/TLS Certificates\n\n+ First install [certbot](https://github.com/certbot/certbot) on your server (machine where you going to run Blynk Server)\n\n        wget https://dl.eff.org/certbot-auto\n        chmod a+x certbot-auto\n        \n+ Generate and verify certificates (your server should be connected to internet and have open 80/443 ports)\n\n        ./certbot-auto certonly --agree-tos --email YOUR_EMAIL --standalone -d YOUR_HOST\n\nFor example \n\n        ./certbot-auto certonly --agree-tos --email pupkin@blynk.cc --standalone -d blynk.cc\n\n+ Then add to your ```server.properties``` file (in folder with server.jar)\n\n        server.ssl.cert=/etc/letsencrypt/live/YOUR_HOST/fullchain.pem\n        server.ssl.key=/etc/letsencrypt/live/YOUR_HOST/privkey.pem\n        server.ssl.key.pass=\n        \n### Generate own SSL certificates\n\n+ Generate self-signed certificate and key\n\n        openssl req -x509 -nodes -days 1825 -newkey rsa:2048 -keyout server.key -out server.crt\n        \n+ Convert server.key to PKCS#8 private key file in PEM format\n\n        openssl pkcs8 -topk8 -v1 PBE-SHA1-2DES -in server.key -out server.enc.key\n        \nIf you connect hardware with [USB script](https://github.com/blynkkk/blynk-library/tree/master/scripts) you have to provide an option '-s' pointing to \"common name\" (hostname) you did specified during certificate generation.\n        \nAs an output you'll retrieve server.crt and server.pem files that you need to provide for server.ssl properties.\n\n### Install java for Ubuntu\n\n        sudo add-apt-repository ppa:openjdk-r/ppa \\\n        && sudo apt-get update -q \\\n        && sudo apt install -y openjdk-11-jdk\n        \nor if above doesn't work:\n\n        sudo apt-add-repository ppa:webupd8team/java\n        sudo apt-get update\n        sudo apt-get install oracle-java8-installer\n        \n### Port forwarding for HTTP/S API\n\n        sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080\n        sudo iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 9443\n\n### Enabling QR generation on server\n        \n        sudo apt-get install libxrender1\n\n### Behind wifi router\nIf you want to run Blynk server behind WiFi-router and want it to be accessible from the Internet, you have to add port-forwarding rule on your router. This is required in order to forward all of the requests that come to the router within the local network to Blynk server.\n\n### How to build\nBlynk has a bunch of integration tests that require DB, so you have to skip tests during build.\n\n        mvn clean install -Dmaven.test.skip=true\n        \n### How Blynk Works?\nWhen hardware connects to Blynk cloud it opens either keep-alive ssl/tls connection on port 443 (9443 for local servers) or keep-alive plain\ntcp/ip connection on port 8080. Blynk app opens mutual ssl/tls connection to Blynk Cloud on port 443 (9443 for local servers).\nBlynk Cloud is responsible for forwarding messages between hardware and app. In both (app and hardware) connections Blynk uses \nown binary protocol described below.\n\n### Blynk protocol\n\n\n#### Hardware side protocol\n\nBlynk transfers binary messages between the server and the hardware with the following structure:\n\n| Command       | Message Id    | Length/Status   | Body     |\n|:-------------:|:-------------:|:---------------:|:--------:|\n| 1 byte        | 2 bytes       | 2 bytes         | Variable |\n\nCommand and Status definitions: [BlynkProtocolDefs.h](https://github.com/Peterkn2001/blynk-library/blob/7e942d661bc54ded310bf5d00edee737d0ca44d7/src/Blynk/BlynkProtocolDefs.h)\n\n\n#### Mobile app side protocol\n\nBlynk transfers binary messages between the server and mobile app with the following structure:\n\n| Command       | Message Id    | Length/Status   | Body     |\n|:-------------:|:-------------:|:---------------:|:--------:|\n| 1 byte        | 2 bytes       | 4 bytes         | Variable |\n\n\n#### Websockets web side protocol\n\nBlynk transfers binary messages between the server and websockets (for web) with the following structure:\n\n| Websocket header   | Command       | Message Id    | Body     |\n|:------------------:|:-------------:|:-------------:|:--------:|\n|                    | 1 byte        | 2 bytes       | Variable |\n\n\nWhen command code == 0, than message structure is next:\n\n| Websocket header   | Command       | Message Id    | Response code |\n|:------------------:|:-------------:|:-------------:|:-------------:|\n|                    | 1 byte        | 2 bytes       | 4 bytes       |\n\n[Possible response codes](https://github.com/Peterkn2001/blynk-server/blob/master/server/core/src/main/java/cc/blynk/server/core/protocol/enums/Response.java#L12).\n[Possible command codes](https://github.com/Peterkn2001/blynk-server/blob/master/server/core/src/main/java/cc/blynk/server/core/protocol/enums/Command.java#L12)\n\nMessage Id and Length are [big endian](http://en.wikipedia.org/wiki/Endianness#Big-endian).\nBody has a command-specific format.\n\n## Licensing\n[GNU GPL license](https://github.com/Peterkn2001/blynk-server/blob/master/license.txt)\n"
  },
  {
    "path": "apiary.apib",
    "content": "FORMAT: 1A\nHOST: http://blynk-cloud.com/\n\n# Blynk HTTP RESTful API\n\nBlynk HTTP RESTful API allows to easily read and write values to/from Pins in Blynk apps and Hardware (microcontrollers and microcomputers like Arduino, Raspberry Pi, ESP8266, Particle, etc.).\n\nEvery ```PUT``` request will update Pin's state both in apps and on the hardware.\nEvery ```GET``` request will return current state/value on the given Pin.\nWe also provide simplified API so you can do updates via GET requests.\n\n## Get pin value [/{auth_token}/get/{pin}]\n\n+ Parameters\n    + auth_token (required, string, `4ae3851817194e2596cf1b7103603ef8`) ... authentification token.\n    + pin (required, string, `D8`) ... pin you want to read.\n\n### Get pin value [GET]\n\n+ Response 200 (application/json)\n\n        [\n        ]\n\n+ Response 200 (application/json)\n\n        [\n            \"1\"\n        ]\n\n+ Response 200 (application/json)\n\n        [\n            \"1\",\n            \"2\"\n        ]\n        \n+ Response 400 (plain/text)\n\n        Invalid token.\n\n+ Response 400 (plain/text)\n\n        Wrong pin format.\n        \n+ Response 400 (plain/text)\n\n        Requested pin not exists in app.\n\n\n## Write pin value via GET [/{auth_token}/update/{pin}?value={value}]\n\n+ Parameters\n    + auth_token (required, string, `4ae3851817194e2596cf1b7103603ef8`) ... authentification token.\n    + pin (required, string, `D8`) ... pin you want to write.\n    + value (required, string, `1`) ... pin value you want to write.\n\n### Write pin value via GET [GET]\n\n+ Response 200\n\n+ Response 400 (plain/text)\n\n        Invalid token.\n\n+ Response 400 (plain/text)\n\n        Wrong pin format.\n\n+ Response 400 (plain/text)\n\n        Requested pin doesn't exist in the app.\n\n+ Response 500 (plain/text)\n\n        Unexpected content type. Expecting application/json.\n\n## Write pin value via PUT [/{auth_token}/update/{pin}]\n\n+ Parameters\n    + auth_token (required, string, `4ae3851817194e2596cf1b7103603ef8`) ... authentification token.\n    + pin (required, string, `D8`) ... pin you want to write.\n\n### Write pin value via PUT [PUT]\n\n+ Request (application/json)\n\n        [\n            \"1\"\n        ]\n        \n+ Request (application/json)\n\n        [\n            \"1\",\n            \"2\"\n        ]\n\n+ Response 200\n\n+ Response 400 (plain/text)\n\n        Invalid token.\n\n+ Response 400 (plain/text)\n\n        Wrong pin format.\n        \n+ Response 400 (plain/text)\n\n        Requested pin not exists in app.\n        \n+ Response 500 (plain/text)\n\n        Unexpected content type. Expecting application/json.\n\n## Set Widget Property via GET [/{auth_token}/update/{pin}?{property}={value}]\n\n+ Parameters\n    + auth_token (required, string, `4ae3851817194e2596cf1b7103603ef8`) ... authentification token.\n    + pin (required, string, `V0`) ... pin you want to write.\n    + property (required, string, `label`) ... property you want to change `label`, `color`, etc. For reserved characters\n    like space and '#' for color property you need to use appropriate encoding. For example `#23C48E` should be passed as\n    `%2323C48E` where %23 is encoded '#' character.\n    + value (required, string, `MyNewLabel`) ... property value.\n\n### Set Widget Property via GET [GET]\n\n+ Response 200\n\n+ Response 400 (plain/text)\n\n        Invalid token.\n\n+ Response 400 (plain/text)\n\n        Wrong pin format.\n\n+ Response 400 (plain/text)\n\n        Requested pin not exists in app.\n\n+ Response 500 (plain/text)\n\n        Unexpected content type. Expecting application/json.\n        \n\n## Hardware network status [/{auth_token}/isHardwareConnected]\n\n+ Parameters\n    + auth_token (required, string, `4ae3851817194e2596cf1b7103603ef8`) ... authentification token.\n\n### Checks that hardware with provided token is online and connected to server [GET]\n\n+ Response 200 (application/json)\n\n        true\n        \n+ Response 200 (application/json)\n\n        false\n        \n+ Response 400 (plain/text)\n\n        Invalid token.\n\n## Application network status [/{auth_token}/isAppConnected]\n\n### Check that application is connected to server and has active project with provided token [GET]\n\n+ Response 200 (application/json)\n\n        true\n        \n+ Response 200 (application/json)\n\n        false\n        \n+ Response 400 (plain/text)\n\n        Invalid token.\n\n## Send push notification [/{auth_token}/notify]\n\n+ Parameters\n    + auth_token (required, string, `4ae3851817194e2596cf1b7103603ef8`) ... authentification token.\n\n### Notify [POST]\n\n+ Request (application/json)\n\n        {\n            \"body\" : \"message no less than 255 chars.\"\n        }\n        \n+ Response 200\n\n+ Response 400 (plain/text)\n\n        Invalid token.\n\n+ Response 400\n\n        Body is empty or larger than 255 chars.\n        \n+ Response 400\n\n        Project is not active.\n        \n+ Response 400\n\n        No notification widget or widget not initialized.\n        \n+ Response 500 (plain/text)\n\n        Unexpected content type. Expecting application/json.\n        \n## Send email [/{auth_token}/email]\n\n+ Parameters\n    + auth_token (required, string, `4ae3851817194e2596cf1b7103603ef8`) ... authentification token.\n\n### Email [POST]\n\n+ Request (application/json)\n\n        {\n            \"to\" : \"email\",\n            \"title\" : \"title\",\n            \"subj\" : \"subj\"\n        }\n        \n+ Response 200\n\n+ Response 400 (plain/text)\n\n        Invalid token.\n        \n+ Response 400\n\n        Email body is wrong. Missing or empty fields 'to', 'subj'.\n\n+ Response 400\n\n        Project is not active.\n\n+ Response 400\n\n        No email widget.\n        \n+ Response 500 (plain/text)\n\n        Unexpected content type. Expecting application/json.\n        \n        \n### Pin history data [/{auth_token}/data/{pin}]\n\n+ Parameters\n    + auth_token (required, string, `4ae3851817194e2596cf1b7103603ef8`) ... authentification token.\n    + pin (required, string, `D8`) ... pin you want to read.\n\n## Get all history data for specific pin [GET]\n\n+ Response 200 (application/x-gzip)\n\n### QR for project cloning [/{auth_token}/qr]\n\n+ Parameters\n    + auth_token (required, string, `4ae3851817194e2596cf1b7103603ef8`) ... authentification token.\n\n## QR for project cloning [GET]\n\n+ Response 200 (image/png)\n\n## Get project [/{auth_token}/project]\n\nReturns project connected with provided token.\n\n+ Parameters\n    + auth_token (required, string, `4ae3851817194e2596cf1b7103603ef8`) ... authentification token.\n\n### Get project [GET]\n\n+ Response 200 (application/json)\n\n        {\n           \"id\":125564119,\n           \"name\":\"New Project\",\n           \"createdAt\":0,\n           \"updatedAt\":0,\n           \"widgets\":[\n              {\n                 \"type\":\"TWO_AXIS_JOYSTICK\",\n                 \"id\":1036300912,\n                 \"x\":0,\n                 \"y\":4,\n                 \"color\":-308477697,\n                 \"width\":5,\n                 \"height\":4,\n                 \"pins\":[\n                    {\n                       \"pin\":15,\n                       \"pwmMode\":false,\n                       \"rangeMappingOn\":false,\n                       \"pinType\":\"ANALOG\",\n                       \"value\":\"1\",\n                       \"min\":0,\n                       \"max\":255\n                    },\n                    {\n                       \"pin\":15,\n                       \"pwmMode\":false,\n                       \"rangeMappingOn\":false,\n                       \"pinType\":\"ANALOG\",\n                       \"value\":\"2\",\n                       \"min\":0,\n                       \"max\":255\n                    }\n                 ],\n                 \"split\":true,\n                 \"autoReturnOn\":true,\n                 \"portraitLocked\":false\n              },\n              {\n                 \"type\":\"BUTTON\",\n                 \"id\":1036300899,\n                 \"x\":5,\n                 \"y\":0,\n                 \"color\":616861439,\n                 \"width\":2,\n                 \"height\":2,\n                 \"pinType\":\"DIGITAL\",\n                 \"pin\":8,\n                 \"pwmMode\":false,\n                 \"rangeMappingOn\":false,\n                 \"min\":0,\n                 \"max\":0,\n                 \"value\":\"0\",\n                 \"pushMode\":true\n              },\n              {\n                 \"type\":\"LED\",\n                 \"id\":1036300908,\n                 \"x\":4,\n                 \"y\":0,\n                 \"color\":1602017535,\n                 \"width\":1,\n                 \"height\":1,\n                 \"label\":\"gggg\",\n                 \"pin\":-1,\n                 \"pwmMode\":false,\n                 \"rangeMappingOn\":false,\n                 \"min\":0,\n                 \"max\":0,\n                 \"frequency\":0\n              },\n              {\n                 \"type\":\"LED\",\n                 \"id\":1036300904,\n                 \"x\":7,\n                 \"y\":0,\n                 \"color\":1602017535,\n                 \"width\":1,\n                 \"height\":1,\n                 \"pinType\":\"VIRTUAL\",\n                 \"pin\":2,\n                 \"pwmMode\":false,\n                 \"rangeMappingOn\":false,\n                 \"min\":0,\n                 \"max\":0,\n                 \"frequency\":0\n              },\n              {\n                 \"type\":\"TIMER\",\n                 \"id\":276297114,\n                 \"x\":0,\n                 \"y\":1,\n                 \"color\":-308477697,\n                 \"width\":3,\n                 \"height\":1,\n                 \"pinType\":\"DIGITAL\",\n                 \"pin\":0,\n                 \"pwmMode\":false,\n                 \"rangeMappingOn\":false,\n                 \"min\":0,\n                 \"max\":0,\n                 \"startTime\":75600,\n                 \"startValue\":\"dw\\u00000\\u00001\",\n                 \"stopTime\":75599,\n                 \"stopValue\":\"dw\\u00000\\u00000\"\n              },\n              {\n                 \"type\":\"LED\",\n                 \"id\":1036300903,\n                 \"x\":4,\n                 \"y\":1,\n                 \"color\":1602017535,\n                 \"width\":1,\n                 \"height\":1,\n                 \"pinType\":\"VIRTUAL\",\n                 \"pin\":1,\n                 \"pwmMode\":false,\n                 \"rangeMappingOn\":false,\n                 \"min\":0,\n                 \"max\":0,\n                 \"frequency\":0\n              },\n              {\n                 \"type\":\"SLIDER\",\n                 \"id\":1036300911,\n                 \"x\":0,\n                 \"y\":0,\n                 \"color\":-308477697,\n                 \"width\":4,\n                 \"height\":1,\n                 \"pinType\":\"DIGITAL\",\n                 \"pin\":3,\n                 \"pwmMode\":true,\n                 \"rangeMappingOn\":false,\n                 \"min\":0,\n                 \"max\":255,\n                 \"value\":\"87\"\n              },\n              {\n                 \"type\":\"LED\",\n                 \"id\":1036300902,\n                 \"x\":3,\n                 \"y\":1,\n                 \"color\":1602017535,\n                 \"width\":1,\n                 \"height\":1,\n                 \"pinType\":\"VIRTUAL\",\n                 \"pin\":0,\n                 \"pwmMode\":false,\n                 \"rangeMappingOn\":false,\n                 \"min\":0,\n                 \"max\":0,\n                 \"frequency\":0\n              },\n              {\n                 \"type\":\"DIGIT4_DISPLAY\",\n                 \"id\":1036300913,\n                 \"x\":5,\n                 \"y\":4,\n                 \"color\":-308477697,\n                 \"width\":2,\n                 \"height\":1,\n                 \"pinType\":\"ANALOG\",\n                 \"pin\":14,\n                 \"pwmMode\":false,\n                 \"rangeMappingOn\":false,\n                 \"min\":0,\n                 \"max\":1023,\n                 \"frequency\":1000\n              },\n              {\n                 \"type\":\"BUTTON\",\n                 \"id\":1036300914,\n                 \"x\":5,\n                 \"y\":5,\n                 \"color\":616861439,\n                 \"width\":2,\n                 \"height\":2,\n                 \"pinType\":\"DIGITAL\",\n                 \"pin\":1,\n                 \"pwmMode\":false,\n                 \"rangeMappingOn\":false,\n                 \"min\":0,\n                 \"max\":0,\n                 \"value\":\"1\",\n                 \"pushMode\":false\n              },\n              {\n                 \"type\":\"NOTIFICATION\",\n                 \"id\":1036300915,\n                 \"x\":5,\n                 \"y\":7,\n                 \"width\":2,\n                 \"height\":1,\n                 \"androidTokens\":{\n                    \"uid\":\"token\"\n                 },\n                 \"notifyWhenOffline\":false,\n                 \"priority\":\"normal\"\n              },\n              {\n                 \"type\":\"EMAIL\",\n                 \"id\":1036300916,\n                 \"x\":5,\n                 \"y\":9,\n                 \"width\":2,\n                 \"height\":1\n              }\n           ],\n           \"boardType\":\"Arduino UNO\",\n           \"keepScreenOn\":false,\n           \"isShared\":false,\n           \"isActive\":true\n        }\n        \n+ Response 400 (plain/text)\n\n        Invalid token.\n"
  },
  {
    "path": "checkstyle.xml",
    "content": "<?xml version=\"1.0\"?>\n<!DOCTYPE module PUBLIC\n        \"-//Puppy Crawl//DTD Check Configuration 1.3//EN\"\n        \"http://checkstyle.sourceforge.net/dtds/configuration_1_3.dtd\">\n\n<!--\n  Checkstyle configuration that checks the sun coding conventions from:\n    - the Java Language Specification at\n      http://java.sun.com/docs/books/jls/second_edition/html/index.html\n    - the Sun Code Conventions at http://java.sun.com/docs/codeconv/\n    - the Javadoc guidelines at\n      http://java.sun.com/j2se/javadoc/writingdoccomments/index.html\n    - the JDK Api documentation http://java.sun.com/j2se/docs/api/index.html\n    - some best practices\n  Checkstyle is very configurable. Be sure to read the documentation at\n  http://checkstyle.sf.net (or in your downloaded distribution).\n  Most Checks are configurable, be sure to consult the documentation.\n  To completely disable a check, just comment it out or delete it from the file.\n  Finally, it is worth reading the documentation.\n-->\n\n<module name=\"Checker\">\n    <!--\n        If you set the basedir property below, then all reported file\n        names will be relative to the specified directory. See\n        http://checkstyle.sourceforge.net/5.x/config.html#Checker\n        <property name=\"basedir\" value=\"${basedir}\"/>\n    -->\n\n    <property name=\"fileExtensions\" value=\"java\"/>\n\n    <!-- Checks that a package-info.java file exists for each package.     -->\n    <!-- See http://checkstyle.sf.net/config_javadoc.html#JavadocPackage -->\n    <!-- todo fix? -->\n    <!-- <module name=\"JavadocPackage\"/> -->\n\n    <!-- Checks whether files end with a new line.                        -->\n    <!-- See http://checkstyle.sf.net/config_misc.html#NewlineAtEndOfFile -->\n    <module name=\"NewlineAtEndOfFile\"/>\n\n\n    <!-- Checks that property files contain the same keys.         -->\n    <!-- See http://checkstyle.sf.net/config_misc.html#Translation -->\n    <module name=\"Translation\"/>\n\n    <!-- Checks for Size Violations.                    -->\n    <!-- See http://checkstyle.sf.net/config_sizes.html -->\n    <module name=\"FileLength\"/>\n\n    <!-- Checks for whitespace                               -->\n    <!-- See http://checkstyle.sf.net/config_whitespace.html -->\n    <module name=\"FileTabCharacter\"/>\n\n    <!-- Miscellaneous other checks.                   -->\n    <!-- See http://checkstyle.sf.net/config_misc.html -->\n    <module name=\"RegexpSingleline\">\n        <property name=\"format\" value=\"\\s+$\"/>\n        <property name=\"minimum\" value=\"0\"/>\n        <property name=\"maximum\" value=\"0\"/>\n        <property name=\"message\" value=\"Line has trailing spaces.\"/>\n    </module>\n\n    <!-- Checks for Headers                                -->\n    <!-- See http://checkstyle.sf.net/config_header.html   -->\n    <!-- <module name=\"Header\"> -->\n    <!--   <property name=\"headerFile\" value=\"${checkstyle.header.file}\"/> -->\n    <!--   <property name=\"fileExtensions\" value=\"java\"/> -->\n    <!-- </module> -->\n\n    <module name=\"TreeWalker\">\n\n        <!-- Checks for Javadoc comments.                     -->\n        <!-- See http://checkstyle.sf.net/config_javadoc.html\n        <module name=\"JavadocMethod\"/>\n        <module name=\"JavadocType\"/>\n        <module name=\"JavadocVariable\"/>\n        <module name=\"JavadocStyle\"/>\n        -->\n\n        <!-- Checks for Naming Conventions.                  -->\n        <!-- See http://checkstyle.sf.net/config_naming.html -->\n        <!--\n        <module name=\"ConstantName\">\n            <property name=\"format\"\n                      value=\"^log?|[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$\"/>\n        </module>\n        -->\n        <module name=\"LocalFinalVariableName\"/>\n        <module name=\"LocalVariableName\"/>\n        <module name=\"MemberName\"/>\n        <module name=\"MethodName\"/>\n        <module name=\"PackageName\"/>\n        <module name=\"ParameterName\"/>\n        <!--\n        <module name=\"StaticVariableName\"/>\n        -->\n        <module name=\"TypeName\"/>\n\n\n        <!-- Checks for imports                              -->\n        <!-- See http://checkstyle.sf.net/config_import.html -->\n        <module name=\"AvoidStarImport\"/>\n        <!--<module name=\"IllegalImport\"/>  defaults to sun.* packages -->\n        <module name=\"RedundantImport\"/>\n        <module name=\"UnusedImports\">\n            <property name=\"processJavadoc\" value=\"false\"/>\n        </module>\n\n        <!-- Checks for Size Violations.                    -->\n        <!-- See http://checkstyle.sf.net/config_sizes.html -->\n        <module name=\"LineLength\">\n            <property name=\"max\" value=\"120\"/>\n        </module>\n        <module name=\"MethodLength\">\n            <property name=\"max\" value=\"250\"/>\n        </module>\n        <!--\n<module name=\"ParameterNumber\"/>\n-->\n\n        <!-- Checks for whitespace                               -->\n        <!-- See http://checkstyle.sf.net/config_whitespace.html -->\n        <module name=\"EmptyForIteratorPad\"/>\n        <module name=\"GenericWhitespace\"/>\n        <module name=\"MethodParamPad\"/>\n        <module name=\"NoWhitespaceAfter\"/>\n        <module name=\"NoWhitespaceBefore\"/>\n        <module name=\"OperatorWrap\"/>\n        <module name=\"ParenPad\"/>\n        <module name=\"TypecastParenPad\"/>\n        <module name=\"WhitespaceAfter\"/>\n        <module name=\"WhitespaceAround\"/>\n\n\n        <!-- Modifier Checks                                    -->\n        <!-- See http://checkstyle.sf.net/config_modifiers.html\n        <module name=\"ModifierOrder\"/>\n        -->\n        <module name=\"RedundantModifier\"/>\n\n        <!-- Checks for blocks. You know, those {}'s         -->\n        <!-- See http://checkstyle.sf.net/config_blocks.html -->\n        <module name=\"AvoidNestedBlocks\"/>\n        <module name=\"EmptyBlock\"/>\n        <module name=\"LeftCurly\"/>\n        <module name=\"NeedBraces\"/>\n        <module name=\"RightCurly\"/>\n\n\n        <!-- Checks for common coding problems               -->\n        <!-- See http://checkstyle.sf.net/config_coding.html -->\n\n        <!--\n        <module name=\"AvoidInlineConditionals\"/>\n        -->\n\n        <module name=\"EmptyStatement\"/>\n        <module name=\"EqualsHashCode\"/>\n\n        <module name=\"IllegalInstantiation\"/>\n\n        <!--\n        <module name=\"HiddenField\"/>\n        <module name=\"InnerAssignment\"/>\n        <module name=\"MagicNumber\">\n            <property name=\"ignoreNumbers\" value=\"0, 1, 31\"/>\n        </module>\n        <module name=\"MissingSwitchDefault\"/>\n\n        <module name=\"SimplifyBooleanReturn\"/>\n        -->\n        <module name=\"SimplifyBooleanExpression\"/>\n        <module name=\"FinalClass\"/>\n\n        <!-- Checks for class design                         -->\n        <!-- See http://checkstyle.sf.net/config_design.html -->\n        <!--\n        <module name=\"DesignForExtension\"/>\n\n        <module name=\"VisibilityModifier\"/>\n        -->\n        <module name=\"InterfaceIsType\"/>\n        <module name=\"HideUtilityClassConstructor\"/>\n\n\n        <!-- Miscellaneous other checks.                   -->\n        <!-- See http://checkstyle.sf.net/config_misc.html -->\n\n        <module name=\"ArrayTypeStyle\"/>\n\n        <module name=\"TodoComment\"/>\n        <module name=\"UpperEll\"/>\n\n\n    </module>\n\n</module>\n"
  },
  {
    "path": "client/README.md",
    "content": "## App Client (emulates Smartphone App)\n\n+ To emulate the Smartphone App client:\n\n        java -jar client-${PUT_LATEST_VERSION_HERE}.jar -mode app -host localhost -port 9443\n\n\n+ In this client: register new user and/or login with the same credentials\n\n        register username@example.com UserPassword\n        login username@example.com UserPassword\n\n\n+ Save profile with simple dashboard\n\n        createDash {\"id\":1, \"name\":\"My Dashboard\", \"boardType\":\"UNO\"}\n\n\n+ Get the Auth Token for hardware (e.g Arduino)\n\n        getToken 1\n\n+ Activate dashboard\n\n        activate 1\n\n+ You will get server response similar to this:\n\n    \t00:05:18.100 TRACE  - Incomming : GetTokenMessage{id=30825, command=GET_TOKEN, length=32, body='33bcbe756b994a6768494d55d1543c74'}\n\nWhere `33bcbe756b994a6768494d55d1543c74` is your Auth Token.\n\n## Hardware Client (emulates Hardware)\n\n+ Start new client and use received Auth Token to login\n\n    \tjava -jar client-${PUT_LATEST_VERSION_HERE}.jar -mode hardware -host localhost -port 8442\n    \tlogin 33bcbe756b994a6768494d55d1543c74\n   \n\nYou can run as many clients as you want. You have only 15 seconds for login until your client will be disconnected from server, \nso hurry up :).\n\nClients with the same credentials and Auth Token are grouped into one Session and can send messages to each other.\nAll client’s commands are human-friendly, so you don't have to remember the codes.\n\n## Hardware Commands\n\nList of hardware commands:\n\n+ Digital write:\n\n    \thardware dw 9 1\n    \thardware dw 9 0\n\n\n+ Digital read:\n\n    \thardware dr 9\n    \tYou should receive response: dw 9 <val>\n\n\n+ Analog write:\n\n    \thardware aw 14 123\n\n\n+ Analog read:\n\n    \thardware ar 14\n        You should receive response: aw 14 <val>\n\n\n+ Virtual write:\n\n    \thardware vw 9 1234\n        hardware vw 9 string\n        hardware vw 9 item1 item2 item3\n        hardware vw 9 key1 val1 key2 val2\n\n \n+ Virtual read:\n\n    \thardware vr 9\n    \tYou should receive response: vw 9 <values>\n"
  },
  {
    "path": "client/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <artifactId>blynk</artifactId>\n        <groupId>cc.blynk</groupId>\n        <version>0.41.17-SNAPSHOT</version>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n\n    <artifactId>client</artifactId>\n\n    <build>\n\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-shade-plugin</artifactId>\n                <version>${maven-shade-plugin.version}</version>\n                <executions>\n                    <execution>\n                        <phase>package</phase>\n                        <goals>\n                            <goal>shade</goal>\n                        </goals>\n                        <configuration>\n                            <finalName>client-${project.version}</finalName>\n                            <transformers>\n                                <transformer implementation=\"org.apache.maven.plugins.shade.resource.ManifestResourceTransformer\">\n                                    <manifestEntries>\n                                        <Main-Class>cc.blynk.client.ClientLauncher</Main-Class>\n                                        <Build-Number>${project.version}</Build-Number>\n                                        <Build-By>Blynk Inc.</Build-By>\n                                    </manifestEntries>\n                                </transformer>\n                            </transformers>\n                            <filters>\n                                <filter>\n                                    <artifact>*:*</artifact>\n                                    <excludes>\n                                        <exclude>META-INF/maven/**</exclude>\n                                        <exclude>META-INF/*.SF</exclude>\n                                        <exclude>META-INF/*.DSA</exclude>\n                                        <exclude>META-INF/*.RSA</exclude>\n                                    </excludes>\n                                </filter>\n                            </filters>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n\n    <dependencies>\n\n        <dependency>\n            <groupId>commons-cli</groupId>\n            <artifactId>commons-cli</artifactId>\n            <version>1.4</version>\n        </dependency>\n\n        <dependency>\n            <groupId>cc.blynk.server</groupId>\n            <artifactId>core</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n\n    </dependencies>\n\n</project>"
  },
  {
    "path": "client/src/main/java/cc/blynk/client/ClientLauncher.java",
    "content": "package cc.blynk.client;\n\nimport cc.blynk.client.core.ActiveHardwareClient;\nimport cc.blynk.client.core.AppClient;\nimport cc.blynk.client.core.HardwareClient;\nimport cc.blynk.client.enums.ClientMode;\nimport org.apache.commons.cli.CommandLine;\nimport org.apache.commons.cli.DefaultParser;\nimport org.apache.commons.cli.Options;\nimport org.apache.commons.cli.ParseException;\n\nimport java.io.BufferedReader;\nimport java.io.InputStreamReader;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 11.03.15.\n */\npublic final class ClientLauncher {\n\n    static final String DEFAULT_HOST = \"localhost\";\n    static final int DEFAULT_HARDWARE_PORT = 8080;\n    static final int DEFAULT_APPLICATION_PORT = 9443;\n\n    private static final Options options = new Options();\n\n    static {\n        options.addOption(\"host\", true, \"Server host or ip.\")\n               .addOption(\"port\", true, \"Port client should connect to.\")\n               .addOption(\"mode\", true, \"Client mode. 'hardware' or 'app'.\")\n               .addOption(\"tokens\", true, \"Tokens\");\n    }\n\n    private ClientLauncher() {\n    }\n\n    public static void main(String[] args) throws ParseException {\n        CommandLine cmd = new DefaultParser().parse(options, args);\n\n        ClientMode mode = ClientMode.parse(cmd.getOptionValue(\"mode\", ClientMode.HARDWARE.name()));\n        String host = cmd.getOptionValue(\"host\", DEFAULT_HOST);\n        int port = Integer.parseInt(cmd.getOptionValue(\"port\",\n                        (mode == ClientMode.APP\n                                ? String.valueOf(DEFAULT_APPLICATION_PORT)\n                                : String.valueOf(DEFAULT_HARDWARE_PORT)))\n        );\n\n        switch (mode) {\n            case APP :\n                new AppClient(host, port).start(new BufferedReader(new InputStreamReader(System.in)));\n                break;\n            case HARDWARE :\n                new HardwareClient(host, port).start(new BufferedReader(new InputStreamReader(System.in)));\n                break;\n            default :\n                String tokensFullString = cmd.getOptionValue(\"tokens\");\n                if (tokensFullString == null) {\n                    throw new RuntimeException(\"Tokens required for TEST mode.\");\n                }\n                String[] tokens = tokensFullString.split(\",\");\n                for (String token : tokens) {\n                    new ActiveHardwareClient(host, port).start(token);\n                }\n        }\n    }\n\n}\n"
  },
  {
    "path": "client/src/main/java/cc/blynk/client/CommandParserUtil.java",
    "content": "package cc.blynk.client;\n\nimport static cc.blynk.server.core.protocol.enums.Command.ACTIVATE_DASHBOARD;\nimport static cc.blynk.server.core.protocol.enums.Command.ADD_ENERGY;\nimport static cc.blynk.server.core.protocol.enums.Command.ADD_PUSH_TOKEN;\nimport static cc.blynk.server.core.protocol.enums.Command.APP_SYNC;\nimport static cc.blynk.server.core.protocol.enums.Command.ASSIGN_TOKEN;\nimport static cc.blynk.server.core.protocol.enums.Command.BLYNK_INTERNAL;\nimport static cc.blynk.server.core.protocol.enums.Command.BRIDGE;\nimport static cc.blynk.server.core.protocol.enums.Command.CREATE_APP;\nimport static cc.blynk.server.core.protocol.enums.Command.CREATE_DASH;\nimport static cc.blynk.server.core.protocol.enums.Command.CREATE_DEVICE;\nimport static cc.blynk.server.core.protocol.enums.Command.CREATE_REPORT;\nimport static cc.blynk.server.core.protocol.enums.Command.CREATE_TAG;\nimport static cc.blynk.server.core.protocol.enums.Command.CREATE_TILE_TEMPLATE;\nimport static cc.blynk.server.core.protocol.enums.Command.CREATE_WIDGET;\nimport static cc.blynk.server.core.protocol.enums.Command.DEACTIVATE_DASHBOARD;\nimport static cc.blynk.server.core.protocol.enums.Command.DELETE_APP;\nimport static cc.blynk.server.core.protocol.enums.Command.DELETE_DASH;\nimport static cc.blynk.server.core.protocol.enums.Command.DELETE_DEVICE;\nimport static cc.blynk.server.core.protocol.enums.Command.DELETE_DEVICE_DATA;\nimport static cc.blynk.server.core.protocol.enums.Command.DELETE_ENHANCED_GRAPH_DATA;\nimport static cc.blynk.server.core.protocol.enums.Command.DELETE_REPORT;\nimport static cc.blynk.server.core.protocol.enums.Command.DELETE_TAG;\nimport static cc.blynk.server.core.protocol.enums.Command.DELETE_TILE_TEMPLATE;\nimport static cc.blynk.server.core.protocol.enums.Command.DELETE_WIDGET;\nimport static cc.blynk.server.core.protocol.enums.Command.EMAIL;\nimport static cc.blynk.server.core.protocol.enums.Command.EMAIL_QR;\nimport static cc.blynk.server.core.protocol.enums.Command.EXPORT_GRAPH_DATA;\nimport static cc.blynk.server.core.protocol.enums.Command.EXPORT_REPORT;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_CLONE_CODE;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_DEVICES;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_ENERGY;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_ENHANCED_GRAPH_DATA;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_PROJECT_BY_CLONE_CODE;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_PROJECT_BY_TOKEN;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_PROVISION_TOKEN;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_SERVER;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_SHARE_TOKEN;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_TAGS;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_WIDGET;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE_LOGIN;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE_RESEND_FROM_BLUETOOTH;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE_SYNC;\nimport static cc.blynk.server.core.protocol.enums.Command.LOAD_PROFILE_GZIPPED;\nimport static cc.blynk.server.core.protocol.enums.Command.LOGIN;\nimport static cc.blynk.server.core.protocol.enums.Command.LOGOUT;\nimport static cc.blynk.server.core.protocol.enums.Command.MOBILE_GET_DEVICE;\nimport static cc.blynk.server.core.protocol.enums.Command.PING;\nimport static cc.blynk.server.core.protocol.enums.Command.PUSH_NOTIFICATION;\nimport static cc.blynk.server.core.protocol.enums.Command.REFRESH_SHARE_TOKEN;\nimport static cc.blynk.server.core.protocol.enums.Command.REFRESH_TOKEN;\nimport static cc.blynk.server.core.protocol.enums.Command.REGISTER;\nimport static cc.blynk.server.core.protocol.enums.Command.RESET_PASSWORD;\nimport static cc.blynk.server.core.protocol.enums.Command.RESOLVE_EVENT;\nimport static cc.blynk.server.core.protocol.enums.Command.SET_WIDGET_PROPERTY;\nimport static cc.blynk.server.core.protocol.enums.Command.SHARE_LOGIN;\nimport static cc.blynk.server.core.protocol.enums.Command.SHARING;\nimport static cc.blynk.server.core.protocol.enums.Command.SMS;\nimport static cc.blynk.server.core.protocol.enums.Command.TWEET;\nimport static cc.blynk.server.core.protocol.enums.Command.UPDATE_APP;\nimport static cc.blynk.server.core.protocol.enums.Command.UPDATE_DASH;\nimport static cc.blynk.server.core.protocol.enums.Command.UPDATE_DEVICE;\nimport static cc.blynk.server.core.protocol.enums.Command.UPDATE_FACE;\nimport static cc.blynk.server.core.protocol.enums.Command.UPDATE_PROJECT_SETTINGS;\nimport static cc.blynk.server.core.protocol.enums.Command.UPDATE_REPORT;\nimport static cc.blynk.server.core.protocol.enums.Command.UPDATE_TAG;\nimport static cc.blynk.server.core.protocol.enums.Command.UPDATE_TILE_TEMPLATE;\nimport static cc.blynk.server.core.protocol.enums.Command.UPDATE_WIDGET;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n * Convertor between user-friendly command and protocol command code\n */\npublic final class CommandParserUtil {\n\n    private CommandParserUtil() {\n    }\n\n    public static Short parseCommand(String stringCommand) {\n        switch (stringCommand.toLowerCase()) {\n            case \"hardware\" :\n                return HARDWARE;\n            case \"hardwarebt\" :\n                return HARDWARE_RESEND_FROM_BLUETOOTH;\n            case \"ping\" :\n                return PING;\n            case \"loadprofilegzipped\" :\n                return LOAD_PROFILE_GZIPPED;\n            case \"appsync\" :\n                return APP_SYNC;\n            case \"sharing\" :\n                return SHARING;\n            case \"assigntoken\" :\n                return ASSIGN_TOKEN;\n            case \"refreshtoken\" :\n                return REFRESH_TOKEN;\n            case \"login\" :\n                return LOGIN;\n            case \"hardwarelogin\" :\n                return HARDWARE_LOGIN;\n            case \"logout\" :\n                return LOGOUT;\n            case \"getenhanceddata\" :\n                return GET_ENHANCED_GRAPH_DATA;\n            case \"deleteenhanceddata\" :\n                return DELETE_ENHANCED_GRAPH_DATA;\n            case \"export\" :\n                return EXPORT_GRAPH_DATA;\n            case \"activate\" :\n                return ACTIVATE_DASHBOARD;\n            case \"deactivate\" :\n                return DEACTIVATE_DASHBOARD;\n            case \"register\" :\n                return REGISTER;\n            case \"setproperty\" :\n                return SET_WIDGET_PROPERTY;\n\n            case \"tweet\" :\n                return TWEET;\n            case \"email\" :\n                return EMAIL;\n            case \"push\" :\n                return PUSH_NOTIFICATION;\n            case \"sms\" :\n                return SMS;\n            case \"addpushtoken\" :\n                return ADD_PUSH_TOKEN;\n\n            case \"bridge\" :\n                return BRIDGE;\n\n            case \"createdash\" :\n                return CREATE_DASH;\n            case \"updatedash\" :\n                return UPDATE_DASH;\n            case \"deletedash\" :\n                return DELETE_DASH;\n            case \"updatesettings\" :\n                return UPDATE_PROJECT_SETTINGS;\n\n            case \"createwidget\" :\n                return CREATE_WIDGET;\n            case \"updatewidget\" :\n                return UPDATE_WIDGET;\n            case \"deletewidget\" :\n                return DELETE_WIDGET;\n            case \"getwidget\" :\n                return GET_WIDGET;\n\n            case \"hardsync\" :\n                return HARDWARE_SYNC;\n            case \"internal\" :\n                return BLYNK_INTERNAL;\n\n            case \"createtemplate\" :\n                return CREATE_TILE_TEMPLATE;\n            case \"updatetemplate\" :\n                return UPDATE_TILE_TEMPLATE;\n            case \"deletetemplate\" :\n                return DELETE_TILE_TEMPLATE;\n\n            case \"createdevice\" :\n                return CREATE_DEVICE;\n            case \"updatedevice\" :\n                return UPDATE_DEVICE;\n            case \"deletedevice\" :\n                return DELETE_DEVICE;\n            case \"getdevices\" :\n                return GET_DEVICES;\n            case \"getdevice\" :\n                return MOBILE_GET_DEVICE;\n\n            case \"createtag\" :\n                return CREATE_TAG;\n            case \"updatetag\" :\n                return UPDATE_TAG;\n            case \"deletetag\" :\n                return DELETE_TAG;\n            case \"gettags\" :\n                return GET_TAGS;\n\n            case \"addenergy\" :\n                return ADD_ENERGY;\n            case \"getenergy\" :\n                return GET_ENERGY;\n\n            case \"getserver\" :\n                return GET_SERVER;\n\n            //sharing section\n            case \"sharelogin\" :\n                return SHARE_LOGIN;\n            case \"getsharetoken\" :\n                return GET_SHARE_TOKEN;\n            case \"refreshsharetoken\" :\n                return REFRESH_SHARE_TOKEN;\n\n            case \"createapp\" :\n                return CREATE_APP;\n            case \"updateapp\" :\n                return UPDATE_APP;\n            case \"deleteapp\" :\n                return DELETE_APP;\n            case \"getprojectbytoken\" :\n                return GET_PROJECT_BY_TOKEN;\n            case \"emailqr\" :\n                return EMAIL_QR;\n            case \"updateface\" :\n                return UPDATE_FACE;\n            case \"getclonecode\" :\n                return GET_CLONE_CODE;\n            case \"getprojectbyclonecode\" :\n                return GET_PROJECT_BY_CLONE_CODE;\n            case \"getprovisiontoken\" :\n                return GET_PROVISION_TOKEN;\n            case \"resolveevent\" :\n                return RESOLVE_EVENT;\n            case \"deletedevicedata\" :\n                return DELETE_DEVICE_DATA;\n\n            case \"createreport\" :\n                return CREATE_REPORT;\n            case \"deletereport\" :\n                return DELETE_REPORT;\n            case \"updatereport\" :\n                return UPDATE_REPORT;\n            case \"exportreport\" :\n                return EXPORT_REPORT;\n            case \"resetpass\" :\n                return RESET_PASSWORD;\n\n            default:\n                throw new IllegalArgumentException(\"Unsupported command\");\n        }\n    }\n\n}\n"
  },
  {
    "path": "client/src/main/java/cc/blynk/client/core/ActiveHardwareClient.java",
    "content": "package cc.blynk.client.core;\n\nimport cc.blynk.client.handlers.ClientReplayingMessageDecoder;\nimport cc.blynk.client.handlers.hardware.HardwareEchoHandler;\nimport cc.blynk.server.core.protocol.handlers.encoders.MessageEncoder;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.server.core.protocol.model.messages.common.HardwareMessage;\nimport cc.blynk.server.core.stats.GlobalStats;\nimport io.netty.channel.ChannelInitializer;\nimport io.netty.channel.ChannelPipeline;\nimport io.netty.channel.socket.SocketChannel;\n\nimport java.util.Random;\nimport java.util.concurrent.ThreadLocalRandom;\nimport java.util.concurrent.TimeUnit;\n\nimport static cc.blynk.server.core.protocol.enums.Command.PING;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 11.03.15.\n */\npublic class ActiveHardwareClient extends BaseClient {\n\n    private int buttonVal = 1;\n    private int ledVal = 20;\n\n    public ActiveHardwareClient(String host, int port) {\n        super(host, port, new Random());\n        log.info(\"Creating hardware client. Host : {}, port : {}\", host, port);\n        //pinging for hardware client to avoid closing from server side for inactivity\n        nioEventLoopGroup.scheduleAtFixedRate(() -> send(new StringMessage(777, PING, \"\")), 12, 12, TimeUnit.SECONDS);\n    }\n\n    private static HardwareMessage makeCommand(String body) {\n        return new HardwareMessage(778, (body.replaceAll(\" \", \"\\0\")));\n    }\n\n    @Override\n    public ChannelInitializer<SocketChannel> getChannelInitializer() {\n        return new ChannelInitializer<>() {\n            @Override\n            public void initChannel(SocketChannel ch) {\n                ChannelPipeline pipeline = ch.pipeline();\n                pipeline.addLast(new ClientReplayingMessageDecoder());\n                pipeline.addLast(new MessageEncoder(new GlobalStats()));\n                pipeline.addLast(new HardwareEchoHandler());\n            }\n        };\n    }\n\n    public void start(String token) {\n        super.start();\n\n        send(\"login \" + token);\n\n        nioEventLoopGroup.scheduleAtFixedRate(() -> {\n            send(makeCommand(\"vw 4 \" + ThreadLocalRandom.current().nextInt(100)));\n            send(makeCommand(\"dw 3 \" + ThreadLocalRandom.current().nextInt(255)));\n            send(makeCommand(\"dw 0 \" + buttonVal));\n            send(makeCommand(\"vw 5 \" + (buttonVal == 1 ? 255 : 0)));\n            send(makeCommand(\"vw 6 \" + ledVal));\n            send(makeCommand(\"aw 6 \" + ledVal));\n            send(makeCommand(\"vw 10 p 0 0 ledVal:\" + ledVal));\n            if (buttonVal == 1) {\n                buttonVal = 0;\n            } else {\n                buttonVal = 1;\n            }\n            if (ledVal > 255) {\n                ledVal = 0;\n                send(makeCommand(\"vw 10 clr\"));\n            } else {\n                ledVal += 20;\n            }\n        }, 1, 1, TimeUnit.SECONDS);\n\n    }\n\n    public void send(String line) {\n        send(produceMessageBaseOnUserInput(line, 1));\n    }\n}\n"
  },
  {
    "path": "client/src/main/java/cc/blynk/client/core/AppClient.java",
    "content": "package cc.blynk.client.core;\n\nimport cc.blynk.client.handlers.ClientReplayingMessageDecoder;\nimport cc.blynk.server.core.protocol.handlers.encoders.MessageEncoder;\nimport cc.blynk.server.core.stats.GlobalStats;\nimport cc.blynk.utils.properties.ServerProperties;\nimport io.netty.channel.ChannelInitializer;\nimport io.netty.channel.ChannelPipeline;\nimport io.netty.channel.socket.SocketChannel;\nimport io.netty.handler.ssl.SslContext;\nimport io.netty.handler.ssl.SslContextBuilder;\nimport io.netty.handler.ssl.SslProvider;\nimport io.netty.handler.ssl.util.InsecureTrustManagerFactory;\n\nimport javax.net.ssl.SSLException;\nimport java.io.File;\nimport java.util.Random;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 11.03.15.\n */\npublic class AppClient extends BaseClient {\n\n    protected SslContext sslCtx;\n\n    public AppClient(String host, int port) {\n        super(host, port, new Random());\n    }\n\n    protected AppClient(String host, int port, Random msgIdGenerator, ServerProperties properties) {\n        super(host, port, msgIdGenerator, properties);\n        log.info(\"Creating app client. Host {}, sslPort : {}\", host, port);\n        File serverCert = makeCertificateFile(\"server.ssl.cert\");\n        File clientCert = makeCertificateFile(\"client.ssl.cert\");\n        File clientKey = makeCertificateFile(\"client.ssl.key\");\n        try {\n            if (!serverCert.exists() || !clientCert.exists() || !clientKey.exists()) {\n                log.info(\"Enabling one-way auth with no certs checks.\");\n                this.sslCtx = SslContextBuilder.forClient().sslProvider(SslProvider.JDK)\n                        .trustManager(InsecureTrustManagerFactory.INSTANCE)\n                        .build();\n            } else {\n                log.info(\"Enabling mutual auth.\");\n                String clientPass = props.getProperty(\"client.ssl.key.pass\");\n                this.sslCtx = SslContextBuilder.forClient()\n                        .sslProvider(SslProvider.JDK)\n                        .trustManager(serverCert)\n                        .keyManager(clientCert, clientKey, clientPass)\n                        .build();\n            }\n        } catch (SSLException e) {\n            log.error(\"Error initializing SSL context. Reason : {}\", e.getMessage());\n            log.debug(e);\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public ChannelInitializer<SocketChannel> getChannelInitializer() {\n        return new ChannelInitializer<>() {\n            @Override\n            public void initChannel(SocketChannel ch) {\n                ChannelPipeline pipeline = ch.pipeline();\n                if (sslCtx != null) {\n                    pipeline.addLast(sslCtx.newHandler(ch.alloc(), host, port));\n                }\n                pipeline.addLast(new ClientReplayingMessageDecoder());\n                pipeline.addLast(new MessageEncoder(new GlobalStats()));\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "client/src/main/java/cc/blynk/client/core/BaseClient.java",
    "content": "package cc.blynk.client.core;\n\nimport cc.blynk.client.CommandParserUtil;\nimport cc.blynk.server.core.protocol.model.messages.MessageBase;\nimport cc.blynk.utils.properties.ServerProperties;\nimport io.netty.bootstrap.Bootstrap;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelFuture;\nimport io.netty.channel.ChannelInitializer;\nimport io.netty.channel.ChannelOption;\nimport io.netty.channel.ConnectTimeoutException;\nimport io.netty.channel.nio.NioEventLoopGroup;\nimport io.netty.channel.socket.SocketChannel;\nimport io.netty.channel.socket.nio.NioSocketChannel;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.io.BufferedReader;\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.channels.UnresolvedAddressException;\nimport java.util.Collections;\nimport java.util.Random;\n\nimport static cc.blynk.server.core.protocol.enums.Command.BRIDGE;\nimport static cc.blynk.server.core.protocol.enums.Command.DELETE_WIDGET;\nimport static cc.blynk.server.core.protocol.enums.Command.EMAIL;\nimport static cc.blynk.server.core.protocol.enums.Command.EXPORT_GRAPH_DATA;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE_RESEND_FROM_BLUETOOTH;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE_SYNC;\nimport static cc.blynk.server.core.protocol.enums.Command.LOAD_PROFILE_GZIPPED;\nimport static cc.blynk.server.core.protocol.enums.Command.RESET_PASSWORD;\nimport static cc.blynk.server.core.protocol.enums.Command.SET_WIDGET_PROPERTY;\nimport static cc.blynk.server.core.protocol.enums.Command.SHARE_LOGIN;\nimport static cc.blynk.server.core.protocol.enums.Command.SHARING;\nimport static cc.blynk.server.core.protocol.model.messages.MessageFactory.produce;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 1/31/2015.\n */\npublic abstract class BaseClient {\n\n    protected static final Logger log = LogManager.getLogger(BaseClient.class);\n\n    protected final ServerProperties props;\n    protected final String host;\n    protected final int port;\n    protected final Random random;\n    protected Channel channel;\n    protected NioEventLoopGroup nioEventLoopGroup;\n\n    public BaseClient(String host, int port, Random messageIdGenerator) {\n        this(host, port, messageIdGenerator, new NioEventLoopGroup(1));\n    }\n\n    public BaseClient(String host, int port, Random messageIdGenerator, ServerProperties serverProperties) {\n        this.host = host;\n        this.port = port;\n        this.random = messageIdGenerator;\n        this.props = serverProperties;\n        this.nioEventLoopGroup = new NioEventLoopGroup(1);\n    }\n\n    public BaseClient(String host, int port, Random messageIdGenerator, NioEventLoopGroup nioEventLoopGroup) {\n        this.host = host;\n        this.port = port;\n        this.random = messageIdGenerator;\n        this.props = new ServerProperties(Collections.emptyMap());\n        this.nioEventLoopGroup = nioEventLoopGroup;\n    }\n\n    public static MessageBase produceMessageBaseOnUserInput(String line, int msgId) {\n        String[] input = line.split(\" \", 2);\n\n        short command;\n\n        try {\n            command = CommandParserUtil.parseCommand(input[0]);\n        } catch (IllegalArgumentException e) {\n            log.error(\"Command not supported {}\", input[0]);\n            return null;\n        }\n\n        String body = input.length == 1 ? \"\" : input[1];\n\n        if (command == HARDWARE\n                || command == SHARE_LOGIN\n                || command == LOAD_PROFILE_GZIPPED\n                || command == HARDWARE_RESEND_FROM_BLUETOOTH\n                || command == BRIDGE\n                || command == EMAIL\n                || command == SHARING\n                || command == EXPORT_GRAPH_DATA\n                || command == SET_WIDGET_PROPERTY\n                || command == HARDWARE_SYNC\n                || command == RESET_PASSWORD\n                || command == DELETE_WIDGET) {\n            body = body.replace(\" \", \"\\0\");\n        }\n        return produce(msgId, command, body);\n    }\n\n    public void start(BufferedReader commandInputStream) {\n        this.nioEventLoopGroup = new NioEventLoopGroup(1);\n        try {\n            Bootstrap b = new Bootstrap();\n            b.group(nioEventLoopGroup)\n                    .channel(NioSocketChannel.class)\n                    .option(ChannelOption.SO_KEEPALIVE, true)\n                    .handler(getChannelInitializer());\n\n            // Start the connection attempt.\n            this.channel = b.connect(host, port).sync().channel();\n            readUserInput(commandInputStream);\n        } catch (UnresolvedAddressException uae) {\n            log.error(\"Host name '{}' is invalid. Please make sure it is correct name.\", host);\n        } catch (ConnectTimeoutException cte) {\n            log.error(\"Timeout exceeded when connecting to '{}:{}'. \"\n                    + \"Please make sure host available and port is open on target host.\", host, port);\n        } catch (IOException | InterruptedException e) {\n            log.error(\"Error running client. Shutting down.\", e);\n        } catch (Exception e) {\n            log.error(e);\n        } finally {\n            // The connection is closed automatically on shutdown.\n            nioEventLoopGroup.shutdownGracefully();\n        }\n    }\n\n    public void start() {\n        Bootstrap b = new Bootstrap();\n        b.group(nioEventLoopGroup).channel(NioSocketChannel.class).handler(getChannelInitializer());\n\n        try {\n            // Start the connection attempt.\n            this.channel = b.connect(host, port).sync().channel();\n        } catch (InterruptedException e) {\n            log.error(e);\n        }\n    }\n\n    protected File makeCertificateFile(String propertyName) {\n        String path = props.getProperty(propertyName);\n        if (path == null || path.isEmpty()) {\n            path = \"\";\n        }\n        File file = new File(path);\n        if (!file.exists()) {\n            log.warn(\"{} file was not found at {} location\", propertyName, path);\n        }\n        return file;\n    }\n\n    protected abstract ChannelInitializer<SocketChannel> getChannelInitializer();\n\n    private void readUserInput(BufferedReader commandInputStream) throws IOException {\n        String line;\n        while ((line = commandInputStream.readLine()) != null) {\n            // If user typed the 'quit' command, wait until the server closes the connection.\n            if (\"quit\".equals(line.toLowerCase())) {\n                log.info(\"Got 'quit' command. Closing client.\");\n                channel.close();\n                break;\n            }\n\n            MessageBase msg = produceMessageBaseOnUserInput(line, (short) random.nextInt(Short.MAX_VALUE));\n            if (msg == null) {\n                continue;\n            }\n\n            send(msg);\n        }\n    }\n\n    public void send(Object msg) {\n        channel.writeAndFlush(msg);\n    }\n\n    public boolean isClosed() {\n        return !channel.isOpen();\n    }\n\n    public ChannelFuture stop() {\n        if (nioEventLoopGroup.isTerminated()) {\n            return channel.voidPromise();\n        }\n        ChannelFuture channelFuture = channel.close().awaitUninterruptibly();\n        nioEventLoopGroup.shutdownGracefully();\n        return channelFuture;\n    }\n}\n"
  },
  {
    "path": "client/src/main/java/cc/blynk/client/core/HardwareClient.java",
    "content": "package cc.blynk.client.core;\n\nimport cc.blynk.client.handlers.ClientReplayingMessageDecoder;\nimport cc.blynk.server.core.protocol.handlers.encoders.MessageEncoder;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.server.core.stats.GlobalStats;\nimport io.netty.channel.ChannelInitializer;\nimport io.netty.channel.ChannelPipeline;\nimport io.netty.channel.socket.SocketChannel;\n\nimport java.util.Random;\nimport java.util.concurrent.TimeUnit;\n\nimport static cc.blynk.server.core.protocol.enums.Command.PING;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 11.03.15.\n */\npublic class HardwareClient extends BaseClient {\n\n    public HardwareClient(String host, int port) {\n        super(host, port, new Random());\n        log.info(\"Creating hardware client. Host : {}, port : {}\", host, port);\n        //pinging for hardware client to avoid closing from server side for inactivity\n        nioEventLoopGroup.scheduleAtFixedRate(() -> send(new StringMessage(777, PING, \"\")), 12, 12, TimeUnit.SECONDS);\n    }\n\n    @Override\n    public ChannelInitializer<SocketChannel> getChannelInitializer() {\n        return new ChannelInitializer<>() {\n            @Override\n            public void initChannel(SocketChannel ch) {\n                ChannelPipeline pipeline = ch.pipeline();\n                pipeline.addLast(new ClientReplayingMessageDecoder());\n                pipeline.addLast(new MessageEncoder(new GlobalStats()));\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "client/src/main/java/cc/blynk/client/enums/ClientMode.java",
    "content": "package cc.blynk.client.enums;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/16/2015.\n */\npublic enum ClientMode {\n\n    APP, HARDWARE, TEST;\n\n    public static ClientMode parse(String val) {\n        for (ClientMode clientMode : values()) {\n            if (clientMode.name().equalsIgnoreCase(val)) {\n                return clientMode;\n            }\n        }\n\n        throw new RuntimeException(\"Wrong client mode. app and hardware only supported.\");\n    }\n\n}\n"
  },
  {
    "path": "client/src/main/java/cc/blynk/client/handlers/ClientReplayingMessageDecoder.java",
    "content": "package cc.blynk.client.handlers;\n\nimport cc.blynk.client.handlers.decoders.ClientMessageDecoder;\nimport io.netty.channel.ChannelHandlerContext;\n\nimport java.io.IOException;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/20/2015.\n */\npublic class ClientReplayingMessageDecoder extends ClientMessageDecoder {\n\n    @Override\n    public void channelInactive(ChannelHandlerContext ctx) throws Exception {\n        throw new IOException(\"Server closed client connection.\");\n    }\n\n    @Override\n    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {\n        //server goes down\n        if (cause instanceof IOException) {\n            ctx.close();\n            log.error(\"Client socket closed. Reason : {}\", cause.getMessage());\n            //todo find better way\n            System.exit(0);\n        }\n    }\n}\n"
  },
  {
    "path": "client/src/main/java/cc/blynk/client/handlers/decoders/AppClientMessageDecoder.java",
    "content": "package cc.blynk.client.handlers.decoders;\n\nimport cc.blynk.server.core.protocol.enums.Command;\nimport cc.blynk.server.core.protocol.handlers.decoders.MobileMessageDecoder;\nimport cc.blynk.server.core.protocol.model.messages.BinaryMessage;\nimport cc.blynk.server.core.protocol.model.messages.MessageBase;\nimport cc.blynk.server.core.protocol.model.messages.ResponseMessage;\nimport io.netty.buffer.ByteBuf;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.handler.codec.ByteToMessageDecoder;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.nio.charset.StandardCharsets;\nimport java.util.List;\n\nimport static cc.blynk.server.core.protocol.enums.Command.GET_ENHANCED_GRAPH_DATA;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_PROJECT_BY_CLONE_CODE;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_PROJECT_BY_TOKEN;\nimport static cc.blynk.server.core.protocol.enums.Command.LOAD_PROFILE_GZIPPED;\nimport static cc.blynk.server.core.protocol.handlers.DefaultExceptionHandler.handleGeneralException;\nimport static cc.blynk.server.core.protocol.model.messages.MessageFactory.produce;\n\n/**\n * Decodes input byte array into java message.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n */\npublic class AppClientMessageDecoder extends ByteToMessageDecoder {\n\n    protected static final Logger log = LogManager.getLogger(AppClientMessageDecoder.class);\n\n    @Override\n    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {\n        if (in.readableBytes() < MobileMessageDecoder.PROTOCOL_APP_HEADER_SIZE) {\n            return;\n        }\n\n        in.markReaderIndex();\n\n        short command = in.readUnsignedByte();\n        int messageId = in.readUnsignedShort();\n\n        MessageBase message;\n        if (command == Command.RESPONSE) {\n            int responseCode = (int) in.readUnsignedInt();\n            message = new ResponseMessage(messageId, responseCode);\n        } else {\n            int length = (int) in.readUnsignedInt();\n\n            if (in.readableBytes() < length) {\n                in.resetReaderIndex();\n                return;\n            }\n\n            ByteBuf buf = in.readSlice(length);\n            switch (command) {\n                case GET_ENHANCED_GRAPH_DATA :\n                case GET_PROJECT_BY_CLONE_CODE :\n                case LOAD_PROFILE_GZIPPED :\n                case GET_PROJECT_BY_TOKEN :\n                    byte[] bytes = new byte[buf.readableBytes()];\n                    buf.readBytes(bytes);\n                    message = new BinaryMessage(messageId, command, bytes);\n                    break;\n                default:\n                    message = produce(messageId, command, buf.toString(StandardCharsets.UTF_8));\n            }\n\n        }\n\n        log.trace(\"Incoming client {}\", message);\n\n        out.add(message);\n    }\n\n    @Override\n    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {\n        handleGeneralException(ctx, cause);\n    }\n}\n"
  },
  {
    "path": "client/src/main/java/cc/blynk/client/handlers/decoders/ClientMessageDecoder.java",
    "content": "package cc.blynk.client.handlers.decoders;\n\nimport cc.blynk.server.core.protocol.enums.Command;\nimport cc.blynk.server.core.protocol.handlers.DefaultExceptionHandler;\nimport cc.blynk.server.core.protocol.model.messages.BinaryMessage;\nimport cc.blynk.server.core.protocol.model.messages.MessageBase;\nimport cc.blynk.server.core.protocol.model.messages.ResponseMessage;\nimport io.netty.buffer.ByteBuf;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.handler.codec.ByteToMessageDecoder;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.nio.charset.StandardCharsets;\nimport java.util.List;\n\nimport static cc.blynk.server.core.protocol.enums.Command.GET_ENHANCED_GRAPH_DATA;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_PROJECT_BY_CLONE_CODE;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_PROJECT_BY_TOKEN;\nimport static cc.blynk.server.core.protocol.enums.Command.LOAD_PROFILE_GZIPPED;\nimport static cc.blynk.server.core.protocol.model.messages.MessageFactory.produce;\n\n/**\n * Decodes input byte array into java message.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n */\npublic class ClientMessageDecoder extends ByteToMessageDecoder {\n\n    protected static final Logger log = LogManager.getLogger(ClientMessageDecoder.class);\n\n    @Override\n    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {\n        if (in.readableBytes() < 5) {\n            return;\n        }\n\n        in.markReaderIndex();\n\n        short command = in.readUnsignedByte();\n        int messageId = in.readUnsignedShort();\n\n        MessageBase message;\n        if (command == Command.RESPONSE) {\n            int responseCode = in.readUnsignedShort();\n            message = new ResponseMessage(messageId, responseCode);\n        } else {\n            int length = in.readUnsignedShort();\n\n            if (in.readableBytes() < length) {\n                in.resetReaderIndex();\n                return;\n            }\n\n            ByteBuf buf = in.readSlice(length);\n            switch (command) {\n                case GET_ENHANCED_GRAPH_DATA :\n                case GET_PROJECT_BY_CLONE_CODE :\n                case LOAD_PROFILE_GZIPPED :\n                case GET_PROJECT_BY_TOKEN :\n                    byte[] bytes = new byte[buf.readableBytes()];\n                    buf.readBytes(bytes);\n                    message = new BinaryMessage(messageId, command, bytes);\n                    break;\n                default:\n                    message = produce(messageId, command, buf.toString(StandardCharsets.UTF_8));\n            }\n\n        }\n\n        log.trace(\"Incoming client {}\", message);\n\n        out.add(message);\n    }\n\n    @Override\n    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {\n        DefaultExceptionHandler.handleGeneralException(ctx, cause);\n    }\n}\n"
  },
  {
    "path": "client/src/main/java/cc/blynk/client/handlers/hardware/HardwareEchoHandler.java",
    "content": "package cc.blynk.client.handlers.hardware;\n\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.protocol.enums.Command;\nimport cc.blynk.server.core.protocol.model.messages.MessageFactory;\nimport cc.blynk.server.core.protocol.model.messages.common.HardwareMessage;\nimport cc.blynk.utils.NumberUtil;\nimport io.netty.channel.ChannelHandler;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.SimpleChannelInboundHandler;\n\nimport java.util.concurrent.ThreadLocalRandom;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 29.07.15.\n */\n@ChannelHandler.Sharable\npublic class HardwareEchoHandler extends SimpleChannelInboundHandler<HardwareMessage> {\n\n    @Override\n    protected void channelRead0(ChannelHandlerContext ctx, HardwareMessage msg) {\n        if (msg.body.charAt(1) == 'm') {\n            return;\n        }\n\n        String[] split = msg.body.split(\"\\0\");\n\n        PinType pinType = PinType.getPinType(split[0].charAt(0));\n\n        short pin = NumberUtil.parsePin(split[1]);\n        //String value = split[2];\n\n        switch (msg.body.charAt(1)) {\n            case 'r' :\n                read(ctx, pinType, pin, msg.id);\n                break;\n            case 'w' :\n                //write();\n                break;\n            default:\n                break;\n        }\n    }\n\n    private void read(ChannelHandlerContext ctx, PinType pinType, short pin, int msgId) {\n        String value = \"\";\n        if (pinType == PinType.VIRTUAL) {\n            if (pin == 0) {\n                value = String.valueOf(ThreadLocalRandom.current().nextDouble(100));\n            }\n            if (pin == 1) {\n                value = String.valueOf(ThreadLocalRandom.current().nextInt(-100, 0));\n            }\n            if (pin == 2) {\n                value = String.valueOf(ThreadLocalRandom.current().nextInt(100));\n            }\n            if (pin == 3) {\n                value = String.valueOf(ThreadLocalRandom.current().nextDouble(-100, 100));\n            }\n            if (pin == 11) {\n                ctx.writeAndFlush(MessageFactory.produce(msgId,\n                        Command.PUSH_NOTIFICATION, \"You pressed button on V11\"));\n            }\n            if (pin == 12) {\n                value = String.valueOf(\"1234567890\" + ThreadLocalRandom.current().nextDouble(100));\n            }\n            if (pin == 13) {\n                value = String.valueOf(\"123456789012345678901234567890\"\n                        + ThreadLocalRandom.current().nextDouble(100));\n            }\n\n        }\n\n        if (!\"\".equals(value)) {\n            ctx.writeAndFlush(MessageFactory.produce(msgId, Command.HARDWARE,\n                    String.valueOf(pinType.pintTypeChar) + 'w' + '\\0' + pin + '\\0' + value));\n        }\n    }\n\n}\n"
  },
  {
    "path": "client/src/main/resources/log4j2.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n    <appenders>\n        <Console name=\"Console\" target=\"SYSTEM_OUT\">\n            <PatternLayout pattern=\"%d{HH:mm:ss.SSS} %-5level - %msg%n\"/>\n        </Console>\n    </appenders>\n    <loggers>\n        <Logger name=\"io.netty\" level=\"OFF\" additivity=\"false\">\n            <appender-ref ref=\"Console\"/>\n        </Logger>\n        <root level=\"trace\">\n            <appender-ref ref=\"Console\"/>\n        </root>\n    </loggers>\n</configuration>"
  },
  {
    "path": "client/src/main/resources/test.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDfjCCAmYCCQD0IGVOPMSODzANBgkqhkiG9w0BAQUFADCBgDELMAkGA1UEBhMC\nVUExCjAIBgNVBAgMAVwxDTALBgNVBAcMBEt5aXYxDjAMBgNVBAoMBUJseW5rMQww\nCgYDVQQLDANXZWIxFzAVBgNVBAMMDmNsb3VkLmJseW5rLmNjMR8wHQYJKoZIhvcN\nAQkBFhBkbWl0cml5QGJseW5rLmNjMB4XDTE1MDIxNTIxMjg1N1oXDTE2MDIxNTIx\nMjg1N1owgYAxCzAJBgNVBAYTAlVBMQowCAYDVQQIDAFcMQ0wCwYDVQQHDARLeWl2\nMQ4wDAYDVQQKDAVCbHluazEMMAoGA1UECwwDV2ViMRcwFQYDVQQDDA5jbG91ZC5i\nbHluay5jYzEfMB0GCSqGSIb3DQEJARYQZG1pdHJpeUBibHluay5jYzCCASIwDQYJ\nKoZIhvcNAQEBBQADggEPADCCAQoCggEBALKSUC1+SIHV7zTKvh6xEZCwqu8hgWZN\nb8vnXQaXqmShVbBosTWp9ToVWUtOJPEgUxYh5WKyI/u8VVVmj5Ka7jaHOzkFhE2T\nusmWONr3UaQ7SMeskNViECapK5hLYnSriXM2Fjcz0PjFxx17uf18EpOPdsoIuZT2\nJcUtsD+kJQDK7iYkcHTqVaOVl9jfb+WDYaPBvDvQ2MY6buksv2anM6IikxQAoED/\n0D/mRNPft4QXmy78/z/f0r0QQsLeYqHN5xyhazzYlz+jzWD5vM4SjUOdFe5mYcFN\nGB85VZ60IRZaSLRw/YsR2edKCZftDSmgbupTMt6P/csNsscvjJUP1tcCAwEAATAN\nBgkqhkiG9w0BAQUFAAOCAQEAHWo2HJ8R3DxYlehxE+SWUb2ra6Y/4hhAAYzAat2o\nvVQzZmkmXF6rYYHUkM96JxRRFNvS6q8wfSyiXktpZwXn4Cvn7TcuMq/OBLjVBv24\nbc6hDhLd8yhsPTkmaDvKGO2BPHVZmM/jUxcLQfZ+/UCQM9cwzULfKVQ2oi626+48\n6gRUupbEQWq1bL0kS80C0qPdjkweQBCZkotTIJKqSV1vgFDR3/V3qUKnSGq8kyEM\ntGRgUGuObXHK+zVZE5hz7PNOIybiKmkfZa/pahVUQE7wvQ07NTOZFNAQYFELv4Cj\n9ClM64GH+E10ukvFKY8ikvxYwwVZD4WuneATusimkA0Kyg==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "client/src/test/java/cc/blynk/client/ClientTest.java",
    "content": "package cc.blynk.client;\n\nimport cc.blynk.client.core.AppClient;\nimport cc.blynk.client.core.HardwareClient;\nimport org.junit.Ignore;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport java.io.BufferedReader;\n\nimport static cc.blynk.client.ClientLauncher.DEFAULT_APPLICATION_PORT;\nimport static cc.blynk.client.ClientLauncher.DEFAULT_HARDWARE_PORT;\nimport static cc.blynk.client.ClientLauncher.DEFAULT_HOST;\nimport static org.mockito.Mockito.when;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 11.03.15.\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class ClientTest {\n\n    @Mock\n    public BufferedReader bufferedReader;\n\n    @Test\n    @Ignore\n    public void testQuitApp() throws Exception {\n        AppClient testAppClient = new AppClient(DEFAULT_HOST, DEFAULT_APPLICATION_PORT);\n        when(bufferedReader.readLine()).thenReturn(\"quit\");\n        testAppClient.start(bufferedReader);\n        //verify(testAppClient.responseMock, never()).channelRead(any(), any());\n    }\n\n    @Test\n    @Ignore\n    public void testQuitHard() throws Exception {\n        HardwareClient testHardClient = new HardwareClient(DEFAULT_HOST, DEFAULT_HARDWARE_PORT);\n        when(bufferedReader.readLine()).thenReturn(\"quit\");\n        testHardClient.start(bufferedReader);\n        //verify(testHardClient.responseMock, never()).channelRead(any(), any());\n    }\n\n}\n"
  },
  {
    "path": "docs/README_FOR_APP_DEVS.md",
    "content": "# Protocol messages\n\nEvery message consists of 2 parts.\n\n+ Header :\n    + Protocol command (1 byte);\n    + MessageId (2 bytes);\n    + Body message length (2 bytes);\n\n+ Body : string (could be up to 2^15 bytes).\n\nBlynk transfers binary messages with the following structure:\n\n| Command       | Message Id    | Length/Status   | Body     |\n|:-------------:|:-------------:|:---------------:|:--------:|\n| 1 byte        | 2 bytes       | 2 bytes         | Variable |\n\nSo, message is always \"1 byte + 2 bytes + 2 bytes + messageBody.length\".\n\n### Command field\nUnsigned byte.\nThis is 1 byte field responsible for storing command code from client, like login, ping, etc. Codes:\n\n    RESPONSE = 0;  \n    REGISTER = 1;              \n    LOGIN = 2;                  \n    GET_TOKEN = 5; \n    PING = 6;      \n    ACTIVATE_DASHBOARD = 7;\n    DEACTIVATE_DASHBOARD = 8;\n    REFRESH_TOKEN = 9;\n    HARDWARE = 20;\n    \n    CREATE_DASH = 21;\n    SAVE_DASH = 22;\n    DELETE_DASH = 23;\n    \n    LOAD_PROFILE_GZIPPED = 24;\n\n    CREATE_WIDGET = 33;\n    UPDATE_WIDGET = 34;\n    DELETE_WIDGET = 35;\n\n[Full list](https://github.com/blynkkk/blynk-server/blob/master/server/core/src/main/java/cc/blynk/server/core/protocol/enums/Command.java#L11) \n\n### Message Id field\nUnsigned short.\nMessage Id field is a 2 bytes field for defining unique message identifier. It’s used in order to distinguish \nhow to manage responses from hardware on mobile client. Message ID field should be generated on client’s side.\n\n### Length field\nUnsigned short.\nLength field is a 2 bytes field for defining body length. Could be 0 if body is empty.\n\n## Response Codes\n\n    OK = 200;\n    QUOTA_LIMIT_EXCEPTION = 1;\n    ILLEGAL_COMMAND = 2;\n    USER_NOT_REGISTERED = 3;\n    USER_ALREADY_REGISTERED = 4;\n    USER_NOT_AUTHENTICATED = 5;\n    NOT_ALLOWED = 6;\n    DEVICE_NOT_IN_NETWORK = 7;\n    NO_ACTIVE_DASHBOARD = 8;\n    INVALID_TOKEN = 9;\n    ILLEGAL_COMMAND_BODY = 11;\n    GET_GRAPH_DATA_EXCEPTION = 12;\n    \n    NOTIFICATION_INVALID_BODY_EXCEPTION = 13;\n    NOTIFICATION_NOT_AUTHORIZED_EXCEPTION = 14;\n    NOTIFICATION_EXCEPTION = 15;\n    \n    //reserved\n     BLYNK_TIMEOUT_EXCEPTION = 16;\n     \n    NO_DATA_EXCEPTION = 17;\n    DEVICE_WENT_OFFLINE = 18;\n    SERVER_EXCEPTION = 19;\n    NOT_SUPPORTED_VERSION = 20;\n\nClient sends commands to the server and gets response for every command sent.\nFor commands (register, login, saveProfile, hardware) that doesn't request any data back - 'response' (command field 0x00) message is returned.\nFor commands (loadProfile, getToken) that request data back - message will be returned with same command code. In case you sent 'loadProfile' you will receive 'loadProfile' command back with filled body.\n\n[Here is the class with all of the codes](https://github.com/blynkkk/blynk-server/blob/master/server/core/src/main/java/cc/blynk/server/core/protocol/enums/Response.java#L12).\nResponse message structure:\n\n| Command       | Message Id    | Response code   |\n|:-------------:|:-------------:|:---------------:|\n| 1 byte        | 2 bytes       | 2 bytes         |\n\n\n## Widget types\n\n    //controls\n    BUTTON,\n    SLIDER,\n    VERTICAL_SLIDER,\n    KNOB,\n    TIMER,\n    ROTARY_KNOB,\n    RGB,\n    TWO_WAY_ARROW,\n    FOUR_WAY_ARROW,\n    ONE_AXIS_JOYSTICK,\n    TWO_AXIS_JOYSTICK,\n    GAMEPAD,\n    KEYPAD,\n\n    //outputs\n    LED,\n    LOGGER, //history_graph\n    DIGIT4_DISPLAY, //same as NUMERICAL_DISPLAY\n    GAUGE,\n    LCD_DISPLAY,\n    GRAPH,\n    LEVEL_DISPLAY,\n    TERMINAL,\n\n    //inputs\n    MICROPHONE,\n    GYROSCOPE,\n    ACCELEROMETER,\n    GPS,\n\n    //notifications\n    TWITTER,\n    EMAIL,\n    NOTIFICATION,\n\n    //other\n    SD_CARD,\n    EVENTOR,\n    RCT,\n    BRIDGE,\n    BLUETOOTH,\n\n    //UI\n    MENU\n\n\n[List is here](https://github.com/blynkkk/blynk-server/blob/master/server/core/src/main/java/cc/blynk/server/core/model/enums/WidgetType.java#L8).\n\n## JSON structure\n\n    {\n        \"dashBoards\" :\n            [\n                {\n                 \"id\":1,\n                 \"name\":\"My Dashboard\",\n                 \"isActive\" : true,\n                 \"isShared\" : true,\n                 \"widgets\"  : [\n                    {\"id\":1, \"x\":1, \"y\":1, \"label\":\"Some Text\", \"type\":\"BUTTON\",         \"pinType\":\"DIGITAL\", \"pin\":1, \"value\":\"1\"},\n                    {\"id\":2, \"x\":1, \"y\":1, \"label\":\"Some Text\", \"type\":\"SLIDER\",  \"pinType\":\"DIGITAL\", \"pin\":2, \"value\":\"1\", \"state\":\"ON\"},\n                    {\"id\":3, \"x\":1, \"y\":1, \"label\":\"Some Text\", \"type\":\"SLIDER\",  \"pinType\":\"ANALOG\", \"pin\":3, \"value\":\"0\", \"state\":\"OFF\"},\n                    {\"id\":4, \"x\":1, \"y\":1, \"label\":\"Some Text\", \"type\":\"SLIDER\",         \"pinType\":\"VIRTUAL\", \"pin\":4, \"value\":\"244\" },\n                    {\"id\":5, \"x\":1, \"y\":1, \"label\":\"Some Text\", \"type\":\"TIMER\",          \"pinType\":\"DIGITAL\", \"pin\":5, \"value\":\"1\", \"startTime\":0},\n                    {\"id\":6, \"x\":1, \"y\":1, \"label\":\"Some Text\", \"type\":\"LED\",            \"pinType\":\"ANALOG\", \"pin\":6, \"frequency\" : 100},\n                    {\"id\":7, \"x\":1, \"y\":1, \"label\":\"Some Text\", \"type\":\"DIGIT4_DISPLAY\", \"pinType\":\"ANALOG\", \"pin\":7, \"frequency\" : 1000},\n                    {\"id\":8, \"x\":1, \"y\":1, \"label\":\"Some Text\", \"type\":\"GRAPH\",          \"pinType\":\"ANALOG\", \"pin\":8},\n                    {\"id\":9, \"x\":1, \"y\":1, \"type\":\"NOTIFICATION\", \"notifyWhenOffline\":true, \"androidTokens\":{\"uid\":\"token\"}},\n                    {\"id\":10, \"x\":1, \"y\":1, \"token\":\"token\", \"secret\":\"secret\", \"type\":\"TWITTER\"},\n                    {\"id\":11, \"x\":1, \"y\":1, \"type\":\"RTC\", \"pinType\":\"VIRTUAL\", \"pin\":9},\n                    {\"id\":12, \"x\":0, \"y\":0, \"color\":-1, \"width\":8, \"height\":2,\n                        \"type\":\"LCD\",\n                        \"pins\": [\n                                    {\n                                        \"pin\":0,\n                                        \"pwmMode\":false,\n                                        \"rangeMappingOn\":false,\n                                        \"pinType\":\"VIRTUAL\",\n                                        \"value\":\"89.888037459418\",\n                                        \"min\":-100,\n                                        \"max\":100\n                                    },\n                                    {   \"pin\":1,\n                                        \"pwmMode\":false,\n                                        \"rangeMappingOn\":false,\n                                        \"pinType\":\"VIRTUAL\",\n                                        \"value\":\"-58.74774244674501\",\n                                        \"min\":-100,\n                                        \"max\":100\n                                    }\n                                ],\n                        \"advancedMode\":false,\n                        \"textFormatLine1\":\"pin1 : /pin0/\",\n                        \"textFormatLine2\":\"pin2 : /pin1/\",\n                        \"textLight\":false,\n                        \"frequency\":1000\n                    }\n                 ],\n                 \"boardType\":\"UNO\"\n                }\n            ]\n    }\n    \n## Hardware command description\n    \nCould be found [here](https://github.com/blynkkk/blynk-library/blob/master/docs/Implementing.md#hardwarebridge-command-body).\n    \n## Command workflow\n\nFor better understanding of how commands should be processed, please have a look in integration test \nit is easy understand what is going on there. You may start from [this](https://github.com/blynkkk/blynk-server/blob/master/integration-tests/src/test/java/cc/blynk/integration/tcp/MainWorkflowTest.java#L78) test."
  },
  {
    "path": "docs/websocket/Command.js",
    "content": "var MsgType = {\n    RESPONSE      :  0,\n    LOGIN         :  2,\n    PING          :  6,\n    TWEET         :  12,\n    EMAIL         :  13,\n    NOTIFY        :  14,\n    BRIDGE        :  15,\n    HW_SYNC       :  16,\n    HW_INFO       :  17,\n    HARDWARE      :  20\n};\n\nvar MsgStatus = {\n    OK                    :  200,\n    ILLEGAL_COMMAND       :  2,\n    NO_ACTIVE_DASHBOARD   :  8,\n    INVALID_TOKEN         :  9,\n    ILLEGAL_COMMAND_BODY  : 11\n};\n\nfunction getCommandByString(cmdString) {\n    switch (cmdString) {\n        case \"ping\" :\n            return MsgType.PING;\n        case \"login\" :\n            return MsgType.LOGIN;\n        case \"hardware\" :\n            return MsgType.HARDWARE;\n    }\n}\n\nfunction getStringByCommandCode(cmd) {\n    switch (cmd) {\n        case 0 :\n            return \"RESPONSE\";\n        case 20 :\n            return \"HARDWARE\";\n    }\n}\n\nfunction getStatusByCode(statusCode) {\n    switch (statusCode) {\n        case 200 :\n            return \"OK\";\n        case 2 :\n            return \"ILLEGAL_COMMAND\";\n        case 8 :\n            return \"NO_ACTIVE_DASHBOARD\";\n        case 9 :\n            return \"INVALID_TOKEN\";\n        case 11 :\n            return \"ILLEGAL_COMMAND_BODY\";\n    }\n}"
  },
  {
    "path": "docs/websocket/README.MD",
    "content": "Usage :\n\n    login AUTH_TOKEN\n    hardware vw 1 1\n    ping\n    \nHave in mind socket will be closed in 15 seconds if no activity. You can change this via ```hard.socket.idle.timeout```.\n"
  },
  {
    "path": "docs/websocket/websocket.js.html",
    "content": "<html>\n<head>\n    <title>Web Socket Performance Test</title>\n</head>\n<body>\n\n<label>Connection Status:</label>\n<label id=\"connectionLabel\"></label><br />\n\n<form onsubmit=\"return false;\">\n    Command : <input type=\"text\" id=\"command\" value=\"login 7038b715824d45269a3662c9648ffb9f\" style=\"width:500px;\"/><br>\n    <input type=\"button\" value=\"Send\"\n           onclick=\"send(command.value)\" />\n\n    <h3>Output</h3>\n    <textarea id=\"output\" style=\"width:500px;height:300px;\"></textarea>\n    <br>\n    <input type=\"button\" value=\"Clear\" onclick=\"clearText()\">\n</form>\n\n<script src=\"Command.js\"></script>\n\n<script type=\"text/javascript\">\n    const PING_INTERVAL_MILLIS = 5000;\n    var isRunning = false;\n\n    var command = document.getElementById('command');\n    var output = document.getElementById('output');\n    var connectionLabel = document.getElementById('connectionLabel');\n\n    if (!window.WebSocket) {\n        window.WebSocket = window.MozWebSocket;\n    }\n\n    if (window.WebSocket) {\n        socket = new WebSocket(\"ws://127.0.0.1:8082/websocket\");\n        socket.binaryType = 'arraybuffer';\n        socket.onmessage = function(event) {\n            if (event.data instanceof ArrayBuffer) {\n                console.log(\">>>\", event.data);\n                output.value += \"Receive : \" +  messageToDebugString(event.data) + \"\\r\\n\";\n            } else {\n                output.value = \"unexpected type : \" + event.data + \"\\r\\n\";\n            }\n        };\n        socket.onopen = function(event) {\n            isRunning = true;\n            connectionLabel.innerHTML = \"Connected\";\n        };\n        socket.onclose = function(event) {\n            isRunning = false;\n            connectionLabel.innerHTML = \"Disconnected\";\n        };\n        socket.onerror = function(event) {\n            connectionLabel.innerHTML = \"Error\";\n        };\n    } else {\n        alert(\"Your browser does not support Web Socket.\");\n    }\n\n    window.setInterval(function() {\n        send(\"ping\");\n    }, PING_INTERVAL_MILLIS);\n\n    function messageToDebugString(bufArray) {\n        var dataview = new DataView(bufArray);\n        var cmdString = getStringByCommandCode(dataview.getInt8(0));\n        var msgId = dataview.getUint16(1);\n        var responseCode = getStatusByCode(dataview.getUint16(3));\n\n        return \"Command : \" + cmdString + \", msgId : \" + msgId + \", responseCode : \" + responseCode;\n    }\n\n    function send(data) {\n        if (!window.WebSocket || !isRunning) {\n            return;\n        }\n\n        if (socket.readyState == WebSocket.OPEN) {\n            var commandAndBody = data.split(\" \");\n            var message = createMessage(commandAndBody);\n            output.value += 'sending : ' + data + '\\r\\n';\n            socket.send(message);\n        } else {\n            alert(\"The socket is not open.\");\n        }\n    }\n\n    function createMessage(commandAndBody) {\n        var cmdString = commandAndBody[0];\n        var cmdBody = commandAndBody.length > 1 ? commandAndBody.slice(1).join('\\0') : null;\n        var cmd = getCommandByString(cmdString);\n        return buildBlynkMessage(cmd, 1, cmdBody);\n    }\n\n    function buildBlynkMessage(cmd, msgId, cmdBody) {\n        const BLYNK_HEADER_SIZE = 5;\n        var bodyLength = (cmdBody ? cmdBody.length : 0);\n\n        var bufArray = new ArrayBuffer(BLYNK_HEADER_SIZE + bodyLength);\n        var dataview = new DataView(bufArray);\n        dataview.setInt8(0, cmd);\n        dataview.setInt16(1, msgId);\n        dataview.setInt16(3, bodyLength);\n\n        if (bodyLength > 0) {\n            //todo optimize. should be better way\n            var buf = new ArrayBuffer(bodyLength); // 2 bytes for each char\n            var bufView = new Uint8Array(buf);\n            for (var i = 0, offset =  5; i < cmdBody.length; i++, offset += 1) {\n                dataview.setInt8(offset, cmdBody.charCodeAt(i));\n            }\n        }\n\n        return new Uint8Array(bufArray);\n    }\n\n    function clearText() {\n        output.value=\"\";\n    }\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "integration-tests/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <artifactId>blynk</artifactId>\n        <groupId>cc.blynk</groupId>\n        <version>0.41.17-SNAPSHOT</version>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n\n    <artifactId>integration-tests</artifactId>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-surefire-plugin</artifactId>\n                <version>${maven-surefire-plugin.version}</version>\n                <configuration>\n                    <argLine>-Dio.netty.leakDetection.level=paranoid</argLine>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n\n    <repositories>\n        <repository>\n            <id>jitpack.io</id>\n            <url>https://jitpack.io</url>\n        </repository>\n    </repositories>\n\n    <dependencies>\n\n        <dependency>\n            <groupId>cc.blynk.server</groupId>\n            <artifactId>core</artifactId>\n            <version>${project.version}</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>cc.blynk.server.application</groupId>\n            <artifactId>tcp-app-server</artifactId>\n            <version>${project.version}</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>cc.blynk.server.admin</groupId>\n            <artifactId>http-admin</artifactId>\n            <version>${project.version}</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>cc.blynk.server.api</groupId>\n            <artifactId>http-api</artifactId>\n            <version>${project.version}</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>cc.blynk.server</groupId>\n            <artifactId>launcher</artifactId>\n            <version>${project.version}</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>cc.blynk</groupId>\n            <artifactId>client</artifactId>\n            <version>${project.version}</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.asynchttpclient</groupId>\n            <artifactId>async-http-client</artifactId>\n            <version>${async-http-client.version}</version>\n            <exclusions>\n                <exclusion>\n                    <groupId>io.netty</groupId>\n                    <artifactId>*</artifactId>\n                </exclusion>\n            </exclusions>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-lang3</artifactId>\n            <version>${commons-lang3.version}</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.github.kenglxn.qrgen</groupId>\n            <artifactId>javase</artifactId>\n            <version>${qrgen.version}</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.httpcomponents</groupId>\n            <artifactId>httpclient</artifactId>\n            <version>${httpclient.version}</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.httpcomponents</groupId>\n            <artifactId>httpmime</artifactId>\n            <version>${httpclient.version}</version>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n</project>"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/BaseTest.java",
    "content": "package cc.blynk.integration;\n\nimport cc.blynk.integration.model.tcp.ClientPair;\nimport cc.blynk.server.Holder;\nimport cc.blynk.utils.properties.ServerProperties;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.BeforeClass;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.net.URI;\nimport java.net.URL;\nimport java.nio.file.Paths;\nimport java.util.Collections;\nimport java.util.zip.InflaterInputStream;\n\nimport static cc.blynk.integration.TestUtil.createDefaultHolder;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 20.01.16.\n */\npublic abstract class BaseTest extends CounterBase {\n\n    public static ServerProperties properties;\n\n    //tcp app/hardware ports\n    public static int tcpHardPort;\n\n    public Holder holder;\n\n    @Before\n    public void initHolderAndDataFolder() {\n        properties.setProperty(\"data.folder\", getDataFolder());\n        this.holder = createDefaultHolder(properties, \"no-db.properties\");\n    }\n\n    @BeforeClass\n    public static void initProps() {\n        properties = new ServerProperties(Collections.emptyMap());\n        tcpHardPort = properties.getHttpPort();\n    }\n\n    @After\n    public void closeTransport() {\n        holder.close();\n    }\n\n    public static String getRelativeDataFolder(String path) {\n        URL resource = BaseTest.class.getResource(path);\n        URI uri = null;\n        try {\n            uri = resource.toURI();\n        } catch (Exception e) {\n            //ignoring. that's fine.\n        }\n        String resourcesPath = Paths.get(uri).toAbsolutePath().toString();\n        System.out.println(\"Resource path : \" + resourcesPath);\n        return resourcesPath;\n    }\n\n    public static ClientPair initAppAndHardPair() throws Exception {\n        return TestUtil.initAppAndHardPair(\"localhost\", properties.getHttpsPort(), tcpHardPort, getUserName(), \"1\", \"user_profile_json.txt\", properties, 10000);\n    }\n\n    //for tests only\n    public static byte[] decompress(byte[] bytes) {\n        try (InputStream in = new InflaterInputStream(new ByteArrayInputStream(bytes))) {\n            ByteArrayOutputStream baos = new ByteArrayOutputStream();\n            byte[] buffer = new byte[4096];\n            int len;\n            while ((len = in.read(buffer)) > 0) {\n                baos.write(buffer, 0, len);\n            }\n            return baos.toByteArray();\n        } catch (IOException e) {\n            throw new AssertionError(e);\n        }\n    }\n\n    public static ClientPair initAppAndHardPair(String jsonProfile) throws Exception {\n        return TestUtil.initAppAndHardPair(\"localhost\", properties.getHttpsPort(), tcpHardPort, getUserName(), \"1\", jsonProfile, properties, 10000);\n    }\n\n    public static ClientPair initAppAndHardPair(int tcpAppPort, int tcpHartPort, ServerProperties properties) throws Exception {\n        return TestUtil.initAppAndHardPair(\"localhost\", tcpAppPort, tcpHartPort, getUserName(), \"1\", \"user_profile_json.txt\", properties, 10000);\n    }\n\n    public static ClientPair initAppAndHardPair(ServerProperties properties) throws Exception {\n        return TestUtil.initAppAndHardPair(\"localhost\", properties.getHttpsPort(), properties.getHttpPort(), getUserName(), \"1\", \"user_profile_json.txt\", properties, 10000);\n    }\n\n}\n\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/CounterBase.java",
    "content": "package cc.blynk.integration;\n\nimport org.bouncycastle.jce.provider.BouncyCastleProvider;\nimport org.junit.Before;\n\nimport java.security.Security;\nimport java.util.concurrent.atomic.AtomicLong;\n\npublic abstract class CounterBase {\n\n    private static final String DEFAULT_TEST_USER = \"dima@mail.ua\";\n    private static final AtomicLong userCounter = new AtomicLong();\n\n    static {\n        Security.addProvider(new BouncyCastleProvider());\n    }\n\n    //generates unique name of a user, so every test is independent from others\n    //name is unique only within the test\n    public static String getUserName() {\n        return userCounter.get() + DEFAULT_TEST_USER;\n    }\n\n    protected static String incrementAndGetUserName() {\n        return userCounter.incrementAndGet() + DEFAULT_TEST_USER;\n    }\n\n    @Before\n    public void incrementCounter() {\n        userCounter.incrementAndGet();\n    }\n\n    public String getDataFolder() {\n        return TestUtil.getDataFolder();\n    }\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/MyHostVerifier.java",
    "content": "package cc.blynk.integration;\n\nimport javax.net.ssl.HostnameVerifier;\nimport javax.net.ssl.SSLSession;\n\npublic class MyHostVerifier implements HostnameVerifier {\n\n    @Override\n    public boolean verify(String s, SSLSession sslSession) {\n        return true;\n    }\n\n}\n\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/SingleServerInstancePerTest.java",
    "content": "package cc.blynk.integration;\n\nimport cc.blynk.integration.model.tcp.ClientPair;\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.servers.BaseServer;\nimport cc.blynk.server.servers.application.MobileAndHttpsServer;\nimport cc.blynk.server.servers.hardware.HardwareAndHttpAPIServer;\nimport cc.blynk.utils.properties.ServerProperties;\nimport org.junit.After;\nimport org.junit.AfterClass;\nimport org.junit.Before;\nimport org.junit.BeforeClass;\n\nimport java.util.Collections;\n\nimport static cc.blynk.integration.TestUtil.createDefaultHolder;\nimport static org.mockito.Mockito.reset;\n\n/**\n * use when you need only 1 server instance per test class and not per test method\n */\npublic abstract class SingleServerInstancePerTest extends CounterBase {\n\n    protected static ServerProperties properties;\n    protected static BaseServer appServer;\n    protected static BaseServer hardwareServer;\n    protected static Holder holder;\n    protected ClientPair clientPair;\n\n    @BeforeClass\n    public static void init() throws Exception {\n        properties = new ServerProperties(Collections.emptyMap());\n        properties.setProperty(\"data.folder\", TestUtil.getDataFolder());\n        holder = createDefaultHolder(properties, \"no-db.properties\");\n        hardwareServer = new HardwareAndHttpAPIServer(holder).start();\n        appServer = new MobileAndHttpsServer(holder).start();\n    }\n\n    @AfterClass\n    public static void shutdown() {\n        appServer.close();\n        hardwareServer.close();\n        holder.close();\n    }\n\n    @After\n    public void closeClients() {\n        this.clientPair.stop();\n    }\n\n    @Before\n    public void resetBeforeTest() throws Exception {\n        this.clientPair = initClientPair();\n        reset(holder.mailWrapper);\n        reset(holder.twitterWrapper);\n        reset(holder.gcmWrapper);\n        reset(holder.smsWrapper);\n    }\n\n    public ClientPair initAppAndHardPair() throws Exception {\n        return TestUtil.initAppAndHardPair(\"localhost\",\n                properties.getHttpsPort(), properties.getHttpPort(),\n                getUserName(), \"1\", changeProfileTo(), properties, 10000);\n    }\n\n    protected String changeProfileTo() {\n        return \"user_profile_json.txt\";\n    }\n\n    protected ClientPair initClientPair() throws Exception {\n        return initAppAndHardPair();\n    }\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/SingleServerInstancePerTestWithDB.java",
    "content": "package cc.blynk.integration;\n\nimport cc.blynk.integration.model.tcp.ClientPair;\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.servers.BaseServer;\nimport cc.blynk.server.servers.application.MobileAndHttpsServer;\nimport cc.blynk.server.servers.hardware.HardwareAndHttpAPIServer;\nimport cc.blynk.utils.properties.ServerProperties;\nimport org.junit.After;\nimport org.junit.AfterClass;\nimport org.junit.Before;\nimport org.junit.BeforeClass;\n\nimport java.util.Collections;\n\nimport static cc.blynk.integration.TestUtil.createDefaultHolder;\nimport static org.junit.Assert.assertNotNull;\nimport static org.mockito.Mockito.reset;\n\n/**\n * use when you need only 1 server instance per test class and not per test method\n */\npublic abstract class SingleServerInstancePerTestWithDB extends CounterBase {\n\n    protected static ServerProperties properties;\n    protected static BaseServer appServer;\n    protected static BaseServer hardwareServer;\n    protected static Holder holder;\n    protected ClientPair clientPair;\n\n    @BeforeClass\n    public static void init() throws Exception {\n        properties = new ServerProperties(Collections.emptyMap());\n        properties.setProperty(\"data.folder\", TestUtil.getDataFolder());\n        holder = createDefaultHolder(properties, \"db-test.properties\");\n        hardwareServer = new HardwareAndHttpAPIServer(holder).start();\n        appServer = new MobileAndHttpsServer(holder).start();\n        assertNotNull(holder.dbManager.getConnection());\n    }\n\n    @AfterClass\n    public static void shutdown() {\n        appServer.close();\n        hardwareServer.close();\n        holder.close();\n    }\n\n    @After\n    public void closeClients() {\n        this.clientPair.stop();\n    }\n\n    @Before\n    public void resetBeforeTest() throws Exception {\n        this.clientPair = initClientPair();\n        reset(holder.mailWrapper);\n        reset(holder.twitterWrapper);\n        reset(holder.gcmWrapper);\n        reset(holder.smsWrapper);\n    }\n\n    public ClientPair initAppAndHardPair() throws Exception {\n        return TestUtil.initAppAndHardPair(\"localhost\",\n                properties.getHttpsPort(), properties.getHttpPort(),\n                getUserName(), \"1\", changeProfileTo(), properties, 10000);\n    }\n\n    protected String changeProfileTo() {\n        return \"user_profile_json.txt\";\n    }\n\n    protected ClientPair initClientPair() throws Exception {\n        return initAppAndHardPair();\n    }\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/TestUtil.java",
    "content": "package cc.blynk.integration;\n\nimport cc.blynk.integration.model.SimpleClientHandler;\nimport cc.blynk.integration.model.tcp.ClientPair;\nimport cc.blynk.integration.model.tcp.TestAppClient;\nimport cc.blynk.integration.model.tcp.TestHardClient;\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.core.BlockingIOProcessor;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.DataStream;\nimport cc.blynk.server.core.model.Profile;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.device.Tag;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.model.widgets.MobileSyncWidget;\nimport cc.blynk.server.core.model.widgets.MultiPinWidget;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.protocol.model.messages.MessageBase;\nimport cc.blynk.server.core.protocol.model.messages.ResponseMessage;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.server.core.protocol.model.messages.appllication.GetServerMessage;\nimport cc.blynk.server.core.protocol.model.messages.common.HardwareMessage;\nimport cc.blynk.server.notifications.mail.MailWrapper;\nimport cc.blynk.server.notifications.push.GCMWrapper;\nimport cc.blynk.server.notifications.sms.SMSWrapper;\nimport cc.blynk.server.notifications.twitter.TwitterWrapper;\nimport cc.blynk.utils.AppNameUtil;\nimport cc.blynk.utils.StringUtils;\nimport cc.blynk.utils.properties.ServerProperties;\nimport com.fasterxml.jackson.databind.ObjectReader;\nimport org.apache.http.client.methods.CloseableHttpResponse;\nimport org.apache.http.util.EntityUtils;\nimport org.mockito.ArgumentCaptor;\n\nimport javax.net.ssl.SSLContext;\nimport javax.net.ssl.TrustManager;\nimport javax.net.ssl.X509TrustManager;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.file.Files;\nimport java.nio.file.attribute.FileAttribute;\nimport java.security.KeyManagementException;\nimport java.security.NoSuchAlgorithmException;\nimport java.util.List;\nimport java.util.concurrent.ThreadLocalRandom;\n\nimport static cc.blynk.server.core.protocol.enums.Command.APP_SYNC;\nimport static cc.blynk.server.core.protocol.enums.Command.BLYNK_INTERNAL;\nimport static cc.blynk.server.core.protocol.enums.Command.BRIDGE;\nimport static cc.blynk.server.core.protocol.enums.Command.CONNECT_REDIRECT;\nimport static cc.blynk.server.core.protocol.enums.Command.CREATE_DEVICE;\nimport static cc.blynk.server.core.protocol.enums.Command.CREATE_TAG;\nimport static cc.blynk.server.core.protocol.enums.Command.DEVICE_OFFLINE;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_PROJECT_BY_CLONE_CODE;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_PROJECT_BY_TOKEN;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_PROVISION_TOKEN;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE_CONNECTED;\nimport static cc.blynk.server.core.protocol.enums.Command.LOAD_PROFILE_GZIPPED;\nimport static cc.blynk.server.core.protocol.enums.Command.OUTDATED_APP_NOTIFICATION;\nimport static cc.blynk.server.core.protocol.enums.Command.SET_WIDGET_PROPERTY;\nimport static cc.blynk.server.core.protocol.enums.Response.ILLEGAL_COMMAND;\nimport static cc.blynk.server.core.protocol.enums.Response.ILLEGAL_COMMAND_BODY;\nimport static cc.blynk.server.core.protocol.enums.Response.INVALID_TOKEN;\nimport static cc.blynk.server.core.protocol.enums.Response.NOT_ALLOWED;\nimport static cc.blynk.server.core.protocol.enums.Response.OK;\nimport static cc.blynk.server.core.protocol.enums.Response.SERVER_ERROR;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.timeout;\nimport static org.mockito.Mockito.verify;\n\npublic final class TestUtil {\n\n    private static final ObjectReader profileReader = JsonParser.init().readerFor(Profile.class);\n\n    private TestUtil() {\n    }\n\n    public static String getBody(SimpleClientHandler responseMock) throws Exception {\n        return getBody(responseMock, 1);\n    }\n\n    public static String getBody(SimpleClientHandler responseMock, int expectedMessageOrder) throws Exception {\n        ArgumentCaptor<MessageBase> objectArgumentCaptor = ArgumentCaptor.forClass(MessageBase.class);\n        verify(responseMock, timeout(1000).times(expectedMessageOrder)).channelRead(any(), objectArgumentCaptor.capture());\n        List<MessageBase> arguments = objectArgumentCaptor.getAllValues();\n        MessageBase messageBase = arguments.get(expectedMessageOrder - 1);\n        if (messageBase instanceof StringMessage) {\n            return ((StringMessage) messageBase).body;\n        } else if (messageBase.command == LOAD_PROFILE_GZIPPED\n                || messageBase.command == GET_PROJECT_BY_TOKEN\n                || messageBase.command == GET_PROVISION_TOKEN\n                || messageBase.command == GET_PROJECT_BY_CLONE_CODE) {\n            return new String(BaseTest.decompress(messageBase.getBytes()));\n        }\n\n        throw new RuntimeException(\"Unexpected message\");\n    }\n\n    public static Profile parseProfile(InputStream reader) throws Exception {\n        return profileReader.readValue(reader);\n    }\n\n    public static Profile parseProfile(String reader) throws Exception {\n        return profileReader.readValue(reader);\n    }\n\n\n    public static String readTestUserProfile(String fileName) throws Exception{\n        InputStream is = TestUtil.class.getResourceAsStream(\"/json_test/\" + fileName);\n        Profile profile = parseProfile(is);\n        return profile.toString();\n    }\n\n    public static String readTestUserProfile() throws Exception {\n        return readTestUserProfile(\"user_profile_json.txt\");\n    }\n\n    public static void saveProfile(TestAppClient appClient, DashBoard... dashBoards) {\n        for (DashBoard dash : dashBoards) {\n            appClient.createDash(dash);\n        }\n    }\n\n    public static String b(String body) {\n        return body.replaceAll(\" \", StringUtils.BODY_SEPARATOR_STRING);\n    }\n\n    public static ResponseMessage illegalCommand(int msgId) {\n        return new ResponseMessage(msgId, ILLEGAL_COMMAND);\n    }\n\n    public static ResponseMessage illegalCommandBody(int msgId) {\n        return new ResponseMessage(msgId, ILLEGAL_COMMAND_BODY);\n    }\n\n    public static ResponseMessage ok(int msgId) {\n        return new ResponseMessage(msgId, OK);\n    }\n\n    public static StringMessage bridge(int msgId, String body) {\n        return new StringMessage(msgId, BRIDGE, b(body));\n    }\n\n    public static StringMessage internal(int msgId, String body) {\n        return new StringMessage(msgId, BLYNK_INTERNAL, b(body));\n    }\n\n    public static StringMessage hardwareConnected(int msgId, String body) {\n        return new StringMessage(msgId, HARDWARE_CONNECTED, body);\n    }\n\n    public static GetServerMessage getServer(int msgId, String body) {\n        return new GetServerMessage(msgId, body);\n    }\n\n    public static StringMessage deviceOffline(int msgId, String body) {\n        return new StringMessage(msgId, DEVICE_OFFLINE, body);\n    }\n\n    public static StringMessage createTag(int msgId, Tag tag) {\n        return createTag(msgId, tag.toString());\n    }\n\n    public static StringMessage createTag(int msgId, String body) {\n        return new StringMessage(msgId, CREATE_TAG, body);\n    }\n\n    public static StringMessage appIsOutdated(int msgId, String body) {\n        return new StringMessage(msgId, OUTDATED_APP_NOTIFICATION, body);\n    }\n\n    public static StringMessage appSync(int msgId, String body) {\n        return new StringMessage(msgId, APP_SYNC, b(body));\n    }\n\n    public static StringMessage hardware(int msgId, String body) {\n        return new HardwareMessage(msgId, b(body));\n    }\n\n    public static StringMessage appSync(String body) {\n        return appSync(MobileSyncWidget.SYNC_DEFAULT_MESSAGE_ID, body);\n    }\n\n    public static StringMessage setProperty(int msgId, String body) {\n        return new StringMessage(msgId, SET_WIDGET_PROPERTY, b(body));\n    }\n\n    public static StringMessage createDevice(int msgId, String body) {\n        return new StringMessage(msgId, CREATE_DEVICE, body);\n    }\n\n    public static StringMessage createDevice(int msgId, Device device) {\n        return createDevice(msgId, device.toString());\n    }\n\n    public static StringMessage connectRedirect(int msgId, String body) {\n        return new StringMessage(msgId, CONNECT_REDIRECT, b(body));\n    }\n\n    public static ResponseMessage serverError(int msgId) {\n        return new ResponseMessage(msgId, SERVER_ERROR);\n    }\n\n    public static ResponseMessage notAllowed(int msgId) {\n        return new ResponseMessage(msgId, NOT_ALLOWED);\n    }\n\n    public static ResponseMessage invalidToken(int msgId) {\n        return new ResponseMessage(msgId, INVALID_TOKEN);\n    }\n\n    public static ClientPair initAppAndHardPair(String host, int appPort, int hardPort,\n                                                String user, String pass,\n                                                String jsonProfile,\n                                                ServerProperties properties, int energy) throws Exception {\n\n        TestAppClient appClient = new TestAppClient(host, appPort, properties);\n        TestHardClient hardClient = new TestHardClient(host, hardPort);\n\n        return initAppAndHardPair(appClient, hardClient, user, pass, jsonProfile, energy);\n    }\n\n    public static ClientPair initAppAndHardPair(TestAppClient appClient, TestHardClient hardClient,\n                                                String user, String pass,\n                                                String jsonProfile, int energy) throws Exception {\n\n        appClient.start();\n        hardClient.start();\n\n        String userProfileString = readTestUserProfile(jsonProfile);\n        Profile profile = parseProfile(userProfileString);\n\n        int expectedSyncCommandsCount = 0;\n        for (Widget widget : profile.dashBoards[0].widgets) {\n            if (widget instanceof OnePinWidget) {\n                if (((OnePinWidget) widget).makeHardwareBody() != null) {\n                    expectedSyncCommandsCount++;\n                }\n            } else if (widget instanceof MultiPinWidget) {\n                MultiPinWidget multiPinWidget = (MultiPinWidget) widget;\n                if (multiPinWidget.dataStreams != null) {\n                    if (multiPinWidget.isSplitMode()) {\n                        for (DataStream dataStream : multiPinWidget.dataStreams) {\n                            if (dataStream.notEmptyAndIsValid()) {\n                                expectedSyncCommandsCount++;\n                            }\n                        }\n                    } else {\n                        if (multiPinWidget.dataStreams[0].notEmptyAndIsValid()) {\n                            expectedSyncCommandsCount++;\n                        }\n                    }\n                }\n            }\n        }\n\n        int dashId = profile.dashBoards[0].id;\n\n        appClient.register(user, pass, AppNameUtil.BLYNK);\n        appClient.login(user, pass, \"Android\", \"1.10.4\");\n        int rand = ThreadLocalRandom.current().nextInt();\n        appClient.send(\"addEnergy \" + energy + \"\\0\" + String.valueOf(rand));\n        //we should wait until login finished. Only after that we can send commands\n        verify(appClient.responseMock, timeout(1000)).channelRead(any(), eq(ok(2)));\n\n        saveProfile(appClient, profile.dashBoards);\n\n        appClient.activate(dashId);\n\n        ArgumentCaptor<Object> objectArgumentCaptor = ArgumentCaptor.forClass(Object.class);\n        verify(appClient.responseMock, timeout(2000).times(4 + profile.dashBoards.length + expectedSyncCommandsCount)).channelRead(any(), objectArgumentCaptor.capture());\n\n        appClient.getDevice(dashId, 0);\n        Device device = appClient.parseDevice(5 + profile.dashBoards.length + expectedSyncCommandsCount);\n        String token = device.token;\n\n        hardClient.login(token);\n        verify(hardClient.responseMock, timeout(2000)).channelRead(any(), eq(ok(1)));\n        verify(appClient.responseMock, timeout(2000)).channelRead(any(), eq(hardwareConnected(1, \"\" + dashId + \"-0\")));\n\n        appClient.reset();\n        hardClient.reset();\n\n        return new ClientPair(appClient, hardClient, token);\n    }\n\n    public static String getDataFolder() {\n        try {\n            return Files.createTempDirectory(\"blynk_test_\", new FileAttribute[0]).toString();\n        } catch (IOException e) {\n            throw new RuntimeException(\"Unable create temp dir.\", e);\n        }\n    }\n\n    public static void sleep(int ms) {\n        try {\n            Thread.sleep(ms);\n        } catch (InterruptedException e) {\n            //we can ignore it\n        }\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static List<String> consumeJsonPinValues(String response) {\n        return JsonParser.readAny(response, List.class);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static List<String> consumeJsonPinValues(CloseableHttpResponse response) throws IOException {\n        return JsonParser.readAny(consumeText(response), List.class);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static String consumeText(CloseableHttpResponse response) throws IOException {\n        return EntityUtils.toString(response.getEntity());\n    }\n\n    public static SSLContext initUnsecuredSSLContext() throws NoSuchAlgorithmException, KeyManagementException {\n        X509TrustManager tm = new X509TrustManager() {\n            @Override\n            public void checkClientTrusted(java.security.cert.X509Certificate[] x509Certificates, String s) {\n\n            }\n\n            @Override\n            public void checkServerTrusted(java.security.cert.X509Certificate[] x509Certificates, String s) {\n\n            }\n\n            @Override\n            public java.security.cert.X509Certificate[] getAcceptedIssuers() {\n                return null;\n            }\n        };\n\n        SSLContext context = SSLContext.getInstance(\"TLS\");\n        context.init(null, new TrustManager[]{ tm }, null);\n\n        return context;\n    }\n\n    public static Holder createHolderWithIOMock(ServerProperties serverProperties,\n                                                String dbFileName) {\n        return new Holder(serverProperties,\n                mock(TwitterWrapper.class),\n                mock(MailWrapper.class),\n                mock(GCMWrapper.class),\n                mock(SMSWrapper.class),\n                mock(BlockingIOProcessor.class),\n                dbFileName);\n    }\n\n    public static Holder createDefaultHolder(ServerProperties serverProperties,\n                                             String dbFileName) {\n        return new Holder(serverProperties,\n                mock(TwitterWrapper.class),\n                mock(MailWrapper.class),\n                mock(GCMWrapper.class),\n                mock(SMSWrapper.class),\n                new BlockingIOProcessor(\n                        serverProperties.getIntProperty(\"blocking.processor.thread.pool.limit\", 1),\n                        serverProperties.getIntProperty(\"notifications.queue.limit\", 100)\n                ), dbFileName);\n    }\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/http/AcmeTest.java",
    "content": "package cc.blynk.integration.http;\n\nimport cc.blynk.integration.BaseTest;\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.SslContextHolder;\nimport cc.blynk.server.acme.AcmeClient;\nimport cc.blynk.server.acme.ContentHolder;\nimport cc.blynk.server.servers.BaseServer;\nimport cc.blynk.server.servers.hardware.HardwareAndHttpAPIServer;\nimport cc.blynk.server.workers.CertificateRenewalWorker;\nimport cc.blynk.utils.properties.ServerProperties;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Ignore;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.Mockito;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport java.util.Collections;\n\nimport static cc.blynk.integration.TestUtil.createDefaultHolder;\nimport static org.junit.Assert.assertNotNull;\nimport static org.junit.Assert.assertNull;\nimport static org.junit.Assert.assertTrue;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 07.01.16.\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class AcmeTest extends BaseTest {\n\n    private BaseServer httpServer;\n    private Holder holder2;\n\n    @After\n    public void shutdown() {\n        httpServer.close();\n    }\n\n    @Before\n    public void init() throws Exception {\n        ServerProperties properties2 = new ServerProperties(Collections.emptyMap(), \"no_certs.properties\");\n        this.holder2 = createDefaultHolder(properties2, \"no-db.properties\");\n        httpServer = new HardwareAndHttpAPIServer(holder2).start();\n    }\n\n    @Override\n    public String getDataFolder() {\n        return getRelativeDataFolder(\"/profiles\");\n    }\n\n    @Test\n    public void testCorrectContext() {\n        SslContextHolder sslContextHolder = holder2.sslContextHolder;\n        assertNotNull(sslContextHolder);\n        assertTrue(sslContextHolder.runRenewalWorker());\n        assertTrue(sslContextHolder.isNeedInitializeOnStart);\n        assertNotNull(sslContextHolder.acmeClient);\n    }\n\n    @Test\n    @Ignore\n    public void testCreateCertificates() throws Exception {\n        final String STAGING = \"acme://letsencrypt.org/staging\";\n        ContentHolder contentHolder = holder2.sslContextHolder.contentHolder;\n        assertNull(contentHolder.content);\n        AcmeClient acmeClient = new AcmeClient(STAGING, \"test@blynk.cc\", \"let.blynk.cc\", contentHolder);\n        acmeClient.requestCertificate();\n        assertNotNull(contentHolder.content);\n    }\n\n    @Test\n    @Ignore\n    public void testWorker() throws Exception {\n        AcmeClient acmeClient = Mockito.mock(AcmeClient.class);\n        SslContextHolder sslContextHolder = Mockito.mock(SslContextHolder.class);\n        CertificateRenewalWorker certificateRenewalWorker = new CertificateRenewalWorker(sslContextHolder);\n        certificateRenewalWorker.run();\n        verify(acmeClient, times(0)).requestCertificate();\n\n        certificateRenewalWorker = new CertificateRenewalWorker(sslContextHolder);\n        certificateRenewalWorker.run();\n        verify(acmeClient, times(1)).requestCertificate();\n    }\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/http/HttpAPIKeepAliveServerTest.java",
    "content": "package cc.blynk.integration.http;\n\nimport cc.blynk.integration.BaseTest;\nimport cc.blynk.server.servers.BaseServer;\nimport cc.blynk.server.servers.hardware.HardwareAndHttpAPIServer;\nimport org.apache.http.client.methods.CloseableHttpResponse;\nimport org.apache.http.client.methods.HttpGet;\nimport org.apache.http.client.methods.HttpPut;\nimport org.apache.http.entity.ContentType;\nimport org.apache.http.entity.StringEntity;\nimport org.apache.http.impl.client.CloseableHttpClient;\nimport org.apache.http.impl.client.HttpClients;\nimport org.apache.http.util.EntityUtils;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport java.util.List;\n\nimport static cc.blynk.integration.TestUtil.consumeJsonPinValues;\nimport static cc.blynk.server.core.protocol.enums.Command.HTTP_GET_PIN_DATA;\nimport static cc.blynk.server.core.protocol.enums.Command.HTTP_UPDATE_PIN_DATA;\nimport static org.junit.Assert.assertEquals;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 07.01.16.\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class HttpAPIKeepAliveServerTest extends BaseTest {\n\n    private BaseServer httpServer;\n    private CloseableHttpClient httpclient;\n    private String httpServerUrl;\n\n    @After\n    public void shutdown() throws Exception {\n        httpclient.close();\n        httpServer.close();\n    }\n\n    @Before\n    public void init() throws Exception {\n        httpServer = new HardwareAndHttpAPIServer(holder).start();\n        httpServerUrl = String.format(\"http://localhost:%s/\", properties.getHttpPort());\n\n        //this http client doesn't close HTTP connection.\n        httpclient = HttpClients.custom()\n                .setConnectionReuseStrategy((response, context) -> true)\n                .setKeepAliveStrategy((response, context) -> 10000000).build();\n    }\n\n    @Override\n    public String getDataFolder() {\n        return getRelativeDataFolder(\"/profiles\");\n    }\n\n    @Test\n    public void testKeepAlive() throws Exception {\n        HttpPut request = new HttpPut(httpServerUrl + \"4ae3851817194e2596cf1b7103603ef8/update/a14\");\n        request.setHeader(\"Connection\", \"keep-alive\");\n\n        HttpGet getRequest = new HttpGet(httpServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/a14\");\n        getRequest.setHeader(\"Connection\", \"keep-alive\");\n\n        for (int i = 0; i < 100; i++) {\n            request.setEntity(new StringEntity(\"[\\\"\"+ i + \"\\\"]\", ContentType.APPLICATION_JSON));\n\n            try (CloseableHttpResponse response = httpclient.execute(request)) {\n                assertEquals(200, response.getStatusLine().getStatusCode());\n                assertEquals(\"keep-alive\", response.getFirstHeader(\"Connection\").getValue());\n                EntityUtils.consume(response.getEntity());\n            }\n\n            try (CloseableHttpResponse response2 = httpclient.execute(getRequest)) {\n                assertEquals(200, response2.getStatusLine().getStatusCode());\n                List<String> values = consumeJsonPinValues(response2);\n                assertEquals(\"keep-alive\", response2.getFirstHeader(\"Connection\").getValue());\n                assertEquals(1, values.size());\n                assertEquals(String.valueOf(i), values.get(0));\n            }\n        }\n    }\n\n    @Test(expected = Exception.class)\n    public void keepAliveIsSupported()  throws Exception{\n        String url = httpServerUrl + \"4ae3851817194e2596cf1b7103603ef8/update/a14\";\n\n        HttpPut request = new HttpPut(url);\n        request.setHeader(\"Connection\", \"close\");\n\n        request.setEntity(new StringEntity(\"[\\\"\"+ 0 + \"\\\"]\", ContentType.APPLICATION_JSON));\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            assertEquals(\"close\", response.getFirstHeader(\"Connection\").getValue());\n            EntityUtils.consume(response.getEntity());\n        }\n\n        request = new HttpPut(url);\n        request.setHeader(\"Connection\", \"close\");\n\n        request.setEntity(new StringEntity(\"[\\\"\"+ 0 + \"\\\"]\", ContentType.APPLICATION_JSON));\n\n        //this should fail as connection is closed and httpClient is reusing connections\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n        }\n    }\n\n    @Test\n    public void testHttpAPICounters() throws Exception {\n        HttpGet getRequest = new HttpGet(httpServerUrl + \"4ae3851817194e2596cf1b7103603ef8/update/v11?value=11\");\n        try (CloseableHttpResponse response = httpclient.execute(getRequest)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            assertEquals(1, holder.stats.specificCounters[HTTP_UPDATE_PIN_DATA].intValue());\n            assertEquals(0, holder.stats.specificCounters[HTTP_GET_PIN_DATA].intValue());\n            assertEquals(1, holder.stats.totalMessages.getCount());\n        }\n\n        getRequest = new HttpGet(httpServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/v11\");\n        try (CloseableHttpResponse response = httpclient.execute(getRequest)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            assertEquals(1, holder.stats.specificCounters[HTTP_UPDATE_PIN_DATA].intValue());\n            assertEquals(1, holder.stats.specificCounters[HTTP_GET_PIN_DATA].intValue());\n            assertEquals(2, holder.stats.totalMessages.getCount());\n        }\n    }\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/http/HttpAPIPinsAsyncClientTest.java",
    "content": "package cc.blynk.integration.http;\n\nimport cc.blynk.integration.SingleServerInstancePerTest;\nimport cc.blynk.integration.TestUtil;\nimport cc.blynk.server.core.model.Profile;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.controls.Button;\nimport cc.blynk.server.servers.application.MobileAndHttpsServer;\nimport cc.blynk.server.servers.hardware.HardwareAndHttpAPIServer;\nimport cc.blynk.utils.FileUtils;\nimport cc.blynk.utils.properties.ServerProperties;\nimport org.asynchttpclient.AsyncHttpClient;\nimport org.asynchttpclient.DefaultAsyncHttpClient;\nimport org.asynchttpclient.DefaultAsyncHttpClientConfig;\nimport org.asynchttpclient.Response;\nimport org.junit.AfterClass;\nimport org.junit.BeforeClass;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.concurrent.Future;\n\nimport static cc.blynk.integration.BaseTest.getRelativeDataFolder;\nimport static cc.blynk.integration.TestUtil.createHolderWithIOMock;\nimport static cc.blynk.integration.TestUtil.ok;\nimport static cc.blynk.integration.TestUtil.setProperty;\nimport static io.netty.handler.codec.http.HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN;\nimport static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;\nimport static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE;\nimport static io.netty.handler.codec.http.HttpHeaderNames.LOCATION;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\nimport static org.junit.Assert.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.timeout;\nimport static org.mockito.Mockito.verify;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 24.12.15.\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class HttpAPIPinsAsyncClientTest extends SingleServerInstancePerTest {\n\n    private static AsyncHttpClient httpclient;\n    private static String httpsServerUrl;\n\n    @AfterClass\n    public static void closeHttp() throws Exception {\n        httpclient.close();\n    }\n\n    @BeforeClass\n    public static void init() throws Exception {\n        properties = new ServerProperties(Collections.emptyMap());\n        properties.setProperty(\"data.folder\", getRelativeDataFolder(\"/profiles\"));\n        holder = createHolderWithIOMock(properties, \"no-db.properties\");\n        hardwareServer = new HardwareAndHttpAPIServer(holder).start();\n        appServer = new MobileAndHttpsServer(holder).start();\n        httpsServerUrl = String.format(\"http://localhost:%s/\", properties.getHttpPort());\n        httpclient = new DefaultAsyncHttpClient(\n                new DefaultAsyncHttpClientConfig.Builder()\n                        .setUserAgent(null)\n                        .setKeepAlive(true)\n                        .build()\n        );\n    }\n\n    //----------------------------GET METHODS SECTION\n\n    @Test\n    public void testGetWithFakeToken() throws Exception {\n        Future<Response> f = httpclient.prepareGet(httpsServerUrl + \"dsadasddasdasdasdasdasdas/get/d8\").execute();\n        Response response = f.get();\n        assertEquals(400, response.getStatusCode());\n        assertEquals(\"Invalid token.\", response.getResponseBody());\n        assertEquals(\"*\", response.getHeader(ACCESS_CONTROL_ALLOW_ORIGIN));\n    }\n\n    @Test\n    public void testGetWithWrongPathToken() throws Exception {\n        Future<Response> f = httpclient.prepareGet(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/w/d8\").execute();\n        assertEquals(404, f.get().getStatusCode());\n    }\n\n    @Test\n    public void testGetWithWrongPin() throws Exception {\n        Future<Response> f = httpclient.prepareGet(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/x8\").execute();\n        Response response = f.get();\n        assertEquals(400, response.getStatusCode());\n        assertEquals(\"Wrong pin format.\", response.getResponseBody());\n        assertEquals(\"*\", response.getHeader(ACCESS_CONTROL_ALLOW_ORIGIN));\n    }\n\n    @Test\n    public void testGetWithNonExistingPin() throws Exception {\n        Future<Response> f = httpclient.prepareGet(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/v11\").execute();\n        Response response = f.get();\n        assertEquals(400, response.getStatusCode());\n        assertEquals(\"Requested pin doesn't exist in the app.\", response.getResponseBody());\n        assertEquals(\"*\", response.getHeader(ACCESS_CONTROL_ALLOW_ORIGIN));\n    }\n\n    @Test\n    public void testPutViaGetRequestSingleValue() throws Exception {\n        Future<Response> f = httpclient.prepareGet(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/update/v11?value=10\").execute();\n        Response response = f.get();\n        assertEquals(200, response.getStatusCode());\n        assertEquals(\"*\", response.getHeader(ACCESS_CONTROL_ALLOW_ORIGIN));\n\n\n        f = httpclient.prepareGet(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/v11\").execute();\n        response = f.get();\n\n        assertEquals(200, response.getStatusCode());\n        List<String> values = TestUtil.consumeJsonPinValues(response.getResponseBody());\n        assertEquals(1, values.size());\n        assertEquals(\"10\", values.get(0));\n        assertEquals(\"*\", response.getHeader(ACCESS_CONTROL_ALLOW_ORIGIN));\n    }\n\n    @Test\n    public void testPutAndGetTerminalValue() throws Exception {\n        Future<Response> f= httpclient.prepareGet(httpsServerUrl + \"7b0a3a61322e41a5b50589cf52d775d1/get/v17\").execute();\n        Response response = f.get();\n\n        assertEquals(200, response.getStatusCode());\n        List<String> values = TestUtil.consumeJsonPinValues(response.getResponseBody());\n        assertEquals(0, values.size());\n\n        f = httpclient.prepareGet(httpsServerUrl\n                + \"7b0a3a61322e41a5b50589cf52d775d1/update/v17?value=10\").execute();\n        response = f.get();\n        assertEquals(200, response.getStatusCode());\n\n        f = httpclient.prepareGet(httpsServerUrl\n                + \"7b0a3a61322e41a5b50589cf52d775d1/update/v17?value=11\").execute();\n        response = f.get();\n        assertEquals(200, response.getStatusCode());\n\n        f = httpclient.prepareGet(httpsServerUrl + \"7b0a3a61322e41a5b50589cf52d775d1/get/v17\").execute();\n        response = f.get();\n\n        assertEquals(200, response.getStatusCode());\n        values = TestUtil.consumeJsonPinValues(response.getResponseBody());\n        assertEquals(2, values.size());\n        assertEquals(\"10\", values.get(0));\n        assertEquals(\"11\", values.get(1));\n    }\n\n    @Test\n    public void testPutViaGetRequestMultipleValue() throws Exception {\n        Future<Response> f = httpclient.prepareGet(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/update/v11?value=10&value=11\").execute();\n        Response response = f.get();\n        assertEquals(200, response.getStatusCode());\n        assertEquals(\"*\", response.getHeader(ACCESS_CONTROL_ALLOW_ORIGIN));\n\n\n        f = httpclient.prepareGet(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/v11\").execute();\n        response = f.get();\n\n        assertEquals(200, response.getStatusCode());\n        List<String> values = TestUtil.consumeJsonPinValues(response.getResponseBody());\n        assertEquals(2, values.size());\n        assertEquals(\"10\", values.get(0));\n        assertEquals(\"11\", values.get(1));\n        assertEquals(\"*\", response.getHeader(ACCESS_CONTROL_ALLOW_ORIGIN));\n    }\n\n    @Test\n    public void testPutGetNonExistingPin() throws Exception {\n        Future<Response> f = httpclient.preparePut(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/update/v10\")\n                .setHeader(\"Content-Type\", \"application/json\")\n                .setBody(\"[\\\"100\\\"]\")\n                .execute();\n        Response response = f.get();\n\n        assertEquals(200, response.getStatusCode());\n        assertEquals(\"*\", response.getHeader(ACCESS_CONTROL_ALLOW_ORIGIN));\n\n        f = httpclient.prepareGet(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/v10\").execute();\n        response = f.get();\n\n        assertEquals(200, response.getStatusCode());\n        List<String> values = TestUtil.consumeJsonPinValues(response.getResponseBody());\n        assertEquals(1, values.size());\n        assertEquals(\"100\", values.get(0));\n        assertEquals(\"*\", response.getHeader(ACCESS_CONTROL_ALLOW_ORIGIN));\n    }\n\n    @Test\n    public void testMultiPutGetNonExistingPin() throws Exception {\n        Future<Response> f = httpclient.preparePut(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/update/v10\")\n                .setHeader(\"Content-Type\", \"application/json\")\n                .setBody(\"[\\\"100\\\", \\\"101\\\", \\\"102\\\"]\")\n                .execute();\n        Response response = f.get();\n\n        assertEquals(200, response.getStatusCode());\n        assertEquals(\"*\", response.getHeader(ACCESS_CONTROL_ALLOW_ORIGIN));\n\n        f = httpclient.prepareGet(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/v10\").execute();\n        response = f.get();\n\n        assertEquals(200, response.getStatusCode());\n        List<String> values = TestUtil.consumeJsonPinValues(response.getResponseBody());\n        assertEquals(3, values.size());\n        assertEquals(\"100\", values.get(0));\n        assertEquals(\"101\", values.get(1));\n        assertEquals(\"102\", values.get(2));\n        assertEquals(\"*\", response.getHeader(ACCESS_CONTROL_ALLOW_ORIGIN));\n    }\n\n    @Test\n    public void testGetPinData() throws Exception {\n        Future<Response> f = httpclient.prepareGet(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/update/v111?value=10\").execute();\n        Response response = f.get();\n        assertEquals(200, response.getStatusCode());\n        assertEquals(\"*\", response.getHeader(ACCESS_CONTROL_ALLOW_ORIGIN));\n\n        f = httpclient.prepareGet(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/data/v111\").execute();\n        response = f.get();\n        assertEquals(400, response.getStatusCode());\n        assertEquals(\"No data.\", response.getResponseBody());\n        assertEquals(\"*\", response.getHeader(ACCESS_CONTROL_ALLOW_ORIGIN));\n\n        f = httpclient.prepareGet(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/data/z111\").execute();\n        response = f.get();\n        assertEquals(400, response.getStatusCode());\n        assertEquals(\"Wrong pin format.\", response.getResponseBody());\n        assertEquals(\"*\", response.getHeader(ACCESS_CONTROL_ALLOW_ORIGIN));\n    }\n\n    @Test\n    public void testGetCSVDataRedirect() throws Exception {\n        Path reportingPath = Paths.get(holder.reportingDiskDao.dataFolder, \"dmitriy@blynk.cc\");\n        Files.createDirectories(reportingPath);\n        FileUtils.write(Paths.get(reportingPath.toString(), \"history_125564119-0_v10_minute.bin\"), 1, 2);\n\n        Future<Response> f = httpclient.prepareGet(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/data/v10\").execute();\n        Response response = f.get();\n        assertEquals(301, response.getStatusCode());\n        String redirectLocation = response.getHeader(LOCATION);\n        assertNotNull(redirectLocation);\n        assertTrue(redirectLocation.contains(\"/dmitriy@blynk.cc_125564119_0_v10_\"));\n        assertEquals(\"*\", response.getHeader(ACCESS_CONTROL_ALLOW_ORIGIN));\n        assertEquals(\"0\", response.getHeader(CONTENT_LENGTH));\n\n        f = httpclient.prepareGet(httpsServerUrl + redirectLocation.replaceFirst(\"/\", \"\")).execute();\n        response = f.get();\n        assertEquals(200, response.getStatusCode());\n        assertEquals(\"application/x-gzip\", response.getHeader(CONTENT_TYPE));\n        assertEquals(\"*\", response.getHeader(ACCESS_CONTROL_ALLOW_ORIGIN));\n    }\n\n    @Test\n    public void testChangeLabelPropertyViaGet() throws Exception {\n        Future<Response> f = httpclient.prepareGet(httpsServerUrl + clientPair.token + \"/update/v4?label=My-New-Label\").execute();\n        Response response = f.get();\n\n        assertEquals(200, response.getStatusCode());\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(setProperty(111, \"1-0 4 label My-New-Label\")));\n\n        clientPair.appClient.reset();\n\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(1);\n\n        Widget widget = profile.dashBoards[0].findWidgetByPin(0, (short) 4, PinType.VIRTUAL);\n        assertNotNull(widget);\n        assertEquals(\"My-New-Label\", widget.label);\n    }\n\n    @Test\n    public void testChangeColorPropertyViaGet() throws Exception {\n        Future<Response> f = httpclient.prepareGet(httpsServerUrl + clientPair.token + \"/update/v4?color=%23000000\").execute();\n        Response response = f.get();\n\n        assertEquals(200, response.getStatusCode());\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(setProperty(111, \"1-0 4 color #000000\")));\n\n        clientPair.appClient.reset();\n\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(1);\n\n        Widget widget = profile.dashBoards[0].findWidgetByPin(0, (short) 4, PinType.VIRTUAL);\n        assertNotNull(widget);\n        assertEquals(255, widget.color);\n    }\n\n    @Test\n    public void testChangeOnLabelPropertyViaGet() throws Exception {\n        clientPair.appClient.reset();\n        clientPair.appClient.updateWidget(1, \"{\\\"id\\\":1, \\\"width\\\":1, \\\"height\\\":1,  \\\"x\\\":1, \\\"y\\\":1, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"BUTTON\\\",         \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":2, \\\"value\\\":\\\"1\\\"}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        Future<Response> f = httpclient.prepareGet(httpsServerUrl + clientPair.token + \"/update/v2?onLabel=newOnButtonLabel\").execute();\n        Response response = f.get();\n\n        assertEquals(200, response.getStatusCode());\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(setProperty(111, \"1-0 2 onLabel newOnButtonLabel\")));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(1);\n\n        Button button = (Button) profile.dashBoards[0].findWidgetByPin(0, (short) 2, PinType.VIRTUAL);\n        assertNotNull(button);\n        assertEquals(\"newOnButtonLabel\", button.onLabel);\n    }\n\n\n    @Test\n    public void testChangeOffLabelPropertyViaGet() throws Exception {\n        clientPair.appClient.reset();\n        clientPair.appClient.updateWidget(1, \"{\\\"id\\\":1, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":1, \\\"y\\\":1, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"BUTTON\\\",         \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":1, \\\"value\\\":\\\"1\\\"}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        Future<Response> f = httpclient.prepareGet(httpsServerUrl + clientPair.token + \"/update/v1?offLabel=newOffButtonLabel\").execute();\n        Response response = f.get();\n\n        assertEquals(200, response.getStatusCode());\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(setProperty(111, \"1-0 1 offLabel newOffButtonLabel\")));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(1);\n\n        Button button = (Button) profile.dashBoards[0].findWidgetByPin(0, (short) 1, PinType.VIRTUAL);\n        assertNotNull(button);\n        assertEquals(\"newOffButtonLabel\", button.offLabel);\n    }\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/http/HttpAPIPinsTest.java",
    "content": "package cc.blynk.integration.http;\n\nimport cc.blynk.integration.SingleServerInstancePerTest;\nimport cc.blynk.integration.TestUtil;\nimport cc.blynk.server.api.http.pojo.EmailPojo;\nimport cc.blynk.server.api.http.pojo.PushMessagePojo;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.servers.application.MobileAndHttpsServer;\nimport cc.blynk.server.servers.hardware.HardwareAndHttpAPIServer;\nimport cc.blynk.utils.properties.ServerProperties;\nimport org.apache.http.client.methods.CloseableHttpResponse;\nimport org.apache.http.client.methods.HttpGet;\nimport org.apache.http.client.methods.HttpPost;\nimport org.apache.http.client.methods.HttpPut;\nimport org.apache.http.entity.ContentType;\nimport org.apache.http.entity.StringEntity;\nimport org.apache.http.impl.client.CloseableHttpClient;\nimport org.apache.http.impl.client.HttpClients;\nimport org.apache.http.util.EntityUtils;\nimport org.junit.AfterClass;\nimport org.junit.BeforeClass;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport java.util.Collections;\nimport java.util.List;\n\nimport static cc.blynk.integration.BaseTest.getRelativeDataFolder;\nimport static cc.blynk.integration.TestUtil.createHolderWithIOMock;\nimport static org.junit.Assert.assertEquals;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 24.12.15.\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class HttpAPIPinsTest extends SingleServerInstancePerTest {\n\n    private static CloseableHttpClient httpclient;\n    private static String httpsServerUrl;\n\n    @BeforeClass\n    //shadow parent method by purpose\n    public static void init() throws Exception {\n        properties = new ServerProperties(Collections.emptyMap());\n        properties.setProperty(\"data.folder\", getRelativeDataFolder(\"/profiles\"));\n        holder = createHolderWithIOMock(properties, \"no-db.properties\");\n        appServer = new MobileAndHttpsServer(holder).start();\n        hardwareServer = new HardwareAndHttpAPIServer(holder).start();\n        httpsServerUrl = String.format(\"http://localhost:%s/\", properties.getHttpPort());\n        httpclient = HttpClients.createDefault();\n    }\n\n    @AfterClass\n    public static void closeHttp() throws Exception {\n        httpclient.close();\n    }\n\n    //----------------------------GET METHODS SECTION\n\n    @Test\n    public void testGetWithFakeToken() throws Exception {\n        HttpGet request = new HttpGet(httpsServerUrl + \"dsadasddasdasdasdasdasdas/get/d8\");\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(400, response.getStatusLine().getStatusCode());\n            assertEquals(\"Invalid token.\", TestUtil.consumeText(response));\n        }\n    }\n\n    @Test\n    public void testGetWithWrongPathToken() throws Exception {\n        HttpGet request = new HttpGet(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/w/d8\");\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(404, response.getStatusLine().getStatusCode());\n        }\n    }\n\n    @Test\n    public void testGetWithWrongPin() throws Exception {\n        HttpGet request = new HttpGet(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/x8\");\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(400, response.getStatusLine().getStatusCode());\n            assertEquals(\"Wrong pin format.\", TestUtil.consumeText(response));\n        }\n    }\n\n    @Test\n    public void testGetWithNonExistingPin() throws Exception {\n        HttpGet request = new HttpGet(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/v11\");\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(400, response.getStatusLine().getStatusCode());\n            assertEquals(\"Requested pin doesn't exist in the app.\", TestUtil.consumeText(response));\n        }\n    }\n\n    @Test\n    public void testGetWringPin() throws Exception {\n        HttpGet request = new HttpGet(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/v256\");\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(400, response.getStatusLine().getStatusCode());\n            assertEquals(\"Wrong pin format.\", TestUtil.consumeText(response));\n        }\n    }\n\n    @Test\n    public void testPutGetNonExistingPin() throws Exception {\n        HttpPut put = new HttpPut(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/update/v10\");\n        put.setEntity(new StringEntity(\"[\\\"100\\\"]\", ContentType.APPLICATION_JSON));\n\n        try (CloseableHttpResponse response = httpclient.execute(put)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n        }\n\n        HttpGet get = new HttpGet(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/v10\");\n\n        try (CloseableHttpResponse response = httpclient.execute(get)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            List<String> values = TestUtil.consumeJsonPinValues(response);\n            assertEquals(1, values.size());\n            assertEquals(\"100\", values.get(0));\n        }\n    }\n\n    @Test\n    public void testMultiPutGetNonExistingPin() throws Exception {\n        HttpPut put = new HttpPut(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/update/v10\");\n        put.setEntity(new StringEntity(\"[\\\"100\\\", \\\"101\\\", \\\"102\\\"]\", ContentType.APPLICATION_JSON));\n\n        try (CloseableHttpResponse response = httpclient.execute(put)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n        }\n\n        HttpGet get = new HttpGet(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/v10\");\n\n        try (CloseableHttpResponse response = httpclient.execute(get)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            List<String> values = TestUtil.consumeJsonPinValues(response);\n            assertEquals(3, values.size());\n            assertEquals(\"100\", values.get(0));\n            assertEquals(\"101\", values.get(1));\n            assertEquals(\"102\", values.get(2));\n        }\n    }\n\n    @Test\n    public void testMultiPutGetNonExistingPinWithNewMethod() throws Exception {\n        HttpPut put = new HttpPut(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/update/v10\");\n        put.setEntity(new StringEntity(\"[\\\"100\\\", \\\"101\\\", \\\"102\\\"]\", ContentType.APPLICATION_JSON));\n\n        try (CloseableHttpResponse response = httpclient.execute(put)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n        }\n\n        HttpGet get = new HttpGet(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/v10\");\n\n        try (CloseableHttpResponse response = httpclient.execute(get)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            List<String> values = TestUtil.consumeJsonPinValues(response);\n            assertEquals(3, values.size());\n            assertEquals(\"100\", values.get(0));\n            assertEquals(\"101\", values.get(1));\n            assertEquals(\"102\", values.get(2));\n        }\n    }\n\n    @Test\n    public void testGetTimerExistingPin() throws Exception {\n        HttpGet request = new HttpGet(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/D0\");\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            List<String> values = TestUtil.consumeJsonPinValues(response);\n            assertEquals(1, values.size());\n            assertEquals(\"1\", values.get(0));\n        }\n    }\n\n    @Test\n    public void testGetWithExistingPin() throws Exception {\n        HttpGet request = new HttpGet(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/D8\");\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            List<String> values = TestUtil.consumeJsonPinValues(response);\n            assertEquals(1, values.size());\n            assertEquals(\"0\", values.get(0));\n        }\n\n        request = new HttpGet(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/d1\");\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            List<String> values = TestUtil.consumeJsonPinValues(response);\n            assertEquals(1, values.size());\n            assertEquals(\"1\", values.get(0));\n        }\n\n        request = new HttpGet(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/d3\");\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            List<String> values = TestUtil.consumeJsonPinValues(response);\n            assertEquals(1, values.size());\n            assertEquals(\"87\", values.get(0));\n        }\n    }\n\n    @Test\n    public void testGetWithExistingEmptyPin() throws Exception {\n        HttpGet request = new HttpGet(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/a14\");\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            List<String> values = TestUtil.consumeJsonPinValues(response);\n            assertEquals(0, values.size());\n        }\n    }\n\n    @Test\n    public void testGetWithExistingMultiPin() throws Exception {\n        HttpGet request = new HttpGet(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/a15\");\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            List<String> values = TestUtil.consumeJsonPinValues(response);\n            assertEquals(2, values.size());\n            assertEquals(\"1\", values.get(0));\n            assertEquals(\"2\", values.get(1));\n        }\n    }\n\n    @Test\n    public void testGetForRGBMerge() throws Exception {\n        HttpGet request = new HttpGet(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/v13\");\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            List<String> values = TestUtil.consumeJsonPinValues(response);\n            assertEquals(3, values.size());\n            assertEquals(\"60\", values.get(0));\n            assertEquals(\"143\", values.get(1));\n            assertEquals(\"158\", values.get(2));\n        }\n    }\n\n    @Test\n    public void testGetForJoystickMerge() throws Exception {\n        HttpGet request = new HttpGet(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/v14\");\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            List<String> values = TestUtil.consumeJsonPinValues(response);\n            assertEquals(2, values.size());\n            assertEquals(\"128\", values.get(0));\n            assertEquals(\"129\", values.get(1));\n        }\n    }\n\n    //----------------------------PUT METHODS SECTION\n\n    @Test\n    public void testPutNoContentType() throws Exception {\n        HttpPut request = new HttpPut(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/update/d8\");\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(500, response.getStatusLine().getStatusCode());\n            assertEquals(\"Unexpected content type. Expecting application/json.\", TestUtil.consumeText(response));\n        }\n    }\n\n    @Test\n    public void testPutFakeToken() throws Exception {\n        HttpPut request = new HttpPut(httpsServerUrl + \"dsadasddasdasdasdasdasdas/update/d8\");\n        request.setHeader(\"Content-Type\", ContentType.APPLICATION_JSON.toString());\n        request.setEntity(new StringEntity(\"[\\\"100\\\"]\", ContentType.APPLICATION_JSON));\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(400, response.getStatusLine().getStatusCode());\n            assertEquals(\"Invalid token.\", TestUtil.consumeText(response));\n        }\n    }\n\n    @Test\n    public void testPutWithWrongPin() throws Exception {\n        HttpPut request = new HttpPut(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/update/x8\");\n        request.setHeader(\"Content-Type\", ContentType.APPLICATION_JSON.toString());\n        request.setEntity(new StringEntity(\"[\\\"100\\\"]\", ContentType.APPLICATION_JSON));\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(400, response.getStatusLine().getStatusCode());\n            assertEquals(\"Wrong pin format.\", TestUtil.consumeText(response));\n        }\n    }\n\n    @Test\n    public void testPutWithNoWidget() throws Exception {\n        HttpPut request = new HttpPut(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/update/v10\");\n        request.setHeader(\"Content-Type\", ContentType.APPLICATION_JSON.toString());\n        request.setEntity(new StringEntity(\"[\\\"100\\\"]\", ContentType.APPLICATION_JSON));\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n        }\n    }\n\n    @Test\n    public void testPutWithNoWidgetNoPinData() throws Exception {\n        HttpPut request = new HttpPut(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/update/v10\");\n        request.setHeader(\"Content-Type\", ContentType.APPLICATION_JSON.toString());\n        request.setEntity(new StringEntity(\"[]\", ContentType.APPLICATION_JSON));\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(400, response.getStatusLine().getStatusCode());\n            assertEquals(\"No pin for update provided.\", TestUtil.consumeText(response));\n        }\n    }\n\n    @Test\n    public void testPutWithNoWidgetMultivalue() throws Exception {\n        HttpPut request = new HttpPut(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/update/v10\");\n        request.setHeader(\"Content-Type\", ContentType.APPLICATION_JSON.toString());\n        request.setEntity(new StringEntity(\"[\\\"100\\\", \\\"101\\\", \\\"102\\\"]\", ContentType.APPLICATION_JSON));\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n        }\n    }\n\n    @Test\n    public void testPutWithLargeValueNotAccepted() throws Exception {\n        HttpPut request = new HttpPut(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/pin/v10\");\n        request.setHeader(\"Content-Type\", ContentType.APPLICATION_JSON.toString());\n\n        StringBuilder val = new StringBuilder(512 * 1024);\n        for (int i = 0; i < val.capacity() / 10; i++) {\n            val.append(\"1234567890\");\n        }\n        val.append(\"1234567890\");\n\n        request.setEntity(new StringEntity(\"[\\\"\" + val.toString() + \"\\\"]\", ContentType.APPLICATION_JSON));\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(413, response.getStatusLine().getStatusCode());\n        }\n    }\n\n    @Test\n    public void testPutExtraWithNoWidget() throws Exception {\n        HttpPut request = new HttpPut(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/extra/pin/v10\");\n        request.setHeader(\"Content-Type\", ContentType.APPLICATION_JSON.toString());\n        request.setEntity(new StringEntity(\"[{\\\"timestamp\\\" : 123, \\\"value\\\":\\\"100\\\"}]\", ContentType.APPLICATION_JSON));\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n        }\n    }\n\n    @Test\n    public void testPutWithExistingPin() throws Exception {\n        HttpPut request = new HttpPut(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/update/a14\");\n        request.setEntity(new StringEntity(\"[\\\"100\\\"]\", ContentType.APPLICATION_JSON));\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n        }\n\n        HttpGet getRequest = new HttpGet(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/a14\");\n\n        try (CloseableHttpResponse response = httpclient.execute(getRequest)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            List<String> values = TestUtil.consumeJsonPinValues(response);\n            assertEquals(1, values.size());\n            assertEquals(\"100\", values.get(0));\n        }\n    }\n\n    @Test\n    public void testPutWithExistingPinWrongBody() throws Exception {\n        HttpPut request = new HttpPut(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/update/a14\");\n        request.setEntity(new StringEntity(\"\\\"100\\\"\", ContentType.APPLICATION_JSON));\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(500, response.getStatusLine().getStatusCode());\n            assertEquals(\"Error parsing body param. \\\"100\\\"\", TestUtil.consumeText(response));\n        }\n    }\n\n    @Test\n    public void testPutWithExistingPinWrongBody2() throws Exception {\n        HttpPut request = new HttpPut(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/update/a14\");\n        request.setEntity(new StringEntity(\"\", ContentType.APPLICATION_JSON));\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(500, response.getStatusLine().getStatusCode());\n            assertEquals(\"Error parsing body param. \", TestUtil.consumeText(response));\n        }\n    }\n\n    //----------------------------NOTIFICATION POST METHODS SECTION\n\n    //----------------------------pushes\n    @Test\n    public void testPostNotifyNoContentType() throws Exception {\n        HttpPost request = new HttpPost(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/notify\");\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(500, response.getStatusLine().getStatusCode());\n            assertEquals(\"Unexpected content type. Expecting application/json.\", TestUtil.consumeText(response));\n        }\n    }\n\n    @Test\n    public void testPostNotifyNoBody() throws Exception {\n        HttpPost request = new HttpPost(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/notify\");\n        request.setHeader(\"Content-Type\", ContentType.APPLICATION_JSON.toString());\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(500, response.getStatusLine().getStatusCode());\n            assertEquals(\"Error parsing body param. \", TestUtil.consumeText(response));\n        }\n    }\n\n    @Test\n    public void testPostNotifyWithWrongBody() throws Exception {\n        HttpPost request = new HttpPost(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/notify\");\n        StringBuilder sb = new StringBuilder();\n        for (int i = 0; i < 256; i++) {\n            sb.append(i);\n        }\n        request.setEntity(new StringEntity(\"{\\\"body\\\":\\\"\" + sb.toString() + \"\\\"}\", ContentType.APPLICATION_JSON));\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(400, response.getStatusLine().getStatusCode());\n            assertEquals(\"Body is empty or larger than 255 chars.\", TestUtil.consumeText(response));\n        }\n    }\n\n    @Test\n    public void testPostNotifyWithBody() throws Exception {\n        HttpPost request = new HttpPost(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/notify\");\n        String message = JsonParser.MAPPER.writeValueAsString(new PushMessagePojo(\"This is Push Message\"));\n        request.setEntity(new StringEntity(message, ContentType.APPLICATION_JSON));\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n        }\n    }\n\n\n    //----------------------------email\n    @Test\n    public void testPostEmailNoContentType() throws Exception {\n        HttpPost request = new HttpPost(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/email\");\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(500, response.getStatusLine().getStatusCode());\n            assertEquals(\"Unexpected content type. Expecting application/json.\", TestUtil.consumeText(response));\n        }\n    }\n\n    @Test\n    public void testPostEmailNoBody() throws Exception {\n        HttpPost request = new HttpPost(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/email\");\n        request.setHeader(\"Content-Type\", ContentType.APPLICATION_JSON.toString());\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(500, response.getStatusLine().getStatusCode());\n            assertEquals(\"Error parsing body param. \", TestUtil.consumeText(response));\n        }\n    }\n\n    @Test\n    public void testPostEmailWithBody() throws Exception {\n        HttpPost request = new HttpPost(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/email\");\n        String message = JsonParser.MAPPER.writeValueAsString(new EmailPojo(\"pupkin@gmail.com\", \"Title\", \"Subject\"));\n        request.setEntity(new StringEntity(message, ContentType.APPLICATION_JSON));\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n        }\n    }\n\n    //------------------------------ SYNC TEST\n    @Test\n    public void testSync() throws Exception {\n        HttpPut request = new HttpPut(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/update/a14\");\n        HttpGet getRequest = new HttpGet(httpsServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/a14\");\n\n        for (int i = 0; i < 100; i++) {\n            request.setEntity(new StringEntity(\"[\\\"\"+ i + \"\\\"]\", ContentType.APPLICATION_JSON));\n\n            try (CloseableHttpResponse response = httpclient.execute(request)) {\n                assertEquals(200, response.getStatusLine().getStatusCode());\n                EntityUtils.consume(response.getEntity());\n            }\n\n            try (CloseableHttpResponse response2 = httpclient.execute(getRequest)) {\n                assertEquals(200, response2.getStatusLine().getStatusCode());\n                List<String> values = TestUtil.consumeJsonPinValues(response2);\n                assertEquals(1, values.size());\n                assertEquals(String.valueOf(i), values.get(0));\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/http/HttpAndTCPSameJVMTest.java",
    "content": "package cc.blynk.integration.http;\n\nimport cc.blynk.integration.SingleServerInstancePerTest;\nimport cc.blynk.integration.TestUtil;\nimport cc.blynk.integration.model.tcp.TestHardClient;\nimport cc.blynk.integration.tcp.EventorTest;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.DataStream;\nimport cc.blynk.server.core.model.device.BoardType;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.widgets.controls.RGB;\nimport cc.blynk.server.core.model.widgets.controls.Timer;\nimport cc.blynk.server.core.model.widgets.others.eventor.Eventor;\nimport cc.blynk.server.core.model.widgets.others.eventor.Rule;\nimport cc.blynk.server.core.model.widgets.others.eventor.TimerTime;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.action.BaseAction;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.action.SetPinAction;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.action.SetPinActionType;\nimport cc.blynk.server.core.model.widgets.others.rtc.RTC;\nimport cc.blynk.server.core.model.widgets.others.webhook.WebHook;\nimport cc.blynk.server.core.model.widgets.outputs.ValueDisplay;\nimport cc.blynk.server.core.model.widgets.outputs.graph.GraphDataStream;\nimport cc.blynk.server.core.model.widgets.outputs.graph.GraphType;\nimport cc.blynk.server.core.model.widgets.outputs.graph.Superchart;\nimport cc.blynk.server.core.model.widgets.ui.table.Table;\nimport cc.blynk.server.servers.application.MobileAndHttpsServer;\nimport cc.blynk.server.servers.hardware.HardwareAndHttpAPIServer;\nimport cc.blynk.utils.DateTimeUtils;\nimport cc.blynk.utils.properties.ServerProperties;\nimport com.google.zxing.BinaryBitmap;\nimport com.google.zxing.LuminanceSource;\nimport com.google.zxing.Result;\nimport com.google.zxing.client.j2se.BufferedImageLuminanceSource;\nimport com.google.zxing.common.HybridBinarizer;\nimport com.google.zxing.qrcode.QRCodeReader;\nimport org.apache.http.client.methods.CloseableHttpResponse;\nimport org.apache.http.client.methods.HttpGet;\nimport org.apache.http.client.methods.HttpPut;\nimport org.apache.http.entity.ContentType;\nimport org.apache.http.entity.StringEntity;\nimport org.apache.http.impl.client.CloseableHttpClient;\nimport org.apache.http.impl.client.HttpClients;\nimport org.apache.http.util.EntityUtils;\nimport org.junit.AfterClass;\nimport org.junit.BeforeClass;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport javax.imageio.ImageIO;\nimport java.awt.image.BufferedImage;\nimport java.io.ByteArrayInputStream;\nimport java.time.LocalTime;\nimport java.time.ZoneId;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.TimeUnit;\n\nimport static cc.blynk.integration.BaseTest.getRelativeDataFolder;\nimport static cc.blynk.integration.TestUtil.b;\nimport static cc.blynk.integration.TestUtil.consumeText;\nimport static cc.blynk.integration.TestUtil.createDefaultHolder;\nimport static cc.blynk.integration.TestUtil.createDevice;\nimport static cc.blynk.integration.TestUtil.hardware;\nimport static cc.blynk.integration.TestUtil.ok;\nimport static cc.blynk.server.core.model.enums.PinType.VIRTUAL;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE;\nimport static cc.blynk.server.core.protocol.model.messages.MessageFactory.produce;\nimport static cc.blynk.server.workers.timer.TimerWorker.TIMER_MSG_ID;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\nimport static org.junit.Assert.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.after;\nimport static org.mockito.Mockito.reset;\nimport static org.mockito.Mockito.timeout;\nimport static org.mockito.Mockito.verify;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 07.01.16.\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class HttpAndTCPSameJVMTest extends SingleServerInstancePerTest {\n\n    private static CloseableHttpClient httpclient;\n    private static String httpServerUrl;\n\n    @AfterClass\n    public static void closeHttp() throws Exception {\n        httpclient.close();\n    }\n\n    @BeforeClass\n    //shadow parent method by purpose\n    public static void init() throws Exception {\n        properties = new ServerProperties(Collections.emptyMap());\n        properties.setProperty(\"data.folder\", getRelativeDataFolder(\"/profiles\"));\n        holder = createDefaultHolder(properties, \"no-db.properties\");\n        hardwareServer = new HardwareAndHttpAPIServer(holder).start();\n        appServer = new MobileAndHttpsServer(holder).start();\n\n        httpServerUrl = String.format(\"http://localhost:%s/\", properties.getHttpPort());\n        httpclient = HttpClients.createDefault();\n    }\n\n    @Test\n    public void testChangeNonWidgetPinValueViaHardwareAndGetViaHTTP() throws Exception {\n        clientPair.hardwareClient.send(\"hardware vw 10 200\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(1, HARDWARE, b(\"1-0 vw 10 200\"))));\n\n        reset(clientPair.appClient.responseMock);\n\n        HttpGet request = new HttpGet(httpServerUrl + clientPair.token + \"/get/v10\");\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            List<String> values = TestUtil.consumeJsonPinValues(response);\n            assertEquals(1, values.size());\n            assertEquals(\"200\", values.get(0));\n        }\n\n        clientPair.appClient.send(\"hardware 1 vw 10 201\");\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(produce(1, HARDWARE, b(\"vw 10 201\"))));\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            List<String> values = TestUtil.consumeJsonPinValues(response);\n            assertEquals(1, values.size());\n            assertEquals(\"201\", values.get(0));\n        }\n    }\n\n    @Test\n    public void testChangePinValueViaAppAndHardware() throws Exception {\n        clientPair.hardwareClient.send(\"hardware vw 4 200\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(1, HARDWARE, b(\"1-0 vw 4 200\"))));\n\n        reset(clientPair.appClient.responseMock);\n\n        HttpGet request = new HttpGet(httpServerUrl + clientPair.token + \"/get/v4\");\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            List<String> values = TestUtil.consumeJsonPinValues(response);\n            assertEquals(1, values.size());\n            assertEquals(\"200\", values.get(0));\n        }\n\n        clientPair.appClient.send(\"hardware 1 vw 4 201\");\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(produce(1, HARDWARE, b(\"vw 4 201\"))));\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            List<String> values = TestUtil.consumeJsonPinValues(response);\n            assertEquals(1, values.size());\n            assertEquals(\"201\", values.get(0));\n        }\n    }\n\n    @Test\n    public void testRTCWorksViaHttpAPI() throws Exception {\n        RTC rtc = new RTC();\n        rtc.id = 434;\n        rtc.height = 1;\n        rtc.width = 2;\n\n        clientPair.appClient.createWidget(1, rtc);\n        clientPair.appClient.verifyResult(ok(1));\n\n        reset(clientPair.appClient.responseMock);\n\n        HttpGet request = new HttpGet(httpServerUrl + clientPair.token + \"/rtc\");\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            List<String> values = TestUtil.consumeJsonPinValues(response);\n            assertEquals(1, values.size());\n        }\n    }\n\n    @Test\n    public void testEventorWorksViaHttpAPI() throws Exception {\n        Eventor eventor = EventorTest.oneRuleEventor(\"if v100 = 37 then setpin v2 123\");\n        eventor.height = 1;\n        eventor.width = 2;\n\n        clientPair.appClient.createWidget(1, eventor);\n        clientPair.appClient.verifyResult(ok(1));\n\n        reset(clientPair.appClient.responseMock);\n\n        HttpPut request = new HttpPut(httpServerUrl + clientPair.token + \"/update/v100\");\n        request.setEntity(new StringEntity(\"[\\\"37\\\"]\", ContentType.APPLICATION_JSON));\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n        }\n\n        clientPair.appClient.verifyResult(hardware(111, \"1-0 vw 100 37\"));\n        clientPair.hardwareClient.verifyResult(hardware(111, \"vw 100 37\"));\n        clientPair.hardwareClient.verifyResult(hardware(888, \"vw 2 123\"));\n    }\n\n    @Test\n    public void testEventorTimerWidgeWorkerWorksAsExpectedWithHttp() throws Exception {\n        Executors.newScheduledThreadPool(1).scheduleAtFixedRate(holder.timerWorker, 0, 1000, TimeUnit.MILLISECONDS);\n\n        TimerTime timerTime = new TimerTime(\n                0,\n                new int[] {1,2,3,4,5,6,7},\n                //adding 2 seconds just to be sure we no gonna miss timer event\n                LocalTime.now(DateTimeUtils.UTC).toSecondOfDay() + 2,\n                DateTimeUtils.UTC\n        );\n\n        DataStream dataStream = new DataStream((short) 4, VIRTUAL);\n        SetPinAction setPinAction = new SetPinAction(dataStream, \"1\", SetPinActionType.CUSTOM);\n\n        Eventor eventor = new Eventor(new Rule[] {\n                new Rule(dataStream, timerTime, null, new BaseAction[] {setPinAction}, true)\n        });\n        eventor.id = 1000;\n\n        clientPair.appClient.createWidget(1, eventor);\n        clientPair.appClient.verifyResult(ok(1));\n\n        verify(clientPair.appClient.responseMock, timeout(3000)).channelRead(any(), eq(produce(TIMER_MSG_ID, HARDWARE, b(\"1-0 vw 4 1\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(3000)).channelRead(any(), eq(produce(TIMER_MSG_ID, HARDWARE, b(\"vw 4 1\"))));\n\n\n\n        clientPair.appClient.reset();\n        HttpGet requestGET = new HttpGet(httpServerUrl + clientPair.token + \"/get/v4\");\n\n        try (CloseableHttpResponse response = httpclient.execute(requestGET)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            List<String> values = TestUtil.consumeJsonPinValues(response);\n            assertEquals(1, values.size());\n            assertEquals(\"1\", values.get(0));\n        }\n    }\n\n    @Test\n    public void testTimerWidgeWorkerWorksAsExpectedWithHttp() throws Exception {\n        Executors.newScheduledThreadPool(1).scheduleAtFixedRate(holder.timerWorker, 0, 1000, TimeUnit.MILLISECONDS);\n        Timer timer = new Timer();\n        timer.id = 112;\n        timer.x = 1;\n        timer.y = 1;\n        timer.pinType = VIRTUAL;\n        timer.pin = 4;\n        timer.width = 2;\n        timer.height = 1;\n        timer.startValue = \"1\";\n        timer.stopValue = \"0\";\n        LocalTime localDateTime = LocalTime.now(ZoneId.of(\"UTC\"));\n        int curTime = localDateTime.toSecondOfDay();\n        timer.startTime = curTime + 1;\n        timer.stopTime = curTime + 1;\n\n        clientPair.appClient.createWidget(1, timer);\n        clientPair.appClient.verifyResult(ok(1));\n\n        verify(clientPair.hardwareClient.responseMock, timeout(2500).times(2)).channelRead(any(), any());\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(produce(7777, HARDWARE, b(\"vw 4 1\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(produce(7777, HARDWARE, b(\"vw 4 0\"))));\n\n        verify(clientPair.appClient.responseMock, timeout(2500).times(3)).channelRead(any(), any());\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(7777, HARDWARE, b(\"1-0 vw 4 1\"))));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(7777, HARDWARE, b(\"1-0 vw 4 0\"))));\n\n        clientPair.appClient.reset();\n        HttpGet requestGET = new HttpGet(httpServerUrl + clientPair.token + \"/get/v4\");\n\n        try (CloseableHttpResponse response = httpclient.execute(requestGET)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            List<String> values = TestUtil.consumeJsonPinValues(response);\n            assertEquals(1, values.size());\n            //todo order is not guarateed here!!! Known issue\n            String res = values.get(0);\n            assertTrue(\"0\".equals(res) || \"1\".equals(res));\n        }\n    }\n\n    @Test\n    public void testChangePinValueViaAppAndHardwareForWrongPWMButton() throws Exception {\n        clientPair.appClient.createWidget(1, \"{\\\"type\\\":\\\"BUTTON\\\",\\\"id\\\":1000,\\\"x\\\":0,\\\"y\\\":0,\\\"color\\\":616861439,\\\"width\\\":2,\\\"height\\\":2,\\\"label\\\":\\\"Relay\\\",\\\"pinType\\\":\\\"DIGITAL\\\",\\\"pin\\\":18,\\\"pwmMode\\\":true,\\\"rangeMappingOn\\\":false,\\\"min\\\":0,\\\"max\\\":0,\\\"value\\\":\\\"1\\\",\\\"pushMode\\\":false}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.reset();\n        HttpGet requestGET = new HttpGet(httpServerUrl + clientPair.token + \"/get/d18\");\n\n        try (CloseableHttpResponse response = httpclient.execute(requestGET)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            List<String> values = TestUtil.consumeJsonPinValues(response);\n            assertEquals(1, values.size());\n            assertEquals(\"1\", values.get(0));\n        }\n\n        HttpPut requestPUT = new HttpPut(httpServerUrl + clientPair.token + \"/update/d18\");\n        requestPUT.setEntity(new StringEntity(\"[\\\"0\\\"]\", ContentType.APPLICATION_JSON));\n\n        try (CloseableHttpResponse response = httpclient.execute(requestPUT)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n        }\n\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(produce(111, HARDWARE, b(\"dw 18 0\"))));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(111, HARDWARE, b(\"1-0 dw 18 0\"))));\n    }\n\n    @Test\n    public void testChangePinValueViaHttpAPI() throws Exception {\n        HttpPut request = new HttpPut(httpServerUrl + clientPair.token + \"/update/v4\");\n        HttpGet getRequest = new HttpGet(httpServerUrl + clientPair.token + \"/get/v4\");\n\n        for (int i = 0; i < 50; i++) {\n            request.setEntity(new StringEntity(\"[\\\"\" + i + \"\\\"]\", ContentType.APPLICATION_JSON));\n            try (CloseableHttpResponse response = httpclient.execute(request)) {\n                assertEquals(200, response.getStatusLine().getStatusCode());\n            }\n\n            clientPair.hardwareClient.sync(VIRTUAL, 4);\n            verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(produce(i + 1, HARDWARE, b(\"vw 4 \" + i))));\n\n            try (CloseableHttpResponse response = httpclient.execute(getRequest)) {\n                assertEquals(200, response.getStatusLine().getStatusCode());\n                List<String> values = TestUtil.consumeJsonPinValues(response);\n                assertEquals(1, values.size());\n                assertEquals(i, Integer.valueOf(values.get(0)).intValue());\n            }\n        }\n    }\n\n    @Test\n    public void testQRWorks() throws Exception {\n        HttpGet getRequest = new HttpGet(httpServerUrl + clientPair.token + \"/qr\");\n        String cloneToken;\n        try (CloseableHttpResponse response = httpclient.execute(getRequest)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            assertEquals(\"image/png\", response.getFirstHeader(\"Content-Type\").getValue());\n            byte[] data = EntityUtils.toByteArray(response.getEntity());\n            assertNotNull(data);\n\n            //get the data from the input stream\n            BufferedImage image = ImageIO.read(new ByteArrayInputStream(data));\n\n            //convert the image to a binary bitmap source\n            LuminanceSource source = new BufferedImageLuminanceSource(image);\n            BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));\n            QRCodeReader reader = new QRCodeReader();\n            Result result = reader.decode(bitmap);\n            String resultString = result.getText();\n            assertTrue(resultString.startsWith(\"blynk://token/clone/\"));\n            assertTrue(resultString.endsWith(\"?server=127.0.0.1&port=10443\"));\n            cloneToken = resultString.substring(\n                    resultString.indexOf(\"blynk://token/clone/\") + \"blynk://token/clone/\".length(),\n                    resultString.indexOf(\"?server=127.0.0.1&port=10443\"));\n            assertEquals(32, cloneToken.length());\n        }\n\n        clientPair.appClient.send(\"getProjectByCloneCode \" + cloneToken);\n        DashBoard dashBoard = clientPair.appClient.parseDash(1);\n        assertEquals(\"My Dashboard\", dashBoard.name);\n    }\n\n    @Test\n    public void testIsHardwareAndAppConnected() throws Exception {\n        HttpGet request = new HttpGet(httpServerUrl + clientPair.token + \"/isHardwareConnected\");\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            String value = consumeText(response);\n            assertNotNull(value);\n            assertEquals(\"true\", value);\n        }\n\n        request = new HttpGet(httpServerUrl + clientPair.token + \"/isAppConnected\");\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            String value = consumeText(response);\n            assertNotNull(value);\n            assertEquals(\"true\", value);\n        }\n    }\n\n    @Test\n    public void testIsHardwareAndAppDisconnected() throws Exception {\n        clientPair.stop();\n\n        HttpGet request = new HttpGet(httpServerUrl + clientPair.token + \"/isHardwareConnected\");\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            String value = consumeText(response);\n            assertNotNull(value);\n            assertEquals(\"false\", value);\n        }\n\n        request = new HttpGet(httpServerUrl + clientPair.token + \"/isAppConnected\");\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            String value = consumeText(response);\n            assertNotNull(value);\n            assertEquals(\"false\", value);\n        }\n    }\n\n    @Test\n    public void testIsHardwareConnecteedWithMultiDevices() throws Exception {\n        HttpGet request = new HttpGet(httpServerUrl + clientPair.token + \"/isHardwareConnected\");\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            String value = consumeText(response);\n            assertNotNull(value);\n            assertEquals(\"true\", value);\n        }\n\n        request = new HttpGet(httpServerUrl + clientPair.token + \"/isAppConnected\");\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            String value = consumeText(response);\n            assertNotNull(value);\n            assertEquals(\"true\", value);\n        }\n\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice(1);\n        assertNotNull(device);\n        assertNotNull(device.token);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(createDevice(1, device)));\n\n        clientPair.appClient.reset();\n\n        clientPair.appClient.send(\"getDevices 1\");\n        Device[] devices = clientPair.appClient.parseDevices();\n\n        assertNotNull(devices);\n        assertEquals(2, devices.length);\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", properties.getHttpPort());\n        hardClient2.start();\n\n        hardClient2.login(devices[1].token);\n        hardClient2.verifyResult(ok(1));\n\n        clientPair.stop();\n\n        request = new HttpGet(httpServerUrl + clientPair.token + \"/isHardwareConnected\");\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            String value = consumeText(response);\n            assertNotNull(value);\n            assertEquals(\"false\", value);\n        }\n\n        request = new HttpGet(httpServerUrl + clientPair.token + \"/isAppConnected\");\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            String value = consumeText(response);\n            assertNotNull(value);\n            assertEquals(\"false\", value);\n        }\n\n        request = new HttpGet(httpServerUrl + devices[1].token + \"/isHardwareConnected\");\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            String value = consumeText(response);\n            assertNotNull(value);\n            assertEquals(\"true\", value);\n        }\n    }\n\n    @Test\n    public void testChangePinValueViaHttpAPIAndNoActiveProject() throws Exception {\n        clientPair.appClient.deactivate(1);\n        clientPair.appClient.verifyResult(ok(1));\n\n        HttpPut request = new HttpPut(httpServerUrl + clientPair.token + \"/update/v31\");\n\n        request.setEntity(new StringEntity(\"[\\\"100\\\"]\", ContentType.APPLICATION_JSON));\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n        }\n\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(produce(111, HARDWARE, b(\"vw 31 100\"))));\n        verify(clientPair.appClient.responseMock, after(400).never()).channelRead(any(), eq(produce(111, HARDWARE, b(\"1 vw 31 100\"))));\n\n        clientPair.appClient.activate(1);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(ok(2)));\n    }\n\n    @Test\n    public void testChangeLCDPinValueViaHttpAPIAndValueChanged() throws Exception {\n        HttpPut request = new HttpPut(httpServerUrl + clientPair.token + \"/update/v0\");\n\n        request.setEntity(new StringEntity(\"[\\\"100\\\"]\", ContentType.APPLICATION_JSON));\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n        }\n\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(produce(111, HARDWARE, b(\"vw 0 100\"))));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(111, HARDWARE, b(\"1-0 vw 0 100\"))));\n\n        request = new HttpPut(httpServerUrl + clientPair.token + \"/update/v1\");\n\n        request.setEntity(new StringEntity(\"[\\\"101\\\"]\", ContentType.APPLICATION_JSON));\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n        }\n\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(produce(111, HARDWARE, b(\"vw 1 101\"))));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(111, HARDWARE, b(\"1-0 vw 1 101\"))));\n    }\n\n    @Test\n    public void testChangePinValueViaHttpAPIAndNoWidgetSinglePinValue() throws Exception {\n        HttpPut request = new HttpPut(httpServerUrl + clientPair.token + \"/update/v31\");\n\n        request.setEntity(new StringEntity(\"[\\\"100\\\"]\", ContentType.APPLICATION_JSON));\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n        }\n\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(produce(111, HARDWARE, b(\"vw 31 100\"))));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(111, HARDWARE, b(\"1-0 vw 31 100\"))));\n    }\n\n    @Test\n    public void testChangePinValueViaHttpAPIAndForTerminal() throws Exception {\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":222, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":2, \\\"y\\\":2, \\\"label\\\":\\\"Some Text 2\\\", \\\"type\\\":\\\"TERMINAL\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":100}\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(ok(1)));\n\n        HttpPut request = new HttpPut(httpServerUrl + clientPair.token + \"/update/V100\");\n\n        request.setEntity(new StringEntity(\"[\\\"100\\\"]\", ContentType.APPLICATION_JSON));\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n        }\n\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(produce(111, HARDWARE, b(\"vw 100 100\"))));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(111, HARDWARE, b(\"1-0 vw 100 100\"))));\n    }\n\n    @Test\n    public void testChangePinValueViaHttpAPIAndNoWidgetMultiPinValue() throws Exception {\n        HttpPut request = new HttpPut(httpServerUrl + clientPair.token + \"/update/v31\");\n\n        request.setEntity(new StringEntity(\"[\\\"100\\\",\\\"101\\\",\\\"102\\\"]\", ContentType.APPLICATION_JSON));\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n        }\n\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(produce(111, HARDWARE, b(\"vw 31 100 101 102\"))));\n    }\n\n    @Test\n    public void tableSetValueViaHttpApi() throws Exception {\n        Table table = new Table();\n        table.pin = 123;\n        table.pinType = VIRTUAL;\n        table.isClickableRows = true;\n        table.isReoderingAllowed = true;\n        table.height = 2;\n        table.width = 2;\n\n        clientPair.appClient.createWidget(1, table);\n        clientPair.appClient.verifyResult(ok(1));\n\n        HttpGet updateTableRow = new HttpGet(httpServerUrl + clientPair.token + \"/update/v123?value=add&value=2&value=Martes&value=120Kwh\");\n        try (CloseableHttpResponse response = httpclient.execute(updateTableRow)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n        }\n    }\n\n    @Test\n    public void sendMultiValueToAppViaHttpApi() throws Exception {\n        HttpGet updateTableRow = new HttpGet(httpServerUrl + clientPair.token + \"/update/V1?value=110&value=230&value=330\");\n        try (CloseableHttpResponse response = httpclient.execute(updateTableRow)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n        }\n\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(),\n                eq(produce(111, HARDWARE, b(\"vw 1 110 230 330\"))));\n    }\n\n    @Test\n    public void sendMultiValueToAppViaHttpApi2() throws Exception {\n        RGB rgb = new RGB();\n        rgb.dataStreams = new DataStream[] {\n                new DataStream((short) 101, VIRTUAL)\n        };\n        rgb.splitMode = false;\n        rgb.height = 2;\n        rgb.width = 2;\n\n        clientPair.appClient.createWidget(1, rgb);\n        clientPair.appClient.verifyResult(ok(1));\n\n        HttpGet updateTableRow = new HttpGet(httpServerUrl + clientPair.token + \"/update/V101?value=110&value=230&value=330\");\n        try (CloseableHttpResponse response = httpclient.execute(updateTableRow)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n        }\n\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(),\n                eq(produce(111, HARDWARE, b(\"vw 101 110 230 330\"))));\n    }\n\n    @Test\n    public void sendMultiValueToAppViaHttpApi3() throws Exception {\n        RGB rgb = new RGB();\n        rgb.dataStreams = new DataStream[] {\n                new DataStream((short) 101, VIRTUAL),\n                new DataStream((short) 102, VIRTUAL),\n                new DataStream((short) 103, VIRTUAL)\n        };\n        rgb.splitMode = false;\n        rgb.height = 2;\n        rgb.width = 2;\n\n        clientPair.appClient.createWidget(1, rgb);\n        clientPair.appClient.verifyResult(ok(1));\n\n        HttpGet updateTableRow = new HttpGet(httpServerUrl + clientPair.token + \"/update/V101?value=110&value=230&value=330\");\n        try (CloseableHttpResponse response = httpclient.execute(updateTableRow)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n        }\n\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(),\n                eq(produce(111, HARDWARE, b(\"vw 101 110 230 330\"))));\n    }\n\n    @Test\n    public void superchartPinsOverlapsWithOtherWidgets() throws Exception {\n        Superchart superchart = new Superchart();\n        superchart.id = 100;\n        superchart.width = 8;\n        superchart.height = 4;\n        DataStream dataStream = new DataStream((short) 44, PinType.VIRTUAL);\n        DataStream dataStream2 = new DataStream((short) 45, PinType.VIRTUAL);\n        GraphDataStream graphDataStream = new GraphDataStream(null, GraphType.LINE, 0, 0, dataStream, null, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        GraphDataStream graphDataStream2 = new GraphDataStream(null, GraphType.LINE, 0, 0, dataStream2, null, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        superchart.dataStreams = new GraphDataStream[] {\n                graphDataStream,\n                graphDataStream2\n        };\n\n        clientPair.appClient.createWidget(1, superchart);\n        clientPair.appClient.verifyResult(ok(1));\n\n        ValueDisplay valueDisplay = new ValueDisplay();\n        valueDisplay.id = 101;\n        valueDisplay.height = 2;\n        valueDisplay.width = 2;\n        valueDisplay.pin = 44;\n        valueDisplay.pinType = VIRTUAL;\n\n        clientPair.appClient.createWidget(1, valueDisplay);\n        clientPair.appClient.verifyResult(ok(2));\n\n        ValueDisplay valueDisplay2 = new ValueDisplay();\n        valueDisplay2.id = 102;\n        valueDisplay2.height = 2;\n        valueDisplay2.width = 2;\n        valueDisplay2.pin = 45;\n        valueDisplay2.pinType = VIRTUAL;\n\n        clientPair.appClient.createWidget(1, valueDisplay2);\n        clientPair.appClient.verifyResult(ok(3));\n\n        clientPair.hardwareClient.send(\"hardware vw 44 123\");\n        clientPair.hardwareClient.send(\"hardware vw 45 124\");\n        clientPair.appClient.verifyResult(hardware(2, \"1-0 vw 45 124\"));\n\n        HttpGet request = new HttpGet(httpServerUrl + clientPair.token + \"/get/v45\");\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            List<String> values = TestUtil.consumeJsonPinValues(response);\n            assertEquals(1, values.size());\n            assertEquals(\"124\", values.get(0));\n        }\n    }\n\n    @Test\n    public void webhookPinsOverlapsWithOtherWidgets() throws Exception {\n        WebHook webHook = new WebHook();\n        webHook.id = 100;\n        webHook.width = 2;\n        webHook.height = 2;\n        webHook.pin = 44;\n        webHook.pinType = VIRTUAL;\n\n        clientPair.appClient.createWidget(1, webHook);\n        clientPair.appClient.verifyResult(ok(1));\n\n        ValueDisplay valueDisplay = new ValueDisplay();\n        valueDisplay.id = 101;\n        valueDisplay.height = 2;\n        valueDisplay.width = 2;\n        valueDisplay.pin = 44;\n        valueDisplay.pinType = VIRTUAL;\n\n        clientPair.appClient.createWidget(1, valueDisplay);\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.hardwareClient.send(\"hardware vw 44 123\");\n\n        HttpGet request = new HttpGet(httpServerUrl + clientPair.token + \"/get/v44\");\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            List<String> values = TestUtil.consumeJsonPinValues(response);\n            assertEquals(1, values.size());\n            assertEquals(\"123\", values.get(0));\n        }\n    }\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/http/OTATest.java",
    "content": "package cc.blynk.integration.http;\n\nimport cc.blynk.integration.BaseTest;\nimport cc.blynk.integration.MyHostVerifier;\nimport cc.blynk.integration.TestUtil;\nimport cc.blynk.integration.model.tcp.ClientPair;\nimport cc.blynk.integration.model.tcp.TestHardClient;\nimport cc.blynk.server.core.dao.ota.OTAManager;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.servers.BaseServer;\nimport cc.blynk.server.servers.application.MobileAndHttpsServer;\nimport cc.blynk.server.servers.hardware.HardwareAndHttpAPIServer;\nimport cc.blynk.utils.SHA256Util;\nimport io.netty.handler.codec.http.HttpHeaderNames;\nimport org.apache.http.HttpEntity;\nimport org.apache.http.client.config.CookieSpecs;\nimport org.apache.http.client.config.RequestConfig;\nimport org.apache.http.client.methods.CloseableHttpResponse;\nimport org.apache.http.client.methods.HttpGet;\nimport org.apache.http.client.methods.HttpPost;\nimport org.apache.http.conn.ssl.SSLConnectionSocketFactory;\nimport org.apache.http.entity.ContentType;\nimport org.apache.http.entity.mime.HttpMultipartMode;\nimport org.apache.http.entity.mime.MultipartEntityBuilder;\nimport org.apache.http.entity.mime.content.ContentBody;\nimport org.apache.http.entity.mime.content.InputStreamBody;\nimport org.apache.http.impl.client.CloseableHttpClient;\nimport org.apache.http.impl.client.HttpClients;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport javax.net.ssl.SSLContext;\nimport java.io.File;\nimport java.io.InputStream;\nimport java.nio.file.Path;\nimport java.util.Base64;\n\nimport static cc.blynk.integration.TestUtil.b;\nimport static cc.blynk.integration.TestUtil.initUnsecuredSSLContext;\nimport static cc.blynk.integration.TestUtil.internal;\nimport static cc.blynk.integration.TestUtil.ok;\nimport static cc.blynk.server.core.protocol.enums.Command.BLYNK_INTERNAL;\nimport static cc.blynk.server.core.protocol.model.messages.MessageFactory.produce;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotEquals;\nimport static org.junit.Assert.assertNotNull;\nimport static org.junit.Assert.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.after;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.timeout;\nimport static org.mockito.Mockito.verify;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 24.12.15.\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class OTATest extends BaseTest {\n\n    private BaseServer httpServer;\n    private BaseServer httpsServer;\n\n    private CloseableHttpClient httpclient;\n    private String httpsAdminServerUrl;\n\n    private ClientPair clientPair;\n    private byte[] auth;\n\n    @After\n    public void shutdown() throws Exception {\n        httpclient.close();\n        httpServer.close();\n        httpsServer.close();\n        clientPair.stop();\n    }\n\n    @Before\n    public void init() throws Exception {\n        httpServer = new HardwareAndHttpAPIServer(holder).start();\n        httpsServer = new MobileAndHttpsServer(holder).start();\n        httpsAdminServerUrl = String.format(\"https://localhost:%s/admin\", properties.getHttpsPort());\n\n        String pass = \"admin\";\n        User user = new User();\n        user.isSuperAdmin = true;\n        user.email = \"admin@blynk.cc\";\n        user.pass = SHA256Util.makeHash(pass, user.email);\n        holder.userDao.add(user);\n\n        auth = (user.email + \":\" + pass).getBytes();\n\n        // Allow TLSv1 protocol only\n        SSLContext sslcontext = initUnsecuredSSLContext();\n        SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslcontext, new MyHostVerifier());\n        this.httpclient = HttpClients.custom()\n                .setSSLSocketFactory(sslsf)\n                .setDefaultRequestConfig(RequestConfig.custom().setCookieSpec(CookieSpecs.STANDARD).build())\n                .build();\n        clientPair = initAppAndHardPair(properties);\n    }\n\n    @Test\n    public void testInitiateOTA() throws Exception {\n        HttpGet request = new HttpGet(httpsAdminServerUrl + \"/ota/start?token=\" + clientPair.token);\n        request.setHeader(HttpHeaderNames.AUTHORIZATION.toString(), \"Basic \" + Base64.getEncoder().encodeToString(auth));\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            assertEquals(\"\", TestUtil.consumeText(response));\n        }\n\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(produce(7777, BLYNK_INTERNAL, b(\"ota http://127.0.0.1:18080/static/ota/firmware_ota.bin\"))));\n    }\n\n    @Test\n    public void testInitiateOTAWithFileName() throws Exception {\n        HttpGet request = new HttpGet(httpsAdminServerUrl + \"/ota/start?fileName=test.bin\" + \"&token=\" + clientPair.token);\n        request.setHeader(HttpHeaderNames.AUTHORIZATION.toString(), \"Basic \" + Base64.getEncoder().encodeToString(auth));\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            assertEquals(\"\", TestUtil.consumeText(response));\n        }\n\n        String expectedResult = \"http://127.0.0.1:18080/static/ota/test.bin\";\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(produce(7777, BLYNK_INTERNAL, b(\"ota \" + expectedResult))));\n\n        request = new HttpGet(httpsAdminServerUrl + \"/ota/start?fileName=test.bin\" + \"&token=\" + clientPair.token);\n        request.setHeader(HttpHeaderNames.AUTHORIZATION.toString(), \"Basic \" + Base64.getEncoder().encodeToString(auth));\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            assertEquals(\"\", TestUtil.consumeText(response));\n        }\n    }\n\n    @Test\n    public void testImprovedUploadMethod() throws Exception {\n        HttpPost post = new HttpPost(httpsAdminServerUrl + \"/ota/start?token=\" + clientPair.token);\n        post.setHeader(HttpHeaderNames.AUTHORIZATION.toString(), \"Basic \" + Base64.getEncoder().encodeToString(auth));\n\n        String fileName = \"test.bin\";\n\n        InputStream binFile = OTATest.class.getResourceAsStream(\"/static/ota/\" + fileName);\n        ContentBody fileBody = new InputStreamBody(binFile, ContentType.APPLICATION_OCTET_STREAM, fileName);\n\n        MultipartEntityBuilder builder = MultipartEntityBuilder.create();\n        builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);\n        builder.addPart(\"upfile\", fileBody);\n        HttpEntity entity = builder.build();\n\n        post.setEntity(entity);\n\n        String path;\n        try (CloseableHttpResponse response = httpclient.execute(post)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            path = TestUtil.consumeText(response);\n\n            assertNotNull(path);\n            assertTrue(path.startsWith(\"/static\"));\n            assertTrue(path.endsWith(\"bin\"));\n        }\n\n        String responseUrl = \"http://127.0.0.1:18080\" + path;\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(internal(7777, \"ota \" + responseUrl)));\n\n        HttpGet index = new HttpGet(\"http://localhost:\" + properties.getHttpPort() + path);\n\n        try (CloseableHttpResponse response = httpclient.execute(index)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            assertEquals(\"application/octet-stream\", response.getHeaders(\"Content-Type\")[0].getValue());\n        }\n    }\n\n    @Test\n    public void testOTAFailedWhenNoDevice() throws Exception {\n        clientPair.hardwareClient.stop();\n\n        HttpPost post = new HttpPost(httpsAdminServerUrl + \"/ota/start?token=\" + clientPair.token);\n        post.setHeader(HttpHeaderNames.AUTHORIZATION.toString(), \"Basic \" + Base64.getEncoder().encodeToString(auth));\n\n        String fileName = \"test.bin\";\n\n        InputStream binFile = OTATest.class.getResourceAsStream(\"/static/ota/\" + fileName);\n        ContentBody fileBody = new InputStreamBody(binFile, ContentType.APPLICATION_OCTET_STREAM, fileName);\n\n        MultipartEntityBuilder builder = MultipartEntityBuilder.create();\n        builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);\n        builder.addPart(\"upfile\", fileBody);\n        HttpEntity entity = builder.build();\n\n        post.setEntity(entity);\n\n        try (CloseableHttpResponse response = httpclient.execute(post)) {\n            assertEquals(400, response.getStatusLine().getStatusCode());\n            String error = TestUtil.consumeText(response);\n\n            assertNotNull(error);\n            assertEquals(\"No device in session.\", error);\n        }\n    }\n\n    @Test\n    public void testOTAWrongToken() throws Exception {\n        HttpPost post = new HttpPost(httpsAdminServerUrl + \"/ota/start?token=\" + 123);\n        post.setHeader(HttpHeaderNames.AUTHORIZATION.toString(), \"Basic \" + Base64.getEncoder().encodeToString(auth));\n\n        String fileName = \"test.bin\";\n\n        InputStream binFile = OTATest.class.getResourceAsStream(\"/static/ota/\" + fileName);\n        ContentBody fileBody = new InputStreamBody(binFile, ContentType.APPLICATION_OCTET_STREAM, fileName);\n\n        MultipartEntityBuilder builder = MultipartEntityBuilder.create();\n        builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);\n        builder.addPart(\"upfile\", fileBody);\n        HttpEntity entity = builder.build();\n\n        post.setEntity(entity);\n\n        try (CloseableHttpResponse response = httpclient.execute(post)) {\n            assertEquals(400, response.getStatusLine().getStatusCode());\n            String error = TestUtil.consumeText(response);\n\n            assertNotNull(error);\n            assertEquals(\"Invalid token.\", error);\n        }\n    }\n\n    @Test\n    public void testAuthorizationFailed() throws Exception {\n        HttpPost post = new HttpPost(httpsAdminServerUrl + \"/ota/start?token=\" + 123);\n        post.setHeader(HttpHeaderNames.AUTHORIZATION.toString(), \"Basic \" + Base64.getEncoder().encodeToString(\"123:123\".getBytes()));\n\n        String fileName = \"test.bin\";\n\n        InputStream binFile = OTATest.class.getResourceAsStream(\"/static/ota/\" + fileName);\n        ContentBody fileBody = new InputStreamBody(binFile, ContentType.APPLICATION_OCTET_STREAM, fileName);\n\n        MultipartEntityBuilder builder = MultipartEntityBuilder.create();\n        builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);\n        builder.addPart(\"upfile\", fileBody);\n        HttpEntity entity = builder.build();\n\n        post.setEntity(entity);\n\n        try (CloseableHttpResponse response = httpclient.execute(post)) {\n            assertEquals(403, response.getStatusLine().getStatusCode());\n            String error = TestUtil.consumeText(response);\n\n            assertNotNull(error);\n            assertEquals(\"Authentication failed.\", error);\n        }\n    }\n\n    @Test\n    public void testImprovedUploadMethodAndCheckOTAStatusForDeviceThatNeverWasOnline() throws Exception {\n        HttpPost post = new HttpPost(httpsAdminServerUrl + \"/ota/start?token=\" + clientPair.token);\n        post.setHeader(HttpHeaderNames.AUTHORIZATION.toString(), \"Basic \" + Base64.getEncoder().encodeToString(auth));\n\n        String fileName = \"test.bin\";\n\n        InputStream binFile = OTATest.class.getResourceAsStream(\"/static/ota/\" + fileName);\n        ContentBody fileBody = new InputStreamBody(binFile, ContentType.APPLICATION_OCTET_STREAM, fileName);\n\n        MultipartEntityBuilder builder = MultipartEntityBuilder.create();\n        builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);\n        builder.addPart(\"upfile\", fileBody);\n        HttpEntity entity = builder.build();\n\n        post.setEntity(entity);\n\n        String path;\n        try (CloseableHttpResponse response = httpclient.execute(post)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            path = TestUtil.consumeText(response);\n\n            assertNotNull(path);\n            assertTrue(path.startsWith(\"/static\"));\n            assertTrue(path.endsWith(\"bin\"));\n        }\n\n        String responseUrl = \"http://127.0.0.1:18080\" + path;\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(internal(7777, \"ota \" + responseUrl)));\n\n        clientPair.appClient.getDevice(1, 0);\n\n        Device device = clientPair.appClient.parseDevice(1);\n        assertNotNull(device);\n        assertEquals(\"admin@blynk.cc\", device.deviceOtaInfo.otaInitiatedBy);\n        assertEquals(System.currentTimeMillis(), device.deviceOtaInfo.otaInitiatedAt, 5000);\n        assertEquals(System.currentTimeMillis(), device.deviceOtaInfo.otaUpdateAt, 5000);\n\n        clientPair.hardwareClient.send(\"internal \" + b(\"ver 0.3.1 h-beat 10 buff-in 256 dev Arduino cpu ATmega328P con W5100 build 111\"));\n\n        assertEquals(\"admin@blynk.cc\", device.deviceOtaInfo.otaInitiatedBy);\n        assertEquals(System.currentTimeMillis(), device.deviceOtaInfo.otaInitiatedAt, 5000);\n        assertEquals(System.currentTimeMillis(), device.deviceOtaInfo.otaUpdateAt, 5000);\n    }\n\n    @Test\n    public void testImprovedUploadMethodAndCheckOTAStatusForDeviceThatWasOnline() throws Exception {\n        clientPair.hardwareClient.send(\"internal \" + b(\"ver 0.3.1 fm 0.3.3 h-beat 10 buff-in 256 dev Arduino cpu ATmega328P con W5100 build 111\"));\n        clientPair.hardwareClient.verifyResult(ok(1));\n\n        HttpPost post = new HttpPost(httpsAdminServerUrl + \"/ota/start?token=\" + clientPair.token);\n        post.setHeader(HttpHeaderNames.AUTHORIZATION.toString(), \"Basic \" + Base64.getEncoder().encodeToString(auth));\n\n        String fileName = \"test.bin\";\n\n        InputStream binFile = OTATest.class.getResourceAsStream(\"/static/ota/\" + fileName);\n        ContentBody fileBody = new InputStreamBody(binFile, ContentType.APPLICATION_OCTET_STREAM, fileName);\n\n        MultipartEntityBuilder builder = MultipartEntityBuilder.create();\n        builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);\n        builder.addPart(\"upfile\", fileBody);\n        HttpEntity entity = builder.build();\n\n        post.setEntity(entity);\n\n        String path;\n        try (CloseableHttpResponse response = httpclient.execute(post)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            path = TestUtil.consumeText(response);\n\n            assertNotNull(path);\n            assertTrue(path.startsWith(\"/static\"));\n            assertTrue(path.endsWith(\"bin\"));\n        }\n\n        String responseUrl = \"http://127.0.0.1:18080\" + path;\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(internal(7777, \"ota \" + responseUrl)));\n\n        clientPair.appClient.getDevice(1, 0);\n        Device device = clientPair.appClient.parseDevice(1);\n\n        assertNotNull(device);\n\n        assertEquals(\"0.3.1\", device.hardwareInfo.blynkVersion);\n        assertEquals(10, device.hardwareInfo.heartbeatInterval);\n        assertEquals(\"111\", device.hardwareInfo.build);\n        assertEquals(\"admin@blynk.cc\", device.deviceOtaInfo.otaInitiatedBy);\n        assertEquals(System.currentTimeMillis(), device.deviceOtaInfo.otaInitiatedAt, 5000);\n        assertEquals(System.currentTimeMillis(), device.deviceOtaInfo.otaUpdateAt, 5000);\n\n        clientPair.hardwareClient.send(\"internal \" + b(\"ver 0.3.1 fm 0.3.3 h-beat 10 buff-in 256 dev Arduino cpu ATmega328P con W5100 build 112\"));\n        clientPair.hardwareClient.verifyResult(ok(2));\n\n        clientPair.appClient.getDevice(1, 0);\n        device = clientPair.appClient.parseDevice(2);\n\n        assertNotNull(device);\n\n        assertEquals(\"0.3.1\", device.hardwareInfo.blynkVersion);\n        assertEquals(10, device.hardwareInfo.heartbeatInterval);\n        assertEquals(\"112\", device.hardwareInfo.build);\n        assertEquals(\"admin@blynk.cc\", device.deviceOtaInfo.otaInitiatedBy);\n        assertEquals(System.currentTimeMillis(), device.deviceOtaInfo.otaInitiatedAt, 5000);\n        assertEquals(System.currentTimeMillis(), device.deviceOtaInfo.otaUpdateAt, 5000);\n    }\n\n    @Test\n    public void takeBuildDateFromBinaryFile() {\n        String fileName = \"test.bin\";\n        Path path = new File(\"src/test/resources/static/ota/\" + fileName).toPath();\n\n        assertEquals(\"Aug 14 2017 20:31:49\", OTAManager.getBuildPatternFromString(path));\n    }\n\n    @Test\n    public void basicOTAForAllDevices() throws Exception {\n        HttpPost post = new HttpPost(httpsAdminServerUrl + \"/ota/start\");\n        post.setHeader(HttpHeaderNames.AUTHORIZATION.toString(), \"Basic \" + Base64.getEncoder().encodeToString(auth));\n\n        String fileName = \"test.bin\";\n\n        InputStream binFile = OTATest.class.getResourceAsStream(\"/static/ota/\" + fileName);\n        ContentBody fileBody = new InputStreamBody(binFile, ContentType.APPLICATION_OCTET_STREAM, fileName);\n\n        MultipartEntityBuilder builder = MultipartEntityBuilder.create();\n        builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);\n        builder.addPart(\"upfile\", fileBody);\n        HttpEntity entity = builder.build();\n\n        post.setEntity(entity);\n\n        String path;\n        try (CloseableHttpResponse response = httpclient.execute(post)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            path = TestUtil.consumeText(response);\n\n            assertNotNull(path);\n            assertTrue(path.startsWith(\"/static\"));\n            assertTrue(path.endsWith(\"bin\"));\n        }\n\n        String responseUrl = \"http://127.0.0.1:18080\" + path;\n        verify(clientPair.hardwareClient.responseMock, after(500).never()).channelRead(any(), eq(internal(7777, \"ota \" + responseUrl)));\n\n        HttpGet index = new HttpGet(\"http://localhost:\" + properties.getHttpPort() + path);\n\n        try (CloseableHttpResponse response = httpclient.execute(index)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            assertEquals(\"application/octet-stream\", response.getHeaders(\"Content-Type\")[0].getValue());\n        }\n    }\n\n    @Test\n    public void testConnectedDeviceGotOTACommand() throws Exception {\n        HttpPost post = new HttpPost(httpsAdminServerUrl + \"/ota/start\");\n        post.setHeader(HttpHeaderNames.AUTHORIZATION.toString(), \"Basic \" + Base64.getEncoder().encodeToString(auth));\n\n        String fileName = \"test.bin\";\n\n        InputStream binFile = OTATest.class.getResourceAsStream(\"/static/ota/\" + fileName);\n        ContentBody fileBody = new InputStreamBody(binFile, ContentType.APPLICATION_OCTET_STREAM, fileName);\n\n        MultipartEntityBuilder builder = MultipartEntityBuilder.create();\n        builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);\n        builder.addPart(\"upfile\", fileBody);\n        HttpEntity entity = builder.build();\n\n        post.setEntity(entity);\n\n        String path;\n        try (CloseableHttpResponse response = httpclient.execute(post)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            path = TestUtil.consumeText(response);\n        }\n\n        String responseUrl = \"http://127.0.0.1:18080\" + path;\n        verify(clientPair.hardwareClient.responseMock, after(500).never()).channelRead(any(), eq(internal(7777, \"ota \" + responseUrl)));\n\n        clientPair.hardwareClient.send(\"internal \" + b(\"ver 0.3.1 h-beat 10 buff-in 256 dev Arduino cpu ATmega328P con W5100 build 123\"));\n        clientPair.hardwareClient.verifyResult(ok(1));\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(internal(7777, \"ota \" + responseUrl)));\n        clientPair.hardwareClient.reset();\n\n        clientPair.appClient.getDevice(1, 0);\n        Device device = clientPair.appClient.parseDevice();\n\n        assertNotNull(device);\n        assertNotNull(device.deviceOtaInfo);\n        assertEquals(\"admin@blynk.cc\", device.deviceOtaInfo.otaInitiatedBy);\n        assertEquals(System.currentTimeMillis(), device.deviceOtaInfo.otaInitiatedAt, 5000);\n        assertEquals(System.currentTimeMillis(), device.deviceOtaInfo.otaInitiatedAt, 5000);\n        assertNotEquals(device.deviceOtaInfo.otaInitiatedAt, device.deviceOtaInfo.otaUpdateAt);\n        assertEquals(\"123\", device.hardwareInfo.build);\n\n        clientPair.hardwareClient.send(\"internal \" + b(\"ver 0.3.1 h-beat 10 buff-in 256 dev Arduino cpu ATmega328P con W5100 build \") + \"Aug 14 2017 20:31:49\");\n        clientPair.hardwareClient.verifyResult(ok(1));\n        verify(clientPair.hardwareClient.responseMock, after(500).never()).channelRead(any(), eq(internal(7777, \"ota \" + responseUrl)));\n\n    }\n\n    @Test\n    public void testStopOTA() throws Exception {\n        HttpPost post = new HttpPost(httpsAdminServerUrl + \"/ota/start\");\n        post.setHeader(HttpHeaderNames.AUTHORIZATION.toString(), \"Basic \" + Base64.getEncoder().encodeToString(auth));\n\n        String fileName = \"test.bin\";\n\n        InputStream binFile = OTATest.class.getResourceAsStream(\"/static/ota/\" + fileName);\n        ContentBody fileBody = new InputStreamBody(binFile, ContentType.APPLICATION_OCTET_STREAM, fileName);\n\n        MultipartEntityBuilder builder = MultipartEntityBuilder.create();\n        builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);\n        builder.addPart(\"upfile\", fileBody);\n        HttpEntity entity = builder.build();\n\n        post.setEntity(entity);\n\n        String path;\n        try (CloseableHttpResponse response = httpclient.execute(post)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            path = TestUtil.consumeText(response);\n\n            assertNotNull(path);\n            assertTrue(path.startsWith(\"/static\"));\n            assertTrue(path.endsWith(\"bin\"));\n        }\n        String responseUrl = \"http://127.0.0.1:18080\" + path;\n\n        HttpGet stopOta = new HttpGet(httpsAdminServerUrl + \"/ota/stop\");\n        stopOta.setHeader(HttpHeaderNames.AUTHORIZATION.toString(), \"Basic \" + Base64.getEncoder().encodeToString(auth));\n\n        try (CloseableHttpResponse response = httpclient.execute(stopOta)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n        }\n\n        clientPair.hardwareClient.send(\"internal \" + b(\"ver 0.3.1 h-beat 10 buff-in 256 dev Arduino cpu ATmega328P con W5100 build 111\"));\n        clientPair.hardwareClient.verifyResult(ok(1));\n        verify(clientPair.hardwareClient.responseMock, never()).channelRead(any(), eq(internal(7777, \"ota \" + responseUrl)));\n    }\n\n    @Test\n    public void basicOTAForNonExistingSingleUser() throws Exception {\n        HttpPost post = new HttpPost(httpsAdminServerUrl + \"/ota/start?user=dimaxxx@mail.ua\");\n        post.setHeader(HttpHeaderNames.AUTHORIZATION.toString(), \"Basic \" + Base64.getEncoder().encodeToString(auth));\n\n        String fileName = \"test.bin\";\n\n        InputStream binFile = OTATest.class.getResourceAsStream(\"/static/ota/\" + fileName);\n        ContentBody fileBody = new InputStreamBody(binFile, ContentType.APPLICATION_OCTET_STREAM, fileName);\n\n        MultipartEntityBuilder builder = MultipartEntityBuilder.create();\n        builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);\n        builder.addPart(\"upfile\", fileBody);\n        HttpEntity entity = builder.build();\n\n        post.setEntity(entity);\n\n        try (CloseableHttpResponse response = httpclient.execute(post)) {\n            assertEquals(400, response.getStatusLine().getStatusCode());\n            String er = TestUtil.consumeText(response);\n            assertNotNull(er);\n            assertEquals(\"Requested user not found.\", er);\n        }\n    }\n\n    @Test\n    public void basicOTAForSingleUser() throws Exception {\n        HttpPost post = new HttpPost(httpsAdminServerUrl + \"/ota/start?user=\" + getUserName());\n        post.setHeader(HttpHeaderNames.AUTHORIZATION.toString(), \"Basic \" + Base64.getEncoder().encodeToString(auth));\n\n        String fileName = \"test.bin\";\n\n        InputStream binFile = OTATest.class.getResourceAsStream(\"/static/ota/\" + fileName);\n        ContentBody fileBody = new InputStreamBody(binFile, ContentType.APPLICATION_OCTET_STREAM, fileName);\n\n        MultipartEntityBuilder builder = MultipartEntityBuilder.create();\n        builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);\n        builder.addPart(\"upfile\", fileBody);\n        HttpEntity entity = builder.build();\n\n        post.setEntity(entity);\n\n        String path;\n        try (CloseableHttpResponse response = httpclient.execute(post)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            path = TestUtil.consumeText(response);\n\n            assertNotNull(path);\n            assertTrue(path.startsWith(\"/static\"));\n            assertTrue(path.endsWith(\"bin\"));\n        }\n\n        String responseUrl = \"http://127.0.0.1:18080\" + path;\n        verify(clientPair.hardwareClient.responseMock, after(500).never()).channelRead(any(), eq(internal(7777, \"ota \" + responseUrl)));\n\n        TestHardClient newHardwareClient = new TestHardClient(\"localhost\", properties.getHttpPort());\n        newHardwareClient.start();\n        newHardwareClient.login(clientPair.token);\n        verify(newHardwareClient.responseMock, timeout(1000)).channelRead(any(), eq(ok(1)));\n        newHardwareClient.reset();\n\n        newHardwareClient.send(\"internal \" + b(\"ver 0.3.1 h-beat 10 buff-in 256 dev Arduino cpu ATmega328P con W5100 build 111\"));\n        verify(newHardwareClient.responseMock, timeout(500)).channelRead(any(), eq(ok(1)));\n        verify(newHardwareClient.responseMock, timeout(500)).channelRead(any(), eq(internal(7777, \"ota \" + responseUrl)));\n    }\n\n    @Test\n    public void basicOTAForSingleUserAndNonExistingProject() throws Exception {\n        HttpPost post = new HttpPost(httpsAdminServerUrl + \"/ota/start?user=\" + getUserName() + \"&project=123\");\n        post.setHeader(HttpHeaderNames.AUTHORIZATION.toString(), \"Basic \" + Base64.getEncoder().encodeToString(auth));\n\n        String fileName = \"test.bin\";\n\n        InputStream binFile = OTATest.class.getResourceAsStream(\"/static/ota/\" + fileName);\n        ContentBody fileBody = new InputStreamBody(binFile, ContentType.APPLICATION_OCTET_STREAM, fileName);\n\n        MultipartEntityBuilder builder = MultipartEntityBuilder.create();\n        builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);\n        builder.addPart(\"upfile\", fileBody);\n        HttpEntity entity = builder.build();\n\n        post.setEntity(entity);\n\n        String path;\n        try (CloseableHttpResponse response = httpclient.execute(post)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            path = TestUtil.consumeText(response);\n\n            assertNotNull(path);\n            assertTrue(path.startsWith(\"/static\"));\n            assertTrue(path.endsWith(\"bin\"));\n        }\n\n        String responseUrl = \"http://127.0.0.1:18080\" + path;\n        verify(clientPair.hardwareClient.responseMock, after(500).never()).channelRead(any(), eq(internal(7777, \"ota \" + responseUrl)));\n\n        TestHardClient newHardwareClient = new TestHardClient(\"localhost\", properties.getHttpPort());\n        newHardwareClient.start();\n        newHardwareClient.login(clientPair.token);\n        verify(newHardwareClient.responseMock, timeout(1000)).channelRead(any(), eq(ok(1)));\n        newHardwareClient.reset();\n\n        newHardwareClient.send(\"internal \" + b(\"ver 0.3.1 h-beat 10 buff-in 256 dev Arduino cpu ATmega328P con W5100 build 111\"));\n        verify(newHardwareClient.responseMock, timeout(500)).channelRead(any(), eq(ok(1)));\n        verify(clientPair.hardwareClient.responseMock, never()).channelRead(any(), eq(internal(7777, \"ota \" + responseUrl)));\n    }\n\n    @Test\n    public void basicOTAForSingleUserAndExistingProject() throws Exception {\n        HttpPost post = new HttpPost(httpsAdminServerUrl + \"/ota/start?user=\" + getUserName() + \"&project=My%20Dashboard\");\n        post.setHeader(HttpHeaderNames.AUTHORIZATION.toString(), \"Basic \" + Base64.getEncoder().encodeToString(auth));\n\n        String fileName = \"test.bin\";\n\n        InputStream binFile = OTATest.class.getResourceAsStream(\"/static/ota/\" + fileName);\n        ContentBody fileBody = new InputStreamBody(binFile, ContentType.APPLICATION_OCTET_STREAM, fileName);\n\n        MultipartEntityBuilder builder = MultipartEntityBuilder.create();\n        builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);\n        builder.addPart(\"upfile\", fileBody);\n        HttpEntity entity = builder.build();\n\n        post.setEntity(entity);\n\n        String path;\n        try (CloseableHttpResponse response = httpclient.execute(post)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            path = TestUtil.consumeText(response);\n\n            assertNotNull(path);\n            assertTrue(path.startsWith(\"/static\"));\n            assertTrue(path.endsWith(\"bin\"));\n        }\n\n        String responseUrl = \"http://127.0.0.1:18080\" + path;\n        verify(clientPair.hardwareClient.responseMock, after(500).never()).channelRead(any(), eq(internal(7777, \"ota \" + responseUrl)));\n\n        TestHardClient newHardwareClient = new TestHardClient(\"localhost\", properties.getHttpPort());\n        newHardwareClient.start();\n        newHardwareClient.login(clientPair.token);\n        verify(newHardwareClient.responseMock, timeout(1000)).channelRead(any(), eq(ok(1)));\n        newHardwareClient.reset();\n\n        newHardwareClient.send(\"internal \" + b(\"ver 0.3.1 h-beat 10 buff-in 256 dev Arduino cpu ATmega328P con W5100 build 111\"));\n        verify(newHardwareClient.responseMock, timeout(500)).channelRead(any(), eq(ok(1)));\n        verify(newHardwareClient.responseMock, timeout(500)).channelRead(any(), eq(internal(7777, \"ota \" + responseUrl)));\n    }\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/http/UploadAPITest.java",
    "content": "package cc.blynk.integration.http;\n\n\nimport cc.blynk.integration.BaseTest;\nimport cc.blynk.integration.TestUtil;\nimport cc.blynk.server.servers.BaseServer;\nimport cc.blynk.server.servers.hardware.HardwareAndHttpAPIServer;\nimport org.apache.http.HttpEntity;\nimport org.apache.http.client.methods.CloseableHttpResponse;\nimport org.apache.http.client.methods.HttpGet;\nimport org.apache.http.client.methods.HttpPost;\nimport org.apache.http.entity.ContentType;\nimport org.apache.http.entity.mime.HttpMultipartMode;\nimport org.apache.http.entity.mime.MultipartEntityBuilder;\nimport org.apache.http.entity.mime.content.ContentBody;\nimport org.apache.http.entity.mime.content.InputStreamBody;\nimport org.apache.http.entity.mime.content.StringBody;\nimport org.apache.http.impl.client.CloseableHttpClient;\nimport org.apache.http.impl.client.HttpClients;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Ignore;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport java.io.InputStream;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\nimport static org.junit.Assert.assertTrue;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 24.12.15.\n */\n@RunWith(MockitoJUnitRunner.class)\n@Ignore(\"due to security reasons, upload via http is not supported\")\npublic class UploadAPITest extends BaseTest {\n\n    private BaseServer httpServer;\n    protected CloseableHttpClient httpclient;\n\n    @Before\n    public void init() throws Exception {\n        httpServer = new HardwareAndHttpAPIServer(holder).start();\n        httpclient = HttpClients.createDefault();\n    }\n\n    @After\n    public void shutdown() throws Exception {\n        httpclient.close();\n        httpServer.close();\n    }\n\n    @Test\n    public void uploadFileToServer() throws Exception {\n        String pathToImage = upload(\"static/ota/test.bin\");\n\n        HttpGet index = new HttpGet(\"http://localhost:\" + properties.getHttpPort() + pathToImage);\n\n        try (CloseableHttpResponse response = httpclient.execute(index)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            assertEquals(\"application/octet-stream\", response.getHeaders(\"Content-Type\")[0].getValue());\n        }\n    }\n\n    private String upload(String filename) throws Exception {\n        InputStream logoStream = UploadAPITest.class.getResourceAsStream(\"/\" + filename);\n\n        HttpPost post = new HttpPost(\"http://localhost:\" + properties.getHttpPort() + \"/upload\");\n        ContentBody fileBody = new InputStreamBody(logoStream, ContentType.APPLICATION_OCTET_STREAM, filename);\n        StringBody stringBody1 = new StringBody(\"Message 1\", ContentType.MULTIPART_FORM_DATA);\n\n        MultipartEntityBuilder builder = MultipartEntityBuilder.create();\n        builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);\n        builder.addPart(\"upfile\", fileBody);\n        builder.addPart(\"text1\", stringBody1);\n        HttpEntity entity = builder.build();\n\n        post.setEntity(entity);\n\n        String staticPath;\n        try (CloseableHttpResponse response = httpclient.execute(post)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            staticPath = TestUtil.consumeText(response);\n\n            assertNotNull(staticPath);\n            assertTrue(staticPath.startsWith(\"/static\"));\n            assertTrue(staticPath.endsWith(\"bin\"));\n        }\n\n        return staticPath;\n    }\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/https/HttpResetPassTest.java",
    "content": "package cc.blynk.integration.https;\n\nimport cc.blynk.integration.BaseTest;\nimport cc.blynk.integration.TestUtil;\nimport cc.blynk.server.servers.BaseServer;\nimport cc.blynk.server.servers.hardware.HardwareAndHttpAPIServer;\nimport cc.blynk.utils.AppNameUtil;\nimport cc.blynk.utils.TokenGeneratorUtil;\nimport org.apache.http.NameValuePair;\nimport org.apache.http.client.entity.UrlEncodedFormEntity;\nimport org.apache.http.client.methods.CloseableHttpResponse;\nimport org.apache.http.client.methods.HttpGet;\nimport org.apache.http.client.methods.HttpPost;\nimport org.apache.http.impl.client.CloseableHttpClient;\nimport org.apache.http.impl.client.HttpClients;\nimport org.apache.http.message.BasicNameValuePair;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\nimport static org.mockito.Mockito.contains;\nimport static org.mockito.Mockito.eq;\nimport static org.mockito.Mockito.verify;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 24.12.15.\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class HttpResetPassTest extends BaseTest {\n\n    private static BaseServer httpServer;\n    private CloseableHttpClient httpclient;\n    private String httpServerUrl;\n\n    @After\n    public void shutdown() throws Exception {\n        httpServer.close();\n        httpclient.close();\n    }\n\n    @Before\n    public void init() throws Exception {\n        httpServerUrl = String.format(\"http://localhost:%s/\", properties.getHttpPort());\n\n        // Allow TLSv1 protocol only\n        this.httpclient = HttpClients.createDefault();\n\n        httpServer = new HardwareAndHttpAPIServer(holder).start();\n    }\n\n    @Override\n    public String getDataFolder() {\n        return getRelativeDataFolder(\"/profiles\");\n    }\n\n    @Test\n    public void submitResetPasswordRequest() throws Exception {\n        String email = \"dmitriy@blynk.cc\";\n        HttpPost resetPassRequest = new HttpPost(httpServerUrl + \"/resetPassword\");\n        List <NameValuePair> nvps = new ArrayList<>();\n        nvps.add(new BasicNameValuePair(\"email\", email));\n        nvps.add(new BasicNameValuePair(\"appName\", AppNameUtil.BLYNK));\n        resetPassRequest.setEntity(new UrlEncodedFormEntity(nvps));\n\n        try (CloseableHttpResponse response = httpclient.execute(resetPassRequest)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            String data = TestUtil.consumeText(response);\n            assertNotNull(data);\n            assertEquals(\"Email was sent.\", data);\n        }\n\n        String productName = properties.productName;\n        verify(holder.mailWrapper).sendHtml(eq(email),\n                eq(\"Password reset request for the \" + productName + \" app.\"),\n                contains(\"/landing?token=\"));\n\n        verify(holder.mailWrapper).sendHtml(eq(email),\n                eq(\"Password reset request for the \" + productName + \" app.\"),\n                contains(\"You recently made a request to reset your password for the \" + productName + \" app. To complete the process, click the link below.\"));\n\n        verify(holder.mailWrapper).sendHtml(eq(email),\n                eq(\"Password reset request for the \" + productName + \" app.\"),\n                contains(\"If you did not request a password reset from \" + productName + \", please ignore this message.\"));\n    }\n\n    @Test\n    public void correctToken() throws Exception {\n        String token = TokenGeneratorUtil.generateNewToken() + TokenGeneratorUtil.generateNewToken();\n        HttpGet getRestorePage = new HttpGet(httpServerUrl + \"/restore?token=\" + token);\n\n        try (CloseableHttpResponse response = httpclient.execute(getRestorePage)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            String data = TestUtil.consumeText(response);\n            assertNotNull(data);\n        }\n    }\n\n    @Test\n    public void getRestorePageXss() throws Exception {\n        HttpGet getRestorePage = new HttpGet(httpServerUrl + \"/restore?token=123\");\n\n        try (CloseableHttpResponse response = httpclient.execute(getRestorePage)) {\n            assertEquals(400, response.getStatusLine().getStatusCode());\n            String data = TestUtil.consumeText(response);\n            assertNotNull(data);\n            assertEquals(\"Invalid request parameters.\", data);\n        }\n    }\n\n    @Test\n    public void getRestorePageXss2() throws Exception {\n        String token = TokenGeneratorUtil.generateNewToken() + TokenGeneratorUtil.generateNewToken();\n        HttpGet getRestorePage = new HttpGet(httpServerUrl + \"/restore?token=\" + token + \"&email=123\");\n\n        try (CloseableHttpResponse response = httpclient.execute(getRestorePage)) {\n            assertEquals(400, response.getStatusLine().getStatusCode());\n            String data = TestUtil.consumeText(response);\n            assertNotNull(data);\n            assertEquals(\"Invalid request parameters.\", data);\n        }\n    }\n\n    @Test\n    public void getRestorePageXss3() throws Exception {\n        String token = \"a\".repeat(63) + \"/\";\n        HttpGet getRestorePage = new HttpGet(httpServerUrl + \"/restore?token=\" + token);\n\n        try (CloseableHttpResponse response = httpclient.execute(getRestorePage)) {\n            assertEquals(400, response.getStatusLine().getStatusCode());\n            String data = TestUtil.consumeText(response);\n            assertNotNull(data);\n            assertEquals(\"Invalid request parameters.\", data);\n        }\n    }\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/https/HttpsAdminServerTest.java",
    "content": "package cc.blynk.integration.https;\n\nimport cc.blynk.integration.BaseTest;\nimport cc.blynk.integration.MyHostVerifier;\nimport cc.blynk.integration.TestUtil;\nimport cc.blynk.integration.model.http.ResponseUserEntity;\nimport cc.blynk.integration.model.tcp.ClientPair;\nimport cc.blynk.integration.model.tcp.TestAppClient;\nimport cc.blynk.integration.model.tcp.TestHardClient;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.servers.BaseServer;\nimport cc.blynk.server.servers.application.MobileAndHttpsServer;\nimport cc.blynk.server.servers.hardware.HardwareAndHttpAPIServer;\nimport cc.blynk.utils.AppNameUtil;\nimport cc.blynk.utils.SHA256Util;\nimport org.apache.http.Header;\nimport org.apache.http.NameValuePair;\nimport org.apache.http.client.config.CookieSpecs;\nimport org.apache.http.client.config.RequestConfig;\nimport org.apache.http.client.entity.UrlEncodedFormEntity;\nimport org.apache.http.client.methods.CloseableHttpResponse;\nimport org.apache.http.client.methods.HttpGet;\nimport org.apache.http.client.methods.HttpPost;\nimport org.apache.http.client.methods.HttpPut;\nimport org.apache.http.conn.ssl.SSLConnectionSocketFactory;\nimport org.apache.http.entity.ContentType;\nimport org.apache.http.entity.StringEntity;\nimport org.apache.http.impl.client.CloseableHttpClient;\nimport org.apache.http.impl.client.HttpClients;\nimport org.apache.http.message.BasicNameValuePair;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport javax.net.ssl.SSLContext;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport static cc.blynk.integration.TestUtil.b;\nimport static cc.blynk.integration.TestUtil.ok;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE;\nimport static cc.blynk.server.core.protocol.model.messages.MessageFactory.produce;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\nimport static org.junit.Assert.assertNull;\nimport static org.junit.Assert.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.after;\nimport static org.mockito.Mockito.timeout;\nimport static org.mockito.Mockito.verify;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 24.12.15.\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class HttpsAdminServerTest extends BaseTest {\n\n    private static BaseServer httpServer;\n    private BaseServer httpAdminServer;\n    private CloseableHttpClient httpclient;\n    private String httpsAdminServerUrl;\n    private String httpServerUrl;\n    private User admin;\n    private ClientPair clientPair;\n\n\n    @After\n    public void shutdown() {\n        httpAdminServer.close();\n        httpServer.close();\n        clientPair.stop();\n    }\n\n    @Before\n    public void init() throws Exception {\n        this.httpAdminServer = new MobileAndHttpsServer(holder).start();\n\n        httpsAdminServerUrl = String.format(\"https://localhost:%s/admin\", properties.getHttpsPort());\n        httpServerUrl = String.format(\"http://localhost:%s/\", properties.getHttpPort());\n\n        SSLContext sslcontext = TestUtil.initUnsecuredSSLContext();\n\n        // Allow TLSv1 protocol only\n        SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslcontext, new MyHostVerifier());\n        this.httpclient = HttpClients.custom()\n                .setSSLSocketFactory(sslsf)\n                .setDefaultRequestConfig(RequestConfig.custom().setCookieSpec(CookieSpecs.STANDARD).build())\n                .build();\n\n        httpServer = new HardwareAndHttpAPIServer(holder).start();\n\n        String name = \"admin@blynk.cc\";\n        String pass = \"admin\";\n        admin = new User(name, SHA256Util.makeHash(pass, name), AppNameUtil.BLYNK, \"local\", \"127.0.0.1\", false, true);\n        holder.userDao.add(admin);\n\n        clientPair = initAppAndHardPair(properties);\n    }\n\n    @Override\n    public String getDataFolder() {\n        return getRelativeDataFolder(\"/profiles\");\n    }\n\n    @Test\n    public void testGetOnExistingUser() throws Exception {\n        String testUser = \"dima@dima.ua\";\n        HttpPut request = new HttpPut(httpsAdminServerUrl + \"/users/\" + \"xxx/\" + testUser);\n        request.setEntity(new StringEntity(new ResponseUserEntity(\"123\").toString(), ContentType.APPLICATION_JSON));\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(404, response.getStatusLine().getStatusCode());\n        }\n    }\n\n    @Test\n    public void testGetWrongUrl() throws Exception {\n        String testUser = \"dima@dima.ua\";\n        HttpPut request = new HttpPut(httpsAdminServerUrl + \"/urs213213/\" + \"xxx/\" + testUser);\n        request.setEntity(new StringEntity(new ResponseUserEntity(\"123\").toString(), ContentType.APPLICATION_JSON));\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(404, response.getStatusLine().getStatusCode());\n        }\n    }\n\n    @Test\n    public void adminLoginFlowSupport()  throws Exception {\n        HttpGet loadLoginPageRequest = new HttpGet(httpsAdminServerUrl);\n        try (CloseableHttpResponse response = httpclient.execute(loadLoginPageRequest)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            String loginPage = TestUtil.consumeText(response);\n            assertTrue(loginPage.contains(\"Use your Admin account to log in\"));\n        }\n\n        login(admin.email, admin.pass);\n\n        HttpGet loadAdminPage = new HttpGet(httpsAdminServerUrl);\n        try (CloseableHttpResponse response = httpclient.execute(loadAdminPage)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            String adminPage = TestUtil.consumeText(response);\n            assertTrue(adminPage.contains(\"Blynk Administration\"));\n            assertTrue(adminPage.contains(\"admin.js\"));\n        }\n    }\n\n    @Test\n    public void adminLoginOnlyForSuperUser()  throws Exception {\n        String name = \"admin@blynk.cc\";\n        String pass = \"admin\";\n\n        User admin = new User(name, SHA256Util.makeHash(pass, name), AppNameUtil.BLYNK, \"local\", \"127.0.0.1\", false, false);\n        holder.userDao.add(admin);\n\n        HttpPost loginRequest = new HttpPost(httpsAdminServerUrl + \"/login\");\n        List <NameValuePair> nvps = new ArrayList<>();\n        nvps.add(new BasicNameValuePair(\"email\", admin.email));\n        nvps.add(new BasicNameValuePair(\"password\", admin.pass));\n        loginRequest.setEntity(new UrlEncodedFormEntity(nvps));\n\n        try (CloseableHttpResponse response = httpclient.execute(loginRequest)) {\n            assertEquals(301, response.getStatusLine().getStatusCode());\n            Header header = response.getFirstHeader(\"Location\");\n            assertNotNull(header);\n            assertEquals(\"/admin\", header.getValue());\n            Header cookieHeader = response.getFirstHeader(\"set-cookie\");\n            assertNull(cookieHeader);\n        }\n    }\n\n    @Test\n    public void testGetUserFromAdminPageNoAccess() throws Exception {\n        String testUser = \"dmitriy@blynk.cc\";\n        String appName = \"Blynk\";\n        HttpGet request = new HttpGet(httpsAdminServerUrl + \"/users/\" + testUser + \"-\" + appName);\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(404, response.getStatusLine().getStatusCode());\n        }\n    }\n\n    @Test\n    public void testGetUserFromAdminPageNoAccessWithFakeCookie() throws Exception {\n        String testUser = \"dmitriy@blynk.cc\";\n        String appName = \"Blynk\";\n        HttpGet request = new HttpGet(httpsAdminServerUrl + \"/users/\" + testUser + \"-\" + appName);\n        request.setHeader(\"Cookie\", \"session=123\");\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(404, response.getStatusLine().getStatusCode());\n        }\n    }\n\n    @Test\n    public void testGetUserFromAdminPageNoAccessWithFakeCookie2() throws Exception {\n        login(admin.email, admin.pass);\n\n        SSLContext sslcontext = TestUtil.initUnsecuredSSLContext();\n        // Allow TLSv1 protocol only\n        SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslcontext, new MyHostVerifier());\n        CloseableHttpClient httpclient2 = HttpClients.custom()\n                .setSSLSocketFactory(sslsf)\n                .setDefaultRequestConfig(RequestConfig.custom().setCookieSpec(CookieSpecs.STANDARD).build())\n                .build();\n\n\n        String testUser = \"dmitriy@blynk.cc\";\n        String appName = \"Blynk\";\n        HttpGet request = new HttpGet(httpsAdminServerUrl + \"/users/\" + testUser + \"-\" + appName);\n        request.setHeader(\"Cookie\", \"session=123\");\n\n        try (CloseableHttpResponse response = httpclient2.execute(request)) {\n            assertEquals(404, response.getStatusLine().getStatusCode());\n        }\n    }\n\n    @Test\n    public void testGetUserFromAdminPage() throws Exception {\n        login(admin.email, admin.pass);\n        String testUser = \"dmitriy@blynk.cc\";\n        String appName = \"Blynk\";\n        HttpGet request = new HttpGet(httpsAdminServerUrl + \"/users/\" + testUser + \"-\" + appName);\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            String jsonProfile = TestUtil.consumeText(response);\n            assertNotNull(jsonProfile);\n            User user = JsonParser.readAny(jsonProfile, User.class);\n            assertNotNull(user);\n            assertEquals(testUser, user.email);\n            assertNotNull(user.profile.dashBoards);\n            assertEquals(5, user.profile.dashBoards.length);\n        }\n    }\n\n    private void login(String name, String pass) throws Exception {\n        HttpPost loginRequest = new HttpPost(httpsAdminServerUrl + \"/login\");\n        List <NameValuePair> nvps = new ArrayList<>();\n        nvps.add(new BasicNameValuePair(\"email\", name));\n        nvps.add(new BasicNameValuePair(\"password\", pass));\n        loginRequest.setEntity(new UrlEncodedFormEntity(nvps));\n\n        try (CloseableHttpResponse response = httpclient.execute(loginRequest)) {\n            assertEquals(301, response.getStatusLine().getStatusCode());\n            Header header = response.getFirstHeader(\"Location\");\n            assertNotNull(header);\n            assertEquals(\"/admin\", header.getValue());\n            Header cookieHeader = response.getFirstHeader(\"set-cookie\");\n            assertNotNull(cookieHeader);\n            assertTrue(cookieHeader.getValue().startsWith(\"session=\"));\n        }\n    }\n\n    @Test\n    public void testChangeUsernameChangesPassToo() throws Exception {\n        login(admin.email, admin.pass);\n\n        User user;\n        HttpGet getUserRequest = new HttpGet(httpsAdminServerUrl + \"/users/admin@blynk.cc-Blynk\");\n        try (CloseableHttpResponse response = httpclient.execute(getUserRequest)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            String userProfile = TestUtil.consumeText(response);\n            assertNotNull(userProfile);\n            user = JsonParser.parseUserFromString(userProfile);\n            assertEquals(admin.email, user.email);\n        }\n\n        user.email = \"123@blynk.cc\";\n\n        //we are no allowed to change username without cahnged password\n        HttpPut changeUserNameRequestWrong = new HttpPut(httpsAdminServerUrl + \"/users/admin@blynk.cc-Blynk\");\n        changeUserNameRequestWrong.setEntity(new StringEntity(user.toString(), ContentType.APPLICATION_JSON));\n        try (CloseableHttpResponse response = httpclient.execute(changeUserNameRequestWrong)) {\n            assertEquals(400, response.getStatusLine().getStatusCode());\n        }\n\n        user.pass = \"123\";\n        HttpPut changeUserNameRequestCorrect = new HttpPut(httpsAdminServerUrl + \"/users/admin@blynk.cc-Blynk\");\n        changeUserNameRequestCorrect.setEntity(new StringEntity(user.toString(), ContentType.APPLICATION_JSON));\n        try (CloseableHttpResponse response = httpclient.execute(changeUserNameRequestCorrect)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n        }\n\n        HttpGet getNonExistingUserRequest = new HttpGet(httpsAdminServerUrl + \"/users/admin@blynk.cc-Blynk\");\n        try (CloseableHttpResponse response = httpclient.execute(getNonExistingUserRequest)) {\n            assertEquals(404, response.getStatusLine().getStatusCode());\n        }\n\n        HttpGet getUserRequest2 = new HttpGet(httpsAdminServerUrl + \"/users/123@blynk.cc-Blynk\");\n        try (CloseableHttpResponse response = httpclient.execute(getUserRequest2)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            String userProfile = TestUtil.consumeText(response);\n            assertNotNull(userProfile);\n            user = JsonParser.parseUserFromString(userProfile);\n            assertEquals(\"123@blynk.cc\", user.email);\n            assertEquals(SHA256Util.makeHash(\"123\", user.email), user.pass);\n        }\n    }\n\n    @Test\n    public void testUpdateUser() throws Exception {\n        login(admin.email, admin.pass);\n\n        clientPair.appClient.deactivate(1);\n        clientPair.appClient.verifyResult(ok(1));\n\n        User user;\n        HttpGet getUserRequest = new HttpGet(httpsAdminServerUrl + \"/users/\" + getUserName() + \"-Blynk\");\n        try (CloseableHttpResponse response = httpclient.execute(getUserRequest)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            String userProfile = TestUtil.consumeText(response);\n            assertNotNull(userProfile);\n            user = JsonParser.parseUserFromString(userProfile);\n            assertEquals(getUserName(), user.email);\n        }\n\n        user.energy = 12333;\n\n        HttpPut changeUserNameRequestCorrect = new HttpPut(httpsAdminServerUrl + \"/users/\" + getUserName() + \"-Blynk\");\n        changeUserNameRequestCorrect.setEntity(new StringEntity(user.toString(), ContentType.APPLICATION_JSON));\n        try (CloseableHttpResponse response = httpclient.execute(changeUserNameRequestCorrect)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n        }\n\n        getUserRequest = new HttpGet(httpsAdminServerUrl + \"/users/\" + getUserName() + \"-Blynk\");\n        try (CloseableHttpResponse response = httpclient.execute(getUserRequest)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            String userProfile = TestUtil.consumeText(response);\n            assertNotNull(userProfile);\n            user = JsonParser.parseUserFromString(userProfile);\n            assertEquals(getUserName(), user.email);\n            assertEquals(12333, user.energy);\n        }\n\n        TestAppClient appClient = new TestAppClient(properties);\n        appClient.start();\n        appClient.login(getUserName(), \"1\",\"iOS\", \"1.10.2\");\n        appClient.verifyResult(ok(1));\n\n        appClient.activate(1);\n        appClient.verifyResult(ok(2));\n\n        clientPair.hardwareClient.send(\"hardware vw 1 112\");\n        verify(appClient.responseMock, after(500).never()).channelRead(any(), eq(produce(1, HARDWARE, b(\"1 vw 1 112\"))));\n\n        appClient.reset();\n\n        appClient.send(\"getDevices 1\");\n        Device[] devices = appClient.parseDevices();\n\n        assertNotNull(devices);\n        assertEquals(1, devices.length);\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient2.start();\n\n        hardClient2.login(devices[0].token);\n        hardClient2.verifyResult(ok(1));\n\n        hardClient2.send(\"hardware vw 1 112\");\n        verify(appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(2, HARDWARE, b(\"1-0 vw 1 112\"))));\n    }\n\n    @Test\n    public void testGetAdminPage() throws Exception {\n        HttpGet request = new HttpGet(httpsAdminServerUrl);\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n        }\n    }\n\n    @Test\n    public void testGetFavIconHttps() throws Exception {\n        HttpGet request = new HttpGet(httpsAdminServerUrl.replace(\"/admin\", \"\") + \"/favicon.ico\");\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n        }\n    }\n\n    @Test\n    public void getStaticFile() throws Exception {\n        HttpGet request = new HttpGet(httpsAdminServerUrl.replace(\"admin\", \"static/admin.html\"));\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n        }\n    }\n\n    @Test\n    public void getStaticFilePathOperationVulnerability() throws Exception {\n        HttpGet request = new HttpGet(httpsAdminServerUrl.replace(\"admin\", \"static/../../../../../../../../etc/passwd\"));\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(404, response.getStatusLine().getStatusCode());\n        }\n    }\n\n    @Test\n    public void getStaticFilePathOperationVulnerability2() throws Exception {\n        HttpGet request = new HttpGet(httpsAdminServerUrl.replace(\"admin\", \"/static/./..././..././..././..././..././..././..././..././.../etc/passwd\"));\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(404, response.getStatusLine().getStatusCode());\n        }\n    }\n\n    @Test\n    public void getStaticFilePathOperationVulnerability3() throws Exception {\n        HttpGet request = new HttpGet(httpsAdminServerUrl.replace(\"admin\", \"static//...//...//...//...//...//...//...//...//.../etc/passwd\"));\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(404, response.getStatusLine().getStatusCode());\n        }\n    }\n\n    @Test\n    public void testGetFavIconHttp() throws Exception {\n        HttpGet request = new HttpGet(httpServerUrl + \"favicon.ico\");\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n        }\n    }\n\n    @Test\n    public void testAssignNewTokenForNonExistingToken() throws Exception {\n        login(admin.email, admin.pass);\n        HttpGet request = new HttpGet(httpsAdminServerUrl + \"/users/token/assign?old=123&new=123\");\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(400, response.getStatusLine().getStatusCode());\n        }\n    }\n\n    @Test\n    public void testAssignNewToken() throws Exception {\n        login(admin.email, admin.pass);\n\n        HttpGet request = new HttpGet(httpsAdminServerUrl + \"/users/token/assign?old=4ae3851817194e2596cf1b7103603ef8&new=123\");\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n        }\n\n        HttpPut put = new HttpPut(httpServerUrl + \"123/update/v10\");\n        put.setEntity(new StringEntity(\"[\\\"100\\\"]\", ContentType.APPLICATION_JSON));\n\n        try (CloseableHttpResponse response = httpclient.execute(put)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n        }\n\n        HttpGet get = new HttpGet(httpServerUrl + \"123/get/v10\");\n\n        try (CloseableHttpResponse response = httpclient.execute(get)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            List<String> values = TestUtil.consumeJsonPinValues(response);\n            assertEquals(1, values.size());\n            assertEquals(\"100\", values.get(0));\n        }\n\n        request = new HttpGet(httpsAdminServerUrl + \"/users/token/assign?old=4ae3851817194e2596cf1b7103603ef8&new=124\");\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(400, response.getStatusLine().getStatusCode());\n        }\n    }\n\n    @Test\n    public void testForceAssignNewToken() throws Exception {\n        login(admin.email, admin.pass);\n        HttpGet request = new HttpGet(httpsAdminServerUrl + \"/users/token/force?email=dmitriy@blynk.cc&app=Blynk&dashId=79780619&deviceId=0&new=123\");\n\n        try (CloseableHttpResponse response = httpclient.execute(request)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n        }\n\n        HttpPut put = new HttpPut(httpServerUrl + \"123/update/v10\");\n        put.setEntity(new StringEntity(\"[\\\"100\\\"]\", ContentType.APPLICATION_JSON));\n\n        try (CloseableHttpResponse response = httpclient.execute(put)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n        }\n\n        HttpGet get = new HttpGet(httpServerUrl + \"123/get/v10\");\n\n        try (CloseableHttpResponse response = httpclient.execute(get)) {\n            assertEquals(200, response.getStatusLine().getStatusCode());\n            List<String> values = TestUtil.consumeJsonPinValues(response);\n            assertEquals(1, values.size());\n            assertEquals(\"100\", values.get(0));\n        }\n    }\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/model/MockHolder.java",
    "content": "package cc.blynk.integration.model;\n\n\nimport cc.blynk.server.core.protocol.model.messages.MessageBase;\nimport cc.blynk.server.core.protocol.model.messages.ResponseMessage;\n\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.eq;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 3/1/2015.\n */\npublic class MockHolder {\n\n    private final SimpleClientHandler mock;\n\n    public MockHolder(SimpleClientHandler mock) {\n        this.mock = mock;\n    }\n\n    public MockHolder check(int responseMessageCode) throws Exception {\n        verify(mock).channelRead(any(), eq(new ResponseMessage(1, responseMessageCode)));\n        return this;\n    }\n\n    public MockHolder check(int times, int responseMessageCode) throws Exception {\n        verify(mock, times(times)).channelRead(any(), eq(new ResponseMessage(1, responseMessageCode)));\n        return this;\n    }\n\n    public void check(MessageBase responseMessage) throws Exception {\n        verify(mock).channelRead(any(), eq(responseMessage));\n    }\n\n}\n\n\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/model/SimpleClientHandler.java",
    "content": "package cc.blynk.integration.model;\n\nimport cc.blynk.server.core.protocol.model.messages.MessageBase;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.SimpleChannelInboundHandler;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 1/31/2015.\n */\npublic class SimpleClientHandler extends SimpleChannelInboundHandler<MessageBase> {\n\n    @Override\n    public void channelRead0(ChannelHandlerContext ctx, MessageBase msg) throws Exception {\n        //System.out.println(msg);\n    }\n\n}"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/model/http/PinJsonResponse.java",
    "content": "package cc.blynk.integration.model.http;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 28.12.15.\n */\npublic class PinJsonResponse {\n\n    String[] values;\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/model/http/ResponseUserEntity.java",
    "content": "package cc.blynk.integration.model.http;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 24.12.15.\n */\npublic class ResponseUserEntity {\n\n    public final String pass;\n\n    public ResponseUserEntity(String pass) {\n        this.pass = pass;\n    }\n\n    @Override\n    public String toString() {\n        return \"{\\\"pass\\\":\\\"\"+ pass +\"\\\"}\";\n    }\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/model/tcp/BaseTestAppClient.java",
    "content": "package cc.blynk.integration.model.tcp;\n\nimport cc.blynk.client.core.AppClient;\nimport cc.blynk.integration.TestUtil;\nimport cc.blynk.integration.model.SimpleClientHandler;\nimport cc.blynk.utils.properties.ServerProperties;\nimport org.mockito.Mockito;\n\nimport java.util.Random;\n\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.after;\nimport static org.mockito.Mockito.timeout;\nimport static org.mockito.Mockito.verify;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 05.02.18.\n */\npublic abstract class BaseTestAppClient extends AppClient {\n\n    public final SimpleClientHandler responseMock = Mockito.mock(SimpleClientHandler.class);\n    int msgId = 0;\n\n    public BaseTestAppClient(String host, int port, Random msgIdGenerator, ServerProperties properties) {\n        super(host, port, msgIdGenerator, properties);\n    }\n\n    public void verifyResult(Object exceptedResult) throws Exception {\n        verifyResult(exceptedResult, 1);\n    }\n\n    public void never(Object exceptedResult) throws Exception {\n        verify(responseMock, Mockito.never()).channelRead(any(), eq(exceptedResult));\n    }\n\n    public void neverAfter(int time, Object exceptedResult) throws Exception {\n        verify(responseMock, after(time).never()).channelRead(any(), eq(exceptedResult));\n    }\n\n    public void verifyResult(Object exceptedResult, int times) throws Exception {\n        verify(responseMock, timeout(1000).times(times)).channelRead(any(), eq(exceptedResult));\n    }\n\n    public void verifyResultAfter(int time, Object exceptedResult) throws Exception {\n        verify(responseMock, after(time)).channelRead(any(), eq(exceptedResult));\n    }\n\n    public void verifyAny() throws Exception {\n        verify(responseMock, timeout(500)).channelRead(any(), any());\n    }\n\n    public String getBody() throws Exception {\n        return TestUtil.getBody(responseMock, 1);\n    }\n\n    public String getBody(int expectedMessageOrder) throws Exception {\n        return TestUtil.getBody(responseMock, expectedMessageOrder);\n    }\n\n    public void reset() {\n        Mockito.reset(responseMock);\n        msgId = 0;\n    }\n\n    public void send(String line) {\n        send(produceMessageBaseOnUserInput(line, ++msgId));\n    }\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/model/tcp/BaseTestHardwareClient.java",
    "content": "package cc.blynk.integration.model.tcp;\n\nimport cc.blynk.client.core.BaseClient;\nimport cc.blynk.integration.TestUtil;\nimport cc.blynk.integration.model.SimpleClientHandler;\nimport org.mockito.Mockito;\n\nimport java.util.Random;\n\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.timeout;\nimport static org.mockito.Mockito.verify;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 05.02.18.\n */\npublic abstract class BaseTestHardwareClient extends BaseClient {\n\n    public final SimpleClientHandler responseMock = Mockito.mock(SimpleClientHandler.class);\n    int msgId = 0;\n\n    public BaseTestHardwareClient(String host, int port, Random messageIdGenerator) {\n        super(host, port, messageIdGenerator);\n    }\n\n    public void never(Object exceptedResult) throws Exception {\n        verify(responseMock, Mockito.never()).channelRead(any(), eq(exceptedResult));\n    }\n\n    public void verifyResult(Object exceptedResult, int times) throws Exception {\n        verify(responseMock, timeout(500).times(times)).channelRead(any(), eq(exceptedResult));\n    }\n\n    public void verifyResult(Object exceptedResult) throws Exception {\n        verifyResult(exceptedResult, 1);\n    }\n\n    public String getBody() throws Exception {\n        return TestUtil.getBody(responseMock, 1);\n    }\n\n    public String getBody(int expectedMessageOrder) throws Exception {\n        return TestUtil.getBody(responseMock, expectedMessageOrder);\n    }\n\n    public void reset() {\n        Mockito.reset(responseMock);\n        msgId = 0;\n    }\n\n    public void send(String line) {\n        send(produceMessageBaseOnUserInput(line, ++msgId));\n    }\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/model/tcp/ClientPair.java",
    "content": "package cc.blynk.integration.model.tcp;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/4/2015.\n */\npublic class ClientPair {\n\n    public final TestAppClient appClient;\n\n    public final TestHardClient hardwareClient;\n\n    public final String token;\n\n    public ClientPair(TestAppClient appClient, TestHardClient hardwareClient, String token) {\n        this.appClient = appClient;\n        this.hardwareClient = hardwareClient;\n        this.token = token;\n    }\n\n    public void stop() {\n        appClient.stop();\n        hardwareClient.stop();\n    }\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/model/tcp/TestAppClient.java",
    "content": "package cc.blynk.integration.model.tcp;\n\nimport cc.blynk.client.handlers.decoders.AppClientMessageDecoder;\nimport cc.blynk.integration.model.SimpleClientHandler;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.Profile;\nimport cc.blynk.server.core.model.auth.App;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.device.Tag;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.outputs.graph.GraphPeriod;\nimport cc.blynk.server.core.model.widgets.ui.reporting.Report;\nimport cc.blynk.server.core.model.widgets.ui.tiles.TileTemplate;\nimport cc.blynk.server.core.protocol.handlers.encoders.MobileMessageEncoder;\nimport cc.blynk.server.core.protocol.model.messages.BinaryMessage;\nimport cc.blynk.server.core.stats.GlobalStats;\nimport cc.blynk.utils.SHA256Util;\nimport cc.blynk.utils.properties.ServerProperties;\nimport io.netty.channel.ChannelInitializer;\nimport io.netty.channel.nio.NioEventLoopGroup;\nimport io.netty.channel.socket.SocketChannel;\nimport org.mockito.ArgumentCaptor;\nimport org.mockito.Mockito;\n\nimport java.util.Collections;\nimport java.util.Random;\nimport java.util.StringJoiner;\n\nimport static cc.blynk.utils.AppNameUtil.BLYNK;\nimport static cc.blynk.utils.StringUtils.BODY_SEPARATOR;\nimport static cc.blynk.utils.StringUtils.BODY_SEPARATOR_STRING;\nimport static cc.blynk.utils.StringUtils.DEVICE_SEPARATOR;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.timeout;\nimport static org.mockito.Mockito.verify;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 1/31/2015.\n */\npublic class TestAppClient extends BaseTestAppClient {\n\n    public TestAppClient(String host, int port) {\n        super(host, port, Mockito.mock(Random.class), new ServerProperties(Collections.emptyMap()));\n    }\n\n    public TestAppClient(ServerProperties properties) {\n        this(\"localhost\", properties.getHttpsPort(), properties, new NioEventLoopGroup());\n    }\n\n    public TestAppClient(String host, ServerProperties properties) {\n        this(host, properties.getHttpsPort(), properties, new NioEventLoopGroup());\n    }\n\n    public TestAppClient(String host, int port, ServerProperties properties) {\n        this(host, port, properties, new NioEventLoopGroup());\n    }\n\n    public TestAppClient(String host, int port, ServerProperties properties, NioEventLoopGroup nioEventLoopGroup) {\n        super(host, port, Mockito.mock(Random.class), properties);\n        this.nioEventLoopGroup = nioEventLoopGroup;\n    }\n\n    public Device parseDevice() throws Exception {\n        return parseDevice(1);\n    }\n\n    public Profile parseProfile(int expectedMessageOrder) throws Exception {\n        return JsonParser.parseProfileFromString(getBody(expectedMessageOrder));\n    }\n\n    public Device parseDevice(int expectedMessageOrder) throws Exception {\n        return JsonParser.parseDevice(getBody(expectedMessageOrder), 0);\n    }\n\n    public Device[] parseDevices() throws Exception {\n        return parseDevices(1);\n    }\n\n    public Device[] parseDevices(int expectedMessageOrder) throws Exception {\n        return JsonParser.MAPPER.readValue(getBody(expectedMessageOrder), Device[].class);\n    }\n\n    public Tag[] parseTags(int expectedMessageOrder) throws Exception {\n        return JsonParser.MAPPER.readValue(getBody(expectedMessageOrder), Tag[].class);\n    }\n\n    public App parseApp(int expectedMessageOrder) throws Exception {\n        return JsonParser.parseApp(getBody(expectedMessageOrder), 0);\n    }\n\n    public DashBoard parseDash(int expectedMessageOrder) throws Exception {\n        return JsonParser.parseDashboard(getBody(expectedMessageOrder), 0);\n    }\n\n    public Report parseReportFromResponse(int expectedMessageOrder) throws Exception {\n        return JsonParser.parseReport(getBody(expectedMessageOrder), 0);\n    }\n\n    public BinaryMessage getBinaryBody() throws Exception {\n        ArgumentCaptor<BinaryMessage> objectArgumentCaptor = ArgumentCaptor.forClass(BinaryMessage.class);\n        verify(responseMock, timeout(1000)).channelRead(any(), objectArgumentCaptor.capture());\n        return objectArgumentCaptor.getValue();\n    }\n\n    @Override\n    public ChannelInitializer<SocketChannel> getChannelInitializer() {\n        return new ChannelInitializer<>() {\n            @Override\n            protected void initChannel(SocketChannel ch) {\n                ch.pipeline().addLast(\n                        sslCtx.newHandler(ch.alloc(), host, port),\n                        new AppClientMessageDecoder(),\n                        new MobileMessageEncoder(new GlobalStats()),\n                        responseMock\n                );\n            }\n        };\n    }\n\n    public void createTag(int dashId, Tag tag) {\n        send(\"createTag \" + dashId + BODY_SEPARATOR + tag.toString());\n    }\n\n    public void updateTag(int dashId, Tag tag) {\n        send(\"updateTag \" + dashId + BODY_SEPARATOR + tag.toString());\n    }\n\n    public void createDevice(int dashId, Device device) {\n        send(\"createDevice \" + dashId + BODY_SEPARATOR + device.toString());\n    }\n\n    public void updateDevice(int dashId, Device device) {\n        send(\"updateDevice \" + dashId + BODY_SEPARATOR + device.toString());\n    }\n\n    public void deleteDevice(int dashId, int deviceId) {\n        send(\"deleteDevice \" + dashId + BODY_SEPARATOR + deviceId);\n    }\n\n    public void createWidget(int dashId, Widget widget) throws Exception {\n        createWidget(dashId, JsonParser.MAPPER.writeValueAsString(widget));\n    }\n\n    public void createWidget(int dashId, long widgetId, long templateId, String widgetJson) {\n        send(\"createWidget \" + dashId + BODY_SEPARATOR + widgetId\n                + BODY_SEPARATOR + templateId + BODY_SEPARATOR + widgetJson);\n    }\n\n    public void createWidget(int dashId, long widgetId, long templateId, Widget widget) throws Exception {\n        send(\"createWidget \" + dashId + BODY_SEPARATOR + widgetId\n                + BODY_SEPARATOR + templateId + BODY_SEPARATOR + JsonParser.MAPPER.writeValueAsString(widget));\n    }\n\n    public void createWidget(int dashId, String widgetJson) {\n        send(\"createWidget \" + dashId + BODY_SEPARATOR + widgetJson);\n    }\n\n    public void updateWidget(int dashId, Widget widget) throws Exception {\n        updateWidget(dashId, JsonParser.MAPPER.writeValueAsString(widget));\n    }\n\n    public void updateWidget(int dashId, String widgetJson) {\n        send(\"updateWidget \" + dashId + BODY_SEPARATOR + widgetJson);\n    }\n\n    public void deleteWidget(int dashId, long widgetId) {\n        send(\"deleteWidget \" + dashId + \" \" + widgetId);\n    }\n\n    public void activate(int dashId) {\n        send(\"activate \" + dashId);\n    }\n\n    public void deactivate(int dashId) {\n        send(\"deactivate \" + dashId);\n    }\n\n    public void updateDash(DashBoard dashBoard) {\n        updateDash(dashBoard.toString());\n    }\n\n    public void updateDash(String dashJson) {\n        send(\"updateDash \" + dashJson);\n    }\n\n    public void deleteDash(int dashId) {\n        send(\"deleteDash \" + dashId);\n    }\n\n    public void deleteTag(int dashId, int tagId) {\n        send(\"deleteTag \" + dashId + BODY_SEPARATOR + tagId);\n    }\n\n    public void createDash(DashBoard dashBoard) {\n        createDash(dashBoard.toString());\n    }\n\n    public void createDash(String dashJson) {\n        send(\"createDash \" + dashJson);\n    }\n\n    public void register(String email, String pass, String appName) {\n        send(\"register \" + email + BODY_SEPARATOR + SHA256Util.makeHash(pass, email) + BODY_SEPARATOR + appName);\n    }\n\n    public void login(String email, String pass) {\n        login(email, pass, \"Android\", \"2.27.0\", BLYNK);\n    }\n\n    public void login(String email, String pass, String os, String version) {\n        login(email, pass, os, version, BLYNK);\n    }\n\n    public void login(String email, String pass, String os, String version, String appName) {\n        send(\"login \" + email + BODY_SEPARATOR + SHA256Util.makeHash(pass, email)\n                + BODY_SEPARATOR + os + BODY_SEPARATOR + version + BODY_SEPARATOR + appName);\n    }\n\n    public void sync(int dashId) {\n        send(\"appsync \" + dashId);\n    }\n\n    public void sync(int dashId, int deviceId) {\n        send(\"appsync \" + dashId + DEVICE_SEPARATOR + deviceId);\n    }\n\n    public void deleteDeviceData(int dashId, int deviceId) {\n        send(\"deletedevicedata \" + dashId + DEVICE_SEPARATOR + deviceId);\n    }\n\n    public void deleteDeviceData(int dashId, int deviceId, String... pins) {\n        StringJoiner sj = new StringJoiner(BODY_SEPARATOR_STRING);\n        for (String pin : pins) {\n            sj.add(pin);\n        }\n        send(\"deletedevicedata \" + dashId + DEVICE_SEPARATOR + deviceId + BODY_SEPARATOR + sj.toString());\n    }\n\n    public void getEnhancedGraphData(int dashId, long widgetId, GraphPeriod period) {\n        send(\"getenhanceddata \" + dashId + BODY_SEPARATOR + widgetId + BODY_SEPARATOR + period.name());\n    }\n\n    public void getEnhancedGraphData(int dashId, long widgetId, GraphPeriod period, int page) {\n        send(\"getenhanceddata \" + dashId + BODY_SEPARATOR + widgetId + BODY_SEPARATOR + period.name() + BODY_SEPARATOR + page);\n    }\n\n    public void createTemplate(int dashId, long widgetId, TileTemplate tileTemplate) throws Exception {\n        createTemplate(dashId, widgetId, JsonParser.MAPPER.writeValueAsString(tileTemplate));\n    }\n\n    public void createTemplate(int dashId, long widgetId, String tileTemplate) {\n        send(\"createTemplate \" + dashId + BODY_SEPARATOR + widgetId + BODY_SEPARATOR + tileTemplate);\n    }\n\n    public void updateTemplate(int dashId, long widgetId, TileTemplate tileTemplate) throws Exception {\n        send(\"updateTemplate \" + dashId + BODY_SEPARATOR + widgetId + BODY_SEPARATOR\n                + JsonParser.MAPPER.writeValueAsString(tileTemplate));\n    }\n\n    public void createReport(int dashId, Report report) {\n        createReport(dashId, report.toString());\n    }\n\n    public void createReport(int dashId, String report) {\n        send(\"createReport \" + dashId + BODY_SEPARATOR + report);\n    }\n\n    public void updateReport(int dashId, Report report) {\n        send(\"updateReport \" + dashId + BODY_SEPARATOR + report.toString());\n    }\n\n    public void getWidget(int dashId, long widgetId) {\n        send(\"getWidget \" + dashId + BODY_SEPARATOR + widgetId);\n    }\n\n    public Widget parseWidget(int expectedMessageOrder) throws Exception {\n        return JsonParser.parseWidget(getBody(expectedMessageOrder));\n    }\n\n    public void deleteReport(int dashId, int reportId) {\n        send(\"deleteReport \" + dashId + BODY_SEPARATOR + reportId);\n    }\n\n    public void exportReport(int dashId, int reportId) {\n        send(\"exportReport \" + dashId + BODY_SEPARATOR + reportId);\n    }\n\n    public void getDevice(int dashId, int deviceId) {\n        send(\"getDevice \" + dashId + BODY_SEPARATOR + deviceId);\n    }\n\n    public void send(String line) {\n        send(produceMessageBaseOnUserInput(line, ++msgId));\n    }\n\n    public void send(String line, int id) {\n        send(produceMessageBaseOnUserInput(line, id));\n    }\n\n    public void getProvisionToken(int dashId, Device device) {\n        send(\"getProvisionToken \" + dashId + BODY_SEPARATOR + device.toString());\n    }\n\n    public void createApp(App app) {\n        send(\"createApp \" + app.toString());\n    }\n\n    public void loadProfileGzipped() {\n        send(\"loadProfileGzipped\");\n    }\n\n    public void replace(SimpleClientHandler simpleClientHandler) {\n        this.channel.pipeline().removeLast();\n        this.channel.pipeline().addLast(simpleClientHandler);\n    }\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/model/tcp/TestHardClient.java",
    "content": "package cc.blynk.integration.model.tcp;\n\nimport cc.blynk.client.handlers.decoders.ClientMessageDecoder;\nimport cc.blynk.integration.model.SimpleClientHandler;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.protocol.handlers.encoders.MessageEncoder;\nimport cc.blynk.server.core.stats.GlobalStats;\nimport cc.blynk.utils.StringUtils;\nimport io.netty.channel.ChannelInitializer;\nimport io.netty.channel.nio.NioEventLoopGroup;\nimport io.netty.channel.socket.SocketChannel;\nimport org.mockito.Mockito;\n\nimport java.util.Random;\n\nimport static cc.blynk.utils.StringUtils.BODY_SEPARATOR;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 1/31/2015.\n */\npublic class TestHardClient extends BaseTestHardwareClient {\n\n    public TestHardClient(String host, int port) {\n        this(host, port, new NioEventLoopGroup());\n    }\n\n    public TestHardClient(String host, int port, NioEventLoopGroup nioEventLoopGroup) {\n        super(host, port, Mockito.mock(Random.class));\n        this.nioEventLoopGroup = nioEventLoopGroup;\n    }\n\n    @Override\n    public ChannelInitializer<SocketChannel> getChannelInitializer() {\n        return new ChannelInitializer<SocketChannel>() {\n            @Override\n            protected void initChannel(SocketChannel ch) throws Exception {\n                ch.pipeline().addLast(\n                        new ClientMessageDecoder(),\n                        new MessageEncoder(new GlobalStats()),\n                        responseMock\n                );\n            }\n        };\n    }\n\n    public void login(String token) {\n        send(\"hardwareLogin \" + token);\n    }\n\n    public void setProperty(int pin, String property, String value) {\n        send(\"setProperty \" + pin + \" \" + property + \" \" + value);\n    }\n\n    public void setProperty(int pin, String property, String... value) {\n        send(\"setProperty \" + pin + \" \" + property + \" \" + String.join(StringUtils.BODY_SEPARATOR_STRING, value));\n    }\n\n    public void sync() {\n        send(\"hardsync\");\n    }\n\n    public void sync(PinType pinType, int pin) {\n        send(\"hardsync \" + pinType.pintTypeChar + \"r\" + BODY_SEPARATOR + pin);\n    }\n\n    public void sync(PinType pinType, int pin1, int pin2) {\n        send(\"hardsync \" + pinType.pintTypeChar + \"r\" + BODY_SEPARATOR + pin1 + BODY_SEPARATOR + pin2);\n    }\n\n    public void replace(SimpleClientHandler simpleClientHandler) {\n        this.channel.pipeline().removeLast();\n        this.channel.pipeline().addLast(simpleClientHandler);\n    }\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/model/tcp/TestSslHardClient.java",
    "content": "package cc.blynk.integration.model.tcp;\n\nimport cc.blynk.client.handlers.decoders.ClientMessageDecoder;\nimport cc.blynk.integration.model.SimpleClientHandler;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.protocol.handlers.encoders.MessageEncoder;\nimport cc.blynk.server.core.stats.GlobalStats;\nimport io.netty.channel.ChannelInitializer;\nimport io.netty.channel.nio.NioEventLoopGroup;\nimport io.netty.channel.socket.SocketChannel;\nimport io.netty.handler.ssl.SslContext;\nimport io.netty.handler.ssl.SslContextBuilder;\nimport io.netty.handler.ssl.SslProvider;\nimport io.netty.handler.ssl.util.InsecureTrustManagerFactory;\nimport org.mockito.Mockito;\n\nimport javax.net.ssl.SSLException;\nimport java.io.File;\nimport java.util.Random;\n\nimport static cc.blynk.utils.StringUtils.BODY_SEPARATOR;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 1/31/2015.\n */\npublic class TestSslHardClient extends BaseTestHardwareClient {\n\n    protected SslContext sslCtx;\n\n    public TestSslHardClient(String host, int port) {\n        this(host, port, new NioEventLoopGroup());\n    }\n\n    private TestSslHardClient(String host, int port, NioEventLoopGroup nioEventLoopGroup) {\n        super(host, port, Mockito.mock(Random.class));\n        this.nioEventLoopGroup = nioEventLoopGroup;\n        log.info(\"Creating app client. Host {}, sslPort : {}\", host, port);\n        File serverCert = makeCertificateFile(\"server.ssl.cert\");\n        File clientCert = makeCertificateFile(\"client.ssl.cert\");\n        File clientKey = makeCertificateFile(\"client.ssl.key\");\n        try {\n            if (!serverCert.exists() || !clientCert.exists() || !clientKey.exists()) {\n                log.info(\"Enabling one-way auth with no certs checks.\");\n                this.sslCtx = SslContextBuilder.forClient().sslProvider(SslProvider.JDK)\n                        .trustManager(InsecureTrustManagerFactory.INSTANCE)\n                        .build();\n            } else {\n                log.info(\"Enabling mutual auth.\");\n                String clientPass = props.getProperty(\"client.ssl.key.pass\");\n                this.sslCtx = SslContextBuilder.forClient()\n                        .sslProvider(SslProvider.JDK)\n                        .trustManager(serverCert)\n                        .keyManager(clientCert, clientKey, clientPass)\n                        .build();\n            }\n        } catch (SSLException e) {\n            log.error(\"Error initializing SSL context. Reason : {}\", e.getMessage());\n            log.debug(e);\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public ChannelInitializer<SocketChannel> getChannelInitializer() {\n        return new ChannelInitializer<SocketChannel>() {\n            @Override\n            protected void initChannel(SocketChannel ch) throws Exception {\n                ch.pipeline().addLast(\n                        sslCtx.newHandler(ch.alloc(), host, port),\n                        new ClientMessageDecoder(),\n                        new MessageEncoder(new GlobalStats()),\n                        responseMock\n                );\n            }\n        };\n    }\n\n    public void login(String token) {\n        send(\"hardwareLogin \" + token);\n    }\n\n    public void setProperty(int pin, String property, String value) {\n        send(\"setProperty \" + pin + \" \" + property + \" \" + value);\n    }\n\n    public void sync() {\n        send(\"hardsync\");\n    }\n\n    public void sync(PinType pinType, int pin) {\n        send(\"hardsync \" + pinType.pintTypeChar + \"r\" + BODY_SEPARATOR + pin);\n    }\n\n    public void sync(PinType pinType, int pin1, int pin2) {\n        send(\"hardsync \" + pinType.pintTypeChar + \"r\" + BODY_SEPARATOR + pin1 + BODY_SEPARATOR + pin2);\n    }\n\n    public void replace(SimpleClientHandler simpleClientHandler) {\n        this.channel.pipeline().removeLast();\n        this.channel.pipeline().addLast(simpleClientHandler);\n    }\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/model/websocket/AppWebSocketClient.java",
    "content": "/*\n * Copyright 2014 The Netty Project\n *\n * The Netty Project licenses this file to you under the Apache License,\n * version 2.0 (the \"License\"); you may not use this file except in compliance\n * with the License. You may obtain a copy of the License at:\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n */\npackage cc.blynk.integration.model.websocket;\n\nimport cc.blynk.integration.model.tcp.BaseTestAppClient;\nimport cc.blynk.server.Limits;\nimport cc.blynk.server.core.protocol.handlers.decoders.WSMessageDecoder;\nimport cc.blynk.server.core.protocol.model.messages.MessageBase;\nimport cc.blynk.server.core.stats.GlobalStats;\nimport cc.blynk.utils.SHA256Util;\nimport cc.blynk.utils.StringUtils;\nimport cc.blynk.utils.properties.ServerProperties;\nimport io.netty.buffer.ByteBuf;\nimport io.netty.buffer.ByteBufAllocator;\nimport io.netty.channel.ChannelInitializer;\nimport io.netty.channel.ChannelPipeline;\nimport io.netty.channel.socket.SocketChannel;\nimport io.netty.handler.codec.http.DefaultHttpHeaders;\nimport io.netty.handler.codec.http.HttpClientCodec;\nimport io.netty.handler.codec.http.HttpObjectAggregator;\nimport io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;\nimport io.netty.handler.codec.http.websocketx.WebSocketClientHandshakerFactory;\nimport io.netty.handler.codec.http.websocketx.WebSocketFrame;\nimport io.netty.handler.codec.http.websocketx.WebSocketVersion;\nimport io.netty.handler.ssl.SslContext;\nimport io.netty.handler.ssl.SslContextBuilder;\nimport io.netty.handler.ssl.SslProvider;\nimport io.netty.handler.ssl.util.InsecureTrustManagerFactory;\n\nimport java.net.URI;\nimport java.util.Collections;\nimport java.util.Random;\n\npublic final class AppWebSocketClient extends BaseTestAppClient {\n\n    private final SslContext sslCtx;\n    private final AppWebSocketClientHandler appHandler;\n    public int msgId = 0;\n\n    public AppWebSocketClient(String host, int port, String path) throws Exception {\n        super(host, port, new Random(), new ServerProperties(Collections.emptyMap()));\n\n        URI uri = new URI(\"wss://\" + host + \":\" + port + path);\n        this.sslCtx = SslContextBuilder.forClient()\n                .sslProvider(SslProvider.JDK)\n                .trustManager(InsecureTrustManagerFactory.INSTANCE)\n                .build();\n        this.appHandler = new AppWebSocketClientHandler(\n                        WebSocketClientHandshakerFactory.newHandshaker(\n                                uri, WebSocketVersion.V13, null, false, new DefaultHttpHeaders()));\n    }\n\n    private static WebSocketFrame produceWebSocketFrame(MessageBase msg) {\n        byte[] data = msg.getBytes();\n        ByteBuf bb = ByteBufAllocator.DEFAULT.buffer(3 + data.length);\n        bb.writeByte(msg.command);\n        bb.writeShort(msg.id);\n        bb.writeBytes(data);\n        return new BinaryWebSocketFrame(bb);\n    }\n\n    @Override\n    public ChannelInitializer<SocketChannel> getChannelInitializer() {\n        return new ChannelInitializer<SocketChannel> () {\n            @Override\n            public void initChannel(SocketChannel ch) throws Exception {\n                ChannelPipeline p = ch.pipeline();\n                p.addLast(\n                        sslCtx.newHandler(ch.alloc(), host, port),\n                        new HttpClientCodec(),\n                        new HttpObjectAggregator(8192),\n                        appHandler,\n                        new WSMessageDecoder(new GlobalStats(),\n                                new Limits(new ServerProperties(Collections.emptyMap()))\n                        )\n                );\n            }\n        };\n    }\n\n    @Override\n    public void start() {\n        super.start();\n        startHandshake();\n    }\n\n    private void startHandshake() {\n        appHandler.startHandshake(channel);\n        try {\n            appHandler.handshakeFuture().sync();\n            this.channel.pipeline().addLast(responseMock);\n        } catch (Exception e) {\n            log.error(e);\n        }\n    }\n\n    public void login(String email, String pass) {\n        send(\"login \" + email + StringUtils.BODY_SEPARATOR + SHA256Util.makeHash(pass, email));\n    }\n\n    public void send(String line) {\n        send(produceWebSocketFrame(produceMessageBaseOnUserInput(line, ++msgId)));\n    }\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/model/websocket/AppWebSocketClientHandler.java",
    "content": "/*\n * Copyright 2012 The Netty Project\n *\n * The Netty Project licenses this file to you under the Apache License,\n * version 2.0 (the \"License\"); you may not use this file except in compliance\n * with the License. You may obtain a copy of the License at:\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n */\n//The MIT License\n//\n//Copyright (c) 2009 Carl Bystršm\n//\n//Permission is hereby granted, free of charge, to any person obtaining a copy\n//of this software and associated documentation files (the \"Software\"), to deal\n//in the Software without restriction, including without limitation the rights\n//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//copies of the Software, and to permit persons to whom the Software is\n//furnished to do so, subject to the following conditions:\n//\n//The above copyright notice and this permission notice shall be included in\n//all copies or substantial portions of the Software.\n//\n//THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//THE SOFTWARE.\n\npackage cc.blynk.integration.model.websocket;\n\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelFuture;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.ChannelPromise;\nimport io.netty.channel.SimpleChannelInboundHandler;\nimport io.netty.handler.codec.http.FullHttpResponse;\nimport io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;\nimport io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;\nimport io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker;\nimport io.netty.handler.codec.http.websocketx.WebSocketFrame;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.nio.charset.StandardCharsets;\n\npublic class AppWebSocketClientHandler extends SimpleChannelInboundHandler<Object> {\n\n    private static final Logger log = LogManager.getLogger(AppWebSocketClientHandler.class);\n\n    private final WebSocketClientHandshaker handshaker;\n    private ChannelPromise handshakeFuture;\n\n    public AppWebSocketClientHandler(WebSocketClientHandshaker handshaker) {\n        this.handshaker = handshaker;\n    }\n\n    public ChannelFuture handshakeFuture() {\n        return handshakeFuture;\n    }\n\n    public void startHandshake(Channel channel) {\n        handshaker.handshake(channel);\n        handshakeFuture = channel.newPromise();\n    }\n\n    @Override\n    public void channelInactive(ChannelHandlerContext ctx) {\n        System.out.println(\"WebSocket Client disconnected!\");\n    }\n\n    @Override\n    public void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {\n        Channel ch = ctx.channel();\n        if (!handshaker.isHandshakeComplete()) {\n            handshaker.finishHandshake(ch, (FullHttpResponse) msg);\n            log.trace(\"WebSocket Client connected!\");\n            if (handshakeFuture != null) {\n                handshakeFuture.setSuccess();\n            }\n            return;\n        }\n\n        if (msg instanceof FullHttpResponse) {\n            FullHttpResponse response = (FullHttpResponse) msg;\n            throw new IllegalStateException(\n                    \"Unexpected FullHttpResponse (getStatus=\" + response.status() +\n                            \", content=\" + response.content().toString(StandardCharsets.UTF_8) + ')');\n        }\n\n        if (msg instanceof BinaryWebSocketFrame) {\n            BinaryWebSocketFrame frame = (BinaryWebSocketFrame) msg;\n            log.trace(\"WebSocket Client received message: \" + frame.content());\n            ctx.fireChannelRead(((WebSocketFrame) msg).retain());\n        } else if (msg instanceof CloseWebSocketFrame) {\n            log.trace(\"WebSocket Client received closing\");\n            ch.close();\n        }\n    }\n\n    @Override\n    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {\n        cause.printStackTrace();\n        if (!handshakeFuture.isDone()) {\n            handshakeFuture.setFailure(cause);\n        }\n        ctx.close();\n    }\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/model/websocket/WebSocketClient.java",
    "content": "/*\n * Copyright 2014 The Netty Project\n *\n * The Netty Project licenses this file to you under the Apache License,\n * version 2.0 (the \"License\"); you may not use this file except in compliance\n * with the License. You may obtain a copy of the License at:\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n */\npackage cc.blynk.integration.model.websocket;\n\nimport cc.blynk.client.core.BaseClient;\nimport cc.blynk.integration.model.SimpleClientHandler;\nimport cc.blynk.server.Limits;\nimport cc.blynk.server.core.protocol.handlers.decoders.MessageDecoder;\nimport cc.blynk.server.core.protocol.model.messages.MessageBase;\nimport cc.blynk.server.core.stats.GlobalStats;\nimport cc.blynk.utils.properties.ServerProperties;\nimport io.netty.buffer.ByteBuf;\nimport io.netty.buffer.ByteBufAllocator;\nimport io.netty.channel.ChannelInitializer;\nimport io.netty.channel.ChannelPipeline;\nimport io.netty.channel.socket.SocketChannel;\nimport io.netty.handler.codec.http.DefaultHttpHeaders;\nimport io.netty.handler.codec.http.HttpClientCodec;\nimport io.netty.handler.codec.http.HttpObjectAggregator;\nimport io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;\nimport io.netty.handler.codec.http.websocketx.WebSocketClientHandshakerFactory;\nimport io.netty.handler.codec.http.websocketx.WebSocketFrame;\nimport io.netty.handler.codec.http.websocketx.WebSocketVersion;\nimport io.netty.handler.ssl.SslContext;\nimport io.netty.handler.ssl.SslContextBuilder;\nimport io.netty.handler.ssl.SslProvider;\nimport io.netty.handler.ssl.util.InsecureTrustManagerFactory;\nimport org.mockito.Mockito;\n\nimport java.net.URI;\nimport java.util.Collections;\nimport java.util.Random;\n\npublic final class WebSocketClient extends BaseClient {\n\n    public final SimpleClientHandler responseMock = Mockito.mock(SimpleClientHandler.class);\n    final SslContext sslCtx;\n    private final WebSocketClientHandler handler;\n    public int msgId = 0;\n\n    public WebSocketClient(String host, int port, String path, boolean isSSL) throws Exception {\n        super(host, port, new Random());\n\n        String scheme = isSSL ? \"wss://\" : \"ws://\";\n        URI uri = new URI(scheme + host + \":\" + port + path);\n\n        if (isSSL) {\n            sslCtx = SslContextBuilder.forClient().sslProvider(SslProvider.JDK).trustManager(InsecureTrustManagerFactory.INSTANCE).build();\n        } else {\n            sslCtx = null;\n        }\n\n        this.handler = new WebSocketClientHandler(\n                        WebSocketClientHandshakerFactory.newHandshaker(\n                                uri, WebSocketVersion.V13, null, false, new DefaultHttpHeaders()));\n    }\n\n    private static WebSocketFrame produceWebSocketFrame(MessageBase msg) {\n        byte[] data = msg.getBytes();\n        ByteBuf bb = ByteBufAllocator.DEFAULT.heapBuffer(5 + data.length);\n        bb.writeByte(msg.command);\n        bb.writeShort(msg.id);\n        bb.writeShort(data.length);\n        bb.writeBytes(data);\n        return new BinaryWebSocketFrame(bb);\n    }\n\n    @Override\n    protected ChannelInitializer<SocketChannel> getChannelInitializer() {\n        return new ChannelInitializer<SocketChannel> () {\n            @Override\n            public void initChannel(SocketChannel ch) throws Exception {\n                ChannelPipeline p = ch.pipeline();\n                if (sslCtx != null) {\n                    p.addLast(sslCtx.newHandler(ch.alloc(), host, port));\n                }\n                p.addLast(\n                        new HttpClientCodec(),\n                        new HttpObjectAggregator(8192),\n                        handler,\n                        new MessageDecoder(new GlobalStats(),\n                                new Limits(new ServerProperties(Collections.emptyMap())))\n                );\n            }\n        };\n    }\n\n    @Override\n    public void start() {\n        super.start();\n        startHandshake();\n    }\n\n    private void startHandshake() {\n        handler.startHandshake(channel);\n        try {\n            handler.handshakeFuture().sync();\n            this.channel.pipeline().addLast(responseMock);\n        } catch (Exception e) {\n            log.error(e);\n        }\n    }\n\n    public void send(String line) {\n        send(produceWebSocketFrame(produceMessageBaseOnUserInput(line, ++msgId)));\n    }\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/model/websocket/WebSocketClientHandler.java",
    "content": "/*\n * Copyright 2012 The Netty Project\n *\n * The Netty Project licenses this file to you under the Apache License,\n * version 2.0 (the \"License\"); you may not use this file except in compliance\n * with the License. You may obtain a copy of the License at:\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n */\n//The MIT License\n//\n//Copyright (c) 2009 Carl Bystršm\n//\n//Permission is hereby granted, free of charge, to any person obtaining a copy\n//of this software and associated documentation files (the \"Software\"), to deal\n//in the Software without restriction, including without limitation the rights\n//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n//copies of the Software, and to permit persons to whom the Software is\n//furnished to do so, subject to the following conditions:\n//\n//The above copyright notice and this permission notice shall be included in\n//all copies or substantial portions of the Software.\n//\n//THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n//THE SOFTWARE.\n\npackage cc.blynk.integration.model.websocket;\n\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelFuture;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.ChannelPromise;\nimport io.netty.channel.SimpleChannelInboundHandler;\nimport io.netty.handler.codec.http.FullHttpResponse;\nimport io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;\nimport io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;\nimport io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker;\nimport io.netty.handler.codec.http.websocketx.WebSocketFrame;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.nio.charset.StandardCharsets;\n\npublic class WebSocketClientHandler extends SimpleChannelInboundHandler<Object> {\n\n    private static final Logger log = LogManager.getLogger(WebSocketClientHandler.class);\n\n    private final WebSocketClientHandshaker handshaker;\n    private ChannelPromise handshakeFuture;\n\n    public WebSocketClientHandler(WebSocketClientHandshaker handshaker) {\n        this.handshaker = handshaker;\n    }\n\n    public ChannelFuture handshakeFuture() {\n        return handshakeFuture;\n    }\n\n    public void startHandshake(Channel channel) {\n        handshaker.handshake(channel);\n        handshakeFuture = channel.newPromise();\n    }\n\n    @Override\n    public void channelInactive(ChannelHandlerContext ctx) {\n        System.out.println(\"WebSocket Client disconnected!\");\n    }\n\n    @Override\n    public void channelRead0(ChannelHandlerContext ctx, Object msg) {\n        Channel ch = ctx.channel();\n        if (!handshaker.isHandshakeComplete()) {\n            handshaker.finishHandshake(ch, (FullHttpResponse) msg);\n            log.trace(\"WebSocket Client connected!\");\n            if (handshakeFuture != null) {\n                handshakeFuture.setSuccess();\n            }\n            return;\n        }\n\n        if (msg instanceof FullHttpResponse) {\n            FullHttpResponse response = (FullHttpResponse) msg;\n            throw new IllegalStateException(\n                    \"Unexpected FullHttpResponse (getStatus=\" + response.status() +\n                            \", content=\" + response.content().toString(StandardCharsets.UTF_8) + ')');\n        }\n\n        WebSocketFrame frame = (WebSocketFrame) msg;\n        if (frame instanceof BinaryWebSocketFrame) {\n            BinaryWebSocketFrame binaryFrame = (BinaryWebSocketFrame) frame;\n            log.trace(\"WebSocket Client received message: \" + binaryFrame.content());\n            ctx.fireChannelRead(binaryFrame.retain().content());\n        } else if (frame instanceof CloseWebSocketFrame) {\n            log.trace(\"WebSocket Client received closing\");\n            ch.close();\n        }\n    }\n\n    @Override\n    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {\n        cause.printStackTrace();\n        if (!handshakeFuture.isDone()) {\n            handshakeFuture.setFailure(cause);\n        }\n        ctx.close();\n    }\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/tcp/AppMailTest.java",
    "content": "package cc.blynk.integration.tcp;\n\nimport cc.blynk.integration.SingleServerInstancePerTest;\nimport cc.blynk.integration.model.tcp.TestAppClient;\nimport cc.blynk.server.core.model.device.BoardType;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.protocol.model.messages.ResponseMessage;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport static cc.blynk.integration.TestUtil.createDevice;\nimport static cc.blynk.integration.TestUtil.illegalCommand;\nimport static cc.blynk.integration.TestUtil.notAllowed;\nimport static cc.blynk.integration.TestUtil.ok;\nimport static cc.blynk.server.core.protocol.enums.Response.QUOTA_LIMIT;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\nimport static org.mockito.Mockito.after;\nimport static org.mockito.Mockito.eq;\nimport static org.mockito.Mockito.startsWith;\nimport static org.mockito.Mockito.timeout;\nimport static org.mockito.Mockito.verify;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/2/2015.\n *\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class AppMailTest extends SingleServerInstancePerTest {\n\n    @Test\n    public void testSendEmail() throws Exception {\n        TestAppClient appClient = new TestAppClient(properties);\n        appClient.start();\n        appClient.login(getUserName(), \"1\");\n        appClient.verifyResult(ok(1));\n\n        appClient.send(\"email 1\");\n        verify(holder.mailWrapper, timeout(1000)).sendText(eq(getUserName()), eq(\"Auth Token for My Dashboard project and device My Device\"), startsWith(\"Auth Token : \"));\n    }\n\n    @Test\n    public void testSendEmailForDevice() throws Exception {\n        TestAppClient appClient = new TestAppClient(properties);\n        appClient.start();\n        appClient.login(getUserName(), \"1\");\n        appClient.verifyResult(ok(1));\n\n        appClient.send(\"email 1 0\");\n        verify(holder.mailWrapper, timeout(1000)).sendText(eq(getUserName()), eq(\"Auth Token for My Dashboard project and device My Device\"), startsWith(\"Auth Token : \"));\n    }\n\n    @Test\n    public void testSendEmailForSingleDevice() throws Exception {\n        TestAppClient appClient = new TestAppClient(properties);\n        appClient.start();\n        appClient.login(getUserName(), \"1\");\n        appClient.verifyResult(ok(1));\n\n        clientPair.appClient.send(\"getDevices 1\");\n        Device[] devices = clientPair.appClient.parseDevices();\n\n        assertEquals(1, devices.length);\n\n        appClient.send(\"email 1\");\n\n        String expectedBody = String.format(\"Auth Token : %s\\n\" +\n                \"\\n\" +\n                \"Happy Blynking!\\n\" +\n                \"-\\n\" +\n                \"Getting Started Guide -> https://www.blynk.cc/getting-started\\n\" +\n                \"Documentation -> http://docs.blynk.cc/\\n\" +\n                \"Sketch generator -> https://examples.blynk.cc/\\n\" +\n                \"\\n\" +\n                \"Latest Blynk library -> https://github.com/blynkkk/blynk-library/releases/download/v0.6.1/Blynk_Release_v0.6.1.zip\\n\" +\n                \"Latest Blynk server -> https://github.com/blynkkk/blynk-server/releases/download/v0.41.16/server-0.41.16.jar\\n\" +\n                \"-\\n\" +\n                \"https://www.blynk.cc\\n\" +\n                \"twitter.com/blynk_app\\n\" +\n                \"www.facebook.com/blynkapp\\n\", devices[0].token);\n\n        verify(holder.mailWrapper, timeout(1000)).sendText(eq(getUserName()), eq(\"Auth Token for My Dashboard project and device My Device\"), eq(expectedBody));\n    }\n\n    @Test\n    public void testSendEmailForMultiDevices() throws Exception {\n        TestAppClient appClient = new TestAppClient(properties);\n        appClient.start();\n        appClient.login(getUserName(), \"1\");\n        appClient.verifyResult(ok(1));\n\n        Device device1 = new Device(1, \"My Device2\", BoardType.ESP8266);\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        clientPair.appClient.send(\"getDevices 1\");\n        Device[] devices = clientPair.appClient.parseDevices(2);\n\n        appClient.send(\"email 1\");\n\n        String expectedBody = String.format(\"Auth Token for device 'My Device' : %s\\n\" +\n                \"Auth Token for device 'My Device2' : %s\\n\" +\n                \"\\n\" +\n                \"Happy Blynking!\\n\" +\n                \"-\\n\" +\n                \"Getting Started Guide -> https://www.blynk.cc/getting-started\\n\" +\n                \"Documentation -> http://docs.blynk.cc/\\n\" +\n                \"Sketch generator -> https://examples.blynk.cc/\\n\" +\n                \"\\n\" +\n                \"Latest Blynk library -> https://github.com/blynkkk/blynk-library/releases/download/v0.6.1/Blynk_Release_v0.6.1.zip\\n\" +\n                \"Latest Blynk server -> https://github.com/blynkkk/blynk-server/releases/download/v0.41.16/server-0.41.16.jar\\n\" +\n                \"-\\n\" +\n                \"https://www.blynk.cc\\n\" +\n                \"twitter.com/blynk_app\\n\" +\n                \"www.facebook.com/blynkapp\\n\", devices[0].token, devices[1].token);\n\n        verify(holder.mailWrapper, timeout(1000)).sendText(eq(getUserName()), eq(\"Auth Tokens for My Dashboard project and 2 devices\"), eq(expectedBody));\n    }\n\n    @Test\n    public void testEmailMininalValidation() throws Exception {\n        //adding email widget\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":432, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"type\\\":\\\"EMAIL\\\"}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"email to subj body\");\n        verify(holder.mailWrapper, after(500).never()).sendHtml(eq(\"to\"), eq(\"subj\"), eq(\"body\"));\n        clientPair.hardwareClient.verifyResult(illegalCommand(1));\n    }\n\n    @Test\n    public void testEmailWorks() throws Exception {\n        //no email widget\n        clientPair.hardwareClient.send(\"email to subj body\");\n        clientPair.hardwareClient.verifyResult(notAllowed(1));\n\n        //adding email widget\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":432, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"type\\\":\\\"EMAIL\\\"}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"email to@to.com subj body\");\n        verify(holder.mailWrapper, timeout(500)).sendHtml(eq(\"to@to.com\"), eq(\"subj\"), eq(\"body\"));\n        clientPair.hardwareClient.verifyResult(ok(2));\n\n        clientPair.hardwareClient.send(\"email to@to.com subj body\");\n        clientPair.hardwareClient.verifyResult(new ResponseMessage(3, QUOTA_LIMIT));\n    }\n\n    @Test\n    public void testPlainTextIsAllowed() throws Exception {\n        //adding email widget\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":432, \\\"contentType\\\":\\\"TEXT_PLAIN\\\", \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"type\\\":\\\"EMAIL\\\"}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"email to@to.com subj body\");\n        verify(holder.mailWrapper, timeout(500)).sendText(eq(\"to@to.com\"), eq(\"subj\"), eq(\"body\"));\n        clientPair.hardwareClient.verifyResult(ok(1));\n    }\n\n    @Test\n    public void testPlaceholderForDeviceNameWorks() throws Exception {\n        //adding email widget\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":432, \\\"contentType\\\":\\\"TEXT_PLAIN\\\", \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"type\\\":\\\"EMAIL\\\"}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"email to@to.com SUBJ_{DEVICE_NAME} BODY_{DEVICE_NAME}\");\n        verify(holder.mailWrapper, timeout(500)).sendText(eq(\"to@to.com\"), eq(\"SUBJ_My Device\"), eq(\"BODY_My Device\"));\n        clientPair.hardwareClient.verifyResult(ok(1));\n    }\n\n    @Test\n    public void testPlaceholderForVendorWorks() throws Exception {\n        //adding email widget\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":432, \\\"contentType\\\":\\\"TEXT_PLAIN\\\", \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"type\\\":\\\"EMAIL\\\"}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"email {VENDOR_EMAIL} SUBJ_{VENDOR_EMAIL} BODY_{VENDOR_EMAIL}\");\n        verify(holder.mailWrapper, timeout(500)).sendText(eq(\"vendor@blynk.cc\"), eq(\"SUBJ_vendor@blynk.cc\"), eq(\"BODY_vendor@blynk.cc\"));\n        clientPair.hardwareClient.verifyResult(ok(1));\n    }\n\n    @Test\n    public void testPlaceholderForDeviceOwnerWorks() throws Exception {\n        //adding email widget\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":432, \\\"contentType\\\":\\\"TEXT_PLAIN\\\", \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"type\\\":\\\"EMAIL\\\"}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"email {DEVICE_OWNER_EMAIL} SUBJ_{DEVICE_OWNER_EMAIL} BODY_{DEVICE_OWNER_EMAIL}\");\n        verify(holder.mailWrapper, timeout(500)).sendText(eq(getUserName()), eq(\"SUBJ_\" + getUserName()), eq(\"BODY_\" + getUserName()));\n        clientPair.hardwareClient.verifyResult(ok(1));\n    }\n\n    @Test\n    public void testEmailWorkWithEmailFromApp() throws Exception {\n        //adding email widget\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":432, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"to\\\":\\\"test@mail.ua\\\", \\\"type\\\":\\\"EMAIL\\\"}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"email subj body\");\n        verify(holder.mailWrapper, timeout(500)).sendHtml(eq(\"test@mail.ua\"), eq(\"subj\"), eq(\"body\"));\n        clientPair.hardwareClient.verifyResult(ok(1));\n    }\n\n    @Test\n    public void testEmailFromAppOverridesEmailFromHardware() throws Exception {\n        //adding email widget\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":432, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"to\\\":\\\"test@mail.ua\\\", \\\"type\\\":\\\"EMAIL\\\"}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"email to@to.com subj body\");\n        verify(holder.mailWrapper, timeout(500)).sendHtml(eq(\"test@mail.ua\"), eq(\"subj\"), eq(\"body\"));\n        clientPair.hardwareClient.verifyResult(ok(1));\n    }\n\n    @Test\n    public void testEmailWorkWithNoEmailInApp() throws Exception {\n        //adding email widget\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":432, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"width\\\":1, \\\"height\\\":1, \\\"type\\\":\\\"EMAIL\\\"}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"email subj body\");\n        verify(holder.mailWrapper, timeout(500)).sendHtml(eq(getUserName()), eq(\"subj\"), eq(\"body\"));\n        clientPair.hardwareClient.verifyResult(ok(1));\n    }\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/tcp/AppOfflineTest.java",
    "content": "package cc.blynk.integration.tcp;\n\nimport cc.blynk.integration.BaseTest;\nimport cc.blynk.integration.TestUtil;\nimport cc.blynk.integration.model.tcp.ClientPair;\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.servers.BaseServer;\nimport cc.blynk.server.servers.application.MobileAndHttpsServer;\nimport cc.blynk.server.servers.hardware.HardwareAndHttpAPIServer;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport static cc.blynk.integration.TestUtil.createDefaultHolder;\nimport static cc.blynk.integration.TestUtil.internal;\nimport static cc.blynk.integration.TestUtil.ok;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/2/2015.\n *\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class AppOfflineTest extends BaseTest {\n\n    private BaseServer appServer;\n    private BaseServer hardwareServer;\n    private ClientPair clientPair;\n\n    @Before\n    public void init() throws Exception {\n        properties.setProperty(\"app.socket.idle.timeout\", \"1\");\n        Holder holder = createDefaultHolder(properties, \"no-db.properties\");\n        this.hardwareServer = new HardwareAndHttpAPIServer(holder).start();\n        this.appServer = new MobileAndHttpsServer(holder).start();\n\n        this.clientPair = initAppAndHardPair(\"user_profile_json_empty_dash.txt\");\n    }\n\n    @After\n    public void shutdown() {\n        this.appServer.close();\n        this.hardwareServer.close();\n        this.clientPair.stop();\n    }\n\n    @Test\n    public void testWarn() throws Exception {\n        clientPair.appClient.updateDash(\"{\\\"id\\\":1, \\\"name\\\":\\\"test board\\\", \\\"isAppConnectedOn\\\":true}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        TestUtil.sleep(1500);\n\n        clientPair.hardwareClient.verifyResult(internal(7777, \"adis\"));\n    }\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/tcp/AppSyncWorkflowTest.java",
    "content": "package cc.blynk.integration.tcp;\n\nimport cc.blynk.integration.SingleServerInstancePerTest;\nimport cc.blynk.integration.model.tcp.TestAppClient;\nimport cc.blynk.server.core.model.DataStream;\nimport cc.blynk.server.core.model.Profile;\nimport cc.blynk.server.core.model.device.BoardType;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.device.Status;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.widgets.outputs.ValueDisplay;\nimport cc.blynk.server.core.model.widgets.outputs.graph.FontSize;\nimport cc.blynk.server.core.model.widgets.ui.DeviceSelector;\nimport cc.blynk.server.core.model.widgets.ui.table.Table;\nimport cc.blynk.server.core.model.widgets.ui.tiles.DeviceTiles;\nimport cc.blynk.server.core.model.widgets.ui.tiles.TileTemplate;\nimport cc.blynk.server.core.model.widgets.ui.tiles.templates.PageTileTemplate;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport static cc.blynk.integration.TestUtil.appSync;\nimport static cc.blynk.integration.TestUtil.b;\nimport static cc.blynk.integration.TestUtil.createDevice;\nimport static cc.blynk.integration.TestUtil.hardware;\nimport static cc.blynk.integration.TestUtil.ok;\nimport static cc.blynk.integration.TestUtil.setProperty;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_ENERGY;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE;\nimport static cc.blynk.server.core.protocol.model.messages.MessageFactory.produce;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.timeout;\nimport static org.mockito.Mockito.verify;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/2/2015.\n *\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class AppSyncWorkflowTest extends SingleServerInstancePerTest {\n\n    @Test\n    public void testLCDOnActivateSendsCorrectBodySimpleMode() throws Exception {\n        clientPair.appClient.createWidget(1, \"{\\\"type\\\":\\\"LCD\\\",\\\"id\\\":1923810267,\\\"x\\\":0,\\\"y\\\":6,\\\"color\\\":600084223,\\\"width\\\":8,\\\"height\\\":2,\\\"tabId\\\":0,\\\"\" +\n                \"pins\\\":[\" +\n                \"{\\\"pin\\\":10,\\\"pinType\\\":\\\"VIRTUAL\\\",\\\"pwmMode\\\":false,\\\"rangeMappingOn\\\":false,\\\"min\\\":0,\\\"max\\\":1023, \\\"value\\\":\\\"10\\\"},\" +\n                \"{\\\"pin\\\":11,\\\"pinType\\\":\\\"VIRTUAL\\\",\\\"pwmMode\\\":false,\\\"rangeMappingOn\\\":false,\\\"min\\\":0,\\\"max\\\":1023, \\\"value\\\":\\\"11\\\"}],\" +\n                \"\\\"advancedMode\\\":false,\\\"textLight\\\":false,\\\"textLightOn\\\":false,\\\"frequency\\\":1000}\");\n\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.sync(1);\n\n        verify(clientPair.appClient.responseMock, timeout(500).times(13)).channelRead(any(), any());\n\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 10 10\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 11 11\"));\n\n\n        clientPair.appClient.verifyResult(appSync(\"1-0 dw 1 1\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 dw 2 1\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 aw 3 0\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 dw 5 1\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 4 244\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 aw 7 3\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 aw 30 3\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 0 89.888037459418\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 11 -58.74774244674501\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 13 60 143 158\"));\n    }\n\n    @Test\n    public void testLCDOnActivateSendsCorrectBodyAdvancedMode() throws Exception {\n        clientPair.appClient.createWidget(1, \"{\\\"type\\\":\\\"LCD\\\",\\\"id\\\":1923810267,\\\"x\\\":0,\\\"y\\\":6,\\\"color\\\":600084223,\\\"width\\\":8,\\\"height\\\":2,\\\"tabId\\\":0,\\\"\" +\n                \"pins\\\":[\" +\n                \"{\\\"pin\\\":10,\\\"pinType\\\":\\\"VIRTUAL\\\",\\\"pwmMode\\\":false,\\\"rangeMappingOn\\\":false,\\\"min\\\":0,\\\"max\\\":1023},\" +\n                \"{\\\"pin\\\":11,\\\"pinType\\\":\\\"VIRTUAL\\\",\\\"pwmMode\\\":false,\\\"rangeMappingOn\\\":false,\\\"min\\\":0,\\\"max\\\":1023}],\" +\n                \"\\\"advancedMode\\\":true,\\\"textLight\\\":false,\\\"textLightOn\\\":false,\\\"frequency\\\":1000}\");\n\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 10 p x y 10\");\n        clientPair.appClient.verifyResult(hardware(1, \"1-0 vw 10 p x y 10\"));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.sync(1);\n\n        verify(clientPair.appClient.responseMock, timeout(500).times(12)).channelRead(any(), any());\n\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.verifyResult(appSync(1111, \"1-0 vw 10 p x y 10\"));\n\n\n        clientPair.appClient.verifyResult(appSync(\"1-0 dw 1 1\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 dw 2 1\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 aw 3 0\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 dw 5 1\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 4 244\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 aw 7 3\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 aw 30 3\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 0 89.888037459418\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 11 -58.74774244674501\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 13 60 143 158\"));\n    }\n\n\n    @Test\n    public void testTerminalSendsSyncOnActivate() throws Exception {\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(1);\n        assertEquals(16, profile.dashBoards[0].widgets.length);\n\n        clientPair.appClient.send(\"getEnergy\");\n        clientPair.appClient.verifyResult(produce(2, GET_ENERGY, \"7500\"));\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":102, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":5, \\\"y\\\":0, \\\"tabId\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"TERMINAL\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":17}\");\n        clientPair.appClient.verifyResult(ok(3));\n\n        clientPair.hardwareClient.send(\"hardware vw 17 a\");\n        clientPair.hardwareClient.send(\"hardware vw 17 b\");\n        clientPair.hardwareClient.send(\"hardware vw 17 c\");\n        clientPair.appClient.verifyResult(hardware(1, \"1-0 vw 17 a\"));\n        clientPair.appClient.verifyResult(hardware(2, \"1-0 vw 17 b\"));\n        clientPair.appClient.verifyResult(hardware(3, \"1-0 vw 17 c\"));\n\n        clientPair.appClient.deactivate(1);\n        clientPair.appClient.verifyResult(ok(4));\n\n        clientPair.appClient.activate(1);\n        clientPair.appClient.verifyResult(ok(5));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vm 17 a b c\"));\n    }\n\n    @Test\n    public void testTerminalStorageRemembersCommands() throws Exception {\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(1);\n        assertEquals(16, profile.dashBoards[0].widgets.length);\n\n        clientPair.appClient.send(\"getEnergy\");\n        clientPair.appClient.verifyResult(produce(2, GET_ENERGY, \"7500\"));\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":102, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":5, \\\"y\\\":0, \\\"tabId\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"TERMINAL\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":17}\");\n        clientPair.appClient.verifyResult(ok(3));\n\n        clientPair.hardwareClient.send(\"hardware vw 17 1\");\n        clientPair.hardwareClient.send(\"hardware vw 17 2\");\n        clientPair.hardwareClient.send(\"hardware vw 17 3\");\n        clientPair.hardwareClient.send(\"hardware vw 17 4\");\n        clientPair.hardwareClient.send(\"hardware vw 17 dddyyyiii\");\n        verify(clientPair.appClient.responseMock, timeout(1000)).channelRead(any(), eq(hardware(5, \"1-0 vw 17 dddyyyiii\")));\n\n        clientPair.appClient.activate(1);\n        clientPair.appClient.verifyResult(appSync(\"1-0 vm 17 1 2 3 4 dddyyyiii\"));\n    }\n\n    @Test\n    public void testTerminalStorageRemembersCommandsInNewFormat() throws Exception {\n        clientPair.appClient.stop();\n\n        TestAppClient appClient = new TestAppClient(properties);\n        appClient.start();\n\n        appClient.login(getUserName(), \"1\", \"Android\", \"2.26.0\");\n        appClient.verifyResult(ok(1));\n\n        appClient.send(\"loadProfileGzipped\");\n        Profile profile = appClient.parseProfile(2);\n        assertEquals(16, profile.dashBoards[0].widgets.length);\n\n        appClient.send(\"getEnergy\");\n        appClient.verifyResult(produce(3, GET_ENERGY, \"7500\"));\n\n        appClient.createWidget(1, \"{\\\"id\\\":102, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":5, \\\"y\\\":0, \\\"tabId\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"TERMINAL\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":56}\");\n        appClient.verifyResult(ok(4));\n\n        clientPair.hardwareClient.send(\"hardware vw 56 1\");\n        clientPair.hardwareClient.send(\"hardware vw 56 2\");\n        clientPair.hardwareClient.send(\"hardware vw 56 3\");\n        clientPair.hardwareClient.send(\"hardware vw 56 4\");\n        clientPair.hardwareClient.send(\"hardware vw 56 dddyyyiii\");\n\n        appClient.verifyResult(hardware(1, \"1-0 vw 56 1\"));\n        appClient.verifyResult(hardware(2, \"1-0 vw 56 2\"));\n        appClient.verifyResult(hardware(3, \"1-0 vw 56 3\"));\n        appClient.verifyResult(hardware(4, \"1-0 vw 56 4\"));\n        appClient.verifyResult(hardware(5, \"1-0 vw 56 dddyyyiii\"));\n\n        appClient.activate(1);\n        appClient.verifyResult(ok(5));\n\n        appClient.verifyResult(appSync(\"1-0 vm 56 1 2 3 4 dddyyyiii\"));\n    }\n\n    @Test\n    public void testTableStorageRemembersCommandsInNewFormat() throws Exception {\n        clientPair.appClient.stop();\n\n        TestAppClient appClient = new TestAppClient(properties);\n        appClient.start();\n\n        appClient.login(getUserName(), \"1\", \"Android\", \"2.26.0\");\n        appClient.verifyResult(ok(1));\n\n        appClient.send(\"loadProfileGzipped\");\n        Profile profile = appClient.parseProfile(2);\n        assertEquals(16, profile.dashBoards[0].widgets.length);\n\n        Table table = new Table();\n        table.id = 102;\n        table.width = 2;\n        table.height = 2;\n        table.pin = 56;\n        table.pinType = PinType.VIRTUAL;\n        table.deviceId = 0;\n\n        appClient.createWidget(1, table);\n        appClient.verifyResult(ok(3));\n\n        clientPair.hardwareClient.send(\"hardware vw 56 add 1 Row1 row1\");\n        clientPair.hardwareClient.send(\"hardware vw 56 add 2 Row2 row2\");\n        clientPair.hardwareClient.send(\"hardware vw 56 add 3 Row3 row3\");\n        clientPair.hardwareClient.send(\"hardware vw 56 add 4 Row4 row4\");\n\n        appClient.verifyResult(hardware(1, \"1-0 vw 56 add 1 Row1 row1\"));\n        appClient.verifyResult(hardware(2, \"1-0 vw 56 add 2 Row2 row2\"));\n        appClient.verifyResult(hardware(3, \"1-0 vw 56 add 3 Row3 row3\"));\n        appClient.verifyResult(hardware(4, \"1-0 vw 56 add 4 Row4 row4\"));\n\n        appClient.activate(1);\n        appClient.verifyResult(ok(4));\n\n        appClient.verifyResult(appSync(\"1-0 vm 56 add 1 Row1 row1 true add 2 Row2 row2 true add 3 Row3 row3 true add 4 Row4 row4 true\"));\n    }\n\n    @Test\n    public void testTerminalAndAnotherWidgetOnTheSamePin() throws Exception {\n        clientPair.appClient.stop();\n\n        TestAppClient appClient = new TestAppClient(properties);\n        appClient.start();\n\n        appClient.login(getUserName(), \"1\", \"Android\", \"2.26.0\");\n        appClient.verifyResult(ok(1));\n\n        appClient.send(\"loadProfileGzipped\");\n        Profile profile = appClient.parseProfile(2);\n        assertEquals(16, profile.dashBoards[0].widgets.length);\n\n        appClient.send(\"getEnergy\");\n        appClient.verifyResult(produce(3, GET_ENERGY, \"7500\"));\n\n        appClient.createWidget(1, \"{\\\"id\\\":102, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":5, \\\"y\\\":0, \\\"tabId\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"TERMINAL\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":56}\");\n        appClient.createWidget(1, \"{\\\"id\\\":103, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":5, \\\"y\\\":0, \\\"tabId\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"DIGIT4_DISPLAY\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":56}\");\n        appClient.verifyResult(ok(4));\n        appClient.verifyResult(ok(5));\n\n        clientPair.hardwareClient.send(\"hardware vw 56 1\");\n        clientPair.hardwareClient.send(\"hardware vw 56 2\");\n        clientPair.hardwareClient.send(\"hardware vw 56 3\");\n        clientPair.hardwareClient.send(\"hardware vw 56 4\");\n        clientPair.hardwareClient.send(\"hardware vw 56 dddyyyiii\");\n\n        appClient.verifyResult(hardware(1, \"1-0 vw 56 1\"));\n        appClient.verifyResult(hardware(2, \"1-0 vw 56 2\"));\n        appClient.verifyResult(hardware(3, \"1-0 vw 56 3\"));\n        appClient.verifyResult(hardware(4, \"1-0 vw 56 4\"));\n        appClient.verifyResult(hardware(5, \"1-0 vw 56 dddyyyiii\"));\n\n        appClient.activate(1);\n        appClient.verifyResult(ok(6));\n\n        appClient.verifyResult(appSync(\"1-0 vm 56 1 2 3 4 dddyyyiii\"));\n        appClient.verifyResult(appSync(\"1-0 vw 56 dddyyyiii\"));\n    }\n\n    @Test\n    public void testTerminalAndAnotherWidgetOnTheSamePinAndDeviceSelector() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        clientPair.appClient.stop();\n\n        TestAppClient appClient = new TestAppClient(properties);\n        appClient.start();\n\n        appClient.login(getUserName(), \"1\", \"Android\", \"2.26.0\");\n        appClient.verifyResult(ok(1));\n\n        appClient.send(\"loadProfileGzipped\");\n        Profile profile = appClient.parseProfile(2);\n        assertEquals(16, profile.dashBoards[0].widgets.length);\n\n        appClient.send(\"getEnergy\");\n        appClient.verifyResult(produce(3, GET_ENERGY, \"7500\"));\n\n        DeviceSelector deviceSelector = new DeviceSelector();\n        deviceSelector.id = 200_000;\n        deviceSelector.height = 4;\n        deviceSelector.width = 1;\n        deviceSelector.deviceIds = new int [] {0, 1};\n\n        appClient.createWidget(1, deviceSelector);\n        appClient.verifyResult(ok(4));\n\n        appClient.createWidget(1, \"{\\\"id\\\":103, \\\"deviceId\\\":200000, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":5, \\\"y\\\":0, \\\"tabId\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"DIGIT4_DISPLAY\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":56}\");\n        appClient.verifyResult(ok(5));\n        appClient.createWidget(1, \"{\\\"id\\\":102, \\\"deviceId\\\":200000, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":5, \\\"y\\\":0, \\\"tabId\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"TERMINAL\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":56}\");\n        appClient.verifyResult(ok(6));\n\n        clientPair.hardwareClient.send(\"hardware vw 56 1\");\n        clientPair.hardwareClient.send(\"hardware vw 56 2\");\n        clientPair.hardwareClient.send(\"hardware vw 56 3\");\n        clientPair.hardwareClient.send(\"hardware vw 56 4\");\n        clientPair.hardwareClient.send(\"hardware vw 56 dddyyyiii\");\n\n        appClient.verifyResult(hardware(1, \"1-0 vw 56 1\"));\n        appClient.verifyResult(hardware(2, \"1-0 vw 56 2\"));\n        appClient.verifyResult(hardware(3, \"1-0 vw 56 3\"));\n        appClient.verifyResult(hardware(4, \"1-0 vw 56 4\"));\n        appClient.verifyResult(hardware(5, \"1-0 vw 56 dddyyyiii\"));\n\n        appClient.sync(1, 0);\n        appClient.verifyResult(ok(7));\n\n        appClient.verifyResult(appSync(\"1-0 vw 56 dddyyyiii\"));\n        appClient.verifyResult(appSync(\"1-0 vm 56 1 2 3 4 dddyyyiii\"));\n    }\n\n    @Test\n    public void testTerminalAndAnotherWidgetOnTheSamePinAndDeviceSelectorAnotherOrder() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        clientPair.appClient.stop();\n\n        TestAppClient appClient = new TestAppClient(properties);\n        appClient.start();\n\n        appClient.login(getUserName(), \"1\", \"Android\", \"2.26.0\");\n        appClient.verifyResult(ok(1));\n\n        appClient.send(\"loadProfileGzipped\");\n        Profile profile = appClient.parseProfile(2);\n        assertEquals(16, profile.dashBoards[0].widgets.length);\n\n        appClient.send(\"getEnergy\");\n        appClient.verifyResult(produce(3, GET_ENERGY, \"7500\"));\n\n        DeviceSelector deviceSelector = new DeviceSelector();\n        deviceSelector.id = 200_000;\n        deviceSelector.height = 4;\n        deviceSelector.width = 1;\n        deviceSelector.deviceIds = new int [] {0, 1};\n\n        appClient.createWidget(1, deviceSelector);\n        appClient.verifyResult(ok(4));\n\n        appClient.createWidget(1, \"{\\\"id\\\":102, \\\"deviceId\\\":200000, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":5, \\\"y\\\":0, \\\"tabId\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"TERMINAL\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":56}\");\n        appClient.verifyResult(ok(5));\n        appClient.createWidget(1, \"{\\\"id\\\":103, \\\"deviceId\\\":200000, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":5, \\\"y\\\":0, \\\"tabId\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"DIGIT4_DISPLAY\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":56}\");\n        appClient.verifyResult(ok(6));\n\n        clientPair.hardwareClient.send(\"hardware vw 56 1\");\n        clientPair.hardwareClient.send(\"hardware vw 56 2\");\n        clientPair.hardwareClient.send(\"hardware vw 56 3\");\n        clientPair.hardwareClient.send(\"hardware vw 56 4\");\n        clientPair.hardwareClient.send(\"hardware vw 56 dddyyyiii\");\n\n        appClient.verifyResult(hardware(1, \"1-0 vw 56 1\"));\n        appClient.verifyResult(hardware(2, \"1-0 vw 56 2\"));\n        appClient.verifyResult(hardware(3, \"1-0 vw 56 3\"));\n        appClient.verifyResult(hardware(4, \"1-0 vw 56 4\"));\n        appClient.verifyResult(hardware(5, \"1-0 vw 56 dddyyyiii\"));\n\n        appClient.sync(1, 0);\n        appClient.verifyResult(ok(7));\n\n        appClient.verifyResult(appSync(\"1-0 vw 56 dddyyyiii\"));\n        appClient.verifyResult(appSync(\"1-0 vm 56 1 2 3 4 dddyyyiii\"));\n    }\n\n    @Test\n    public void testTerminalStorageRemembersCommandsInOldFormatAndDeviceSelector() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        clientPair.appClient.stop();\n\n        TestAppClient appClient = new TestAppClient(properties);\n        appClient.start();\n\n        appClient.login(getUserName(), \"1\", \"Android\", \"2.25.0\");\n        appClient.verifyResult(ok(1));\n\n        appClient.send(\"loadProfileGzipped\");\n        Profile profile = appClient.parseProfile(2);\n        assertEquals(16, profile.dashBoards[0].widgets.length);\n\n        appClient.send(\"getEnergy\");\n        appClient.verifyResult(produce(3, GET_ENERGY, \"7500\"));\n\n        DeviceSelector deviceSelector = new DeviceSelector();\n        deviceSelector.id = 200_000;\n        deviceSelector.height = 4;\n        deviceSelector.width = 1;\n        deviceSelector.deviceIds = new int [] {0, 1};\n\n        appClient.createWidget(1, deviceSelector);\n        appClient.verifyResult(ok(4));\n\n        appClient.createWidget(1, \"{\\\"id\\\":102, \\\"deviceId\\\":200000, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":5, \\\"y\\\":0, \\\"tabId\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"TERMINAL\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":56}\");\n        appClient.verifyResult(ok(5));\n\n        clientPair.hardwareClient.send(\"hardware vw 56 1\");\n        clientPair.hardwareClient.send(\"hardware vw 56 2\");\n        clientPair.hardwareClient.send(\"hardware vw 56 3\");\n        clientPair.hardwareClient.send(\"hardware vw 56 4\");\n        clientPair.hardwareClient.send(\"hardware vw 56 dddyyyiii\");\n\n        appClient.verifyResult(hardware(1, \"1-0 vw 56 1\"));\n        appClient.verifyResult(hardware(2, \"1-0 vw 56 2\"));\n        appClient.verifyResult(hardware(3, \"1-0 vw 56 3\"));\n        appClient.verifyResult(hardware(4, \"1-0 vw 56 4\"));\n        appClient.verifyResult(hardware(5, \"1-0 vw 56 dddyyyiii\"));\n\n        appClient.sync(1, 0);\n        appClient.verifyResult(ok(6));\n\n        appClient.verifyResult(appSync(\"1-0 vm 56 1 2 3 4 dddyyyiii\"));\n    }\n\n    @Test\n    public void testTerminalStorageRemembersCommandsInNewFormatAndDeviceTiles() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        clientPair.appClient.stop();\n\n        TestAppClient appClient = new TestAppClient(properties);\n        appClient.start();\n\n        appClient.login(getUserName(), \"1\", \"Android\", \"2.26.0\");\n        appClient.verifyResult(ok(1));\n\n        appClient.send(\"loadProfileGzipped\");\n        Profile profile = appClient.parseProfile(2);\n        assertEquals(16, profile.dashBoards[0].widgets.length);\n\n        appClient.send(\"getEnergy\");\n        appClient.verifyResult(produce(3, GET_ENERGY, \"7500\"));\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = 21321;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        appClient.createWidget(1, deviceTiles);\n        appClient.verifyResult(ok(4));\n\n        TileTemplate tileTemplate = new PageTileTemplate(1,\n                null, new int[] {0}, \"name\", \"name\", \"iconName\", BoardType.ESP8266, new DataStream((short) 1, PinType.VIRTUAL),\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        appClient.createTemplate(1, deviceTiles.id, tileTemplate);\n        appClient.verifyResult(ok(5));\n\n        appClient.createWidget(1, deviceTiles.id, tileTemplate.id, \"{\\\"id\\\":102, \\\"deviceId\\\":-1, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":5, \\\"y\\\":0, \\\"tabId\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"TERMINAL\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":56}\");\n        appClient.verifyResult(ok(6));\n\n        clientPair.hardwareClient.send(\"hardware vw 56 1\");\n        clientPair.hardwareClient.send(\"hardware vw 56 2\");\n        clientPair.hardwareClient.send(\"hardware vw 56 3\");\n        clientPair.hardwareClient.send(\"hardware vw 56 4\");\n        clientPair.hardwareClient.send(\"hardware vw 56 dddyyyiii\");\n\n        appClient.verifyResult(hardware(1, \"1-0 vw 56 1\"));\n        appClient.verifyResult(hardware(2, \"1-0 vw 56 2\"));\n        appClient.verifyResult(hardware(3, \"1-0 vw 56 3\"));\n        appClient.verifyResult(hardware(4, \"1-0 vw 56 4\"));\n        appClient.verifyResult(hardware(5, \"1-0 vw 56 dddyyyiii\"));\n\n        appClient.sync(1, 0);\n        appClient.verifyResult(ok(7));\n\n        appClient.verifyResult(appSync(\"1-0 vm 56 1 2 3 4 dddyyyiii\"));\n    }\n\n    @Test\n    public void testTerminalStorageCleanedAfterTilesAreRemoved() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        clientPair.appClient.stop();\n\n        TestAppClient appClient = new TestAppClient(properties);\n        appClient.start();\n\n        appClient.login(getUserName(), \"1\", \"Android\", \"2.26.0\");\n        appClient.verifyResult(ok(1));\n\n        appClient.send(\"loadProfileGzipped\");\n        Profile profile = appClient.parseProfile(2);\n        assertEquals(16, profile.dashBoards[0].widgets.length);\n\n        appClient.send(\"getEnergy\");\n        appClient.verifyResult(produce(3, GET_ENERGY, \"7500\"));\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = 21321;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        appClient.createWidget(1, deviceTiles);\n        appClient.verifyResult(ok(4));\n\n        TileTemplate tileTemplate = new PageTileTemplate(1,\n                null, new int[] {0}, \"name\", \"name\", \"iconName\", BoardType.ESP8266, new DataStream((short) 1, PinType.VIRTUAL),\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        appClient.createTemplate(1, deviceTiles.id, tileTemplate);\n        appClient.verifyResult(ok(5));\n\n        appClient.createWidget(1, deviceTiles.id, tileTemplate.id, \"{\\\"id\\\":102, \\\"deviceId\\\":-1, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":5, \\\"y\\\":0, \\\"tabId\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"TERMINAL\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":56}\");\n        appClient.verifyResult(ok(6));\n\n        ValueDisplay valueDisplay = new ValueDisplay();\n        valueDisplay.id = 103;\n        valueDisplay.width = 2;\n        valueDisplay.height = 2;\n        valueDisplay.deviceId = -1;\n        valueDisplay.pinType = PinType.VIRTUAL;\n        valueDisplay.pin = 57;\n\n        appClient.createWidget(1, deviceTiles.id, tileTemplate.id, valueDisplay);\n        appClient.verifyResult(ok(7));\n\n        clientPair.hardwareClient.send(\"hardware vw 1 0\");\n        clientPair.hardwareClient.send(\"hardware vw 56 1\");\n        clientPair.hardwareClient.send(\"hardware vw 57 2\");\n\n        appClient.verifyResult(hardware(1, \"1-0 vw 1 0\"));\n        appClient.verifyResult(hardware(2, \"1-0 vw 56 1\"));\n        appClient.verifyResult(hardware(3, \"1-0 vw 57 2\"));\n\n        appClient.sync(1, 0);\n        appClient.verifyResult(ok(8));\n\n        appClient.verifyResult(appSync(\"1-0 vm 56 1\"));\n        appClient.verifyResult(appSync(\"1-0 vw 1 0\"));\n        appClient.verifyResult(appSync(\"1-0 vw 57 2\"));\n\n        appClient.deleteWidget(1, deviceTiles.id);\n        appClient.verifyResult(ok(9));\n\n        appClient.reset();\n        appClient.sync(1, 0);\n        appClient.verifyResult(ok(1));\n\n        appClient.neverAfter(100, appSync(\"1-0 vm 56 1\"));\n        appClient.neverAfter(100, appSync(\"1-0 vw 1 0\"));\n        appClient.neverAfter(100, appSync(\"1-0 vw 57 2\"));\n\n    }\n\n    @Test\n    public void testTerminalStorageRemembersCommandsInNewFormatAndDeviceSelector() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        clientPair.appClient.stop();\n\n        TestAppClient appClient = new TestAppClient(properties);\n        appClient.start();\n\n        appClient.login(getUserName(), \"1\", \"Android\", \"2.26.0\");\n        appClient.verifyResult(ok(1));\n\n        appClient.send(\"loadProfileGzipped\");\n        Profile profile = appClient.parseProfile(2);\n        assertEquals(16, profile.dashBoards[0].widgets.length);\n\n        appClient.send(\"getEnergy\");\n        appClient.verifyResult(produce(3, GET_ENERGY, \"7500\"));\n\n        DeviceSelector deviceSelector = new DeviceSelector();\n        deviceSelector.id = 200_000;\n        deviceSelector.height = 4;\n        deviceSelector.width = 1;\n        deviceSelector.deviceIds = new int [] {0, 1};\n\n        appClient.createWidget(1, deviceSelector);\n        appClient.verifyResult(ok(4));\n\n        appClient.createWidget(1, \"{\\\"id\\\":102, \\\"deviceId\\\":200000, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":5, \\\"y\\\":0, \\\"tabId\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"TERMINAL\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":56}\");\n        appClient.verifyResult(ok(5));\n\n        clientPair.hardwareClient.send(\"hardware vw 56 1\");\n        clientPair.hardwareClient.send(\"hardware vw 56 2\");\n        clientPair.hardwareClient.send(\"hardware vw 56 3\");\n        clientPair.hardwareClient.send(\"hardware vw 56 4\");\n        clientPair.hardwareClient.send(\"hardware vw 56 dddyyyiii\");\n\n        appClient.verifyResult(hardware(1, \"1-0 vw 56 1\"));\n        appClient.verifyResult(hardware(2, \"1-0 vw 56 2\"));\n        appClient.verifyResult(hardware(3, \"1-0 vw 56 3\"));\n        appClient.verifyResult(hardware(4, \"1-0 vw 56 4\"));\n        appClient.verifyResult(hardware(5, \"1-0 vw 56 dddyyyiii\"));\n\n        appClient.sync(1, 0);\n        appClient.verifyResult(ok(6));\n\n        appClient.verifyResult(appSync(\"1-0 vm 56 1 2 3 4 dddyyyiii\"));\n    }\n\n    @Test\n    public void testTableSyncWorkForNewCommandFormat() throws Exception {\n        clientPair.appClient.stop();\n\n        TestAppClient appClient = new TestAppClient(properties);\n        appClient.start();\n\n        appClient.login(getUserName(), \"1\", \"Android\", \"2.26.0\");\n        appClient.verifyResult(ok(1));\n\n        appClient.send(\"loadProfileGzipped\");\n        Profile profile = appClient.parseProfile(2);\n        assertEquals(16, profile.dashBoards[0].widgets.length);\n\n        appClient.send(\"getEnergy\");\n        appClient.verifyResult(produce(3, GET_ENERGY, \"7500\"));\n\n        Table table = new Table();\n        table.pin = 56;\n        table.pinType = PinType.VIRTUAL;\n        table.isClickableRows = true;\n        table.isReoderingAllowed = true;\n        table.height = 2;\n        table.width = 2;\n\n        appClient.createWidget(1, table);\n        appClient.verifyResult(ok(4));\n\n        clientPair.hardwareClient.send(\"hardware vw 56 add 0 Row1 1\");\n        clientPair.hardwareClient.send(\"hardware vw 56 add 1 Row2 2\");\n        clientPair.hardwareClient.send(\"hardware vw 56 add 2 Row3 3\");\n        clientPair.hardwareClient.send(\"hardware vw 56 add 3 Row4 4\");\n        clientPair.hardwareClient.send(\"hardware vw 56 add 4 Row5 dddyyyiii\");\n        appClient.verifyResult(produce(1, HARDWARE, b(\"1-0 vw 56 add 0 Row1 1\")));\n        appClient.verifyResult(produce(2, HARDWARE, b(\"1-0 vw 56 add 1 Row2 2\")));\n        appClient.verifyResult(produce(3, HARDWARE, b(\"1-0 vw 56 add 2 Row3 3\")));\n        appClient.verifyResult(produce(4, HARDWARE, b(\"1-0 vw 56 add 3 Row4 4\")));\n        appClient.verifyResult(produce(5, HARDWARE, b(\"1-0 vw 56 add 4 Row5 dddyyyiii\")));\n\n        appClient.activate(1);\n        appClient.verifyResult(ok(5));\n\n        appClient.verifyResult(appSync(\"1-0 vm 56 add 0 Row1 1 true add 1 Row2 2 true add 2 Row3 3 true add 3 Row4 4 true add 4 Row5 dddyyyiii true\"));\n    }\n\n    @Test\n    public void testLCDSendsSyncOnActivate() throws Exception {\n        clientPair.hardwareClient.send(\"hardware vw 20 p 0 0 Hello\");\n        clientPair.hardwareClient.send(\"hardware vw 20 p 0 1 World\");\n\n        clientPair.appClient.verifyResult(produce(1, HARDWARE, b(\"1-0 vw 20 p 0 0 Hello\")));\n        clientPair.appClient.verifyResult(produce(2, HARDWARE, b(\"1-0 vw 20 p 0 1 World\")));\n\n        clientPair.appClient.sync(1);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 20 p 0 0 Hello\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 20 p 0 1 World\"));\n    }\n\n    @Test\n    public void testLCDSendsSyncOnActivate2() throws Exception {\n        clientPair.hardwareClient.send(\"hardware vw 20 p 0 0 H1\");\n        clientPair.hardwareClient.send(\"hardware vw 20 p 0 1 H2\");\n        clientPair.hardwareClient.send(\"hardware vw 20 p 0 2 H3\");\n        clientPair.hardwareClient.send(\"hardware vw 20 p 0 3 H4\");\n        clientPair.hardwareClient.send(\"hardware vw 20 p 0 4 H5\");\n        clientPair.hardwareClient.send(\"hardware vw 20 p 0 5 H6\");\n        clientPair.hardwareClient.send(\"hardware vw 20 p 0 6 H7\");\n\n        clientPair.appClient.verifyResult(hardware(1, \"1-0 vw 20 p 0 0 H1\"));\n        clientPair.appClient.verifyResult(hardware(2, \"1-0 vw 20 p 0 1 H2\"));\n        clientPair.appClient.verifyResult(hardware(3, \"1-0 vw 20 p 0 2 H3\"));\n        clientPair.appClient.verifyResult(hardware(4, \"1-0 vw 20 p 0 3 H4\"));\n        clientPair.appClient.verifyResult(hardware(5, \"1-0 vw 20 p 0 4 H5\"));\n        clientPair.appClient.verifyResult(hardware(6, \"1-0 vw 20 p 0 5 H6\"));\n        clientPair.appClient.verifyResult(hardware(7, \"1-0 vw 20 p 0 6 H7\"));\n\n        clientPair.appClient.sync(1);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 20 p 0 1 H2\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 20 p 0 2 H3\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 20 p 0 3 H4\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 20 p 0 4 H5\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 20 p 0 5 H6\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 20 p 0 6 H7\"));\n    }\n\n    @Test\n    public void testActivateAndGetSync() throws Exception {\n        clientPair.appClient.sync(1);\n\n        verify(clientPair.appClient.responseMock, timeout(500).times(11)).channelRead(any(), any());\n\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.verifyResult(appSync(\"1-0 dw 1 1\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 dw 2 1\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 aw 3 0\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 dw 5 1\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 4 244\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 aw 7 3\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 aw 30 3\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 0 89.888037459418\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 11 -58.74774244674501\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 13 60 143 158\"));\n    }\n\n    @Test\n    //https://github.com/blynkkk/blynk-server/issues/443\n    public void testSyncWidgetValueOverlapsWithPinStorage() throws Exception {\n        clientPair.hardwareClient.send(\"hardware vw 125 1\");\n        clientPair.appClient.verifyResult(hardware(1, \"1-0 vw 125 1\"));\n        clientPair.appClient.reset();\n\n        clientPair.appClient.sync(1);\n\n        verify(clientPair.appClient.responseMock, timeout(500).times(12)).channelRead(any(), any());\n\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.verifyResult(appSync(\"1-0 dw 1 1\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 dw 2 1\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 aw 3 0\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 dw 5 1\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 4 244\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 aw 7 3\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 aw 30 3\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 0 89.888037459418\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 11 -58.74774244674501\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 13 60 143 158\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 125 1\"));\n\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":88, \\\"width\\\":1, \\\"height\\\":1, \\\"deviceId\\\":0, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Button\\\", \\\"type\\\":\\\"BUTTON\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":125}\");\n        clientPair.appClient.verifyResult(ok(2));\n        clientPair.appClient.reset();\n\n        clientPair.hardwareClient.send(\"hardware vw 125 2\");\n        clientPair.appClient.verifyResult(hardware(2, \"1-0 vw 125 2\"));\n        clientPair.appClient.reset();\n\n        clientPair.appClient.sync(1);\n\n        verify(clientPair.appClient.responseMock, timeout(500).times(12)).channelRead(any(), any());\n\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.verifyResult(appSync(\"1-0 dw 1 1\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 dw 2 1\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 aw 3 0\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 dw 5 1\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 4 244\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 aw 7 3\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 aw 30 3\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 0 89.888037459418\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 11 -58.74774244674501\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 13 60 143 158\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 125 2\"));\n    }\n\n    @Test\n    public void testActivateAndGetSyncForSpecificDeviceId() throws Exception {\n        clientPair.appClient.sync(1, 0);\n\n        verify(clientPair.appClient.responseMock, timeout(500).times(11)).channelRead(any(), any());\n\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.verifyResult(appSync(\"1-0 dw 1 1\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 dw 2 1\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 aw 3 0\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 dw 5 1\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 4 244\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 aw 7 3\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 aw 30 3\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 0 89.888037459418\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 11 -58.74774244674501\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 13 60 143 158\"));\n    }\n\n    @Test\n    public void testSyncForDeviceSelectorAndSetProperty() throws Exception {\n        Device device0 = new Device(0, \"My Dashboard\", BoardType.ESP8266);\n        device0.status = Status.ONLINE;\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":200000, \\\"deviceIds\\\":[0], \\\"width\\\":1, \\\"height\\\":1, \\\"value\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"DEVICE_SELECTOR\\\"}\");\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":88, \\\"width\\\":1, \\\"height\\\":1, \\\"deviceId\\\":200000, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Button\\\", \\\"type\\\":\\\"BUTTON\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":88}\");\n        clientPair.appClient.verifyResult(ok(2));\n        clientPair.appClient.verifyResult(ok(3));\n\n        clientPair.hardwareClient.setProperty(88, \"label\", \"newLabel\");\n        clientPair.hardwareClient.setProperty(88, \"label\", \"newLabel2\");\n        clientPair.appClient.verifyResult(setProperty(1, \"1-0 88 label newLabel\"));\n        clientPair.appClient.verifyResult(setProperty(2, \"1-0 88 label newLabel2\"));\n\n        clientPair.appClient.reset();\n\n        clientPair.appClient.sync(1);\n\n        verify(clientPair.appClient.responseMock, timeout(500).times(12)).channelRead(any(), any());\n\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.verifyResult(appSync(\"1-0 dw 1 1\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 dw 2 1\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 aw 3 0\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 dw 5 1\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 4 244\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 aw 7 3\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 aw 30 3\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 0 89.888037459418\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 11 -58.74774244674501\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 13 60 143 158\"));\n        clientPair.appClient.verifyResult(setProperty(1111, \"1-0 88 label newLabel2\"));\n        clientPair.appClient.never(setProperty(1111, \"1-0 88 label newLabel\"));\n    }\n\n    @Test\n    public void testSyncForDeviceTilesAndSetProperty() throws Exception {\n        Device device0 = new Device(0, \"My Dashboard\", BoardType.ESP8266);\n        device0.status = Status.ONLINE;\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        long widgetId = 21321;\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = widgetId;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n        deviceTiles.color = -231;\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(2));\n\n        PageTileTemplate tileTemplate = new PageTileTemplate(\n                1,\n                null,\n                new int[] {0, device.id},\n                \"name\", \"name\", \"iconName\", BoardType.ESP8266, new DataStream((short) 1, PinType.VIRTUAL),\n                false, null, null, null, -75056000, -231, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.createTemplate(1, widgetId, tileTemplate);\n        clientPair.appClient.verifyResult(ok(3));\n\n        ValueDisplay valueDisplay = new ValueDisplay();\n        valueDisplay.id = 2322;\n        valueDisplay.width = 2;\n        valueDisplay.height = 2;\n        valueDisplay.pin = 1;\n        valueDisplay.pinType = PinType.VIRTUAL;\n\n        clientPair.appClient.createWidget(1, widgetId, tileTemplate.id, valueDisplay);\n        clientPair.appClient.verifyResult(ok(4));\n\n        clientPair.hardwareClient.setProperty(1, \"label\", \"newLabel\");\n        clientPair.appClient.verifyResult(setProperty(1, \"1-0 1 label newLabel\"));\n        clientPair.appClient.reset();\n\n        clientPair.appClient.sync(1, 0);\n\n        verify(clientPair.appClient.responseMock, timeout(500).times(12)).channelRead(any(), any());\n\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.verifyResult(appSync(\"1-0 dw 1 1\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 dw 2 1\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 aw 3 0\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 dw 5 1\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 4 244\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 aw 7 3\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 aw 30 3\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 0 89.888037459418\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 11 -58.74774244674501\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 13 60 143 158\"));\n        clientPair.appClient.verifyResult(setProperty(1111, \"1-0 1 label newLabel\"));\n    }\n\n    @Test\n    public void testSyncForDeviceSelectorAndSetPropertyAndMultiValueWidget() throws Exception {\n        Device device0 = new Device(0, \"My Dashboard\", BoardType.ESP8266);\n        device0.status = Status.ONLINE;\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":200000, \\\"deviceIds\\\":[0], \\\"width\\\":1, \\\"height\\\":1, \\\"value\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"DEVICE_SELECTOR\\\"}\");\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":88, \\\"width\\\":1, \\\"deviceId\\\":200000, \\\"height\\\":1, \\\"x\\\":5, \\\"y\\\":0, \\\"tabId\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"TERMINAL\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":88}\");\n        clientPair.appClient.verifyResult(ok(2));\n        clientPair.appClient.verifyResult(ok(3));\n\n        clientPair.hardwareClient.setProperty(88, \"label\", \"newLabel\");\n        clientPair.hardwareClient.setProperty(88, \"label\", \"newLabel2\");\n        clientPair.appClient.verifyResult(setProperty(1, \"1-0 88 label newLabel\"));\n        clientPair.appClient.verifyResult(setProperty(2, \"1-0 88 label newLabel2\"));\n\n        clientPair.appClient.reset();\n\n        clientPair.appClient.sync(1);\n\n        verify(clientPair.appClient.responseMock, timeout(500).times(12)).channelRead(any(), any());\n\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.verifyResult(appSync(\"1-0 dw 1 1\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 dw 2 1\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 aw 3 0\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 dw 5 1\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 4 244\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 aw 7 3\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 aw 30 3\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 0 89.888037459418\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 11 -58.74774244674501\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 13 60 143 158\"));\n        clientPair.appClient.verifyResult(setProperty(1111, \"1-0 88 label newLabel2\"));\n        clientPair.appClient.never(setProperty(1111, \"1-0 88 label newLabel\"));\n    }\n\n    @Test\n    public void testActivateAndGetSyncForTimeInput() throws Exception {\n        clientPair.appClient.createWidget(1, \"{\\\"type\\\":\\\"TIME_INPUT\\\",\\\"id\\\":99, \\\"pin\\\":99, \\\"pinType\\\":\\\"VIRTUAL\\\", \" +\n                \"\\\"x\\\":0,\\\"y\\\":0,\\\"width\\\":1,\\\"height\\\":1}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.send(\"hardware 1 vw \" + \"99 82800 82860 Europe/Kiev 1\");\n        verify(clientPair.hardwareClient.responseMock, timeout(500).times(1)).channelRead(any(), eq(hardware(2, \"vw 99 82800 82860 Europe/Kiev 1\")));\n\n        clientPair.appClient.reset();\n\n        clientPair.appClient.sync(1, 0);\n\n        verify(clientPair.appClient.responseMock, timeout(500).times(12)).channelRead(any(), any());\n\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.verifyResult(appSync(\"1-0 dw 1 1\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 dw 2 1\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 aw 3 0\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 dw 5 1\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 4 244\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 aw 7 3\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 aw 30 3\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 0 89.888037459418\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 11 -58.74774244674501\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 13 60 143 158\"));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 99 82800 82860 Europe/Kiev 1\"));\n    }\n\n    @Test\n    public void testActivateAndGetSyncForNonExistingDeviceId() throws Exception {\n        clientPair.appClient.sync(1, 1);\n\n        verify(clientPair.appClient.responseMock, timeout(500).times(1)).channelRead(any(), any());\n\n        clientPair.appClient.verifyResult(ok(1));\n    }\n\n    @Test\n    public void testLCDOnActivateSendsCorrectBodySimpleModeAndAnotherDevice() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        clientPair.appClient.createWidget(1, \"{\\\"deviceId\\\":1,\\\"type\\\":\\\"LCD\\\",\\\"id\\\":1923810267,\\\"x\\\":0,\\\"y\\\":6,\\\"color\\\":600084223,\\\"width\\\":8,\\\"height\\\":2,\\\"tabId\\\":0,\\\"\" +\n                \"pins\\\":[\" +\n                \"{\\\"pin\\\":10,\\\"pinType\\\":\\\"VIRTUAL\\\",\\\"pwmMode\\\":false,\\\"rangeMappingOn\\\":false,\\\"min\\\":0,\\\"max\\\":1023, \\\"value\\\":\\\"10\\\"},\" +\n                \"{\\\"pin\\\":11,\\\"pinType\\\":\\\"VIRTUAL\\\",\\\"pwmMode\\\":false,\\\"rangeMappingOn\\\":false,\\\"min\\\":0,\\\"max\\\":1023, \\\"value\\\":\\\"11\\\"}],\" +\n                \"\\\"advancedMode\\\":false,\\\"textLight\\\":false,\\\"textLightOn\\\":false,\\\"frequency\\\":1000}\");\n\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.sync(1, 1);\n\n        verify(clientPair.appClient.responseMock, timeout(500).times(3)).channelRead(any(), any());\n\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.verifyResult(appSync(\"1-1 vw 10 10\"));\n        clientPair.appClient.verifyResult(appSync(\"1-1 vw 11 11\"));\n    }\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/tcp/AppWorkflowTest.java",
    "content": "package cc.blynk.integration.tcp;\n\nimport cc.blynk.integration.SingleServerInstancePerTest;\nimport cc.blynk.server.core.model.Profile;\nimport cc.blynk.server.core.model.auth.App;\nimport cc.blynk.server.core.model.enums.ProvisionType;\nimport cc.blynk.server.core.model.enums.Theme;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport java.nio.file.Files;\nimport java.nio.file.Paths;\n\nimport static cc.blynk.integration.TestUtil.ok;\nimport static org.junit.Assert.assertArrayEquals;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\nimport static org.junit.Assert.assertTrue;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/2/2015.\n *\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class AppWorkflowTest extends SingleServerInstancePerTest {\n\n    @Before\n    public void deleteFolder() throws Exception {\n        Files.deleteIfExists(Paths.get(getDataFolder(), \"blynk\", \"userProfiles\"));\n    }\n\n    @Test\n    public void testPrintApp() throws Exception {\n        App app = new App(\"1\", Theme.Blynk, ProvisionType.STATIC, 0, false, \"My App\", \"myIcon\", new int[] {1});\n        System.out.println(JsonParser.MAPPER.writeValueAsString(app));\n    }\n\n    @Test\n    public void testAppCreated() throws Exception {\n        clientPair.appClient.send(\"createApp {\\\"theme\\\":\\\"Blynk\\\",\\\"isMultiFace\\\":true,\\\"provisionType\\\":\\\"STATIC\\\",\\\"color\\\":0,\\\"name\\\":\\\"My App\\\",\\\"icon\\\":\\\"myIcon\\\",\\\"projectIds\\\":[1]}\");\n        App app = clientPair.appClient.parseApp(1);\n        assertNotNull(app);\n        assertNotNull(app.id);\n        assertEquals(13, app.id.length());\n        assertEquals(Theme.Blynk, app.theme);\n        assertEquals(ProvisionType.STATIC, app.provisionType);\n        assertEquals(0, app.color);\n        assertEquals(\"My App\", app.name);\n        assertEquals(\"myIcon\", app.icon);\n        assertTrue(app.isMultiFace);\n        assertArrayEquals(new int[]{1}, app.projectIds);\n    }\n\n    @Test\n    public void testAppCreated2() throws Exception {\n        clientPair.appClient.send(\"createApp {\\\"theme\\\":\\\"Blynk\\\",\\\"provisionType\\\":\\\"STATIC\\\",\\\"color\\\":0,\\\"name\\\":\\\"My App\\\",\\\"icon\\\":\\\"myIcon\\\",\\\"projectIds\\\":[1]}\");\n        App app = clientPair.appClient.parseApp(1);\n        assertNotNull(app);\n        assertNotNull(app.id);\n\n        clientPair.appClient.send(\"createApp {\\\"theme\\\":\\\"Blynk\\\",\\\"provisionType\\\":\\\"STATIC\\\",\\\"color\\\":0,\\\"name\\\":\\\"My App\\\",\\\"icon\\\":\\\"myIcon\\\",\\\"projectIds\\\":[2]}\");\n        app = clientPair.appClient.parseApp(2);\n        assertNotNull(app);\n        assertNotNull(app.id);\n    }\n\n    @Test\n    public void testUnicodeName() throws Exception {\n        clientPair.appClient.send(\"createApp {\\\"theme\\\":\\\"Blynk\\\",\\\"provisionType\\\":\\\"STATIC\\\",\\\"color\\\":0,\\\"name\\\":\\\"Моя апка\\\",\\\"icon\\\":\\\"myIcon\\\",\\\"projectIds\\\":[1]}\");\n        App app = clientPair.appClient.parseApp(1);\n        assertNotNull(app);\n        assertNotNull(app.id);\n        assertEquals(\"Моя апка\", app.name);\n    }\n\n    @Test\n    public void testCantCreateWithSameId() throws Exception {\n        clientPair.appClient.send(\"createApp {\\\"theme\\\":\\\"Blynk\\\",\\\"provisionType\\\":\\\"STATIC\\\",\\\"color\\\":0,\\\"name\\\":\\\"My App\\\",\\\"icon\\\":\\\"myIcon\\\",\\\"projectIds\\\":[1]}\");\n        App app = clientPair.appClient.parseApp(1);\n        assertNotNull(app);\n        assertNotNull(app.id);\n\n        clientPair.appClient.send(\"createApp {\\\"id\\\":\\\"\" + app.id + \"\\\",\\\"theme\\\":\\\"Blynk\\\",\\\"provisionType\\\":\\\"STATIC\\\",\\\"color\\\":0,\\\"name\\\":\\\"My App\\\",\\\"icon\\\":\\\"myIcon\\\",\\\"projectIds\\\":[2]}\");\n        app = clientPair.appClient.parseApp(2);\n        assertNotNull(app);\n        assertNotNull(app.id);\n    }\n\n    @Test\n    public void testAppUpdated() throws Exception {\n        clientPair.appClient.send(\"createApp {\\\"theme\\\":\\\"Blynk\\\",\\\"provisionType\\\":\\\"STATIC\\\",\\\"color\\\":0,\\\"name\\\":\\\"My App\\\",\\\"icon\\\":\\\"myIcon\\\",\\\"projectIds\\\":[1]}\");\n        App app = clientPair.appClient.parseApp(1);\n        assertNotNull(app);\n        assertNotNull(app.id);\n\n        clientPair.appClient.send(\"updateApp {\\\"id\\\":\\\"\" + app.id  + \"\\\",\\\"theme\\\":\\\"BlynkLight\\\",\\\"provisionType\\\":\\\"DYNAMIC\\\",\\\"color\\\":1,\\\"name\\\":\\\"My App 2\\\",\\\"icon\\\":\\\"myIcon2\\\",\\\"projectIds\\\":[1,2]}\");\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(3);\n        assertNotNull(profile);\n\n        assertNotNull(profile.apps);\n        assertEquals(1, profile.apps.length);\n        App app2 = profile.apps[0];\n        assertEquals(app.id, app2.id);\n        assertEquals(Theme.BlynkLight, app2.theme);\n        assertEquals(ProvisionType.DYNAMIC, app2.provisionType);\n        assertEquals(1, app2.color);\n        assertEquals(\"My App 2\", app2.name);\n        assertEquals(\"myIcon2\", app2.icon);\n        assertArrayEquals(new int[]{1, 2}, app2.projectIds);\n    }\n\n    @Test\n    public void testAppDelete() throws Exception {\n        clientPair.appClient.send(\"createApp {\\\"id\\\":1,\\\"theme\\\":\\\"Blynk\\\",\\\"provisionType\\\":\\\"STATIC\\\",\\\"color\\\":0,\\\"name\\\":\\\"My App\\\",\\\"icon\\\":\\\"myIcon\\\",\\\"projectIds\\\":[1]}\");\n        App app = clientPair.appClient.parseApp(1);\n        assertNotNull(app);\n        assertNotNull(app.id);\n\n        clientPair.appClient.send(\"deleteApp \" + app.id);\n        clientPair.appClient.verifyResult(ok(2));\n        clientPair.appClient.reset();\n\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(1);\n        assertNotNull(profile);\n\n        assertNotNull(profile.apps);\n        assertEquals(0, profile.apps.length);\n        assertEquals(0, profile.dashBoards.length);\n    }\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/tcp/AssignTokenTest.java",
    "content": "package cc.blynk.integration.tcp;\n\nimport cc.blynk.integration.SingleServerInstancePerTestWithDB;\nimport cc.blynk.integration.model.tcp.TestHardClient;\nimport cc.blynk.integration.model.tcp.TestSslHardClient;\nimport cc.blynk.server.core.model.device.BoardType;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.device.Status;\nimport cc.blynk.server.db.model.FlashedToken;\nimport cc.blynk.utils.AppNameUtil;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport java.util.UUID;\n\nimport static cc.blynk.integration.TestUtil.notAllowed;\nimport static cc.blynk.integration.TestUtil.ok;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/2/2015.\n *\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class AssignTokenTest extends SingleServerInstancePerTestWithDB {\n\n    @Before\n    public void cleanTable() throws Exception {\n        holder.dbManager.executeSQL(\"DELETE FROM flashed_tokens\");\n    }\n\n    @Test\n    public void testNoTokenExists() throws Exception {\n        clientPair.appClient.send(\"assignToken 1\\0\" + \"123\");\n        clientPair.appClient.verifyResult(notAllowed(1));\n    }\n\n    @Test\n    public void testTokenActivate() throws Exception {\n        FlashedToken[] list = new FlashedToken[1];\n        String token = UUID.randomUUID().toString().replace(\"-\", \"\");\n        FlashedToken flashedToken = new FlashedToken(\"test@blynk.cc\", token, AppNameUtil.BLYNK, 1, 0);\n        list[0] = flashedToken;\n        holder.dbManager.insertFlashedTokens(list);\n\n        clientPair.appClient.send(\"assignToken 1\\0\" + flashedToken.token);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.send(\"assignToken 1\\0\" + flashedToken.token);\n        clientPair.appClient.verifyResult(notAllowed(2));\n    }\n\n    @Test\n    public void testCorrectToken() throws Exception {\n        FlashedToken[] list = new FlashedToken[1];\n        String token = UUID.randomUUID().toString().replace(\"-\", \"\");\n        FlashedToken flashedToken = new FlashedToken(\"test@blynk.cc\", token, AppNameUtil.BLYNK, 1, 0);\n        list[0] = flashedToken;\n        holder.dbManager.insertFlashedTokens(list);\n\n        clientPair.appClient.send(\"assignToken 1\\0\" + flashedToken.token);\n        clientPair.appClient.verifyResult(ok(1));\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", properties.getHttpPort());\n        hardClient2.start();\n\n        hardClient2.login(flashedToken.token);\n        hardClient2.verifyResult(ok(1));\n\n        clientPair.appClient.send(\"getDevices 1\");\n\n        Device[] devices = clientPair.appClient.parseDevices(3);\n        assertNotNull(devices);\n        assertEquals(1, devices.length);\n        assertEquals(flashedToken.token, devices[0].token);\n        assertEquals(flashedToken.deviceId, devices[0].id);\n        assertEquals(Status.ONLINE, devices[0].status);\n    }\n\n    @Test\n    public void testConnectTo443PortForHardware() throws Exception {\n        clientPair.appClient.createDevice(1, new Device(1, \"My Device\", BoardType.ESP8266));\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n\n        TestSslHardClient hardClient2 = new TestSslHardClient(\"localhost\", properties.getHttpsPort());\n        hardClient2.start();\n\n        hardClient2.login(device.token);\n        hardClient2.verifyResult(ok(1));\n    }\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/tcp/BlynkInternalTest.java",
    "content": "package cc.blynk.integration.tcp;\n\nimport cc.blynk.integration.SingleServerInstancePerTest;\nimport cc.blynk.integration.model.tcp.TestAppClient;\nimport cc.blynk.integration.model.tcp.TestHardClient;\nimport cc.blynk.server.core.model.Profile;\nimport cc.blynk.server.core.model.device.HardwareInfo;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport static cc.blynk.integration.TestUtil.b;\nimport static cc.blynk.integration.TestUtil.hardware;\nimport static cc.blynk.integration.TestUtil.internal;\nimport static cc.blynk.integration.TestUtil.ok;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/2/2015.\n *\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class BlynkInternalTest extends SingleServerInstancePerTest {\n\n    @Test\n    public void testGetRTC() throws Exception {\n        clientPair.appClient.createWidget(1, \"{\\\"type\\\":\\\"RTC\\\",\\\"id\\\":99, \\\"pin\\\":99, \\\"pinType\\\":\\\"VIRTUAL\\\", \" +\n                \"\\\"x\\\":0,\\\"y\\\":0,\\\"width\\\":0,\\\"height\\\":0}\");\n\n        clientPair.hardwareClient.send(\"internal rtc\");\n        String rtcResponse = clientPair.hardwareClient.getBody();\n        assertNotNull(rtcResponse);\n\n        String rtcTime = rtcResponse.split(\"\\0\")[1];\n\n        assertNotNull(rtcTime);\n        assertEquals(10, rtcTime.length());\n        assertEquals(System.currentTimeMillis(), Long.parseLong(rtcTime) * 1000, 10000L);\n    }\n\n    @Test\n    public void testHardwareLoginWithInfo() throws Exception {\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", properties.getHttpPort());\n        hardClient2.start();\n\n        hardClient2.login(clientPair.token);\n        hardClient2.verifyResult(ok(1));\n\n        hardClient2.send(\"internal \" + b(\"ver 0.3.1 fw 3.3.3 h-beat 10 buff-in 256 dev Arduino cpu ATmega328P con W5100 tmpl tmpl00123\"));\n\n        hardClient2.verifyResult(ok(2));\n\n        clientPair.appClient.reset();\n\n        HardwareInfo hardwareInfo = new HardwareInfo(\"3.3.3\", \"0.3.1\", \"Arduino\", \"ATmega328P\", \"W5100\", null, \"tmpl00123\", 10, 256);\n\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(1);\n\n        assertEquals(JsonParser.toJson(hardwareInfo), JsonParser.toJson(profile.dashBoards[0].devices[0].hardwareInfo));\n\n\n        hardClient2.stop().awaitUninterruptibly();\n    }\n\n    @Test\n    public void appConnectedEvent() throws Exception {\n        clientPair.appClient.updateDash(\"{\\\"id\\\":1, \\\"name\\\":\\\"test board\\\", \\\"isAppConnectedOn\\\":true}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        TestAppClient appClient = new TestAppClient(properties);\n        appClient.start();\n\n        appClient.login(getUserName(), \"1\", \"Android\", \"1.13.3\");\n        appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.verifyResult(internal(7777, \"acon\"));\n    }\n\n    @Test\n    public void appDisconnectedEvent() throws Exception {\n        clientPair.appClient.updateDash(\"{\\\"id\\\":1, \\\"name\\\":\\\"test board\\\", \\\"isAppConnectedOn\\\":true}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.stop().await();\n\n        clientPair.hardwareClient.verifyResult(internal(7777, \"adis\"));\n    }\n\n    @Test\n    public void testBuffInIsHandled() throws Exception {\n        clientPair.hardwareClient.send(\"internal \" + b(\"ver 0.3.1 h-beat 10 buff-in 12 dev Arduino cpu ATmega328P con W5100 tmpl tmpl00123\"));\n        clientPair.hardwareClient.verifyResult(ok(1));\n\n        clientPair.appClient.send(\"hardware 1-0 vw 1 12\");\n        clientPair.hardwareClient.verifyResult(hardware(1, \"vw 1 12\"));\n\n        clientPair.appClient.send(\"hardware 1-0 vw 1 123\");\n        clientPair.hardwareClient.never(hardware(2, \"vw 1 123\"));\n\n        clientPair.hardwareClient.send(\"internal \" + b(\"ver 0.3.1 h-beat 10 dev Arduino cpu ATmega328P con W5100 tmpl tmpl00123\"));\n        clientPair.hardwareClient.verifyResult(ok(2));\n\n        clientPair.appClient.send(\"hardware 1-0 vw 1 12\");\n        clientPair.hardwareClient.verifyResult(hardware(3, \"vw 1 12\"));\n\n        clientPair.hardwareClient.send(\"internal \" + b(\"ver 0.3.1 h-beat 10 buff-in 0 dev Arduino cpu ATmega328P con W5100 tmpl tmpl00123\"));\n        clientPair.hardwareClient.verifyResult(ok(3));\n\n        clientPair.appClient.send(\"hardware 1-0 vw 1 12\");\n        clientPair.hardwareClient.verifyResult(hardware(4, \"vw 1 12\"));\n\n        StringBuilder sb = new StringBuilder();\n        for (int i = 0; i < 245; i++) {\n            sb.append(\"a\");\n        }\n\n        String s = sb.toString();\n\n        clientPair.appClient.send(\"hardware 1-0 vw 1 \" + s);\n        clientPair.hardwareClient.never(hardware(5, \"vw 1 \" + s));\n    }\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/tcp/BridgeWorkflowTest.java",
    "content": "package cc.blynk.integration.tcp;\n\nimport cc.blynk.integration.SingleServerInstancePerTest;\nimport cc.blynk.integration.model.tcp.TestAppClient;\nimport cc.blynk.integration.model.tcp.TestHardClient;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.device.BoardType;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.device.Status;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.protocol.model.messages.ResponseMessage;\nimport cc.blynk.utils.AppNameUtil;\nimport org.junit.BeforeClass;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport static cc.blynk.integration.TestUtil.bridge;\nimport static cc.blynk.integration.TestUtil.createDevice;\nimport static cc.blynk.integration.TestUtil.hardware;\nimport static cc.blynk.integration.TestUtil.hardwareConnected;\nimport static cc.blynk.integration.TestUtil.illegalCommand;\nimport static cc.blynk.integration.TestUtil.notAllowed;\nimport static cc.blynk.integration.TestUtil.ok;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE_CONNECTED;\nimport static cc.blynk.server.core.protocol.enums.Response.DEVICE_NOT_IN_NETWORK;\nimport static cc.blynk.server.core.protocol.model.messages.MessageFactory.produce;\nimport static org.junit.Assert.assertNotNull;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/2/2015.\n *\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class BridgeWorkflowTest extends SingleServerInstancePerTest {\n\n    private static int tcpHardPort;\n\n    @BeforeClass\n    public static void initPort() {\n        tcpHardPort = properties.getHttpPort();\n    }\n\n    @Override\n    public String changeProfileTo() {\n        return \"user_profile_json_3_dashes.txt\";\n    }\n\n    @Test\n    public void testBridgeInitOk() throws Exception {\n        clientPair.hardwareClient.send(\"bridge 1 i \" + clientPair.token);\n        clientPair.hardwareClient.verifyResult(ok(1));\n    }\n\n    @Test\n    public void testBridgeInitIllegalCommand() throws Exception {\n        clientPair.hardwareClient.send(\"bridge 1 i\");\n        clientPair.hardwareClient.verifyResult(illegalCommand(1));\n\n        clientPair.hardwareClient.send(\"bridge i\");\n        clientPair.hardwareClient.verifyResult(illegalCommand(2));\n\n        clientPair.hardwareClient.send(\"bridge 1 auth_tone\");\n        clientPair.hardwareClient.verifyResult(illegalCommand(3));\n\n        clientPair.hardwareClient.send(\"bridge 1\");\n        clientPair.hardwareClient.verifyResult(illegalCommand(4));\n\n        clientPair.hardwareClient.send(\"bridge 1\");\n        clientPair.hardwareClient.verifyResult(illegalCommand(5));\n    }\n\n    @Test\n    public void testSeveralBridgeInitOk() throws Exception {\n        clientPair.hardwareClient.send(\"bridge 1 i \" + clientPair.token);\n        clientPair.hardwareClient.send(\"bridge 2 i \" + clientPair.token);\n        clientPair.hardwareClient.send(\"bridge 3 i \" + clientPair.token);\n        clientPair.hardwareClient.send(\"bridge 4 i \" + clientPair.token);\n        clientPair.hardwareClient.verifyResult(ok(1));\n        clientPair.hardwareClient.verifyResult(ok(2));\n        clientPair.hardwareClient.verifyResult(ok(3));\n        clientPair.hardwareClient.verifyResult(ok(4));\n\n        clientPair.hardwareClient.send(\"bridge 5 i \" + clientPair.token);\n        clientPair.hardwareClient.send(\"bridge 5 i \" + clientPair.token);\n        clientPair.hardwareClient.send(\"bridge 5 i \" + clientPair.token);\n        clientPair.hardwareClient.send(\"bridge 5 i \" + clientPair.token);\n        clientPair.hardwareClient.verifyResult(ok(5));\n        clientPair.hardwareClient.verifyResult(ok(6));\n        clientPair.hardwareClient.verifyResult(ok(7));\n        clientPair.hardwareClient.verifyResult(ok(8));\n    }\n\n    @Test\n    public void testBridgeInitAndOk() throws Exception {\n        clientPair.hardwareClient.send(\"bridge 1 i \" + clientPair.token);\n        clientPair.hardwareClient.verifyResult(ok(1));\n    }\n\n    @Test\n    public void testBridgeWithoutInit() throws Exception {\n        clientPair.hardwareClient.send(\"bridge 1 aw 10 10\");\n        clientPair.hardwareClient.verifyResult(notAllowed(1));\n    }\n\n    @Test\n    public void testBridgeInitAndSendNoOtherDevices() throws Exception {\n        clientPair.hardwareClient.send(\"bridge 1 i \" + clientPair.token);\n        clientPair.hardwareClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"bridge 1 aw 10 10\");\n        clientPair.hardwareClient.verifyResult(new ResponseMessage(2, DEVICE_NOT_IN_NETWORK));\n    }\n\n    @Test\n    public void testBridgeInitAndSendOtherDevicesButNoBridgeDevices() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        clientPair.appClient.reset();\n\n        //creating 1 new hard client\n        TestHardClient hardClient1 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient1.start();\n        hardClient1.login(clientPair.token);\n        hardClient1.verifyResult(ok(1));\n        hardClient1.reset();\n\n        clientPair.hardwareClient.send(\"bridge 1 i \" + device.token);\n        clientPair.hardwareClient.verifyResult(ok(1));\n        clientPair.hardwareClient.send(\"bridge 1 aw 10 10\");\n        clientPair.hardwareClient.verifyResult(new ResponseMessage(2, DEVICE_NOT_IN_NETWORK));\n    }\n\n    @Test\n    public void testSecondTokenNotInitialized() throws Exception {\n        clientPair.hardwareClient.send(\"bridge 1 i \" + clientPair.token);\n        clientPair.hardwareClient.verifyResult(ok(1));\n        clientPair.hardwareClient.send(\"bridge 2 aw 10 10\");\n        clientPair.hardwareClient.verifyResult(notAllowed(2));\n    }\n\n    @Test\n    public void testCorrectWorkflow2HardsSameToken() throws Exception {\n        //creating 1 new hard client\n        TestHardClient hardClient1 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient1.start();\n        hardClient1.login(clientPair.token);\n        hardClient1.verifyResult(ok(1));\n        hardClient1.reset();\n\n        clientPair.hardwareClient.send(\"bridge 1 i \" + clientPair.token);\n        clientPair.hardwareClient.verifyResult(ok(1));\n        clientPair.hardwareClient.send(\"bridge 1 aw 10 10\");\n        hardClient1.verifyResult(bridge(2, \"aw 10 10\"));\n        clientPair.appClient.verifyResult(hardware(2, \"1-0 aw 10 10\"));\n    }\n\n    @Test\n    public void testWrongPinForBridge() throws Exception {\n        //creating 1 new hard client\n        TestHardClient hardClient1 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient1.start();\n        hardClient1.login(clientPair.token);\n        hardClient1.verifyResult(ok(1));\n        hardClient1.reset();\n\n        clientPair.hardwareClient.send(\"bridge 1 i \" + clientPair.token);\n        clientPair.hardwareClient.verifyResult(ok(1));\n        clientPair.hardwareClient.send(\"bridge 1 vw 256 10\");\n        clientPair.hardwareClient.verifyResult(illegalCommand(2));\n    }\n\n    @Test\n    public void testCorrectWorkflow2HardsDifferentToken() throws Exception {\n        clientPair.appClient.createDevice(2, new Device(4, \"123\", BoardType.ESP8266));\n        Device device = clientPair.appClient.parseDevice();\n        String token = device.token;\n\n        clientPair.appClient.activate(2);\n        clientPair.appClient.verifyResult(new ResponseMessage(2, DEVICE_NOT_IN_NETWORK));\n        clientPair.appClient.reset();\n\n        //creating 1 new hard client\n        TestHardClient hardClient1 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient1.start();\n        hardClient1.login(token);\n        hardClient1.verifyResult(ok(1));\n        hardClient1.reset();\n\n        clientPair.hardwareClient.send(\"bridge 1 i \" + token);\n        clientPair.hardwareClient.verifyResult(ok(1));\n        clientPair.hardwareClient.send(\"bridge 1 aw 11 11\");\n        hardClient1.verifyResult(bridge(2, \"aw 11 11\"));\n        clientPair.appClient.verifyResult(hardwareConnected(1, \"2-\" + device.id));\n        clientPair.appClient.verifyResult(hardware(2, \"2-\" + device.id +\" aw 11 11\"));\n    }\n\n    @Test\n    public void testCorrectWorkflow3HardsDifferentToken() throws Exception {\n        clientPair.appClient.createDevice(2, new Device(4, \"123\", BoardType.ESP8266));\n        Device device = clientPair.appClient.parseDevice();\n        String token = device.token;\n        clientPair.appClient.reset();\n\n        //creating 2 new hard clients\n        TestHardClient hardClient1 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient1.start();\n        hardClient1.login(token);\n        hardClient1.verifyResult(ok(1));\n        hardClient1.reset();\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient2.start();\n        hardClient2.login(token);\n        hardClient2.verifyResult(ok(1));\n        hardClient2.reset();\n\n\n        clientPair.hardwareClient.send(\"bridge 1 i \" + token);\n        clientPair.hardwareClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"bridge 1 aw 11 11\");\n        hardClient1.verifyResult(bridge(2, \"aw 11 11\"));\n        hardClient2.verifyResult(bridge(2, \"aw 11 11\"));\n\n        clientPair.appClient.verifyResult(hardwareConnected(1, \"2-\" + device.id), 2);\n        clientPair.appClient.never(hardware(2, \"2 aw 11 11\"));\n    }\n\n    @Test\n    public void testCorrectWorkflow4HardsDifferentToken() throws Exception {\n        clientPair.appClient.createDevice(2, new Device(4, \"123\", BoardType.ESP8266));\n        Device device = clientPair.appClient.parseDevice();\n        String token2 = device.token;\n\n        clientPair.appClient.createDevice(3, new Device(5, \"123\", BoardType.ESP8266));\n        device = clientPair.appClient.parseDevice(2);\n        String token3 = device.token;\n\n        //creating 2 new hard clients\n        TestHardClient hardClient1 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient1.start();\n        hardClient1.login(token2);\n        hardClient1.verifyResult(ok(1));\n        hardClient1.reset();\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient2.start();\n        hardClient2.login(token2);\n        hardClient2.verifyResult(ok(1));\n        hardClient2.reset();\n\n        TestHardClient hardClient3 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient3.start();\n        hardClient3.login(token3);\n        hardClient3.verifyResult(ok(1));\n        hardClient3.reset();\n\n\n        clientPair.hardwareClient.send(\"bridge 1 i \" + token2);\n        clientPair.hardwareClient.verifyResult(ok(1));\n        clientPair.hardwareClient.send(\"bridge 2 i \" + token3);\n        clientPair.hardwareClient.verifyResult(ok(1));\n\n\n        clientPair.hardwareClient.send(\"bridge 1 aw 11 11\");\n        hardClient1.verifyResult(bridge(3, \"aw 11 11\"));\n        hardClient2.verifyResult(bridge(3, \"aw 11 11\"));\n\n        clientPair.hardwareClient.send(\"bridge 2 aw 13 13\");\n        hardClient3.verifyResult(bridge(4, \"aw 13 13\"));\n    }\n\n    @Test\n    public void testCorrectWorkflow3HardsDifferentTokenAndSync() throws Exception {\n        clientPair.appClient.createDevice(2, new Device(4, \"123\", BoardType.ESP8266));\n        Device device = clientPair.appClient.parseDevice();\n        String token = device.token;\n        clientPair.appClient.reset();\n\n        //creating 2 new hard clients\n        TestHardClient hardClient1 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient1.start();\n        hardClient1.login(token);\n        hardClient1.verifyResult(ok(1));\n        hardClient1.reset();\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient2.start();\n        hardClient2.login(token);\n        hardClient2.verifyResult(ok(1));\n        hardClient2.reset();\n\n\n        clientPair.hardwareClient.send(\"bridge 1 i \" + token);\n        clientPair.hardwareClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"bridge 1 aw 11 11\");\n        hardClient1.verifyResult(bridge(2, \"aw 11 11\"));\n        hardClient2.verifyResult(bridge(2, \"aw 11 11\"));\n\n        clientPair.appClient.verifyResult(produce(1, HARDWARE_CONNECTED, \"2-\" + device.id), 2);\n        clientPair.appClient.never(hardware(2, \"2 aw 11 11\"));\n\n        hardClient1.sync(PinType.ANALOG, 11);\n        hardClient1.verifyResult(hardware(1, \"aw 11 11\"));\n        hardClient2.sync(PinType.ANALOG, 11);\n        hardClient2.verifyResult(hardware(1, \"aw 11 11\"));\n    }\n\n    @Test\n    public void testCorrectWorkflow4HardsDifferentTokenAndSync() throws Exception {\n        clientPair.appClient.createDevice(2, new Device(4, \"123\", BoardType.ESP8266));\n        Device device = clientPair.appClient.parseDevice();\n        String token2 = device.token;\n\n        clientPair.appClient.createDevice(3, new Device(5, \"123\", BoardType.ESP8266));\n        device = clientPair.appClient.parseDevice(2);\n        String token3 = device.token;\n\n        //creating 2 new hard clients\n        TestHardClient hardClient1 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient1.start();\n        hardClient1.login(token2);\n        hardClient1.verifyResult(ok(1));\n        hardClient1.reset();\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient2.start();\n        hardClient2.login(token2);\n        hardClient2.verifyResult(ok(1));\n        hardClient2.reset();\n\n        TestHardClient hardClient3 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient3.start();\n        hardClient3.login(token3);\n        hardClient3.verifyResult(ok(1));\n        hardClient3.reset();\n\n\n        clientPair.hardwareClient.send(\"bridge 1 i \" + token2);\n        clientPair.hardwareClient.verifyResult(ok(1));\n        clientPair.hardwareClient.send(\"bridge 2 i \" + token3);\n        clientPair.hardwareClient.verifyResult(ok(1));\n\n\n        clientPair.hardwareClient.send(\"bridge 1 vw 11 12\");\n        hardClient1.verifyResult(bridge(3, \"vw 11 12\"));\n        hardClient2.verifyResult(bridge(3, \"vw 11 12\"));\n\n        clientPair.hardwareClient.send(\"bridge 2 aw 13 13\");\n        hardClient3.verifyResult(bridge(4, \"aw 13 13\"));\n\n        hardClient1.sync(PinType.VIRTUAL, 11);\n        hardClient1.verifyResult(hardware(1, \"vw 11 12\"));\n        hardClient2.sync(PinType.VIRTUAL, 11);\n        hardClient2.verifyResult(hardware(1, \"vw 11 12\"));\n        hardClient3.sync(PinType.ANALOG, 13);\n        hardClient3.verifyResult(hardware(1, \"aw 13 13\"));\n        hardClient3.sync(PinType.ANALOG, 13);\n        hardClient3.never(hardware(2, \"aw 13 13\"));\n    }\n\n    @Test\n    public void bridgeOnlyWorksWithinOneAccount() throws Exception {\n        TestAppClient appClient = new TestAppClient(properties);\n\n        appClient.start();\n\n        appClient.register(\"test@test.com\", \"1\", AppNameUtil.BLYNK);\n        appClient.verifyResult(ok(1));\n\n        appClient.login(\"test@test.com\", \"1\", \"Android\", \"RC13\");\n        appClient.verifyResult(ok(2));\n\n        appClient.createDash(\"{\\\"id\\\":1, \\\"createdAt\\\":1, \\\"name\\\":\\\"test board\\\"}\");\n        appClient.verifyResult(ok(3));\n\n        appClient.reset();\n\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        appClient.createDevice(1, device1);\n        Device device = appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        appClient.verifyResult(createDevice(1, device));\n\n        appClient.reset();\n\n        clientPair.hardwareClient.send(\"bridge 1 i \" + device.token);\n        clientPair.hardwareClient.verifyResult(notAllowed(1));\n    }\n\n    @Test\n    public void testCorrectWorkflow2HardsOnDifferentProjects() throws Exception {\n        DashBoard dash = new DashBoard();\n        dash.id = 5;\n        dash.name = \"test\";\n        dash.activate();\n        clientPair.appClient.createDash(dash);\n        clientPair.appClient.verifyResult(ok(1));\n\n        Device device = new Device(1, \"My Device\", BoardType.ESP8266);\n        clientPair.appClient.createDevice(dash.id, device);\n        device = clientPair.appClient.parseDevice(2);\n\n        //creating 1 new hard client\n        TestHardClient hardClient1 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient1.start();\n        hardClient1.login(device.token);\n        hardClient1.verifyResult(ok(1));\n        hardClient1.reset();\n\n        clientPair.hardwareClient.send(\"bridge 1 i \" + device.token);\n        clientPair.hardwareClient.verifyResult(ok(1));\n        clientPair.hardwareClient.send(\"bridge 1 vw 11 11\");\n        hardClient1.verifyResult(bridge(2, \"vw 11 11\"));\n        clientPair.appClient.verifyResult(hardwareConnected(1, \"5-1\"));\n        clientPair.appClient.verifyResult(hardware(2, \"5-1 vw 11 11\"));\n        clientPair.appClient.never(hardware(2, \"5-0 vw 11 11\"));\n        clientPair.appClient.never(hardware(2, \"1-0 vw 11 11\"));\n    }\n\n    @Test\n    public void testCorrectWorkflow3HardsOnDifferentProjectsSameId() throws Exception {\n        DashBoard dash = new DashBoard();\n        dash.id = 5;\n        dash.name = \"test\";\n        dash.activate();\n        clientPair.appClient.createDash(dash);\n        clientPair.appClient.verifyResult(ok(1));\n\n        Device device = new Device(0, \"My Device\", BoardType.ESP8266);\n        clientPair.appClient.createDevice(dash.id, device);\n        device = clientPair.appClient.parseDevice(2);\n\n        TestHardClient hardClient1 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient1.start();\n        hardClient1.login(device.token);\n        hardClient1.verifyResult(ok(1));\n        hardClient1.reset();\n        clientPair.appClient.verifyResult(hardwareConnected(1, \"5-0\"));\n        clientPair.appClient.reset();\n\n        dash.id = 6;\n        clientPair.appClient.createDash(dash);\n        clientPair.appClient.verifyResult(ok(1));\n\n        device = new Device(0, \"My Device\", BoardType.ESP8266);\n        clientPair.appClient.createDevice(dash.id, device);\n        device = clientPair.appClient.parseDevice(2);\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient2.start();\n        hardClient2.login(device.token);\n        hardClient2.verifyResult(ok(1));\n        hardClient2.reset();\n        clientPair.appClient.verifyResult(hardwareConnected(1, \"6-0\"));\n\n        clientPair.hardwareClient.send(\"bridge 1 i \" + device.token);\n        clientPair.hardwareClient.verifyResult(ok(1));\n        clientPair.hardwareClient.send(\"bridge 1 vw 11 11\");\n\n        clientPair.appClient.verifyResult(hardware(2, \"6-0 vw 11 11\"));\n        hardClient2.verifyResult(bridge(2, \"vw 11 11\"));\n        hardClient1.never(bridge(2, \"vw 11 11\"));\n        clientPair.appClient.never(hardware(2, \"5-0 vw 11 11\"));\n        clientPair.appClient.never(hardware(2, \"1-0 vw 11 11\"));\n    }\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/tcp/CloneWorkFlowTest.java",
    "content": "package cc.blynk.integration.tcp;\n\nimport cc.blynk.integration.SingleServerInstancePerTestWithDB;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.Profile;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.model.widgets.controls.Slider;\nimport cc.blynk.utils.StringUtils;\nimport org.asynchttpclient.AsyncHttpClient;\nimport org.asynchttpclient.DefaultAsyncHttpClient;\nimport org.asynchttpclient.DefaultAsyncHttpClientConfig;\nimport org.asynchttpclient.Response;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport java.util.concurrent.Future;\n\nimport static cc.blynk.integration.TestUtil.serverError;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\nimport static org.junit.Assert.assertNull;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/2/2015.\n *\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class CloneWorkFlowTest extends SingleServerInstancePerTestWithDB {\n\n    @Before\n    public void deleteTable() throws Exception {\n        holder.dbManager.executeSQL(\"DELETE FROM cloned_projects\");\n    }\n\n    @Test\n    public void testGetNonExistingQR() throws Exception  {\n        clientPair.appClient.send(\"getProjectByCloneCode \" + 123);\n        clientPair.appClient.verifyResult(serverError(1));\n    }\n\n    @Test\n    public void getCloneCode() throws Exception {\n        clientPair.appClient.send(\"getCloneCode 1\");\n        String token = clientPair.appClient.getBody();\n        assertNotNull(token);\n        assertEquals(32, token.length());\n    }\n\n    @Test\n    public void getProjectByCloneCode() throws Exception {\n        clientPair.hardwareClient.send(\"hardware vw 4 4\");\n        clientPair.hardwareClient.send(\"hardware vw 44 44\");\n\n        clientPair.appClient.send(\"getCloneCode 1\");\n        String token = clientPair.appClient.getBody(3);\n        assertNotNull(token);\n        assertEquals(32, token.length());\n\n        clientPair.appClient.send(\"getProjectByCloneCode \" + token);\n        DashBoard dashBoard = clientPair.appClient.parseDash(4);\n        assertEquals(\"My Dashboard\", dashBoard.name);\n        Device device = dashBoard.devices[0];\n        assertEquals(0, device.connectTime);\n        assertEquals(0, device.dataReceivedAt);\n        assertEquals(0, device.disconnectTime);\n        assertEquals(0, device.firstConnectTime);\n        assertNull(device.deviceOtaInfo);\n        assertNull(device.hardwareInfo);\n        Slider slider = (Slider) dashBoard.getWidgetById(4);\n        assertNotNull(slider);\n        assertNull(slider.value);\n        assertNotNull(dashBoard.pinsStorage);\n        assertEquals(0, dashBoard.pinsStorage.size());\n\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(5);\n        assertEquals(1, profile.dashBoards.length);\n    }\n\n    @Test\n    public void getProjectByCloneCodeNew() throws Exception {\n        clientPair.appClient.send(\"getCloneCode 1\");\n        String token = clientPair.appClient.getBody();\n        assertNotNull(token);\n        assertEquals(32, token.length());\n\n        clientPair.appClient.send(\"getProjectByCloneCode \" + token + \"\\0\" + \"new\");\n        DashBoard dashBoard = clientPair.appClient.parseDash(2);\n        assertEquals(\"My Dashboard\", dashBoard.name);\n        Device device = dashBoard.devices[0];\n        assertEquals(-1, dashBoard.parentId);\n        assertEquals(2, dashBoard.id);\n        assertEquals(0, device.connectTime);\n        assertEquals(0, device.dataReceivedAt);\n        assertEquals(0, device.disconnectTime);\n        assertEquals(0, device.firstConnectTime);\n        assertNull(device.deviceOtaInfo);\n        assertNull(device.hardwareInfo);\n        assertNotNull(device.token);\n\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(3);\n        assertEquals(2, profile.dashBoards.length);\n    }\n\n    @Test\n    public void getProjectByCloneCodeNewFormat() throws Exception {\n        clientPair.appClient.send(\"getCloneCode 1\");\n        String token = clientPair.appClient.getBody();\n        assertNotNull(token);\n        assertEquals(32, token.length());\n\n        clientPair.appClient.send(\"getProjectByCloneCode \" + token + StringUtils.BODY_SEPARATOR_STRING + \"new\");\n        DashBoard dashBoard = clientPair.appClient.parseDash(2);\n        assertEquals(\"My Dashboard\", dashBoard.name);\n\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(3);\n        assertEquals(2, profile.dashBoards.length);\n        assertEquals(2, profile.dashBoards[1].id);\n    }\n\n    @Test\n    public void getProjectByCloneCodeViaHttp() throws Exception {\n        clientPair.appClient.send(\"getCloneCode 1\");\n        String token = clientPair.appClient.getBody();\n        assertNotNull(token);\n        assertEquals(32, token.length());\n\n        AsyncHttpClient httpclient = new DefaultAsyncHttpClient(\n                new DefaultAsyncHttpClientConfig.Builder()\n                        .setUserAgent(null)\n                        .setKeepAlive(true)\n                        .build()\n        );\n\n        Future<Response> f = httpclient.prepareGet(\"http://localhost:\" + properties.getHttpPort() + \"/\" + token + \"/clone\").execute();\n        Response response = f.get();\n        assertEquals(200, response.getStatusCode());\n        String responseBody = response.getResponseBody();\n        assertNotNull(responseBody);\n        DashBoard dashBoard = JsonParser.parseDashboard(responseBody, 0);\n        assertEquals(\"My Dashboard\", dashBoard.name);\n        httpclient.close();\n    }\n\n    @Test\n    public void getProjectByNonExistingCloneCodeViaHttp() throws Exception {\n        AsyncHttpClient httpclient = new DefaultAsyncHttpClient(\n                new DefaultAsyncHttpClientConfig.Builder()\n                        .setUserAgent(null)\n                        .setKeepAlive(true)\n                        .build()\n        );\n\n        Future<Response> f = httpclient.prepareGet(\"http://localhost:\" + properties.getHttpPort() + \"/\" + 123 + \"/clone\").execute();\n        Response response = f.get();\n        assertEquals(500, response.getStatusCode());\n        String responseBody = response.getResponseBody();\n        assertEquals(\"Requested QR not found.\", responseBody);\n        httpclient.close();\n    }\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/tcp/DeviceCommandsTest.java",
    "content": "package cc.blynk.integration.tcp;\n\nimport cc.blynk.integration.SingleServerInstancePerTest;\nimport cc.blynk.server.core.model.device.BoardType;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.device.Status;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport static cc.blynk.integration.TestUtil.createDevice;\nimport static cc.blynk.integration.TestUtil.illegalCommandBody;\nimport static cc.blynk.integration.TestUtil.ok;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/2/2015.\n *\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class DeviceCommandsTest extends SingleServerInstancePerTest {\n\n    @Test\n    public void testAddNewDevice() throws Exception {\n        Device device0 = new Device(0, \"My Dashboard\", BoardType.Arduino_UNO);\n        device0.status = Status.ONLINE;\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        clientPair.appClient.reset();\n\n        clientPair.appClient.send(\"getDevices 1\");\n\n        Device[] devices = clientPair.appClient.parseDevices();\n        assertNotNull(devices);\n        assertEquals(2, devices.length);\n\n        assertEqualDevice(device0, devices[0]);\n        assertEqualDevice(device1, devices[1]);\n    }\n\n    @Test\n    public void testUpdateExistingDevice() throws Exception {\n        Device device0 = new Device(0, \"My Dashboard Updated\", BoardType.Arduino_UNO);\n        device0.status = Status.ONLINE;\n\n        clientPair.appClient.updateDevice(1, device0);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.reset();\n\n        clientPair.appClient.send(\"getDevices 1\");\n        Device[] devices = clientPair.appClient.parseDevices();\n\n        assertNotNull(devices);\n        assertEquals(1, devices.length);\n\n        assertEqualDevice(device0, devices[0]);\n    }\n\n    @Test\n    public void testUpdateNonExistingDevice() throws Exception {\n        Device device = new Device(100, \"My Dashboard Updated\", BoardType.Arduino_UNO);\n\n        clientPair.appClient.updateDevice(1, device);\n        clientPair.appClient.verifyResult(illegalCommandBody(1));\n    }\n\n    @Test\n    public void testGetDevices() throws Exception {\n        Device device0 = new Device(0, \"My Dashboard\", BoardType.Arduino_UNO);\n        device0.status = Status.ONLINE;\n\n        clientPair.appClient.send(\"getDevices 1\");\n\n        Device[] devices = clientPair.appClient.parseDevices();\n        assertNotNull(devices);\n        assertEquals(1, devices.length);\n\n        assertEqualDevice(device0, devices[0]);\n    }\n\n    @Test\n    public void testTokenNotUpdatedForExistingDevice() throws Exception {\n        Device device0 = new Device(0, \"My Dashboard\", BoardType.Arduino_UNO);\n        device0.status = Status.ONLINE;\n\n        clientPair.appClient.send(\"getDevices 1\");\n\n        Device[] devices = clientPair.appClient.parseDevices();\n        assertNotNull(devices);\n        assertEquals(1, devices.length);\n\n        assertEqualDevice(device0, devices[0]);\n        String token = devices[0].token;\n\n        device0.name = \"My Dashboard UPDATED\";\n        device0.token = \"123\";\n\n        clientPair.appClient.updateDevice(1, device0);\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.appClient.reset();\n\n        clientPair.appClient.send(\"getDevices 1\");\n        devices = clientPair.appClient.parseDevices();\n\n        assertNotNull(devices);\n        assertEquals(1, devices.length);\n\n        assertEqualDevice(device0, devices[0]);\n        assertEquals(\"My Dashboard UPDATED\", devices[0].name);\n        assertEquals(token, devices[0].token);\n    }\n\n\n    @Test\n    public void testDeletedNewlyAddedDevice() throws Exception {\n        Device device0 = new Device(0, \"My Dashboard\", BoardType.Arduino_UNO);\n        device0.status = Status.ONLINE;\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        clientPair.appClient.reset();\n\n        clientPair.appClient.send(\"getDevices 1\");\n\n        Device[] devices = clientPair.appClient.parseDevices();\n        assertNotNull(devices);\n        assertEquals(2, devices.length);\n\n        assertEqualDevice(device0, devices[0]);\n        assertEqualDevice(device1, devices[1]);\n\n        clientPair.appClient.send(\"deleteDevice 1\\0\" + device1.id);\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.appClient.reset();\n\n        clientPair.appClient.send(\"getDevices 1\");\n        devices = clientPair.appClient.parseDevices();\n\n        assertNotNull(devices);\n        assertEquals(1, devices.length);\n\n        assertEqualDevice(device0, devices[0]);\n    }\n\n    private static void assertEqualDevice(Device expected, Device real) {\n        assertEquals(expected.id, real.id);\n        //assertEquals(expected.name, real.name);\n        assertEquals(expected.boardType, real.boardType);\n        assertNotNull(real.token);\n        assertEquals(expected.status, real.status);\n    }\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/tcp/DeviceSelectorWorkflowTest.java",
    "content": "package cc.blynk.integration.tcp;\n\nimport cc.blynk.integration.SingleServerInstancePerTest;\nimport cc.blynk.integration.model.tcp.TestAppClient;\nimport cc.blynk.integration.model.tcp.TestHardClient;\nimport cc.blynk.server.core.model.DataStream;\nimport cc.blynk.server.core.model.device.BoardType;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.device.Status;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.widgets.controls.Terminal;\nimport cc.blynk.server.core.model.widgets.outputs.LCD;\nimport cc.blynk.server.core.model.widgets.ui.DeviceSelector;\nimport cc.blynk.server.core.model.widgets.ui.table.Table;\nimport org.junit.BeforeClass;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport static cc.blynk.integration.TestUtil.appSync;\nimport static cc.blynk.integration.TestUtil.b;\nimport static cc.blynk.integration.TestUtil.createDevice;\nimport static cc.blynk.integration.TestUtil.hardware;\nimport static cc.blynk.integration.TestUtil.hardwareConnected;\nimport static cc.blynk.integration.TestUtil.ok;\nimport static cc.blynk.integration.TestUtil.setProperty;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.after;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.timeout;\nimport static org.mockito.Mockito.verify;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/2/2015.\n *\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class DeviceSelectorWorkflowTest extends SingleServerInstancePerTest {\n\n    private static int tcpHardPort;\n\n    @BeforeClass\n    public static void initPort() {\n        tcpHardPort = properties.getHttpPort();\n    }\n\n    private static void assertEqualDevice(Device expected, Device real) {\n        assertEquals(expected.id, real.id);\n        //assertEquals(expected.name, real.name);\n        assertEquals(expected.boardType, real.boardType);\n        assertNotNull(real.token);\n        assertEquals(expected.status, real.status);\n    }\n\n    @Test\n    public void testSendHardwareCommandViaDeviceSelector() throws Exception {\n        Device device0 = new Device(0, \"My Dashboard\", BoardType.Arduino_UNO);\n        device0.status = Status.ONLINE;\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":200000, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"DEVICE_SELECTOR\\\"}\");\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":88, \\\"width\\\":1, \\\"height\\\":1, \\\"deviceId\\\":200000, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"BUTTON\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":88}\");\n        clientPair.appClient.verifyResult(ok(2));\n        clientPair.appClient.verifyResult(ok(3));\n\n        clientPair.appClient.reset();\n\n        clientPair.appClient.send(\"getDevices 1\");\n        Device[] devices = clientPair.appClient.parseDevices();\n        assertNotNull(devices);\n        assertEquals(2, devices.length);\n\n        assertEqualDevice(device0, devices[0]);\n        assertEqualDevice(device1, devices[1]);\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient2.start();\n\n        hardClient2.login(devices[1].token);\n        hardClient2.verifyResult(ok(1));\n        device1.status = Status.ONLINE;\n\n        clientPair.appClient.send(\"hardware 1-200000 vw 88 1\");\n        clientPair.hardwareClient.verifyResult(hardware(2, \"vw 88 1\"));\n        hardClient2.never(hardware(2, \"vw 88 1\"));\n\n        //change device\n        clientPair.appClient.send(\"hardware 1 vu 200000 1\");\n        clientPair.appClient.verifyResult(ok(3));\n        clientPair.hardwareClient.never(hardware(3, \"vu 200000 1\"));\n        hardClient2.never(hardware(3, \"vu 200000 1\"));\n\n        clientPair.appClient.send(\"hardware 1-200000 vw 88 2\");\n        clientPair.hardwareClient.never(hardware(4, \"vw 88 2\"));\n        hardClient2.verifyResult(hardware(4, \"vw 88 2\"));\n\n        //change device back\n        clientPair.appClient.send(\"hardware 1 vu 200000 0\");\n        clientPair.appClient.verifyResult(ok(5));\n        clientPair.hardwareClient.never(hardware(5, \"vu 200000 0\"));\n        hardClient2.never(hardware(5, \"vu 200000 0\"));\n        clientPair.appClient.verifyResult(appSync(1111, b(\"1-0 vw 88 1\")));\n\n        clientPair.appClient.send(\"hardware 1-200000 vw 88 0\");\n        clientPair.hardwareClient.verifyResult(hardware(6, \"vw 88 0\"));\n        hardClient2.never(hardware(6, \"vw 88 0\"));\n    }\n\n    @Test\n    public void testSendHardwareCommandViaDeviceSelectorInSharedApp() throws Exception {\n        Device device0 = new Device(0, \"My Dashboard\", BoardType.Arduino_UNO);\n        device0.status = Status.ONLINE;\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":200000, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"DEVICE_SELECTOR\\\"}\");\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":88, \\\"width\\\":1, \\\"height\\\":1, \\\"deviceId\\\":200000, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"BUTTON\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":88}\");\n        clientPair.appClient.verifyResult(ok(2));\n        clientPair.appClient.verifyResult(ok(3));\n\n        clientPair.appClient.send(\"getShareToken 1\");\n\n        String sharedToken = clientPair.appClient.getBody(4);\n        assertNotNull(sharedToken);\n        assertEquals(32, sharedToken.length());\n\n        clientPair.appClient.reset();\n\n        clientPair.appClient.send(\"getDevices 1\");\n        Device[] devices = clientPair.appClient.parseDevices();\n        assertNotNull(devices);\n        assertEquals(2, devices.length);\n\n        assertEqualDevice(device0, devices[0]);\n        assertEqualDevice(device1, devices[1]);\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient2.start();\n\n        hardClient2.login(devices[1].token);\n        hardClient2.verifyResult(ok(1));\n        device1.status = Status.ONLINE;\n        clientPair.appClient.verifyResult(hardwareConnected(1, \"1-1\"));\n\n\n        //login with shared app\n        TestAppClient appClient2 = new TestAppClient(properties);\n        appClient2.start();\n        appClient2.send(\"shareLogin \" + getUserName() + \" \" + sharedToken + \" Android 24\");\n        verify(appClient2.responseMock, timeout(500)).channelRead(any(), eq(ok(1)));\n\n        appClient2.send(\"hardware 1-200000 vw 88 1\");\n        clientPair.hardwareClient.verifyResult(hardware(2, \"vw 88 1\"));\n        hardClient2.never(hardware(2, \"vw 88 1\"));\n        clientPair.appClient.verifyResult(appSync(2, b(\"1-200000 vw 88 1\")));\n\n        clientPair.hardwareClient.send(\"hardware vw 88 value_from_device_0\");\n        hardClient2.send(\"hardware vw 88 value_from_device_1\");\n\n        clientPair.appClient.verifyResult(hardware(1, \"1-0 vw 88 value_from_device_0\"));\n        clientPair.appClient.verifyResult(hardware(2, \"1-1 vw 88 value_from_device_1\"));\n\n        appClient2.verifyResult(hardware(1, \"1-0 vw 88 value_from_device_0\"));\n        appClient2.verifyResult(hardware(2, \"1-1 vw 88 value_from_device_1\"));\n\n        //change device\n        appClient2.send(\"hardware 1 vu 200000 1\");\n        appClient2.verifyResult(ok(3));\n        clientPair.hardwareClient.never(hardware(3, \"vu 200000 1\"));\n        hardClient2.never(hardware(3, \"vu 200000 1\"));\n        clientPair.appClient.verifyResult(appSync(3, b(\"1 vu 200000 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-1 vw 88 value_from_device_1\")));\n\n        appClient2.send(\"hardware 1-200000 vw 88 2\");\n        clientPair.hardwareClient.never(hardware(4, \"vw 88 2\"));\n        hardClient2.verifyResult(hardware(4, \"vw 88 2\"));\n        clientPair.appClient.verifyResult(appSync(4, b(\"1-200000 vw 88 2\")));\n\n        //change device back\n        appClient2.send(\"hardware 1 vu 200000 0\");\n        appClient2.verifyResult(ok(5));\n        clientPair.hardwareClient.never(hardware(5, \"vu 200000 0\"));\n        hardClient2.never(hardware(5, \"vu 200000 0\"));\n        clientPair.appClient.verifyResult(appSync(5, b(\"1 vu 200000 0\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 88 value_from_device_0\")));\n\n        appClient2.send(\"hardware 1-200000 vw 88 0\");\n        clientPair.hardwareClient.verifyResult(hardware(6, \"vw 88 0\"));\n        hardClient2.never(hardware(6, \"vw 88 0\"));\n        clientPair.appClient.verifyResult(appSync(6, b(\"1-200000 vw 88 0\")));\n    }\n\n    @Test\n    public void testSetPropertyIsSentForDeviceSelectorWidget() throws Exception {\n        Device device0 = new Device(0, \"My Dashboard\", BoardType.Arduino_UNO);\n        device0.status = Status.ONLINE;\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":200000, \\\"width\\\":1, \\\"height\\\":1, \\\"value\\\":0, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"DEVICE_SELECTOR\\\"}\");\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":88, \\\"width\\\":1, \\\"height\\\":1, \\\"deviceId\\\":200000, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Button\\\", \\\"type\\\":\\\"BUTTON\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":88}\");\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":89, \\\"width\\\":1, \\\"height\\\":1, \\\"deviceId\\\":200000, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Display\\\", \\\"type\\\":\\\"DIGIT4_DISPLAY\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":89}\");\n        clientPair.appClient.verifyResult(ok(2));\n        clientPair.appClient.verifyResult(ok(3));\n        clientPair.appClient.verifyResult(ok(4));\n\n        clientPair.appClient.reset();\n\n        clientPair.appClient.send(\"getDevices 1\");\n        Device[] devices = clientPair.appClient.parseDevices();\n        assertNotNull(devices);\n        assertEquals(2, devices.length);\n\n        assertEqualDevice(device0, devices[0]);\n        assertEqualDevice(device1, devices[1]);\n\n        clientPair.hardwareClient.setProperty(89, \"label\", \"123\");\n        clientPair.appClient.verifyResult(setProperty(1, \"1-0 89 label 123\"));\n    }\n\n    @Test\n    public void testSetPropertyIsSentForDeviceSelectorWidgetOnActivateForExistingWidget() throws Exception {\n        testSetPropertyIsSentForDeviceSelectorWidget();\n\n        clientPair.hardwareClient.send(\"hardware vw 89 1\");\n        clientPair.appClient.verifyResult(hardware(2, \"1-0 vw 89 1\"));\n\n        clientPair.appClient.activate(1);\n        clientPair.appClient.verifyResult(setProperty(1111, \"1-0 89 label 123\"));\n        clientPair.appClient.verifyResult(appSync(1111, b(\"1-0 vw 89 1\")));\n    }\n\n    @Test\n    public void testSetPropertyIsRememberedBetweenDevices() throws Exception {\n        Device device0 = new Device(0, \"My Dashboard\", BoardType.Arduino_UNO);\n        device0.status = Status.ONLINE;\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":200000, \\\"width\\\":1, \\\"height\\\":1, \\\"value\\\":0, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"DEVICE_SELECTOR\\\"}\");\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":88, \\\"width\\\":1, \\\"height\\\":1, \\\"deviceId\\\":200000, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Button\\\", \\\"type\\\":\\\"BUTTON\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":88}\");\n        clientPair.appClient.verifyResult(ok(2));\n        clientPair.appClient.verifyResult(ok(3));\n\n        clientPair.appClient.reset();\n\n        clientPair.appClient.send(\"getDevices 1\");\n        Device[] devices = clientPair.appClient.parseDevices();\n        assertNotNull(devices);\n        assertEquals(2, devices.length);\n\n        assertEqualDevice(device0, devices[0]);\n        assertEqualDevice(device1, devices[1]);\n\n        clientPair.hardwareClient.setProperty(88, \"label\", \"123\");\n        clientPair.appClient.verifyResult(setProperty(1, \"1-0 88 label 123\"));\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient2.start();\n\n        hardClient2.login(devices[1].token);\n        hardClient2.verifyResult(ok(1));\n        clientPair.appClient.verifyResult(hardwareConnected(1, \"1-1\"));\n\n        hardClient2.setProperty(88, \"label\", \"124\");\n        clientPair.appClient.verifyResult(setProperty(2, \"1-1 88 label 124\"));\n\n        clientPair.appClient.send(\"hardware 1 vu 200000 1\");\n        clientPair.appClient.verifyResult(ok(2));\n        clientPair.appClient.verifyResult(setProperty(1111, \"1-1 88 label 124\"));\n    }\n\n    @Test\n    public void testBasicSelectorWorkflow() throws Exception {\n        Device device0 = new Device(0, \"My Dashboard\", BoardType.Arduino_UNO);\n        device0.status = Status.ONLINE;\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":200000, \\\"width\\\":1, \\\"height\\\":1, \\\"value\\\":0, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"DEVICE_SELECTOR\\\"}\");\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":88, \\\"width\\\":1, \\\"height\\\":1, \\\"deviceId\\\":200000, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Button\\\", \\\"type\\\":\\\"BUTTON\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":88}\");\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":89, \\\"width\\\":1, \\\"height\\\":1, \\\"deviceId\\\":200000, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Display\\\", \\\"type\\\":\\\"DIGIT4_DISPLAY\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":89}\");\n        clientPair.appClient.verifyResult(ok(2));\n        clientPair.appClient.verifyResult(ok(3));\n        clientPair.appClient.verifyResult(ok(4));\n\n        clientPair.appClient.reset();\n\n        clientPair.appClient.send(\"getDevices 1\");\n        Device[] devices = clientPair.appClient.parseDevices();\n\n        assertNotNull(devices);\n        assertEquals(2, devices.length);\n\n        assertEqualDevice(device0, devices[0]);\n        assertEqualDevice(device1, devices[1]);\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient2.start();\n\n        hardClient2.login(devices[1].token);\n        hardClient2.verifyResult(ok(1));\n        clientPair.appClient.verifyResult(hardwareConnected(1, \"1-1\"));\n        device1.status = Status.ONLINE;\n\n        clientPair.hardwareClient.send(\"hardware vw 89 value_from_device_0\");\n        hardClient2.send(\"hardware vw 89 value_from_device_1\");\n\n        clientPair.appClient.verifyResult(hardware(1, \"1-0 vw 89 value_from_device_0\"));\n        clientPair.appClient.verifyResult(hardware(2, \"1-1 vw 89 value_from_device_1\"));\n\n        clientPair.appClient.send(\"hardware 1 vw 88 100\");\n\n        clientPair.hardwareClient.verifyResult(hardware(2, \"vw 88 100\"));\n\n        //change device, expecting syncs and OK\n        clientPair.appClient.send(\"hardware 1 vu 200000 1\");\n        clientPair.appClient.verifyResult(ok(3));\n        clientPair.hardwareClient.never(hardware(3, \"vu 200000 1\"));\n        hardClient2.never(hardware(3, \"vu 200000 1\"));\n\n        clientPair.appClient.verifyResult(ok(3));\n        clientPair.appClient.verifyResult(appSync(b(\"1-1 vw 89 value_from_device_1\")));\n\n        //switch device back, expecting syncs and OK\n        clientPair.appClient.send(\"hardware 1 vu 200000 0\");\n        clientPair.appClient.verifyResult(ok(4));\n        clientPair.hardwareClient.never(hardware(4, \"vu 200000 0\"));\n        hardClient2.never(hardware(4, \"vu 200000 0\"));\n\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 89 value_from_device_0\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 88 100\")));\n    }\n\n    @Test\n    public void testDeviceSelectorSyncTimeInput() throws Exception {\n        Device device0 = new Device(0, \"My Dashboard\", BoardType.Arduino_UNO);\n        device0.status = Status.ONLINE;\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":200000, \\\"width\\\":1, \\\"height\\\":1, \\\"value\\\":0, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"DEVICE_SELECTOR\\\"}\");\n        clientPair.appClient.createWidget(1, \"{\\\"type\\\":\\\"TIME_INPUT\\\",\\\"id\\\":99, \\\"pin\\\":99, \\\"pinType\\\":\\\"VIRTUAL\\\", \" +\n                \"\\\"x\\\":0,\\\"y\\\":0,\\\"width\\\":1,\\\"height\\\":1, \\\"deviceId\\\":200000}\");\n\n        clientPair.appClient.verifyResult(ok(2));\n        clientPair.appClient.verifyResult(ok(3));\n\n        clientPair.appClient.reset();\n\n        clientPair.appClient.send(\"getDevices 1\");\n        Device[] devices = clientPair.appClient.parseDevices();\n\n        assertNotNull(devices);\n        assertEquals(2, devices.length);\n\n        assertEqualDevice(device0, devices[0]);\n        assertEqualDevice(device1, devices[1]);\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient2.start();\n\n        hardClient2.login(devices[1].token);\n        hardClient2.verifyResult(ok(1));\n        clientPair.appClient.verifyResult(hardwareConnected(1, \"1-1\"));\n        device1.status = Status.ONLINE;\n\n        clientPair.appClient.send(\"hardware 1 vw \" + b(\"99 82800 82860 Europe/Kiev 1\"));\n        verify(clientPair.hardwareClient.responseMock, timeout(500).times(1)).channelRead(any(), eq(hardware(2, \"vw 99 82800 82860 Europe/Kiev 1\")));\n\n        //change device, expecting syncs and OK\n        clientPair.appClient.send(\"hardware 1 vu 200000 1\");\n        clientPair.appClient.verifyResult(ok(3));\n        verify(clientPair.hardwareClient.responseMock, never()).channelRead(any(), eq(hardware(3, \"vu 200000 1\")));\n        verify(hardClient2.responseMock, never()).channelRead(any(), eq(hardware(3, \"vu 200000 1\")));\n\n        //switch device back, expecting syncs and OK\n        clientPair.appClient.send(\"hardware 1 vu 200000 0\");\n        clientPair.appClient.verifyResult(ok(4));\n        verify(clientPair.hardwareClient.responseMock, never()).channelRead(any(), eq(hardware(4, \"vu 200000 0\")));\n        verify(hardClient2.responseMock, never()).channelRead(any(), eq(hardware(4, \"vu 200000 0\")));\n\n        clientPair.appClient.verifyResult(ok(4));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 1 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 2 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 3 0\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 5 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 4 244\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 7 3\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 30 3\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 0 89.888037459418\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 11 -58.74774244674501\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 13 60 143 158\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 99 82800 82860 Europe/Kiev 1\")));\n    }\n\n    @Test\n    public void testNoSyncForDeviceSelectorWidget() throws Exception {\n        Device device0 = new Device(0, \"My Dashboard\", BoardType.Arduino_UNO);\n        device0.status = Status.ONLINE;\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":200000, \\\"width\\\":1, \\\"height\\\":1, \\\"value\\\":0, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"DEVICE_SELECTOR\\\"}\");\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":88, \\\"width\\\":1, \\\"height\\\":1, \\\"deviceId\\\":200000, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Button\\\", \\\"type\\\":\\\"BUTTON\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":88}\");\n        clientPair.appClient.verifyResult(ok(2));\n        clientPair.appClient.verifyResult(ok(3));\n\n        clientPair.appClient.send(\"hardware 1 vw 88 100\");\n        clientPair.hardwareClient.verifyResult(hardware(4, \"vw 88 100\"));\n\n        clientPair.appClient.sync(1);\n        verify(clientPair.appClient.responseMock, timeout(1000).times(15)).channelRead(any(), any());\n        clientPair.appClient.verifyResult(ok(5));\n        clientPair.appClient.never(appSync(b(\"1-200000 vw 88 100\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 88 100\")));\n    }\n\n    @Test\n    public void testDeviceSelectorWorksAfterDeviceRemoval() throws Exception {\n        Device device0 = new Device(0, \"My Dashboard\", BoardType.Arduino_UNO);\n        device0.status = Status.ONLINE;\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":200000, \\\"width\\\":1, \\\"height\\\":1, \\\"value\\\":0, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"DEVICE_SELECTOR\\\"}\");\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":88, \\\"width\\\":1, \\\"height\\\":1, \\\"deviceId\\\":200000, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Button\\\", \\\"type\\\":\\\"BUTTON\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":88}\");\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":89, \\\"width\\\":1, \\\"height\\\":1, \\\"deviceId\\\":200000, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Display\\\", \\\"type\\\":\\\"DIGIT4_DISPLAY\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":89}\");\n        clientPair.appClient.verifyResult(ok(2));\n        clientPair.appClient.verifyResult(ok(3));\n        clientPair.appClient.verifyResult(ok(4));\n\n        clientPair.appClient.reset();\n\n        clientPair.appClient.send(\"getDevices 1\");\n        Device[] devices = clientPair.appClient.parseDevices();\n        assertNotNull(devices);\n        assertEquals(2, devices.length);\n\n        assertEqualDevice(device0, devices[0]);\n        assertEqualDevice(device1, devices[1]);\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient2.start();\n\n        hardClient2.login(devices[1].token);\n        hardClient2.verifyResult(ok(1));\n\n        clientPair.appClient.send(\"hardware 1-200000 vw 88 100\");\n        clientPair.hardwareClient.verifyResult(hardware(2, \"vw 88 100\"));\n\n        //change device, expecting syncs and OK\n        clientPair.appClient.send(\"hardware 1 vu 200000 1\");\n        clientPair.appClient.verifyResult(ok(3));\n        clientPair.hardwareClient.never(hardware(3, \"vu 200000 1\"));\n\n        clientPair.appClient.send(\"hardware 1-200000 vw 88 101\");\n        hardClient2.verifyResult(hardware(4, \"vw 88 101\"));\n\n        clientPair.appClient.send(\"deleteDevice 1\\0\" + \"1\");\n        clientPair.appClient.verifyResult(ok(5));\n\n        //channel should be closed. so will not receive message\n        clientPair.appClient.send(\"hardware 1-200000 vw 88 102\");\n        verify(hardClient2.responseMock, after(500).never()).channelRead(any(), eq(hardware(5, \"vw 88 100\")));\n    }\n\n    @Test\n    public void terminalWithDeviceSelectorStoreMultipleCommands() throws Exception {\n        var device0 = new Device(0, \"My Dashboard\", BoardType.Arduino_UNO);\n        device0.status = Status.ONLINE;\n        var device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        var device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        var deviceSelector = new DeviceSelector();\n        deviceSelector.id = 200000;\n        deviceSelector.x = 0;\n        deviceSelector.y = 0;\n        deviceSelector.width = 1;\n        deviceSelector.height = 1;\n        deviceSelector.deviceIds = new int[] {0, 1};\n\n        var terminal = new Terminal();\n        terminal.id = 88;\n        terminal.width = 1;\n        terminal.height = 1;\n        terminal.deviceId = (int) deviceSelector.id;\n        terminal.pinType = PinType.VIRTUAL;\n        terminal.pin = 88;\n\n        clientPair.appClient.createWidget(1, deviceSelector);\n        clientPair.appClient.createWidget(1, terminal);\n        clientPair.appClient.verifyResult(ok(2));\n        clientPair.appClient.verifyResult(ok(3));\n\n        for (var i = 1; i <= 26; i++) {\n            clientPair.hardwareClient.send(\"hardware vw 88 \" + i);\n            clientPair.appClient.verifyResult(hardware(i, \"1-0 vw 88 \" + i));\n        }\n\n        clientPair.appClient.reset();\n        clientPair.appClient.sync(1, 0);\n        clientPair.appClient.verifyResult(ok(1));\n        //expecting 25 syncs and not 26\n        verify(clientPair.appClient.responseMock, timeout(1000).times(11 + 2)).channelRead(any(), any());\n\n        for (var i = 2; i <= 26; i++) {\n            clientPair.appClient.verifyResult(appSync(\"1-0 vm 88 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26\"));\n        }\n    }\n\n    @Test\n    public void TableWithDeviceSelectorStoreMultipleCommands() throws Exception {\n        var device0 = new Device(0, \"My Dashboard\", BoardType.Arduino_UNO);\n        device0.status = Status.ONLINE;\n        var device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        var device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        var deviceSelector = new DeviceSelector();\n        deviceSelector.id = 200000;\n        deviceSelector.x = 0;\n        deviceSelector.y = 0;\n        deviceSelector.width = 1;\n        deviceSelector.height = 1;\n        deviceSelector.deviceIds = new int[] {0, 1};\n\n        var table = new Table();\n        table.id = 88;\n        table.width = 1;\n        table.height = 1;\n        table.deviceId = (int) deviceSelector.id;\n        table.pinType = PinType.VIRTUAL;\n        table.pin = 88;\n\n        clientPair.appClient.createWidget(1, deviceSelector);\n        clientPair.appClient.createWidget(1, table);\n        clientPair.appClient.verifyResult(ok(2));\n        clientPair.appClient.verifyResult(ok(3));\n\n        for (var i = 1; i <= 11; i++) {\n            clientPair.hardwareClient.send(\"hardware vw 88 \" + i);\n            clientPair.appClient.verifyResult(hardware(i, \"1-0 vw 88 \" + i));\n        }\n\n        clientPair.appClient.reset();\n        clientPair.appClient.sync(1, 0);\n        clientPair.appClient.verifyResult(ok(1));\n        verify(clientPair.appClient.responseMock, timeout(1000).times(11 + 2)).channelRead(any(), any());\n\n        clientPair.appClient.verifyResult(appSync(\"1-0 vm 88 1 2 3 4 5 6 7 8 9 10 11\"));\n    }\n\n    @Test\n    public void LCDWithDeviceSelectorStoreMultipleCommands() throws Exception {\n        var device0 = new Device(0, \"My Dashboard\", BoardType.Arduino_UNO);\n        device0.status = Status.ONLINE;\n        var device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        var device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        var deviceSelector = new DeviceSelector();\n        deviceSelector.id = 200000;\n        deviceSelector.x = 0;\n        deviceSelector.y = 0;\n        deviceSelector.width = 1;\n        deviceSelector.height = 1;\n        deviceSelector.deviceIds = new int[] {0, 1};\n\n        var lcd = new LCD();\n        lcd.id = 88;\n        lcd.width = 1;\n        lcd.height = 1;\n        lcd.deviceId = (int) deviceSelector.id;\n        lcd.dataStreams = new DataStream[] {new DataStream((short) 88, PinType.VIRTUAL)};\n\n        clientPair.appClient.createWidget(1, deviceSelector);\n        clientPair.appClient.createWidget(1, lcd);\n        clientPair.appClient.verifyResult(ok(2));\n        clientPair.appClient.verifyResult(ok(3));\n\n        for (var i = 1; i <= 7; i++) {\n            clientPair.hardwareClient.send(\"hardware vw 88 \" + i);\n            clientPair.appClient.verifyResult(hardware(i, \"1-0 vw 88 \" + i));\n        }\n\n        clientPair.appClient.reset();\n        clientPair.appClient.sync(1, 0);\n        clientPair.appClient.verifyResult(ok(1));\n        verify(clientPair.appClient.responseMock, timeout(1000).times(11 + 2)).channelRead(any(), any());\n\n        clientPair.appClient.verifyResult(appSync(\"1-0 vm 88 2 3 4 5 6 7\"));\n    }\n\n    @Test\n    public void testDeviceSelectorForSharedApp() throws Exception {\n        Device device0 = new Device(0, \"My Dashboard\", BoardType.Arduino_UNO);\n        device0.status = Status.ONLINE;\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":200000, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"DEVICE_SELECTOR\\\"}\");\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":88, \\\"width\\\":1, \\\"height\\\":1, \\\"deviceId\\\":200000, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"STEP\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":88}\");\n        clientPair.appClient.verifyResult(ok(2));\n        clientPair.appClient.verifyResult(ok(3));\n\n        clientPair.appClient.send(\"getShareToken 1\");\n        String token = clientPair.appClient.getBody(4);\n        assertNotNull(token);\n        assertEquals(32, token.length());\n\n        TestAppClient appClient2 = new TestAppClient(properties);\n        appClient2.start();\n\n        appClient2.send(\"shareLogin \" + getUserName() + \" \" + token + \" Android 24\");\n        verify(appClient2.responseMock, timeout(500)).channelRead(any(), eq(ok(1)));\n\n        //change device\n        clientPair.appClient.send(\"hardware 1 vu 200000 1\");\n        clientPair.appClient.verifyResult(ok(5));\n        clientPair.hardwareClient.never(hardware(5, \"vu 200000 1\"));\n        appClient2.verifyResult(appSync(5, \"1 vu 200000 1\"));\n\n        appClient2.send(\"hardware 1 vu 200000 0\");\n        appClient2.verifyResult(ok(2));\n        clientPair.hardwareClient.never(hardware(2, \"vu 200000 0\"));\n        clientPair.appClient.verifyResult(appSync(2, \"1 vu 200000 0\"));\n    }\n}"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/tcp/DeviceTilesWidgetTest.java",
    "content": "package cc.blynk.integration.tcp;\n\nimport cc.blynk.integration.SingleServerInstancePerTest;\nimport cc.blynk.integration.model.tcp.TestHardClient;\nimport cc.blynk.server.core.dao.ReportingDiskDao;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.DataStream;\nimport cc.blynk.server.core.model.device.BoardType;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.device.Status;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.controls.Button;\nimport cc.blynk.server.core.model.widgets.controls.NumberInput;\nimport cc.blynk.server.core.model.widgets.controls.Terminal;\nimport cc.blynk.server.core.model.widgets.outputs.ValueDisplay;\nimport cc.blynk.server.core.model.widgets.outputs.graph.AggregationFunctionType;\nimport cc.blynk.server.core.model.widgets.outputs.graph.FontSize;\nimport cc.blynk.server.core.model.widgets.outputs.graph.GraphDataStream;\nimport cc.blynk.server.core.model.widgets.outputs.graph.GraphGranularityType;\nimport cc.blynk.server.core.model.widgets.outputs.graph.GraphType;\nimport cc.blynk.server.core.model.widgets.outputs.graph.Superchart;\nimport cc.blynk.server.core.model.widgets.ui.Menu;\nimport cc.blynk.server.core.model.widgets.ui.Tab;\nimport cc.blynk.server.core.model.widgets.ui.Tabs;\nimport cc.blynk.server.core.model.widgets.ui.tiles.DeviceTiles;\nimport cc.blynk.server.core.model.widgets.ui.tiles.Tile;\nimport cc.blynk.server.core.model.widgets.ui.tiles.TileTemplate;\nimport cc.blynk.server.core.model.widgets.ui.tiles.templates.ButtonTileTemplate;\nimport cc.blynk.server.core.model.widgets.ui.tiles.templates.PageTileTemplate;\nimport cc.blynk.server.core.protocol.model.messages.ResponseMessage;\nimport cc.blynk.utils.FileUtils;\nimport org.asynchttpclient.AsyncHttpClient;\nimport org.asynchttpclient.DefaultAsyncHttpClient;\nimport org.asynchttpclient.DefaultAsyncHttpClientConfig;\nimport org.asynchttpclient.Response;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\n\nimport static cc.blynk.integration.TestUtil.appSync;\nimport static cc.blynk.integration.TestUtil.b;\nimport static cc.blynk.integration.TestUtil.createDevice;\nimport static cc.blynk.integration.TestUtil.hardware;\nimport static cc.blynk.integration.TestUtil.notAllowed;\nimport static cc.blynk.integration.TestUtil.ok;\nimport static cc.blynk.integration.TestUtil.sleep;\nimport static cc.blynk.server.core.model.widgets.FrequencyWidget.READING_MSG_ID;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_ENERGY;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE;\nimport static cc.blynk.server.core.protocol.enums.Response.NO_DATA;\nimport static cc.blynk.server.core.protocol.model.messages.MessageFactory.produce;\nimport static junit.framework.TestCase.assertNull;\nimport static org.junit.Assert.assertArrayEquals;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\nimport static org.junit.Assert.assertTrue;\nimport static org.mockito.ArgumentMatchers.contains;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.eq;\nimport static org.mockito.Mockito.timeout;\nimport static org.mockito.Mockito.verify;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 7/09/2016.\n *\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class DeviceTilesWidgetTest extends SingleServerInstancePerTest {\n\n    @Test\n    public void createPageTemplate() throws Exception {\n        long widgetId = 21321;\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = widgetId;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n        deviceTiles.color = -231;\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(1));\n\n        PageTileTemplate tileTemplate = new PageTileTemplate(1,\n                null, null, \"name\", \"name\", \"iconName\", BoardType.ESP8266, new DataStream((short) 1, PinType.VIRTUAL),\n                false, null, null, null, -75056000, -231, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.createTemplate(1, widgetId, tileTemplate);\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.appClient.send(\"getWidget 1\\0\" + widgetId);\n        deviceTiles = (DeviceTiles) JsonParser.parseWidget(clientPair.appClient.getBody(3), 0);\n        assertNotNull(deviceTiles);\n        assertEquals(widgetId, deviceTiles.id);\n        assertEquals(-231, deviceTiles.color);\n        assertNotNull(deviceTiles.templates);\n        assertEquals(1, deviceTiles.templates.length);\n        assertEquals(\"name\", deviceTiles.templates[0].name);\n        assertTrue(deviceTiles.templates[0] instanceof PageTileTemplate);\n        PageTileTemplate pageTileTemplate = (PageTileTemplate) deviceTiles.templates[0];\n        assertEquals(0, deviceTiles.tiles.length);\n        assertEquals(-75056000, pageTileTemplate.color);\n        assertEquals(-231, pageTileTemplate.tileColor);\n    }\n\n    @Test\n    public void createDeviceTilesAndEditColors() throws Exception {\n        long widgetId = 21321;\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = widgetId;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n        deviceTiles.color = 0;\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(1));\n\n        PageTileTemplate tileTemplate = new PageTileTemplate(1,\n                null, null, \"name\", \"name\", \"iconName\", BoardType.ESP8266, new DataStream((short) 1, PinType.VIRTUAL),\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.createTemplate(1, widgetId, tileTemplate);\n        clientPair.appClient.verifyResult(ok(2));\n\n        deviceTiles.color = -231;\n\n        clientPair.appClient.updateWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(3));\n\n        tileTemplate = new PageTileTemplate(1,\n                null, null, \"name\", \"name\", \"iconName\", BoardType.ESP8266, new DataStream((short) 1, PinType.VIRTUAL),\n                false, null, null, null, -1, -231, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.updateTemplate(1, widgetId, tileTemplate);\n        clientPair.appClient.verifyResult(ok(4));\n\n        clientPair.appClient.send(\"getWidget 1\\0\" + widgetId);\n        deviceTiles = (DeviceTiles) JsonParser.parseWidget(clientPair.appClient.getBody(5), 0);\n        assertNotNull(deviceTiles);\n        assertEquals(widgetId, deviceTiles.id);\n        assertEquals(-231, deviceTiles.color);\n        assertNotNull(deviceTiles.templates);\n        assertEquals(1, deviceTiles.templates.length);\n        assertEquals(\"name\", deviceTiles.templates[0].name);\n        assertTrue(deviceTiles.templates[0] instanceof PageTileTemplate);\n        PageTileTemplate pageTileTemplate = (PageTileTemplate) deviceTiles.templates[0];\n        assertEquals(0, deviceTiles.tiles.length);\n        assertEquals(-1, pageTileTemplate.color);\n        assertEquals(-231, pageTileTemplate.tileColor);\n    }\n\n    @Test\n    public void createPageTemplateWithOutModeField() throws Exception {\n        long widgetId = 21321;\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = widgetId;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.createTemplate(1, widgetId, \"{\\\"id\\\":1,\\\"templateId\\\":\\\"123\\\",\\\"name\\\":\\\"name\\\",\\\"iconName\\\":\\\"iconName\\\",\\\"boardType\\\":\\\"ESP8266\\\",\\\"showDeviceName\\\":false,\\\"color\\\":0,\\\"tileColor\\\":0,\\\"fontSize\\\":\\\"LARGE\\\",\\\"showTileLabel\\\":false,\\\"pin\\\":{\\\"pin\\\":1,\\\"pwmMode\\\":false,\\\"rangeMappingOn\\\":false,\\\"pinType\\\":\\\"VIRTUAL\\\",\\\"min\\\":0.0,\\\"max\\\":255.0}}\");\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.appClient.send(\"getWidget 1\\0\" + widgetId);\n        deviceTiles = (DeviceTiles) JsonParser.parseWidget(clientPair.appClient.getBody(3), 0);\n        assertNotNull(deviceTiles);\n        assertEquals(widgetId, deviceTiles.id);\n        assertNotNull(deviceTiles.templates);\n        assertEquals(1, deviceTiles.templates.length);\n        assertEquals(\"name\", deviceTiles.templates[0].name);\n        assertTrue(deviceTiles.templates[0] instanceof PageTileTemplate);\n        assertEquals(0, deviceTiles.tiles.length);\n    }\n\n    @Test\n    public void createButtonTileTemplate() throws Exception {\n        long widgetId = 21321;\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = widgetId;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(1));\n\n        ButtonTileTemplate tileTemplate = new ButtonTileTemplate(1,\n                null, null, \"name\", \"name\", \"iconName\", BoardType.ESP8266, new DataStream((short) 1, PinType.VIRTUAL),\n                false, false, false, null, null);\n\n        clientPair.appClient.createTemplate(1, widgetId, tileTemplate);\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.appClient.send(\"getWidget 1\\0\" + widgetId);\n        deviceTiles = (DeviceTiles) JsonParser.parseWidget(clientPair.appClient.getBody(3), 0);\n        assertNotNull(deviceTiles);\n        assertEquals(widgetId, deviceTiles.id);\n        assertNotNull(deviceTiles.templates);\n        assertEquals(1, deviceTiles.templates.length);\n        assertEquals(\"name\", deviceTiles.templates[0].name);\n        assertTrue(deviceTiles.templates[0] instanceof ButtonTileTemplate);\n        assertEquals(0, deviceTiles.tiles.length);\n    }\n\n    @Test\n    public void createTemplateAndUpdate() throws Exception {\n        long widgetId = 21321;\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = widgetId;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(1));\n\n        PageTileTemplate tileTemplate = new PageTileTemplate(1,\n                null, null, \"name\", \"name\", \"iconName\", BoardType.ESP8266, null,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.createTemplate(1, widgetId, tileTemplate);\n        clientPair.appClient.verifyResult(ok(2));\n\n        tileTemplate = new PageTileTemplate(1,\n                null, new int[] {0}, \"name\", \"name\", \"iconName\", BoardType.ESP8266, null,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.updateTemplate(1, widgetId, tileTemplate);\n        clientPair.appClient.verifyResult(ok(3));\n\n\n        clientPair.appClient.send(\"getWidget 1\\0\" + widgetId);\n        deviceTiles = (DeviceTiles) JsonParser.parseWidget(clientPair.appClient.getBody(4), 0);\n        assertNotNull(deviceTiles);\n        assertEquals(widgetId, deviceTiles.id);\n        assertNotNull(deviceTiles.templates);\n        assertEquals(1, deviceTiles.templates.length);\n        assertArrayEquals(new int[] {0}, deviceTiles.templates[0].deviceIds);\n        assertEquals(\"name\", deviceTiles.templates[0].name);\n        assertEquals(1, deviceTiles.tiles.length);\n        assertEquals(0, deviceTiles.tiles[0].deviceId);\n        assertEquals(tileTemplate.id, deviceTiles.tiles[0].templateId);\n        assertNull(deviceTiles.tiles[0].dataStream);\n    }\n\n    @Test\n    public void createTemplateAndUpdate2() throws Exception {\n        long widgetId = 21321;\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = widgetId;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(1));\n\n        PageTileTemplate tileTemplate = new PageTileTemplate(0,\n                null, null, \"name\", \"name\", \"iconName\", BoardType.ESP8266, null,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.createTemplate(1, widgetId, tileTemplate);\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.appClient.send(\"updateTemplate \" + b(\"1 \" + widgetId + \" \")\n                + \"{\\\"alignment\\\":\\\"LEFT\\\",\\\"color\\\":600084223,\\\"deviceIds\\\":[0],\\\"disableWhenOffline\\\":false,\" +\n                \"\\\"id\\\":0,\\\"mode\\\":\\\"PAGE\\\",\\\"name\\\":\\\"Template 1\\\",\" +\n                \"\\\"pin\\\":{\\\"max\\\":255,\\\"min\\\":0,\\\"pin\\\":5,\\\"pinType\\\":\\\"VIRTUAL\\\",\\\"pwmMode\\\":false,\" +\n                \"\\\"rangeMappingOn\\\":false},\\\"showDeviceName\\\":true,\\\"valueName\\\":\\\"Temperature\\\",\" +\n                \"\\\"valueSuffix\\\":\\\"%\\\",\\\"widgets\\\":[]}}\");\n        clientPair.appClient.verifyResult(ok(3));\n\n\n        clientPair.appClient.send(\"getWidget 1\\0\" + widgetId);\n        deviceTiles = (DeviceTiles) JsonParser.parseWidget(clientPair.appClient.getBody(4), 0);\n        assertNotNull(deviceTiles);\n        assertEquals(widgetId, deviceTiles.id);\n        assertNotNull(deviceTiles.templates);\n        assertEquals(1, deviceTiles.templates.length);\n        assertArrayEquals(new int[] {0}, deviceTiles.templates[0].deviceIds);\n        assertEquals(\"Template 1\", deviceTiles.templates[0].name);\n        assertEquals(1, deviceTiles.tiles.length);\n        assertEquals(0, deviceTiles.tiles[0].deviceId);\n        assertEquals(tileTemplate.id, deviceTiles.tiles[0].templateId);\n        assertNotNull(deviceTiles.tiles[0].dataStream);\n        assertEquals(5, deviceTiles.tiles[0].dataStream.pin);\n    }\n\n    @Test\n    public void createTemplateAndUpdatePin() throws Exception {\n        long widgetId = 21321;\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = widgetId;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(1));\n\n        PageTileTemplate tileTemplate = new PageTileTemplate(1,\n                null, null, \"name\", \"name\", \"iconName\", BoardType.ESP8266, null,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.createTemplate(1, widgetId, tileTemplate);\n        clientPair.appClient.verifyResult(ok(2));\n\n        DataStream dataStream = new DataStream((short) 1, PinType.VIRTUAL);\n        tileTemplate = new PageTileTemplate(1,\n                null, new int[]{0}, \"name\", \"name\", \"iconName\", BoardType.ESP8266, dataStream,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.updateTemplate(1, widgetId, tileTemplate);\n        clientPair.appClient.verifyResult(ok(3));\n\n        clientPair.appClient.send(\"getWidget 1\\0\" + widgetId);\n        deviceTiles = (DeviceTiles) JsonParser.parseWidget(clientPair.appClient.getBody(4), 0);\n        assertNotNull(deviceTiles);\n        assertEquals(widgetId, deviceTiles.id);\n        assertNotNull(deviceTiles.templates);\n        assertEquals(1, deviceTiles.templates.length);\n        assertArrayEquals(new int[] {0}, deviceTiles.templates[0].deviceIds);\n        assertEquals(\"name\", deviceTiles.templates[0].name);\n        assertEquals(1, deviceTiles.tiles.length);\n        assertEquals(0, deviceTiles.tiles[0].deviceId);\n        assertEquals(tileTemplate.id, deviceTiles.tiles[0].templateId);\n        assertNotNull(deviceTiles.tiles[0].dataStream);\n        assertEquals(1, deviceTiles.tiles[0].dataStream.pin);\n        assertEquals(PinType.VIRTUAL, deviceTiles.tiles[0].dataStream.pinType);\n\n        dataStream = new DataStream((short) 2, PinType.VIRTUAL);\n        tileTemplate = new PageTileTemplate(1,\n                null, new int[]{0}, \"name\", \"name\", \"iconName\", BoardType.ESP8266, dataStream,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.updateTemplate(1, widgetId, tileTemplate);\n        clientPair.appClient.verifyResult(ok(5));\n\n        clientPair.appClient.send(\"getWidget 1\\0\" + widgetId);\n        deviceTiles = (DeviceTiles) JsonParser.parseWidget(clientPair.appClient.getBody(6), 0);\n        assertNotNull(deviceTiles);\n        assertEquals(widgetId, deviceTiles.id);\n        assertNotNull(deviceTiles.templates);\n        assertEquals(1, deviceTiles.templates.length);\n        assertArrayEquals(new int[] {0}, deviceTiles.templates[0].deviceIds);\n        assertEquals(\"name\", deviceTiles.templates[0].name);\n        assertEquals(1, deviceTiles.tiles.length);\n        assertEquals(0, deviceTiles.tiles[0].deviceId);\n        assertEquals(tileTemplate.id, deviceTiles.tiles[0].templateId);\n        assertNotNull(deviceTiles.tiles[0].dataStream);\n        assertEquals(2, deviceTiles.tiles[0].dataStream.pin);\n        assertEquals(PinType.VIRTUAL, deviceTiles.tiles[0].dataStream.pinType);\n    }\n\n    @Test\n    public void createTemplateAndUpdatePinFor2Templates() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        clientPair.appClient.reset();\n\n        long widgetId = 21321;\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = widgetId;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(1));\n\n        TileTemplate tileTemplate = new PageTileTemplate(1,\n                null, null, \"name\", \"name\", \"iconName\", BoardType.ESP8266, null,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.createTemplate(1, widgetId, tileTemplate);\n        clientPair.appClient.verifyResult(ok(2));\n\n        DataStream dataStream = new DataStream((short) 1, PinType.VIRTUAL);\n        tileTemplate = new PageTileTemplate(1,\n                null, new int[]{0, 1}, \"name\", \"name\", \"iconName\", BoardType.ESP8266, dataStream,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.updateTemplate(1, widgetId, tileTemplate);\n        clientPair.appClient.verifyResult(ok(3));\n\n        clientPair.appClient.send(\"getWidget 1\\0\" + widgetId);\n        deviceTiles = (DeviceTiles) JsonParser.parseWidget(clientPair.appClient.getBody(4), 0);\n        assertNotNull(deviceTiles);\n        assertEquals(widgetId, deviceTiles.id);\n        assertNotNull(deviceTiles.templates);\n        assertEquals(1, deviceTiles.templates.length);\n        assertArrayEquals(new int[] {0, 1}, deviceTiles.templates[0].deviceIds);\n        assertEquals(\"name\", deviceTiles.templates[0].name);\n        assertEquals(2, deviceTiles.tiles.length);\n\n        int deviceIdIndex = 0;\n        for (Tile tile : deviceTiles.tiles) {\n            assertEquals(deviceIdIndex++, tile.deviceId);\n            assertEquals(tileTemplate.id, tile.templateId);\n            assertNotNull(tile.dataStream);\n            assertEquals(1, tile.dataStream.pin);\n            assertEquals(PinType.VIRTUAL, tile.dataStream.pinType);\n        }\n\n        dataStream = new DataStream((short) 2, PinType.VIRTUAL);\n        tileTemplate = new PageTileTemplate(1,\n                null, new int[]{0, 1}, \"name\", \"name\", \"iconName\", BoardType.ESP8266, dataStream,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.updateTemplate(1, widgetId, tileTemplate);\n        clientPair.appClient.verifyResult(ok(5));\n\n        clientPair.appClient.send(\"getWidget 1\\0\" + widgetId);\n        deviceTiles = (DeviceTiles) JsonParser.parseWidget(clientPair.appClient.getBody(6), 0);\n        assertNotNull(deviceTiles);\n        assertEquals(widgetId, deviceTiles.id);\n        assertNotNull(deviceTiles.templates);\n        assertEquals(1, deviceTiles.templates.length);\n        assertArrayEquals(new int[] {0, 1}, deviceTiles.templates[0].deviceIds);\n        assertEquals(\"name\", deviceTiles.templates[0].name);\n        assertEquals(2, deviceTiles.tiles.length);\n\n        deviceIdIndex = 0;\n        for (Tile tile : deviceTiles.tiles) {\n            assertEquals(deviceIdIndex++, tile.deviceId);\n            assertEquals(tileTemplate.id, tile.templateId);\n            assertNotNull(tile.dataStream);\n            assertEquals(2, tile.dataStream.pin);\n            assertEquals(PinType.VIRTUAL, tile.dataStream.pinType);\n        }\n    }\n\n    @Test\n    public void syncForSpecificDeviceTile() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        clientPair.appClient.reset();\n\n        long widgetId = 21321;\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = widgetId;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(1));\n\n        TileTemplate tileTemplate = new PageTileTemplate(1,\n                null, null, \"name\", \"name\", \"iconName\", BoardType.ESP8266, null,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.createTemplate(1, widgetId, tileTemplate);\n        clientPair.appClient.verifyResult(ok(2));\n\n        DataStream dataStream = new DataStream((short) 5, PinType.VIRTUAL);\n\n        Button button = new Button();\n        button.id = 2321;\n        button.width = 2;\n        button.height = 2;\n        button.pin = 2;\n        button.pinType = PinType.VIRTUAL;\n\n        ValueDisplay valueDisplay = new ValueDisplay();\n        valueDisplay.id = 2322;\n        valueDisplay.width = 2;\n        valueDisplay.height = 2;\n        valueDisplay.pin = 77;\n        valueDisplay.pinType = PinType.VIRTUAL;\n\n        clientPair.appClient.createWidget(1, widgetId, tileTemplate.id, button);\n        clientPair.appClient.verifyResult(ok(3));\n\n        clientPair.appClient.createWidget(1, widgetId, tileTemplate.id, valueDisplay);\n        clientPair.appClient.verifyResult(ok(4));\n\n        tileTemplate = new PageTileTemplate(1,\n                null, new int[]{0, 1}, \"name\", \"name\", \"iconName\", BoardType.ESP8266, dataStream,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.updateTemplate(1, widgetId, tileTemplate);\n        clientPair.appClient.verifyResult(ok(5));\n\n        clientPair.appClient.send(\"getWidget 1\\0\" + widgetId);\n        deviceTiles = (DeviceTiles) JsonParser.parseWidget(clientPair.appClient.getBody(6), 0);\n        assertNotNull(deviceTiles);\n        assertEquals(widgetId, deviceTiles.id);\n        assertNotNull(deviceTiles.templates);\n        assertEquals(1, deviceTiles.templates.length);\n        assertEquals(2, deviceTiles.templates[0].widgets.length);\n        assertArrayEquals(new int[] {0, 1}, deviceTiles.templates[0].deviceIds);\n        assertEquals(\"name\", deviceTiles.templates[0].name);\n        assertEquals(2, deviceTiles.tiles.length);\n\n        int deviceIdIndex = 0;\n        for (Tile tile : deviceTiles.tiles) {\n            assertEquals(deviceIdIndex++, tile.deviceId);\n            assertEquals(tileTemplate.id, tile.templateId);\n            assertNotNull(tile.dataStream);\n            assertEquals(5, tile.dataStream.pin);\n            assertEquals(PinType.VIRTUAL, tile.dataStream.pinType);\n        }\n\n\n        clientPair.hardwareClient.send(\"hardware vw 5 101\");\n        clientPair.hardwareClient.send(\"hardware vw 6 102\");\n        clientPair.appClient.verifyResult(hardware(1, \"1-0 vw 5 101\"));\n        clientPair.appClient.verifyResult(hardware(2, \"1-0 vw 6 102\"));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"getWidget 1\\0\" + widgetId);\n        deviceTiles = (DeviceTiles) JsonParser.parseWidget(clientPair.appClient.getBody(), 0);\n        assertNotNull(deviceTiles);\n\n        Tile tile = deviceTiles.tiles[0];\n        assertEquals(0, tile.deviceId);\n        assertNotNull(tile.dataStream);\n        assertEquals(5, tile.dataStream.pin);\n        assertEquals(PinType.VIRTUAL, tile.dataStream.pinType);\n        assertEquals(\"101\", tile.dataStream.value);\n\n        Tile tile2 = deviceTiles.tiles[1];\n        assertEquals(1, tile2.deviceId);\n        assertNotNull(tile2.dataStream);\n        assertEquals(5, tile2.dataStream.pin);\n        assertEquals(PinType.VIRTUAL, tile2.dataStream.pinType);\n        assertNull(tile2.dataStream.value);\n\n        clientPair.appClient.reset();\n        clientPair.appClient.sync(1, 0);\n\n        verify(clientPair.appClient.responseMock, timeout(500).times(13)).channelRead(any(), any());\n\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 1 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 2 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 3 0\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 5 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 4 244\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 7 3\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 30 3\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 0 89.888037459418\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 11 -58.74774244674501\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 13 60 143 158\")));\n\n        clientPair.appClient.verifyResult(appSync(1111, b(\"1-0 vw 5 101\")));\n        clientPair.appClient.verifyResult(appSync(1111, b(\"1-0 vw 6 102\")));\n    }\n\n    @Test\n    public void readingWidgetWorksForDeviceTiles() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        clientPair.appClient.reset();\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = 21321;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(1));\n\n        TileTemplate tileTemplate = new PageTileTemplate(1,\n                null, null, \"name\", \"name\", \"iconName\", BoardType.ESP8266, null,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.createTemplate(1, deviceTiles.id, tileTemplate);\n        clientPair.appClient.verifyResult(ok(2));\n\n        DataStream dataStream = new DataStream((short) 5, PinType.VIRTUAL);\n\n        ValueDisplay valueDisplay = new ValueDisplay();\n        valueDisplay.id = 1234;\n        valueDisplay.width = 2;\n        valueDisplay.height = 2;\n        valueDisplay.pin = 77;\n        valueDisplay.pinType = PinType.VIRTUAL;\n        valueDisplay.frequency = 1000;\n        valueDisplay.deviceId = -1;\n\n        Executors.newScheduledThreadPool(1).scheduleAtFixedRate(holder.readingWidgetsWorker, 0, 1000, TimeUnit.MILLISECONDS);\n\n        tileTemplate = new PageTileTemplate(1,\n                null, new int[]{0}, \"name\", \"name\", \"iconName\", BoardType.ESP8266, dataStream,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.createWidget(1, deviceTiles.id, tileTemplate.id, valueDisplay);\n        clientPair.appClient.verifyResult(ok(3));\n\n        clientPair.appClient.updateTemplate(1, deviceTiles.id, tileTemplate);\n        clientPair.appClient.verifyResult(ok(4));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.sync(1, 0);\n\n        verify(clientPair.appClient.responseMock, timeout(500).times(11)).channelRead(any(), any());\n\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 1 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 2 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 3 0\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 5 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 4 244\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 7 3\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 30 3\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 0 89.888037459418\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 11 -58.74774244674501\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 13 60 143 158\")));\n\n        verify(clientPair.hardwareClient.responseMock, timeout(2000)).channelRead(any(), eq(produce(READING_MSG_ID, HARDWARE, b(\"vr 77\"))));\n    }\n\n    @Test\n    public void readingWidgetWorksForAllTilesWithinDeviceTiles() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", properties.getHttpPort());\n        hardClient2.start();\n        hardClient2.login(device.token);\n        hardClient2.verifyResult(ok(1));\n\n        clientPair.appClient.reset();\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = 21321;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(1));\n\n        TileTemplate tileTemplate = new PageTileTemplate(1,\n                null, null, \"name\", \"name\", \"iconName\", BoardType.ESP8266, null,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.createTemplate(1, deviceTiles.id, tileTemplate);\n        clientPair.appClient.verifyResult(ok(2));\n\n        ValueDisplay valueDisplay = new ValueDisplay();\n        valueDisplay.id = 1234;\n        valueDisplay.width = 2;\n        valueDisplay.height = 2;\n        valueDisplay.pin = 77;\n        valueDisplay.pinType = PinType.VIRTUAL;\n        valueDisplay.frequency = 1000;\n        valueDisplay.deviceId = -1;\n\n        tileTemplate = new PageTileTemplate(1,\n                null, new int[]{0, 1}, \"name\", \"name\", \"iconName\", BoardType.ESP8266, null,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.createWidget(1, deviceTiles.id, tileTemplate.id, valueDisplay);\n        clientPair.appClient.verifyResult(ok(3));\n\n        clientPair.appClient.updateTemplate(1, deviceTiles.id, tileTemplate);\n        clientPair.appClient.verifyResult(ok(4));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.sync(1, device1.id);\n        clientPair.appClient.verifyResult(ok(1));\n\n        Executors.newScheduledThreadPool(1).scheduleAtFixedRate(holder.readingWidgetsWorker, 0, 1000, TimeUnit.MILLISECONDS);\n\n        hardClient2.verifyResult(produce(READING_MSG_ID, HARDWARE, b(\"vr 77\")));\n        clientPair.hardwareClient.verifyResult(produce(READING_MSG_ID, HARDWARE, b(\"vr 77\")));\n    }\n\n    @Test\n    public void doNotPerformReadCommandWhenNoReadingWidgetInsideTileTemplate() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", properties.getHttpPort());\n        hardClient2.start();\n        hardClient2.login(device.token);\n        hardClient2.verifyResult(ok(1));\n\n        clientPair.appClient.reset();\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = 21321;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(1));\n\n        TileTemplate tileTemplate = new PageTileTemplate(1,\n                null, null, \"name\", \"name\", \"iconName\", BoardType.ESP8266, null,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.createTemplate(1, deviceTiles.id, tileTemplate);\n        clientPair.appClient.verifyResult(ok(2));\n\n        ValueDisplay valueDisplay = new ValueDisplay();\n        valueDisplay.id = 1234;\n        valueDisplay.width = 2;\n        valueDisplay.height = 2;\n        valueDisplay.pin = 77;\n        valueDisplay.pinType = PinType.VIRTUAL;\n        valueDisplay.frequency = 0;\n        valueDisplay.deviceId = -1;\n\n        tileTemplate = new PageTileTemplate(1,\n                null, new int[]{0, 1}, \"name\", \"name\", \"iconName\", BoardType.ESP8266, null,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.createWidget(1, deviceTiles.id, tileTemplate.id, valueDisplay);\n        clientPair.appClient.verifyResult(ok(3));\n\n        clientPair.appClient.updateTemplate(1, deviceTiles.id, tileTemplate);\n        clientPair.appClient.verifyResult(ok(4));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.sync(1, device1.id);\n        clientPair.appClient.verifyResult(ok(1));\n\n        Executors.newScheduledThreadPool(1).scheduleAtFixedRate(holder.readingWidgetsWorker, 0, 1000, TimeUnit.MILLISECONDS);\n\n        sleep(1200);\n\n        hardClient2.never(produce(READING_MSG_ID, HARDWARE, b(\"vr 77\")));\n        clientPair.hardwareClient.never(produce(READING_MSG_ID, HARDWARE, b(\"vr 77\")));\n    }\n\n    @Test\n    public void deviceRemovalDoesntEraseAllTiles() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", properties.getHttpPort());\n        hardClient2.start();\n        hardClient2.login(device.token);\n        hardClient2.verifyResult(ok(1));\n\n        clientPair.appClient.reset();\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = 21321;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(1));\n\n        DataStream dataStream = new DataStream((short) 66, PinType.VIRTUAL);\n        TileTemplate tileTemplate = new PageTileTemplate(1,\n                null, null, \"name\", \"name\", \"iconName\", BoardType.ESP8266, dataStream,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.createTemplate(1, deviceTiles.id, tileTemplate);\n        clientPair.appClient.verifyResult(ok(2));\n\n        ValueDisplay valueDisplay = new ValueDisplay();\n        valueDisplay.id = 1234;\n        valueDisplay.width = 2;\n        valueDisplay.height = 2;\n        valueDisplay.pin = 77;\n        valueDisplay.pinType = PinType.VIRTUAL;\n        valueDisplay.frequency = 0;\n        valueDisplay.deviceId = -1;\n\n        tileTemplate = new PageTileTemplate(1,\n                null, new int[]{0, 1}, \"name\", \"name\", \"iconName\", BoardType.ESP8266, dataStream,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.createWidget(1, deviceTiles.id, tileTemplate.id, valueDisplay);\n        clientPair.appClient.verifyResult(ok(3));\n\n        clientPair.appClient.updateTemplate(1, deviceTiles.id, tileTemplate);\n        clientPair.appClient.verifyResult(ok(4));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.sync(1, device1.id);\n        clientPair.appClient.verifyResult(ok(1));\n\n        hardClient2.send(\"hardware vw 66 444\");\n        clientPair.appClient.verifyResult(hardware(2, \"1-1 vw 66 444\"));\n\n        clientPair.hardwareClient.send(\"hardware vw 66 555\");\n        clientPair.appClient.verifyResult(hardware(1, \"1-0 vw 66 555\"));\n\n        clientPair.appClient.deleteDevice(1, 1);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.sync(1, 0);\n        clientPair.appClient.verifyResult(ok(1));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 66 555\"));\n    }\n\n    @Test\n    public void addingNewDeviceToTheTilesPreservesTileValue() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", properties.getHttpPort());\n        hardClient2.start();\n        hardClient2.login(device.token);\n        hardClient2.verifyResult(ok(1));\n\n        clientPair.appClient.reset();\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = 21321;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(1));\n\n        DataStream dataStream = new DataStream((short) 66, PinType.VIRTUAL);\n        TileTemplate tileTemplate = new PageTileTemplate(1,\n                null, null, \"name\", \"name\", \"iconName\", BoardType.ESP8266, dataStream,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.createTemplate(1, deviceTiles.id, tileTemplate);\n        clientPair.appClient.verifyResult(ok(2));\n\n        ValueDisplay valueDisplay = new ValueDisplay();\n        valueDisplay.id = 1234;\n        valueDisplay.width = 2;\n        valueDisplay.height = 2;\n        valueDisplay.pin = 77;\n        valueDisplay.pinType = PinType.VIRTUAL;\n        valueDisplay.frequency = 0;\n        valueDisplay.deviceId = -1;\n\n        tileTemplate = new PageTileTemplate(1,\n                null, new int[] {0}, \"name\", \"name\", \"iconName\", BoardType.ESP8266, dataStream,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.createWidget(1, deviceTiles.id, tileTemplate.id, valueDisplay);\n        clientPair.appClient.verifyResult(ok(3));\n\n        clientPair.appClient.updateTemplate(1, deviceTiles.id, tileTemplate);\n        clientPair.appClient.verifyResult(ok(4));\n\n        clientPair.hardwareClient.send(\"hardware vw 66 444\");\n        clientPair.appClient.verifyResult(hardware(1, \"1-0 vw 66 444\"));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.sync(1, 0);\n        clientPair.appClient.verifyResult(ok(1));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 66 444\"));\n\n        tileTemplate = new PageTileTemplate(1,\n                null, new int[] {0, 1}, \"name\", \"name\", \"iconName\", BoardType.ESP8266, dataStream,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n        clientPair.appClient.updateTemplate(1, deviceTiles.id, tileTemplate);\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.sync(1, 0);\n        clientPair.appClient.verifyResult(ok(1));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 66 444\"));\n    }\n\n    @Test\n    public void addingNewDeviceToTheTilesPreservesTemplateValue() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", properties.getHttpPort());\n        hardClient2.start();\n        hardClient2.login(device.token);\n        hardClient2.verifyResult(ok(1));\n\n        clientPair.appClient.reset();\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = 21321;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(1));\n\n        DataStream dataStream = new DataStream((short) 66, PinType.VIRTUAL);\n        TileTemplate tileTemplate = new PageTileTemplate(1,\n                null, null, \"name\", \"name\", \"iconName\", BoardType.ESP8266, dataStream,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.createTemplate(1, deviceTiles.id, tileTemplate);\n        clientPair.appClient.verifyResult(ok(2));\n\n        ValueDisplay valueDisplay = new ValueDisplay();\n        valueDisplay.id = 1234;\n        valueDisplay.width = 2;\n        valueDisplay.height = 2;\n        valueDisplay.pin = 77;\n        valueDisplay.pinType = PinType.VIRTUAL;\n        valueDisplay.frequency = 0;\n        valueDisplay.deviceId = -1;\n\n        tileTemplate = new PageTileTemplate(1,\n                null, new int[] {0}, \"name\", \"name\", \"iconName\", BoardType.ESP8266, dataStream,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.createWidget(1, deviceTiles.id, tileTemplate.id, valueDisplay);\n        clientPair.appClient.verifyResult(ok(3));\n\n        clientPair.appClient.updateTemplate(1, deviceTiles.id, tileTemplate);\n        clientPair.appClient.verifyResult(ok(4));\n\n        clientPair.hardwareClient.send(\"hardware vw 77 444\");\n        clientPair.appClient.verifyResult(hardware(1, \"1-0 vw 77 444\"));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.sync(1, 0);\n        clientPair.appClient.verifyResult(ok(1));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 77 444\"));\n\n        tileTemplate = new PageTileTemplate(1,\n                null, new int[] {0, 1}, \"name\", \"name\", \"iconName\", BoardType.ESP8266, dataStream,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n        clientPair.appClient.updateTemplate(1, deviceTiles.id, tileTemplate);\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.sync(1, 0);\n        clientPair.appClient.verifyResult(ok(1));\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 77 444\"));\n    }\n\n    @Test\n    public void createTemplateWithTiles() throws Exception {\n        long widgetId = 21321;\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = widgetId;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(1));\n\n        int[] deviceIds = new int[] {0};\n        DataStream dataStream = new DataStream((short) 1, PinType.VIRTUAL);\n\n        TileTemplate tileTemplate = new PageTileTemplate(1,\n                null, deviceIds, \"name\", \"name\", \"iconName\", BoardType.ESP8266, dataStream,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.createTemplate(1, widgetId, tileTemplate);\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.appClient.send(\"getWidget 1\\0\" + widgetId);\n        deviceTiles = (DeviceTiles) JsonParser.parseWidget(clientPair.appClient.getBody(3), 0);\n        assertNotNull(deviceTiles);\n        assertEquals(widgetId, deviceTiles.id);\n        assertNotNull(deviceTiles.templates);\n        assertArrayEquals(new int[] {0}, deviceTiles.templates[0].deviceIds);\n        assertEquals(1, deviceTiles.templates[0].deviceIds.length);\n        assertEquals(\"name\", deviceTiles.templates[0].name);\n        assertEquals(1, deviceTiles.tiles.length);\n        assertEquals(0, deviceTiles.tiles[0].deviceId);\n        assertEquals(tileTemplate.id, deviceTiles.tiles[0].templateId);\n        assertNotNull(deviceTiles.tiles[0].dataStream);\n        assertEquals(1, deviceTiles.tiles[0].dataStream.pin);\n        assertEquals(PinType.VIRTUAL, deviceTiles.tiles[0].dataStream.pinType);\n    }\n\n    @Test\n    public void checkDeviceTilesWidgetSettingsUpdatedWithoutTemplateAndTilefields() throws Exception {\n        long widgetId = 21321;\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = widgetId;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(1));\n\n        int[] deviceIds = new int[] {0};\n        DataStream dataStream = new DataStream((short) 1, PinType.VIRTUAL);\n\n        TileTemplate tileTemplate = new PageTileTemplate(1,\n                null, deviceIds, \"name\", \"name\", \"iconName\", BoardType.ESP8266, dataStream,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.createTemplate(1, widgetId, tileTemplate);\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.appClient.send(\"getWidget 1\\0\" + widgetId);\n        deviceTiles = (DeviceTiles) JsonParser.parseWidget(clientPair.appClient.getBody(3), 0);\n        assertNotNull(deviceTiles);\n        assertEquals(widgetId, deviceTiles.id);\n        assertNotNull(deviceTiles.templates);\n        assertArrayEquals(new int[] {0}, deviceTiles.templates[0].deviceIds);\n        assertEquals(1, deviceTiles.templates[0].deviceIds.length);\n        assertEquals(\"name\", deviceTiles.templates[0].name);\n        assertEquals(1, deviceTiles.tiles.length);\n        assertEquals(0, deviceTiles.tiles[0].deviceId);\n        assertEquals(tileTemplate.id, deviceTiles.tiles[0].templateId);\n        assertNotNull(deviceTiles.tiles[0].dataStream);\n        assertEquals(1, deviceTiles.tiles[0].dataStream.pin);\n        assertEquals(PinType.VIRTUAL, deviceTiles.tiles[0].dataStream.pinType);\n\n        deviceTiles.templates = null;\n        deviceTiles.tiles = null;\n\n        clientPair.appClient.updateWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(4));\n\n        clientPair.appClient.send(\"getWidget 1\\0\" + widgetId);\n        deviceTiles = (DeviceTiles) JsonParser.parseWidget(clientPair.appClient.getBody(5), 0);\n        assertNotNull(deviceTiles);\n        assertEquals(widgetId, deviceTiles.id);\n        assertNotNull(deviceTiles.templates);\n        assertArrayEquals(new int[] {0}, deviceTiles.templates[0].deviceIds);\n        assertEquals(1, deviceTiles.templates[0].deviceIds.length);\n        assertEquals(\"name\", deviceTiles.templates[0].name);\n        assertEquals(1, deviceTiles.tiles.length);\n        assertEquals(0, deviceTiles.tiles[0].deviceId);\n        assertEquals(tileTemplate.id, deviceTiles.tiles[0].templateId);\n        assertNotNull(deviceTiles.tiles[0].dataStream);\n        assertEquals(1, deviceTiles.tiles[0].dataStream.pin);\n        assertEquals(PinType.VIRTUAL, deviceTiles.tiles[0].dataStream.pinType);\n    }\n\n    @Test\n    public void createTemplateWithTilesAndDelete() throws Exception {\n        long widgetId = 21321;\n        int templateId = 1;\n        createTemplateWithTiles();\n\n        clientPair.appClient.send(\"deleteTemplate \" + b(\"1 \" + widgetId + \" \" + templateId));\n        clientPair.appClient.verifyResult(ok(4));\n\n        clientPair.appClient.send(\"getWidget 1\\0\" + widgetId);\n        DeviceTiles deviceTiles = (DeviceTiles) JsonParser.parseWidget(clientPair.appClient.getBody(5), 0);\n        assertNotNull(deviceTiles);\n        assertEquals(widgetId, deviceTiles.id);\n        assertNotNull(deviceTiles.templates);\n        assertEquals(0, deviceTiles.templates.length);\n        assertEquals(0, deviceTiles.tiles.length);\n    }\n\n    @Test\n    public void deleteTemplate() throws Exception {\n        long widgetId = 21321;\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = widgetId;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        TileTemplate tileTemplate = new PageTileTemplate(1,\n                null, null, \"name\", \"name\", \"iconName\", BoardType.ESP8266, null,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n        deviceTiles.templates = new TileTemplate[] {\n                tileTemplate\n        };\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.send(\"getWidget 1\\0\" + widgetId);\n        deviceTiles = (DeviceTiles) JsonParser.parseWidget(clientPair.appClient.getBody(2), 0);\n        assertNotNull(deviceTiles);\n        assertEquals(widgetId, deviceTiles.id);\n        assertNotNull(deviceTiles.templates);\n        assertEquals(1, deviceTiles.templates.length);\n        assertEquals(\"name\", deviceTiles.templates[0].name);\n\n\n        clientPair.appClient.send(\"deleteTemplate \" + b(\"1 \" + widgetId + \" \" + tileTemplate.id));\n        clientPair.appClient.verifyResult(ok(3));\n\n        clientPair.appClient.send(\"getWidget 1\\0\" + widgetId);\n        deviceTiles = (DeviceTiles) JsonParser.parseWidget(clientPair.appClient.getBody(4), 0);\n        assertNotNull(deviceTiles);\n        assertEquals(widgetId, deviceTiles.id);\n        assertNotNull(deviceTiles.templates);\n        assertEquals(0, deviceTiles.templates.length);\n    }\n\n    @Test\n    public void updateTemplateCreateWithWidget() throws Exception {\n        long widgetId = 21321;\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = widgetId;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        TileTemplate tileTemplate = new PageTileTemplate(1,\n                null, null, \"name\", \"name\", \"iconName\", BoardType.ESP8266, new DataStream((short) -1, null),\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n        deviceTiles.templates = new TileTemplate[] {\n                tileTemplate\n        };\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(1));\n\n        tileTemplate = new PageTileTemplate(1,\n                null, new int[] {0}, \"name\", \"name\", \"iconName\", BoardType.ESP8266, new DataStream((short) 1, PinType.VIRTUAL),\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.updateTemplate(1, widgetId, tileTemplate);\n        clientPair.appClient.verifyResult(ok(2));\n\n\n        clientPair.appClient.send(\"getWidget 1\\0\" + widgetId);\n        deviceTiles = (DeviceTiles) JsonParser.parseWidget(clientPair.appClient.getBody(3), 0);\n        assertNotNull(deviceTiles);\n        assertEquals(widgetId, deviceTiles.id);\n        assertNotNull(deviceTiles.templates);\n        assertEquals(1, deviceTiles.templates.length);\n        assertArrayEquals(new int[] {0}, deviceTiles.templates[0].deviceIds);\n        assertEquals(\"name\", deviceTiles.templates[0].name);\n        assertEquals(1, deviceTiles.tiles.length);\n        assertEquals(0, deviceTiles.tiles[0].deviceId);\n        assertEquals(tileTemplate.id, deviceTiles.tiles[0].templateId);\n        assertNotNull(deviceTiles.tiles[0].dataStream);\n        assertEquals(1, deviceTiles.tiles[0].dataStream.pin);\n        assertEquals(PinType.VIRTUAL, deviceTiles.tiles[0].dataStream.pinType);\n    }\n\n    @Test\n    public void getSuperchartGraphWorksForTiles() throws Exception {\n        long widgetId = 21321;\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = widgetId;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(1));\n\n        int[] deviceIds = new int[] {0};\n\n        Superchart SuperchartGraph = new Superchart();\n        SuperchartGraph.id = 432;\n        SuperchartGraph.width = 8;\n        SuperchartGraph.height = 4;\n        GraphDataStream graphDataStream = new GraphDataStream(\n                null, GraphType.LINE, 0, 100_000,\n                new DataStream((short) 88, PinType.VIRTUAL),\n                AggregationFunctionType.MAX, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        SuperchartGraph.dataStreams = new GraphDataStream[] {\n                graphDataStream\n        };\n\n        TileTemplate tileTemplate = new PageTileTemplate(1,\n                new Widget[]{SuperchartGraph}, deviceIds, \"name\", \"name\", \"iconName\", BoardType.ESP8266, new DataStream((short) 1, PinType.VIRTUAL),\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.createTemplate(1, widgetId, tileTemplate);\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.appClient.send(\"getenhanceddata 1\" + b(\" 432 DAY\"));\n        clientPair.appClient.verifyResult(new ResponseMessage(3, NO_DATA));\n    }\n\n    @Test\n    public void getSuperchartGraphWorksForTiles2() throws Exception {\n        long widgetId = 21321;\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = widgetId;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(1));\n\n        int[] deviceIds = new int[] {0};\n\n        Superchart SuperchartGraph = new Superchart();\n        SuperchartGraph.id = 432;\n        SuperchartGraph.width = 8;\n        SuperchartGraph.height = 4;\n        GraphDataStream graphDataStream = new GraphDataStream(\n                null, GraphType.LINE, 0, 100_000,\n                new DataStream((short) 88, PinType.VIRTUAL),\n                AggregationFunctionType.MAX, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        SuperchartGraph.dataStreams = new GraphDataStream[] {\n                graphDataStream\n        };\n\n        TileTemplate tileTemplate = new PageTileTemplate(1,\n                new Widget[]{SuperchartGraph}, deviceIds, \"name\", \"name\", \"iconName\", BoardType.ESP8266, new DataStream((short)1, PinType.VIRTUAL),\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.createTemplate(1, widgetId, tileTemplate);\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.appClient.send(\"getenhanceddata 1-0\" + b(\" 432 DAY\"));\n        clientPair.appClient.verifyResult(new ResponseMessage(3, NO_DATA));\n    }\n\n    @Test\n    public void exportSuperchartGraphWorksForTiles() throws Exception {\n        long widgetId = 21321;\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = widgetId;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(1));\n\n        int[] deviceIds = new int[] {0};\n\n        Superchart SuperchartGraph = new Superchart();\n        SuperchartGraph.id = 432;\n        SuperchartGraph.width = 8;\n        SuperchartGraph.height = 4;\n        GraphDataStream graphDataStream = new GraphDataStream(\n                null, GraphType.LINE, 0, 0,\n                new DataStream((short) 88, PinType.VIRTUAL),\n                AggregationFunctionType.MAX, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        SuperchartGraph.dataStreams = new GraphDataStream[] {\n                graphDataStream\n        };\n\n        TileTemplate tileTemplate = new PageTileTemplate(1,\n                new Widget[]{SuperchartGraph}, deviceIds, \"name\", \"name\", \"iconName\", BoardType.ESP8266, new DataStream((short) 1, PinType.VIRTUAL),\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.createTemplate(1, widgetId, tileTemplate);\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.appClient.send(\"export 1 432\");\n        clientPair.appClient.verifyResult(new ResponseMessage(3, NO_DATA));\n\n        Path userReportDirectory = Paths.get(holder.props.getProperty(\"data.folder\"), \"data\", getUserName());\n        Files.createDirectories(userReportDirectory);\n        Path userReportFile = Paths.get(userReportDirectory.toString(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.VIRTUAL, (short) 88, GraphGranularityType.MINUTE));\n        FileUtils.write(userReportFile, 1.1, 1L);\n        FileUtils.write(userReportFile, 2.2, 2L);\n\n        clientPair.appClient.send(\"export 1 432\");\n        clientPair.appClient.verifyResult(ok(4));\n        verify(holder.mailWrapper, timeout(1000)).sendHtml(eq(getUserName()), eq(\"History graph data for project My Dashboard\"), contains(\"/\" + getUserName() + \"_1_0_v88_\"));\n\n        clientPair.appClient.send(\"deleteEnhancedData 1\\0\" + \"432\");\n        clientPair.appClient.verifyResult(ok(5));\n\n        clientPair.appClient.send(\"export 1 432\");\n        clientPair.appClient.verifyResult(new ResponseMessage(6, NO_DATA));\n    }\n\n    @Test\n    public void energyCalculationsAreCorrectWhenAddingRemovingWidgets() throws Exception {\n        long widgetId = 21321;\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = widgetId;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.send(\"getEnergy\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(2, GET_ENERGY, \"5800\")));\n\n        TileTemplate tileTemplate = new PageTileTemplate(1,\n                null, null, \"name\", \"name\", \"iconName\", BoardType.ESP8266, null,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.createTemplate(1, widgetId, tileTemplate);\n        clientPair.appClient.verifyResult(ok(3));\n\n        clientPair.appClient.createWidget(1, 21321, 1, \"{\\\"id\\\":100, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":2, \\\"y\\\":2, \\\"label\\\":\\\"Some Text 2\\\", \\\"type\\\":\\\"BUTTON\\\", \\\"pinType\\\":\\\"DIGITAL\\\", \\\"pin\\\":2}\");\n        clientPair.appClient.verifyResult(ok(4));\n\n        clientPair.appClient.send(\"getEnergy\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(5, GET_ENERGY, \"5600\")));\n\n        clientPair.appClient.createWidget(1, 21321, 1, \"{\\\"id\\\":101, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":2, \\\"y\\\":2, \\\"label\\\":\\\"Some Text 2\\\", \\\"type\\\":\\\"BUTTON\\\", \\\"pinType\\\":\\\"DIGITAL\\\", \\\"pin\\\":3}\");\n        clientPair.appClient.verifyResult(ok(6));\n\n        clientPair.appClient.send(\"getEnergy\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(7, GET_ENERGY, \"5400\")));\n\n        clientPair.appClient.deleteWidget(1, 101);\n        clientPair.appClient.verifyResult(ok(8));\n\n        clientPair.appClient.send(\"getEnergy\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(9, GET_ENERGY, \"5600\")));\n\n        clientPair.appClient.deleteWidget(1, 21321);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(ok(10)));\n\n        clientPair.appClient.send(\"getEnergy\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(11, GET_ENERGY, \"7500\")));\n    }\n\n    @Test\n    public void updateCommandWorksForWidgetWithinDeviceTiles() throws Exception {\n        long widgetId = 21321;\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = widgetId;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(1));\n\n        TileTemplate tileTemplate = new PageTileTemplate(1,\n                null, null, \"name\", \"name\", \"iconName\", BoardType.ESP8266, null,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.createTemplate(1, widgetId, tileTemplate);\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.appClient.createWidget(1, 21321, 1, \"{\\\"id\\\":100, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":2, \\\"y\\\":2, \\\"label\\\":\\\"Some Text 2\\\", \\\"type\\\":\\\"BUTTON\\\", \\\"pinType\\\":\\\"DIGITAL\\\", \\\"pin\\\":2}\");\n        clientPair.appClient.verifyResult(ok(3));\n\n        clientPair.appClient.createWidget(1, 21321, 1, \"{\\\"id\\\":100, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":2, \\\"y\\\":2, \\\"label\\\":\\\"Some Text 2\\\", \\\"type\\\":\\\"BUTTON\\\", \\\"pinType\\\":\\\"DIGITAL\\\", \\\"pin\\\":2}\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(notAllowed(4)));\n\n        clientPair.appClient.updateWidget(1, \"{\\\"id\\\":100, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":2, \\\"y\\\":2, \\\"label\\\":\\\"Some Text 3\\\", \\\"type\\\":\\\"BUTTON\\\", \\\"pinType\\\":\\\"DIGITAL\\\", \\\"pin\\\":3}\");\n        clientPair.appClient.verifyResult(ok(5));\n\n        clientPair.appClient.send(\"getWidget 1\\0\" + widgetId);\n        deviceTiles = (DeviceTiles) JsonParser.parseWidget(clientPair.appClient.getBody(6), 0);\n        assertNotNull(deviceTiles);\n        assertEquals(widgetId, deviceTiles.id);\n        assertNotNull(deviceTiles.templates);\n        assertEquals(1, deviceTiles.templates.length);\n        assertEquals(\"name\", deviceTiles.templates[0].name);\n        assertNotNull(deviceTiles.templates[0].widgets[0]);\n        assertEquals(\"Some Text 3\", deviceTiles.templates[0].widgets[0].label);\n    }\n\n    @Test\n    public void testHugeWidgetIsCreatedWithinDeviceTiles() throws Exception {\n        long widgetId = 21321;\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = widgetId;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(1));\n\n        TileTemplate tileTemplate = new PageTileTemplate(1,\n                null, null, \"name\", \"name\", \"iconName\", BoardType.ESP8266, null,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.createTemplate(1, widgetId, tileTemplate);\n        clientPair.appClient.verifyResult(ok(2));\n\n        Menu menu = new Menu();\n        menu.id = 172652;\n        menu.x = 2;\n        menu.y = 34;\n        menu.color = 600084223;\n        menu.width = 6;\n        menu.height = 1;\n        menu.label = \"Set Volume\";\n        menu.deviceId = 252521;\n        menu.labels = new String[] {\"Item1\", \"Item2\"};\n\n        clientPair.appClient.createWidget(1, 21321, 1, menu);\n        clientPair.appClient.verifyResult(ok(3));\n\n        List<String> list = new ArrayList<>();\n        for (float i = 0; i < 49.99; i += 0.1F) {\n            list.add(String.format(\"%.2f\", i));\n        }\n        menu.labels = list.toArray(new String[0]);\n\n        clientPair.appClient.updateWidget(1, JsonParser.MAPPER.writeValueAsString(menu));\n        clientPair.appClient.verifyResult(ok(4));\n\n        clientPair.appClient.send(\"getWidget 1\\0\" + widgetId);\n        deviceTiles = (DeviceTiles) JsonParser.parseWidget(clientPair.appClient.getBody(5), 0);\n        assertNotNull(deviceTiles);\n        assertEquals(widgetId, deviceTiles.id);\n        assertNotNull(deviceTiles.templates);\n        assertEquals(1, deviceTiles.templates.length);\n        assertEquals(\"name\", deviceTiles.templates[0].name);\n        assertNotNull(deviceTiles.templates[0].widgets[0]);\n        assertEquals(500, ((Menu) deviceTiles.templates[0].widgets[0]).labels.length);\n    }\n\n    @Test\n    public void createpageTempalteWithIdAndSendEmail() throws Exception {\n        long widgetId = 21321;\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = widgetId;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(1));\n\n        long templateId = 1;\n        clientPair.appClient.send(\"createTemplate \" + b(\"1 \" + widgetId + \" \")\n                + \"{\\\"id\\\":\" + templateId + \",\\\"templateId\\\":\\\"TMPL123\\\",\\\"name\\\":\\\"My New Template\\\",\\\"iconName\\\":\\\"iconName\\\",\\\"boardType\\\":\\\"ESP8266\\\",\\\"showDeviceName\\\":false,\\\"color\\\":0,\\\"tileColor\\\":0,\\\"fontSize\\\":\\\"LARGE\\\",\\\"showTileLabel\\\":false,\\\"pin\\\":{\\\"pin\\\":1,\\\"pwmMode\\\":false,\\\"rangeMappingOn\\\":false,\\\"pinType\\\":\\\"VIRTUAL\\\",\\\"min\\\":0.0,\\\"max\\\":255.0}}\");\n        clientPair.appClient.verifyResult(ok(2));\n\n\n        clientPair.appClient.send(\"email template 1 \" + widgetId + \" \" + templateId);\n\n        String expectedBody = \"Template ID for {template_name} is: {template_id}.<br>\\n\" +\n                \"<br>\\n\" +\n                \"This ID should be added in <a href=\\\"https://github.com/blynkkk/blynk-library/blob/master/examples/Export_Demo/Template_ESP32/Settings.h\\\">Settings.h</a>. Simply change this line\\n\" +\n                \"<br>\\n\" +\n                \"<p>\\n\" +\n                \"    <i>\\n\" +\n                \"#define BOARD_TEMPLATE_ID             \\\"{template_id}\\\" // ID of the Tile Template. Can be found in Tile Template Settings\\n\" +\n                \"    </i>\\n\" +\n                \"</p>\\n\" +\n                \"Template ID is used during device provisioning process and defines which template will be assigned to the device of this particular type.\\n\" +\n                \"<br>\\n\" +\n                \"<br>\\n\" +\n                \"--<br>\\n\" +\n                \"<br>\\n\" +\n                \"Blynk Team<br>\\n\" +\n                \"<br>\\n\" +\n                \"<a href=\\\"https://www.blynk.io\\\">blynk.io</a>\\n\" +\n                \"<br>\\n\" +\n                \"<a href=\\\"https://www.blynk.cc\\\">blynk.cc</a>\\n\";\n\n        expectedBody = expectedBody\n                        .replace(\"{template_name}\", \"My New Template\")\n                        .replace(\"{template_id}\", \"TMPL123\");\n\n        verify(holder.mailWrapper, timeout(1000)).sendHtml(eq(getUserName()), eq(\"Template ID for My New Template\"), eq(expectedBody));\n\n    }\n\n    @Test\n    public void testAddAndRemoveTabs() throws Exception {\n        long widgetId = 21321;\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = widgetId;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(1));\n\n        TileTemplate tileTemplate = new PageTileTemplate(1,\n                null, null, \"name\", \"name\", \"iconName\", BoardType.ESP8266, null,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.createTemplate(1, widgetId, tileTemplate);\n        clientPair.appClient.verifyResult(ok(2));\n\n        Tabs tabs = new Tabs();\n        tabs.id = 172649;\n        tabs.width = 10;\n        tabs.height = 1;\n        tabs.tabs = new Tab[] {\n                new Tab(0, \"0\"),\n                new Tab(1, \"1\")\n        };\n\n        clientPair.appClient.createWidget(1, 21321, 1, tabs);\n        clientPair.appClient.verifyResult(ok(3));\n\n        Menu menu = new Menu();\n        menu.id = 172650;\n        menu.x = 2;\n        menu.y = 34;\n        menu.width = 6;\n        menu.height = 1;\n        menu.label = \"Set Volume\";\n        menu.deviceId = 252521;\n        menu.tabId = 0;\n\n        clientPair.appClient.createWidget(1, 21321, 1, menu);\n        clientPair.appClient.verifyResult(ok(4));\n\n        menu = new Menu();\n        menu.id = 172651;\n        menu.x = 2;\n        menu.y = 34;\n        menu.width = 6;\n        menu.height = 1;\n        menu.label = \"Set Volume\";\n        menu.deviceId = 252521;\n        menu.tabId = 1;\n\n        clientPair.appClient.createWidget(1, 21321, 1, menu);\n        clientPair.appClient.verifyResult(ok(5));\n\n        Tabs tabs2 = new Tabs();\n        tabs2.id = 172648;\n        tabs2.width = 10;\n        tabs2.height = 1;\n        tabs2.tabs = new Tab[] {\n                new Tab(0, \"0\"),\n                new Tab(1, \"1\")\n        };\n\n        clientPair.appClient.createWidget(1, tabs2);\n        clientPair.appClient.verifyResult(ok(6));\n\n        menu = new Menu();\n        menu.id = 172652;\n        menu.x = 2;\n        menu.y = 34;\n        menu.width = 6;\n        menu.height = 1;\n        menu.label = \"Set Volume\";\n        menu.deviceId = 252521;\n        menu.tabId = 0;\n\n        clientPair.appClient.createWidget(1, menu);\n        clientPair.appClient.verifyResult(ok(7));\n\n        Menu menu2 = new Menu();\n        menu2.id = 172653;\n        menu2.x = 2;\n        menu2.y = 34;\n        menu2.width = 6;\n        menu2.height = 1;\n        menu2.label = \"Set Volume\";\n        menu2.deviceId = 252521;\n        menu2.tabId = 1;\n\n        clientPair.appClient.createWidget(1, menu2);\n        clientPair.appClient.verifyResult(ok(8));\n\n        clientPair.appClient.deleteWidget(1, tabs.id);\n        clientPair.appClient.verifyResult(ok(9));\n\n        clientPair.appClient.send(\"loadProfileGzipped 1\");\n        DashBoard dashBoard = clientPair.appClient.parseDash(10);\n        assertNotNull(dashBoard);\n        Tabs dashTabs = dashBoard.getWidgetByType(Tabs.class);\n        assertNotNull(dashTabs);\n        assertEquals(2, dashTabs.tabs.length);\n        assertNotNull(dashBoard.getWidgetById(menu.id));\n        assertNotNull(dashBoard.getWidgetById(menu2.id));\n        DeviceTiles deviceTiles1 = dashBoard.getWidgetByType(DeviceTiles.class);\n        assertNotNull(deviceTiles1);\n        assertNull(deviceTiles1.getWidgetById(tabs.id));\n        assertEquals(1, deviceTiles1.templates[0].widgets.length);\n        assertEquals(0, deviceTiles1.templates[0].getWidgetIndexByIdOrThrow(172650));\n        assertTrue(deviceTiles1.templates[0].widgets[0] instanceof Menu);\n    }\n\n    @Test\n    public void testAddAndRemoveTabs2() throws Exception {\n        Tabs tabs = new Tabs();\n        tabs.id = 172648;\n        tabs.width = 10;\n        tabs.height = 1;\n        tabs.tabs = new Tab[] {\n                new Tab(0, \"0\"),\n                new Tab(1, \"1\")\n        };\n\n        clientPair.appClient.createWidget(1, tabs);\n        clientPair.appClient.verifyResult(ok(1));\n\n        long widgetId = 21321;\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = widgetId;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(2));\n\n        Button button = new Button();\n        button.id = 172649;\n        button.x = 2;\n        button.y = 34;\n        button.width = 6;\n        button.height = 1;\n        button.label = \"Set Volume\";\n        button.deviceId = 0;\n        button.tabId = 0;\n\n        clientPair.appClient.createWidget(1, button);\n        clientPair.appClient.verifyResult(ok(3));\n\n        Button button2 = new Button();\n        button2.id = 172650;\n        button2.x = 2;\n        button2.y = 34;\n        button2.width = 6;\n        button2.height = 1;\n        button2.label = \"Set Volume\";\n        button2.deviceId = 0;\n        button2.tabId = 1;\n\n        clientPair.appClient.createWidget(1, button2);\n        clientPair.appClient.verifyResult(ok(4));\n\n        ButtonTileTemplate tileTemplate = new ButtonTileTemplate(1,\n                null, null, \"name\", \"name\", \"iconName\", BoardType.ESP8266, new DataStream((short) 1, PinType.VIRTUAL),\n                false, false, false, null, null);\n\n        clientPair.appClient.createTemplate(1, widgetId, tileTemplate);\n        clientPair.appClient.verifyResult(ok(5));\n\n        tileTemplate = new ButtonTileTemplate(1,\n                null, new int[] {0}, \"name\", \"name\", \"iconName\", BoardType.ESP8266, new DataStream((short) 1, PinType.VIRTUAL),\n                false, false, false, null, null);\n\n        clientPair.appClient.updateTemplate(1, widgetId, tileTemplate);\n        clientPair.appClient.verifyResult(ok(6));\n\n        Tabs tabs2 = new Tabs();\n        tabs2.id = 172651;\n        tabs2.width = 10;\n        tabs2.height = 1;\n        tabs2.tabs = new Tab[] {\n                new Tab(0, \"0\"),\n                new Tab(1, \"1\")\n        };\n\n        clientPair.appClient.createWidget(1, 21321, 1, tabs2);\n        clientPair.appClient.verifyResult(ok(7));\n\n        Button button3 = new Button();\n        button3.id = 172652;\n        button3.x = 2;\n        button3.y = 34;\n        button3.width = 6;\n        button3.height = 1;\n        button3.label = \"Set Volume\";\n        button3.deviceId = 0;\n        button3.tabId = 0;\n\n        clientPair.appClient.createWidget(1, 21321, 1, button3);\n        clientPair.appClient.verifyResult(ok(8));\n\n        Button button4 = new Button();\n        button4.id = 172653;\n        button4.x = 2;\n        button4.y = 34;\n        button4.width = 6;\n        button4.height = 1;\n        button4.label = \"Set Volume\";\n        button4.deviceId = 0;\n        button4.tabId = 1;\n\n        clientPair.appClient.createWidget(1, 21321, 1, button4);\n        clientPair.appClient.verifyResult(ok(9));\n\n        clientPair.appClient.deleteWidget(1, tabs2.id);\n        clientPair.appClient.verifyResult(ok(10));\n\n        clientPair.appClient.send(\"loadProfileGzipped 1\");\n        DashBoard dashBoard = clientPair.appClient.parseDash(11);\n        assertNotNull(dashBoard);\n        Tabs searchTabs = (Tabs) dashBoard.getWidgetById(tabs.id);\n        assertNotNull(searchTabs);\n        assertNotNull(dashBoard.getWidgetById(button.id));\n        assertNotNull(dashBoard.getWidgetById(button2.id));\n    }\n\n    @Test\n    public void testAddAndUpdateTabs() throws Exception {\n        long widgetId = 21321;\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = widgetId;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(1));\n\n        TileTemplate tileTemplate = new PageTileTemplate(1,\n                null, null, \"name\", \"name\", \"iconName\", BoardType.ESP8266, null,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.createTemplate(1, widgetId, tileTemplate);\n        clientPair.appClient.verifyResult(ok(2));\n\n        Tabs tabs = new Tabs();\n        tabs.id = 172649;\n        tabs.width = 10;\n        tabs.height = 1;\n        tabs.tabs = new Tab[] {\n                new Tab(0, \"0\"),\n                new Tab(1, \"1\")\n        };\n\n        clientPair.appClient.createWidget(1, 21321, 1, tabs);\n        clientPair.appClient.verifyResult(ok(3));\n\n        Menu menu = new Menu();\n        menu.id = 172650;\n        menu.x = 2;\n        menu.y = 34;\n        menu.width = 6;\n        menu.height = 1;\n        menu.label = \"Set Volume\";\n        menu.deviceId = 252521;\n        menu.tabId = 0;\n\n        clientPair.appClient.createWidget(1, 21321, 1, menu);\n        clientPair.appClient.verifyResult(ok(4));\n\n        menu = new Menu();\n        menu.id = 172651;\n        menu.x = 2;\n        menu.y = 34;\n        menu.width = 6;\n        menu.height = 1;\n        menu.label = \"Set Volume\";\n        menu.deviceId = 252521;\n        menu.tabId = 1;\n\n        clientPair.appClient.createWidget(1, 21321, 1, menu);\n        clientPair.appClient.verifyResult(ok(5));\n\n        Tabs tabs2 = new Tabs();\n        tabs2.id = 172648;\n        tabs2.width = 10;\n        tabs2.height = 1;\n        tabs2.tabs = new Tab[] {\n                new Tab(0, \"0\"),\n                new Tab(1, \"1\")\n        };\n\n        clientPair.appClient.createWidget(1, tabs2);\n        clientPair.appClient.verifyResult(ok(6));\n\n        menu = new Menu();\n        menu.id = 172652;\n        menu.x = 2;\n        menu.y = 34;\n        menu.width = 6;\n        menu.height = 1;\n        menu.label = \"Set Volume\";\n        menu.deviceId = 252521;\n        menu.tabId = 0;\n\n        clientPair.appClient.createWidget(1, menu);\n        clientPair.appClient.verifyResult(ok(7));\n\n        Menu menu2 = new Menu();\n        menu2.id = 172653;\n        menu2.x = 2;\n        menu2.y = 34;\n        menu2.width = 6;\n        menu2.height = 1;\n        menu2.label = \"Set Volume\";\n        menu2.deviceId = 252521;\n        menu2.tabId = 1;\n\n        clientPair.appClient.createWidget(1, menu2);\n        clientPair.appClient.verifyResult(ok(8));\n\n        tabs.tabs = new Tab[] {\n                new Tab(0, \"0\")\n        };\n\n        clientPair.appClient.updateWidget(1, tabs);\n        clientPair.appClient.verifyResult(ok(9));\n\n        clientPair.appClient.send(\"loadProfileGzipped 1\");\n        DashBoard dashBoard = clientPair.appClient.parseDash(10);\n        assertNotNull(dashBoard);\n        Tabs dashTabs = dashBoard.getWidgetByType(Tabs.class);\n        assertNotNull(dashTabs);\n        assertEquals(2, dashTabs.tabs.length);\n        assertNotNull(dashBoard.getWidgetById(menu.id));\n        assertNotNull(dashBoard.getWidgetById(menu2.id));\n        DeviceTiles deviceTiles1 = dashBoard.getWidgetByType(DeviceTiles.class);\n        assertNotNull(deviceTiles1.getWidgetById(tabs.id));\n        assertTrue(deviceTiles1.getWidgetById(tabs.id) instanceof Tabs);\n        assertEquals(2, deviceTiles1.templates[0].widgets.length);\n        int menuWidgetIndex = deviceTiles1.templates[0].getWidgetIndexByIdOrThrow(172650);\n        assertEquals(1, menuWidgetIndex);\n        assertTrue(deviceTiles1.templates[0].widgets[menuWidgetIndex] instanceof Menu);\n    }\n\n    @Test\n    public void testGetPinViaHttpApiWorksForDeviceTiles() throws Exception {\n        long widgetId = 21321;\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = widgetId;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(1));\n\n        TileTemplate tileTemplate = new ButtonTileTemplate(1,\n                null, new int[] {0}, \"name\", \"name\", \"iconName\", BoardType.ESP8266, new DataStream((short) 111, PinType.VIRTUAL),\n                false, false, false, null, null);\n\n        clientPair.appClient.createTemplate(1, widgetId, tileTemplate);\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.appClient.send(\"getDevices 1\");\n        Device[] devices = clientPair.appClient.parseDevices(3);\n        Device device = devices[0];\n        assertEquals(0, device.id);\n\n        clientPair.appClient.send(\"hardware 1-0 vw 111 1\");\n\n        AsyncHttpClient httpclient = new DefaultAsyncHttpClient(\n                new DefaultAsyncHttpClientConfig.Builder()\n                        .setUserAgent(null)\n                        .setKeepAlive(true)\n                        .build()\n        );\n\n        String httpsServerUrl = String.format(\"http://localhost:%s/\", properties.getHttpPort());\n        Future<Response> f = httpclient.prepareGet(httpsServerUrl + device.token + \"/get/v111\").execute();\n        Response response = f.get();\n        assertEquals(200, response.getStatusCode());\n        assertEquals(\"1\", response.getResponseBody());\n        httpclient.close();\n    }\n\n    @Test\n    public void testDeviceTileIsUpdatedFromHardware() throws Exception {\n        long widgetId = 21321;\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = widgetId;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(1));\n\n        TileTemplate tileTemplate = new ButtonTileTemplate(1,\n                null, new int[] {0}, \"name\", \"name\", \"iconName\", BoardType.ESP8266, new DataStream((short) 111, PinType.VIRTUAL),\n                false, false, false, null, null);\n\n        clientPair.appClient.createTemplate(1, widgetId, tileTemplate);\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.hardwareClient.send(\"hardware vw 111 1\");\n        clientPair.appClient.verifyResult(hardware(1, \"1-0 vw 111 1\"));\n\n        clientPair.appClient.send(\"loadProfileGzipped 1\");\n        DashBoard dashBoard = clientPair.appClient.parseDash(4);\n        assertNotNull(dashBoard);\n        deviceTiles = dashBoard.getWidgetByType(DeviceTiles.class);\n        assertNotNull(deviceTiles);\n        assertEquals(1, deviceTiles.tiles.length);\n        assertEquals(\"1\", deviceTiles.tiles[0].dataStream.value);\n    }\n\n    @Test\n    public void testDeviceTileAndWidgetWithinTemplateHasSamePin() throws Exception {\n        long widgetId = 21321;\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = widgetId;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(1));\n\n        DataStream dataStream = new DataStream((short) 5, PinType.VIRTUAL);\n        TileTemplate tileTemplate = new PageTileTemplate(1,\n                null, new int[] {0}, \"name\", \"name\", \"iconName\", BoardType.ESP8266, dataStream,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.createTemplate(1, widgetId, tileTemplate);\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.hardwareClient.send(\"hardware vw 5 111\");\n        clientPair.appClient.verifyResult(hardware(1, \"1-0 vw 5 111\"));\n\n        ValueDisplay valueDisplay = new ValueDisplay();\n        valueDisplay.width = 2;\n        valueDisplay.height = 2;\n        valueDisplay.pin = dataStream.pin;\n        valueDisplay.pinType = dataStream.pinType;\n\n        clientPair.appClient.createWidget(1, deviceTiles.id, tileTemplate.id, valueDisplay);\n        clientPair.appClient.verifyResult(ok(3));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.sync(1, 0);\n\n        verify(clientPair.appClient.responseMock, timeout(500).times(12)).channelRead(any(), any());\n\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 1 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 2 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 3 0\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 5 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 4 244\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 7 3\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 30 3\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 0 89.888037459418\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 11 -58.74774244674501\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 13 60 143 158\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 5 111\")));\n\n        clientPair.hardwareClient.send(\"hardware vw 5 112\");\n        clientPair.appClient.verifyResult(hardware(2, \"1-0 vw 5 112\"));\n\n\n        clientPair.appClient.reset();\n        clientPair.appClient.sync(1, 0);\n\n        verify(clientPair.appClient.responseMock, timeout(500).times(12)).channelRead(any(), any());\n\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 1 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 2 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 3 0\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 5 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 4 244\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 7 3\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 30 3\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 0 89.888037459418\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 11 -58.74774244674501\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 13 60 143 158\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 5 112\")));\n\n        clientPair.hardwareClient.sync(PinType.VIRTUAL, 5);\n        clientPair.hardwareClient.verifyResult(produce(3, HARDWARE, b(\"vw 5 112\")));\n    }\n\n    @Test\n    public void testDeviceTileAndWidgetWithinTemplateHasSamePinAndUpdateFromApp() throws Exception {\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = 21321;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(1));\n\n        DataStream dataStream = new DataStream((short) 5, PinType.VIRTUAL);\n        TileTemplate tileTemplate = new PageTileTemplate(1,\n                null, new int[] {0}, \"name\", \"name\", \"iconName\", BoardType.ESP8266, dataStream,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.createTemplate(1, deviceTiles.id, tileTemplate);\n        clientPair.appClient.verifyResult(ok(2));\n\n        NumberInput numberInput = new NumberInput();\n        numberInput.width = 2;\n        numberInput.height = 2;\n        numberInput.pin = dataStream.pin;\n        numberInput.pinType = dataStream.pinType;\n\n        clientPair.appClient.createWidget(1, deviceTiles.id, tileTemplate.id, numberInput);\n        clientPair.appClient.verifyResult(ok(3));\n\n        clientPair.appClient.send(\"hardware 1-0 vw 5 111\");\n        clientPair.hardwareClient.verifyResult(hardware(4, \"vw 5 111\"));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.sync(1);\n\n        verify(clientPair.appClient.responseMock, timeout(500).times(12)).channelRead(any(), any());\n\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 1 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 2 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 3 0\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 5 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 4 244\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 7 3\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 30 3\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 0 89.888037459418\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 11 -58.74774244674501\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 13 60 143 158\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 5 111\")));\n    }\n\n    @Test\n    public void testDeviceTileAndWidgetWithinTemplateHasSamePin2() throws Exception {\n        long widgetId = 21321;\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = widgetId;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(1));\n\n        TileTemplate tileTemplate = new PageTileTemplate(1,\n                null, new int[] {0}, \"name\", \"name\", \"iconName\", BoardType.ESP8266, null,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.createTemplate(1, widgetId, tileTemplate);\n        clientPair.appClient.verifyResult(ok(2));\n\n        DataStream dataStream = new DataStream((short) 5, PinType.VIRTUAL);\n\n        ValueDisplay valueDisplay = new ValueDisplay();\n        valueDisplay.width = 2;\n        valueDisplay.height = 2;\n        valueDisplay.pin = dataStream.pin;\n        valueDisplay.pinType = dataStream.pinType;\n\n        clientPair.appClient.createWidget(1, deviceTiles.id, 1, valueDisplay);\n        clientPair.appClient.verifyResult(ok(3));\n\n        //send value after we have tile for that pin\n        clientPair.hardwareClient.send(\"hardware vw 5 111\");\n        clientPair.appClient.verifyResult(hardware(1, \"1-0 vw 5 111\"));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.sync(1, 0);\n\n        verify(clientPair.appClient.responseMock, timeout(500).times(12)).channelRead(any(), any());\n\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 1 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 2 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 3 0\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 5 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 4 244\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 7 3\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 30 3\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 0 89.888037459418\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 11 -58.74774244674501\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 13 60 143 158\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 5 111\")));\n\n        clientPair.hardwareClient.send(\"hardware vw 5 112\");\n        clientPair.appClient.verifyResult(hardware(2, \"1-0 vw 5 112\"));\n\n\n        clientPair.appClient.reset();\n        clientPair.appClient.sync(1, 0);\n\n        verify(clientPair.appClient.responseMock, timeout(500).times(12)).channelRead(any(), any());\n\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 1 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 2 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 3 0\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 5 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 4 244\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 7 3\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 30 3\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 0 89.888037459418\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 11 -58.74774244674501\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 13 60 143 158\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 5 112\")));\n    }\n\n    @Test\n    public void testDeviceTileAndWidgetWithMultipleValues() throws Exception {\n        var deviceTiles = new DeviceTiles();\n        deviceTiles.id = 21321;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(1));\n\n        var tileTemplate = new PageTileTemplate(1,\n                null, new int[] {0}, \"name\", \"name\", \"iconName\", BoardType.ESP8266, new DataStream((short) 5, PinType.VIRTUAL),\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.createTemplate(1, deviceTiles.id, tileTemplate);\n        clientPair.appClient.verifyResult(ok(2));\n\n        var terminal = new Terminal();\n        terminal.width = 2;\n        terminal.height = 2;\n        terminal.pin = 6;\n        terminal.pinType = PinType.VIRTUAL;\n\n        clientPair.appClient.createWidget(1, deviceTiles.id, 1, terminal);\n        clientPair.appClient.verifyResult(ok(3));\n\n        //send value after we have tile for that pin\n        clientPair.hardwareClient.send(\"hardware vw 6 111\");\n        clientPair.hardwareClient.send(\"hardware vw 6 112\");\n        clientPair.appClient.verifyResult(hardware(1, \"1-0 vw 6 111\"));\n        clientPair.appClient.verifyResult(hardware(2, \"1-0 vw 6 112\"));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.sync(1, 0);\n\n        verify(clientPair.appClient.responseMock, timeout(500).times(11 + 2)).channelRead(any(), any());\n\n        clientPair.appClient.verifyResult(ok(1));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 1 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 2 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 3 0\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 5 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 4 244\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 7 3\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 30 3\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 0 89.888037459418\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 11 -58.74774244674501\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 13 60 143 158\")));\n\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vm 6 111 112\")));\n    }\n\n    @Test\n    public void updateViaHttpAPIWorksForDeviceTiles() throws Exception {\n        long widgetId = 21321;\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = widgetId;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(1));\n\n        DataStream dataStream = new DataStream((short) 5, PinType.VIRTUAL);\n        TileTemplate tileTemplate = new PageTileTemplate(1,\n                null, new int[] {0}, \"name\", \"name\", \"iconName\", BoardType.ESP8266, dataStream,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.createTemplate(1, widgetId, tileTemplate);\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.appClient.send(\"getDevices 1\");\n        Device[] devices = clientPair.appClient.parseDevices(3);\n        Device device = devices[0];\n        assertNotNull(device);\n        assertNotNull(device.token);\n\n        AsyncHttpClient httpclient = new DefaultAsyncHttpClient(\n                new DefaultAsyncHttpClientConfig.Builder()\n                        .setUserAgent(null)\n                        .setKeepAlive(true)\n                        .build()\n        );\n\n        String httpsServerUrl = String.format(\"http://localhost:%s/\", properties.getHttpPort());\n        Future<Response> f = httpclient\n                .prepareGet(httpsServerUrl + device.token + \"/update/v5?value=111\")\n                .execute();\n        Response response = f.get();\n        assertEquals(200, response.getStatusCode());\n\n        clientPair.appClient.verifyResult(hardware(111, \"1-0 vw 5 111\"));\n        httpclient.close();\n    }\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/tcp/DeviceWorkflowTest.java",
    "content": "package cc.blynk.integration.tcp;\n\nimport cc.blynk.integration.SingleServerInstancePerTest;\nimport cc.blynk.integration.model.tcp.TestHardClient;\nimport cc.blynk.server.core.dao.ReportingDiskDao;\nimport cc.blynk.server.core.dao.TemporaryTokenValue;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.device.BoardType;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.device.Status;\nimport cc.blynk.server.core.model.device.Tag;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.widgets.controls.Terminal;\nimport cc.blynk.server.core.model.widgets.outputs.ValueDisplay;\nimport cc.blynk.server.core.model.widgets.outputs.graph.FontSize;\nimport cc.blynk.server.core.model.widgets.outputs.graph.GraphGranularityType;\nimport cc.blynk.server.core.model.widgets.ui.tiles.DeviceTiles;\nimport cc.blynk.server.core.model.widgets.ui.tiles.templates.PageTileTemplate;\nimport cc.blynk.server.core.protocol.model.messages.ResponseMessage;\nimport cc.blynk.server.core.protocol.model.messages.common.HardwareMessage;\nimport cc.blynk.server.notifications.push.android.AndroidGCMMessage;\nimport cc.blynk.server.notifications.push.enums.Priority;\nimport cc.blynk.utils.FileUtils;\nimport org.junit.BeforeClass;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.ArgumentCaptor;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\n\nimport static cc.blynk.integration.TestUtil.appSync;\nimport static cc.blynk.integration.TestUtil.b;\nimport static cc.blynk.integration.TestUtil.createDevice;\nimport static cc.blynk.integration.TestUtil.createTag;\nimport static cc.blynk.integration.TestUtil.deviceOffline;\nimport static cc.blynk.integration.TestUtil.hardware;\nimport static cc.blynk.integration.TestUtil.hardwareConnected;\nimport static cc.blynk.integration.TestUtil.ok;\nimport static cc.blynk.integration.TestUtil.sleep;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE;\nimport static cc.blynk.server.core.protocol.enums.Response.DEVICE_NOT_IN_NETWORK;\nimport static cc.blynk.server.core.protocol.model.messages.MessageFactory.produce;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertFalse;\nimport static org.junit.Assert.assertNotNull;\nimport static org.junit.Assert.assertTrue;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.eq;\nimport static org.mockito.Mockito.timeout;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/2/2015.\n *\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class DeviceWorkflowTest extends SingleServerInstancePerTest {\n\n    private static int tcpHardPort;\n\n    @BeforeClass\n    public static void initPort() {\n        tcpHardPort = properties.getHttpPort();\n    }\n\n    @Test\n    public void testSendHardwareCommandToMultipleDevices() throws Exception {\n        Device device0 = new Device(0, \"My Dashboard\", BoardType.Arduino_UNO);\n        device0.status = Status.ONLINE;\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        clientPair.appClient.reset();\n\n        clientPair.appClient.send(\"getDevices 1\");\n        Device[] devices = clientPair.appClient.parseDevices();\n        assertNotNull(devices);\n        assertEquals(2, devices.length);\n\n        assertEqualDevice(device0, devices[0]);\n        assertEqualDevice(device1, devices[1]);\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient2.start();\n\n        hardClient2.login(devices[1].token);\n        hardClient2.verifyResult(ok(1));\n        device1.status = Status.ONLINE;\n\n        clientPair.appClient.send(\"hardware 1 vw 100 100\");\n        clientPair.hardwareClient.verifyResult(hardware(2, \"vw 100 100\"));\n        hardClient2.never(hardware(2, \"vw 1 100\"));\n\n        clientPair.appClient.send(\"hardware 1-0 vw 100 101\");\n        clientPair.hardwareClient.verifyResult(hardware(3, \"vw 100 101\"));\n        hardClient2.never(hardware(3, \"vw 1 101\"));\n\n        clientPair.appClient.send(\"hardware 1-1 vw 100 102\");\n        hardClient2.verifyResult(hardware(4, \"vw 100 102\"));\n        clientPair.hardwareClient.never(hardware(4, \"vw 100 102\"));\n    }\n\n    @Test\n    public void testDeviceWentOfflineMessage() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient2.start();\n\n        hardClient2.login(device.token);\n        hardClient2.verifyResult(ok(1));\n\n        hardClient2.stop().await();\n\n        ArgumentCaptor<AndroidGCMMessage> objectArgumentCaptor = ArgumentCaptor.forClass(AndroidGCMMessage.class);\n        verify(holder.gcmWrapper, timeout(500).times(1)).send(objectArgumentCaptor.capture(), any(), any());\n        AndroidGCMMessage message = objectArgumentCaptor.getValue();\n\n        String expectedJson = new AndroidGCMMessage(\"token\", Priority.normal, \"Your My Device went offline.\", 1).toJson();\n        assertEquals(expectedJson, message.toJson());\n    }\n\n    @Test\n    public void testSendHardwareCommandToAppFromMultipleDevices() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        clientPair.appClient.reset();\n\n        clientPair.appClient.send(\"getDevices 1\");\n        Device[] devices = clientPair.appClient.parseDevices();\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient2.start();\n\n        hardClient2.login(devices[1].token);\n        hardClient2.verifyResult(ok(1));\n        clientPair.appClient.verifyResult(hardwareConnected(1, \"1-1\"));\n\n        clientPair.hardwareClient.send(\"hardware vw 100 101\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new HardwareMessage(1, b(\"1-0 vw 100 101\"))));\n\n        hardClient2.send(\"hardware vw 100 100\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new HardwareMessage(2, b(\"1-1 vw 100 100\"))));\n    }\n\n    @Test\n    public void testSendDeviceSpecificPMMessage() throws Exception {\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":188, \\\"width\\\":1, \\\"height\\\":1, \\\"deviceId\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"BUTTON\\\", \\\"pinType\\\":\\\"DIGITAL\\\", \\\"pin\\\":1}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice(2);\n        assertNotNull(device);\n        assertNotNull(device.token);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(createDevice(2, device)));\n\n        TestHardClient hardClient = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient.start();\n\n        hardClient.login(device.token);\n        hardClient.verifyResult(ok(1));\n        clientPair.appClient.verifyResult(hardwareConnected(1, \"1-1\"));\n\n        String expectedBody = \"pm 1 out\";\n        verify(hardClient.responseMock, timeout(500)).channelRead(any(), eq(produce(1, HARDWARE, b(expectedBody))));\n        verify(hardClient.responseMock, times(2)).channelRead(any(), any());\n        hardClient.stop().awaitUninterruptibly();\n    }\n\n    @Test\n    public void testSendPMOnActivateForMultiDevices() throws Exception {\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":188, \\\"width\\\":1, \\\"height\\\":1, \\\"deviceId\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"BUTTON\\\", \\\"pinType\\\":\\\"DIGITAL\\\", \\\"pin\\\":33}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice(2);\n        assertNotNull(device);\n        assertNotNull(device.token);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(createDevice(2, device)));\n\n        TestHardClient hardClient = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient.start();\n\n        hardClient.login(device.token);\n        hardClient.verifyResult(ok(1));\n        clientPair.appClient.verifyResult(hardwareConnected(1, \"1-1\"));\n\n        verify(hardClient.responseMock, timeout(500)).channelRead(any(), eq(produce(1, HARDWARE, b(\"pm 33 out\"))));\n        verify(hardClient.responseMock, times(2)).channelRead(any(), any());\n\n        clientPair.appClient.deactivate(1);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(ok(3)));\n\n        hardClient.reset();\n        clientPair.hardwareClient.reset();\n\n        clientPair.appClient.activate(1);\n        clientPair.appClient.verifyResult(ok(4));\n\n        verify(hardClient.responseMock, timeout(500)).channelRead(any(), eq(produce(1, HARDWARE, b(\"pm 33 out\"))));\n        verify(hardClient.responseMock, times(1)).channelRead(any(), any());\n\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(produce(1, HARDWARE, b(\"pm 1 out 2 out 3 out 5 out 6 in 7 in 30 in 8 in\"))));\n        verify(clientPair.hardwareClient.responseMock, times(1)).channelRead(any(), any());\n\n\n        hardClient.stop().awaitUninterruptibly();\n    }\n\n    @Test\n    public void testActivateForMultiDevices() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        clientPair.appClient.deactivate(1);\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.appClient.activate(1);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(ok(3)));\n\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new ResponseMessage(1, DEVICE_NOT_IN_NETWORK)));\n    }\n\n    @Test\n    public void testTagWorks() throws Exception {\n        Tag tag = new Tag(100_000, \"My New Tag\");\n        tag.deviceIds = new int[] {1};\n\n        clientPair.appClient.createTag(1, tag);\n        verify(clientPair.appClient.responseMock, timeout(1000)).channelRead(any(), eq(createTag(1, tag)));\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":188, \\\"width\\\":1, \\\"height\\\":1, \\\"deviceId\\\":100000, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"BUTTON\\\", \\\"pinType\\\":\\\"DIGITAL\\\", \\\"pin\\\":33, \\\"value\\\":1}\");\n        clientPair.appClient.verifyResult(ok(2));\n\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice(3);\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(3, device));\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient2.start();\n\n        hardClient2.login(device.token);\n        hardClient2.verifyResult(ok(1));\n\n        clientPair.appClient.reset();\n\n        clientPair.appClient.send(\"hardware 1-100000 dw 33 1\");\n        verify(clientPair.hardwareClient.responseMock, timeout(500).times(0)).channelRead(any(), eq(new HardwareMessage(3, b(\"dw 33 10\"))));\n        verify(hardClient2.responseMock, timeout(500)).channelRead(any(), eq(new HardwareMessage(1, b(\"dw 33 1\"))));\n\n        tag.deviceIds = new int[] {0, 1};\n\n        clientPair.appClient.updateTag(1, tag);\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.appClient.send(\"hardware 1-100000 dw 33 10\");\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(new HardwareMessage(3, b(\"dw 33 10\"))));\n        verify(hardClient2.responseMock, timeout(500)).channelRead(any(), eq(new HardwareMessage(3, b(\"dw 33 10\"))));\n    }\n\n    @Test\n    public void testActivateAndGetSyncForMultiDevices() throws Exception {\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":188, \\\"width\\\":1, \\\"height\\\":1, \\\"deviceId\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"BUTTON\\\", \\\"pinType\\\":\\\"DIGITAL\\\", \\\"pin\\\":33, \\\"value\\\":1}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice(2);\n\n        assertNotNull(device);\n        assertNotNull(device.token);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(createDevice(2, device)));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.activate(1);\n\n        verify(clientPair.appClient.responseMock, timeout(500).times(13)).channelRead(any(), any());\n\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 1 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 2 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 3 0\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 5 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 4 244\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 7 3\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 30 3\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 0 89.888037459418\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 11 -58.74774244674501\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 13 60 143 158\")));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(appSync(b(\"1-1 dw 33 1\"))));\n    }\n\n    @Test\n    public void testOfflineOnlineStatusForMultiDevices() throws Exception {\n        Device device0 = new Device(0, \"My Dashboard\", BoardType.Arduino_UNO);\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient2.start();\n\n        hardClient2.login(device.token);\n        hardClient2.verifyResult(ok(1));\n\n        device0.status = Status.ONLINE;\n        device1.status = Status.ONLINE;\n\n        clientPair.appClient.send(\"getDevices 1\");\n        Device[] devices = clientPair.appClient.parseDevices(3);\n        assertNotNull(devices);\n        assertEquals(2, devices.length);\n\n        assertEqualDevice(device0, devices[0]);\n        assertEqualDevice(device1, devices[1]);\n\n        hardClient2.stop().await();\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"getDevices 1\");\n\n        devices = clientPair.appClient.parseDevices();\n        assertNotNull(devices);\n        assertEquals(2, devices.length);\n\n        assertEqualDevice(device0, devices[0]);\n        assertEqualDevice(device1, devices[1]);\n    }\n\n    @Test\n    public void testCorrectOnlineStatusForDisconnect() throws Exception {\n        Device device0 = new Device(0, \"My Dashboard\", BoardType.Arduino_UNO);\n        device0.status = Status.ONLINE;\n\n        clientPair.appClient.send(\"getDevices 1\");\n        Device[] devices = clientPair.appClient.parseDevices();\n        assertNotNull(devices);\n        assertEquals(1, devices.length);\n\n        assertEqualDevice(device0, devices[0]);\n\n        clientPair.hardwareClient.stop().await();\n        device0.status = Status.OFFLINE;\n        clientPair.appClient.verifyResult(deviceOffline(0, \"1-0\"));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"getDevices 1\");\n        devices = clientPair.appClient.parseDevices(1);\n\n        assertNotNull(devices);\n        assertEquals(1, devices.length);\n\n        assertEqualDevice(device0, devices[0]);\n        assertEquals(System.currentTimeMillis(), devices[0].disconnectTime, 5000);\n    }\n\n    @Test\n    public void testCorrectConnectTime() throws Exception {\n        long now = System.currentTimeMillis();\n        clientPair.appClient.send(\"getDevices 1\");\n        Device[] devices = clientPair.appClient.parseDevices();\n        assertNotNull(devices);\n        assertEquals(1, devices.length);\n        assertEquals(now, devices[0].connectTime, 10000);\n    }\n\n    @Test\n    public void testCorrectOnlineStatusForReconnect() throws Exception {\n        Device device0 = new Device(0, \"My Dashboard\", BoardType.Arduino_UNO);\n        device0.status = Status.ONLINE;\n\n        clientPair.appClient.send(\"getDevices 1\");\n        Device[] devices = clientPair.appClient.parseDevices();\n        assertNotNull(devices);\n        assertEquals(1, devices.length);\n\n        assertEqualDevice(device0, devices[0]);\n\n        clientPair.hardwareClient.stop().await();\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient2.start();\n\n        hardClient2.login(devices[0].token);\n        hardClient2.verifyResult(ok(1));\n        clientPair.appClient.verifyResult(hardwareConnected(1, \"1-0\"));\n\n        clientPair.appClient.reset();\n\n        clientPair.appClient.send(\"getDevices 1\");\n        devices = clientPair.appClient.parseDevices();\n\n        assertNotNull(devices);\n        assertEquals(1, devices.length);\n\n        assertEqualDevice(device0, devices[0]);\n    }\n\n\n    @Test\n    public void testHardwareChannelClosedOnDashRemoval() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient2.start();\n\n        hardClient2.login(device.token);\n        hardClient2.verifyResult(ok(1));\n\n        clientPair.appClient.deleteDash(1);\n        clientPair.appClient.verifyResult(ok(2));\n\n        long tries = 0;\n        //waiting for channel to be closed.\n        //but only limited amount if time\n        while (!clientPair.hardwareClient.isClosed() && tries < 100) {\n            sleep(10);\n            tries++;\n        }\n\n        assertTrue(clientPair.hardwareClient.isClosed());\n        assertTrue(hardClient2.isClosed());\n    }\n\n    @Test\n    public void testHardwareChannelClosedOnDeviceRemoval() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        verify(clientPair.appClient.responseMock, timeout(1000)).channelRead(any(), eq(createDevice(1, device)));\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient2.start();\n\n        hardClient2.login(device.token);\n        hardClient2.verifyResult(ok(1));\n        clientPair.appClient.verifyResult(hardwareConnected(1, \"1-1\"));\n\n        String tempDir = holder.props.getProperty(\"data.folder\");\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n\n        Path pinReportingDataPath10 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 1, PinType.DIGITAL, (short) 8, GraphGranularityType.MINUTE));\n        Path pinReportingDataPath11 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 1, PinType.DIGITAL, (short) 8, GraphGranularityType.HOURLY));\n        Path pinReportingDataPath12 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 1, PinType.DIGITAL, (short) 8, GraphGranularityType.DAILY));\n        Path pinReportingDataPath13 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 1, PinType.VIRTUAL, (short) 9, GraphGranularityType.DAILY));\n\n        FileUtils.write(pinReportingDataPath10, 1.11D, 1111111);\n        FileUtils.write(pinReportingDataPath11, 1.11D, 1111111);\n        FileUtils.write(pinReportingDataPath12, 1.11D, 1111111);\n        FileUtils.write(pinReportingDataPath13, 1.11D, 1111111);\n\n        clientPair.appClient.send(\"deleteDevice 1\\0\" + \"1\");\n        clientPair.appClient.verifyResult(ok(2));\n\n        assertFalse(clientPair.hardwareClient.isClosed());\n        assertTrue(hardClient2.isClosed());\n\n        assertTrue(Files.notExists(pinReportingDataPath10));\n        assertTrue(Files.notExists(pinReportingDataPath11));\n        assertTrue(Files.notExists(pinReportingDataPath12));\n        assertTrue(Files.notExists(pinReportingDataPath13));\n    }\n\n    @Test\n    public void testHardwareDataRemovedWhenDeviceRemoved() throws Exception {\n        clientPair.appClient.createDevice(1, new Device(1, \"My Device\", BoardType.ESP8266));\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        ValueDisplay valueDisplay = new ValueDisplay();\n        valueDisplay.id = 11111;\n        valueDisplay.x = 1;\n        valueDisplay.y = 2;\n        valueDisplay.height = 1;\n        valueDisplay.width = 1;\n        valueDisplay.deviceId = 1;\n        valueDisplay.pin = 1;\n        valueDisplay.pinType = PinType.VIRTUAL;\n        clientPair.appClient.createWidget(1, valueDisplay);\n        clientPair.appClient.verifyResult(ok(2));\n\n        Terminal terminal = new Terminal();\n        terminal.id = 11112;\n        terminal.x = 1;\n        terminal.y = 2;\n        terminal.height = 1;\n        terminal.width = 1;\n        terminal.deviceId = 1;\n        terminal.pin = 3;\n        terminal.pinType = PinType.VIRTUAL;\n        clientPair.appClient.createWidget(1, terminal);\n        clientPair.appClient.verifyResult(ok(3));\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient2.start();\n\n        hardClient2.login(device.token);\n        hardClient2.verifyResult(ok(1));\n        clientPair.appClient.verifyResult(hardwareConnected(1, \"1-1\"));\n\n        hardClient2.send(\"hardware vw 1 123\");\n        clientPair.appClient.verifyResult(hardware(2, \"1-1 vw 1 123\"));\n\n        hardClient2.send(\"hardware vw 2 124\");\n        clientPair.appClient.verifyResult(hardware(3, \"1-1 vw 2 124\"));\n\n        hardClient2.send(\"hardware vw 3 125\");\n        clientPair.appClient.verifyResult(hardware(4, \"1-1 vw 3 125\"));\n\n        hardClient2.send(\"hardware vw 3 126\");\n        clientPair.appClient.verifyResult(hardware(5, \"1-1 vw 3 126\"));\n\n        clientPair.appClient.send(\"deleteDevice 1\\0\" + \"1\");\n        verify(clientPair.appClient.responseMock, timeout(1000)).channelRead(any(), eq(ok(4)));\n\n        clientPair.appClient.sync(1, 1);\n        clientPair.appClient.neverAfter(500, appSync(1111, \"1-1 vw 1 123\"));\n        clientPair.appClient.never(appSync(1111, \"1-1 vw 2 124\"));\n        clientPair.appClient.never(appSync(1111, \"1-1 vw 3 125\"));\n        clientPair.appClient.never(appSync(1111, \"1-1 vw 3 126\"));\n    }\n\n    @Test\n    public void testTemporaryTokenWorksAsExpected() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n\n        clientPair.appClient.getProvisionToken(1, device1);\n        device1 = clientPair.appClient.parseDevice(1);\n        assertNotNull(device1);\n        assertEquals(1, device1.id);\n        assertEquals(32, device1.token.length());\n\n        clientPair.appClient.send(\"loadProfileGzipped 1\");\n        DashBoard dash = clientPair.appClient.parseDash(2);\n        assertNotNull(dash);\n        assertEquals(1, dash.devices.length);\n\n        assertTrue(holder.tokenManager.getTokenValueByToken(device1.token) instanceof TemporaryTokenValue);\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient2.start();\n\n        hardClient2.login(device1.token);\n        hardClient2.verifyResult(ok(1));\n        clientPair.appClient.verifyResult(hardwareConnected(1, \"1-1\"));\n\n        clientPair.appClient.send(\"loadProfileGzipped 1\");\n        dash = clientPair.appClient.parseDash(4);\n        assertNotNull(dash);\n        assertEquals(2, dash.devices.length);\n\n        clientPair.appClient.reset();\n\n        hardClient2 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient2.start();\n\n        hardClient2.login(device1.token);\n        hardClient2.verifyResult(ok(1));\n        clientPair.appClient.verifyResult(hardwareConnected(1, \"1-1\"));\n\n        clientPair.appClient.send(\"loadProfileGzipped 1\");\n        dash = clientPair.appClient.parseDash(2);\n        assertNotNull(dash);\n        assertEquals(2, dash.devices.length);\n\n        assertFalse(holder.tokenManager.getTokenValueByToken(device1.token) instanceof TemporaryTokenValue);\n        assertFalse(holder.tokenManager.clearTemporaryTokens());\n    }\n\n    @Test\n    public void testCorrectRemovalForTags() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        Tag tag1 = new Tag(100_001, \"tag1\");\n        Tag tag2 = new Tag(100_002, \"tag2\", new int[] {0});\n        Tag tag3 = new Tag(100_003, \"tag3\", new int[] {1});\n        Tag tag4 = new Tag(100_004, \"tag4\", new int[] {0, 1});\n\n        clientPair.appClient.createTag(1, tag1);\n        clientPair.appClient.createTag(1, tag2);\n        clientPair.appClient.createTag(1, tag3);\n        clientPair.appClient.createTag(1, tag4);\n        clientPair.appClient.verifyResult(createTag(2, tag1));\n        clientPair.appClient.verifyResult(createTag(3, tag2));\n        clientPair.appClient.verifyResult(createTag(4, tag3));\n        clientPair.appClient.verifyResult(createTag(5, tag4));\n\n        clientPair.appClient.deleteDevice(1, 1);\n        clientPair.appClient.verifyResult(ok(6));\n\n        clientPair.appClient.send(\"getTags 1\");\n        Tag[] tags = clientPair.appClient.parseTags(7);\n        assertNotNull(tags);\n\n        assertEquals(100_001, tags[0].id);\n        assertEquals(0, tags[0].deviceIds.length);\n\n        assertEquals(100_002, tags[1].id);\n        assertEquals(1, tags[1].deviceIds.length);\n        assertEquals(0, tags[1].deviceIds[0]);\n\n        assertEquals(100_003, tags[2].id);\n        assertEquals(0, tags[2].deviceIds.length);\n\n        assertEquals(100_004, tags[3].id);\n        assertEquals(1, tags[3].deviceIds.length);\n        assertEquals(0, tags[3].deviceIds[0]);\n    }\n\n    @Test\n    public void testCorrectRemovalForDeviceSelector() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = 21321;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(2));\n        PageTileTemplate tileTemplate = new PageTileTemplate(1,\n                null, null, \"name\", \"name\", \"iconName\", BoardType.ESP8266, null,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n        clientPair.appClient.createTemplate(1, deviceTiles.id, tileTemplate);\n        clientPair.appClient.verifyResult(ok(3));\n\n        deviceTiles = new DeviceTiles();\n        deviceTiles.id = 21322;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(4));\n        tileTemplate = new PageTileTemplate(1,\n                null, new int[]{0}, \"name\", \"name\", \"iconName\", BoardType.ESP8266, null,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n        clientPair.appClient.createTemplate(1, deviceTiles.id, tileTemplate);\n        clientPair.appClient.verifyResult(ok(5));\n\n        clientPair.appClient.send(\"addEnergy \" + \"100000\" + \"\\0\" + \"1370-3990-1414-55681\");\n        clientPair.appClient.verifyResult(ok(6));\n\n        deviceTiles = new DeviceTiles();\n        deviceTiles.id = 21323;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(7));\n        tileTemplate = new PageTileTemplate(1,\n                null, new int[]{1}, \"name\", \"name\", \"iconName\", BoardType.ESP8266, null,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n        clientPair.appClient.createTemplate(1, deviceTiles.id, tileTemplate);\n        clientPair.appClient.verifyResult(ok(8));\n\n        deviceTiles = new DeviceTiles();\n        deviceTiles.id = 21324;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(9));\n        tileTemplate = new PageTileTemplate(1,\n                null, new int[]{0, 1}, \"name\", \"name\", \"iconName\", BoardType.ESP8266, null,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n        clientPair.appClient.createTemplate(1, deviceTiles.id, tileTemplate);\n        clientPair.appClient.verifyResult(ok(10));\n\n        clientPair.appClient.deleteDevice(1, 1);\n        clientPair.appClient.verifyResult(ok(11));\n\n        clientPair.appClient.getWidget(1, 21321);\n        deviceTiles = (DeviceTiles) clientPair.appClient.parseWidget(12);\n        assertNotNull(deviceTiles);\n        assertEquals(21321, deviceTiles.id);\n        assertEquals(0, deviceTiles.tiles.length);\n        assertEquals(0, deviceTiles.templates[0].deviceIds.length);\n\n        clientPair.appClient.getWidget(1, 21322);\n        deviceTiles = (DeviceTiles) clientPair.appClient.parseWidget(13);\n        assertNotNull(deviceTiles);\n        assertEquals(21322, deviceTiles.id);\n        assertEquals(1, deviceTiles.tiles.length);\n        assertEquals(1, deviceTiles.templates[0].deviceIds.length);\n        assertEquals(0, deviceTiles.templates[0].deviceIds[0]);\n\n        clientPair.appClient.getWidget(1, 21323);\n        deviceTiles = (DeviceTiles) clientPair.appClient.parseWidget(14);\n        assertNotNull(deviceTiles);\n        assertEquals(21323, deviceTiles.id);\n        assertEquals(0, deviceTiles.tiles.length);\n        assertEquals(0, deviceTiles.templates[0].deviceIds.length);\n\n        clientPair.appClient.getWidget(1, 21324);\n        deviceTiles = (DeviceTiles) clientPair.appClient.parseWidget(15);\n        assertNotNull(deviceTiles);\n        assertEquals(21324, deviceTiles.id);\n        assertEquals(1, deviceTiles.tiles.length);\n        assertEquals(1, deviceTiles.templates[0].deviceIds.length);\n        assertEquals(0, deviceTiles.templates[0].deviceIds[0]);\n    }\n\n    private static void assertEqualDevice(Device expected, Device real) {\n        assertEquals(expected.id, real.id);\n        //assertEquals(expected.name, real.name);\n        assertEquals(expected.boardType, real.boardType);\n        assertNotNull(real.token);\n        assertEquals(expected.status, real.status);\n    }\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/tcp/EnergyWorkflowTest.java",
    "content": "package cc.blynk.integration.tcp;\n\nimport cc.blynk.integration.SingleServerInstancePerTest;\nimport cc.blynk.integration.TestUtil;\nimport cc.blynk.integration.model.tcp.ClientPair;\nimport cc.blynk.server.core.protocol.model.messages.ResponseMessage;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport static cc.blynk.integration.TestUtil.ok;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_ENERGY;\nimport static cc.blynk.server.core.protocol.enums.Response.ENERGY_LIMIT;\nimport static cc.blynk.server.core.protocol.enums.Response.OK;\nimport static cc.blynk.server.core.protocol.model.messages.MessageFactory.produce;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.eq;\nimport static org.mockito.Mockito.timeout;\nimport static org.mockito.Mockito.verify;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/2/2015.\n *\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class EnergyWorkflowTest extends SingleServerInstancePerTest {\n\n    @Override\n    protected ClientPair initClientPair() throws Exception {\n        return TestUtil.initAppAndHardPair(\"localhost\",\n                properties.getHttpsPort(), properties.getHttpPort(),\n                getUserName(), \"1\", \"user_profile_json.txt\", properties,\n                4500);\n    }\n\n    @Test\n    public void testReach1500LimitOfEnergy() throws Exception {\n        clientPair.appClient.createDash(\"{\\\"id\\\":2, \\\"createdAt\\\":1458856800001, \\\"name\\\":\\\"test board\\\"}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        for (int i = 2; i < 12; i++) {\n            clientPair.appClient.createWidget(2, \"{\\\"id\\\":X, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":2, \\\"y\\\":2, \\\"label\\\":\\\"Some Text 2\\\", \\\"type\\\":\\\"BUTTON\\\", \\\"pinType\\\":\\\"DIGITAL\\\", \\\"pin\\\":2}\".replace(\"X\", \"\" + i));\n            verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new ResponseMessage(i, OK)));\n        }\n\n        clientPair.appClient.createWidget(2, \"{\\\"id\\\":100, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":2, \\\"y\\\":2, \\\"label\\\":\\\"Some Text 2\\\", \\\"type\\\":\\\"BUTTON\\\", \\\"pinType\\\":\\\"DIGITAL\\\", \\\"pin\\\":2}\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new ResponseMessage(12, ENERGY_LIMIT)));\n    }\n\n    @Test\n    public void testGetEnergy() throws Exception {\n        clientPair.appClient.send(\"getEnergy\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(1, GET_ENERGY, \"2000\")));\n    }\n\n    @Test\n    public void testAddEnergy() throws Exception {\n        clientPair.appClient.send(\"addEnergy 1000\" + \"\\0\" + \"random123\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.send(\"getEnergy\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(2, GET_ENERGY, \"3000\")));\n    }\n\n    @Test\n    public void testEnergyAfterCreateRemoveProject() throws Exception {\n        clientPair.appClient.createDash(\"{\\\"id\\\":2, \\\"createdAt\\\":1458856800001, \\\"name\\\":\\\"test board\\\"}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.send(\"getEnergy\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(2, GET_ENERGY, \"2000\")));\n\n        clientPair.appClient.deleteDash(2);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(ok(3)));\n\n        clientPair.appClient.send(\"getEnergy\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(4, GET_ENERGY, \"2000\")));\n    }\n\n\n    @Test\n    public void testEnergyAfterCreateRemoveWidget() throws Exception {\n        clientPair.appClient.createDash(\"{\\\"id\\\":2, \\\"createdAt\\\":1458856800001, \\\"name\\\":\\\"test board\\\"}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.send(\"getEnergy\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(2, GET_ENERGY, \"2000\")));\n\n        clientPair.appClient.createWidget(2, \"{\\\"id\\\":2, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":2, \\\"y\\\":2, \\\"label\\\":\\\"Some Text 2\\\", \\\"type\\\":\\\"LCD\\\", \\\"pinType\\\":\\\"DIGITAL\\\", \\\"pin\\\":2}\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(ok(3)));\n\n        clientPair.appClient.createWidget(2, \"{\\\"id\\\":3, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":2, \\\"y\\\":2, \\\"label\\\":\\\"Some Text 2\\\", \\\"type\\\":\\\"LCD\\\", \\\"pinType\\\":\\\"DIGITAL\\\", \\\"pin\\\":2}\");\n        clientPair.appClient.verifyResult(ok(4));\n\n        clientPair.appClient.createWidget(2, \"{\\\"id\\\":4, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":2, \\\"y\\\":2, \\\"label\\\":\\\"Some Text 2\\\", \\\"type\\\":\\\"LCD\\\", \\\"pinType\\\":\\\"DIGITAL\\\", \\\"pin\\\":2}\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(ok(5)));\n\n        clientPair.appClient.createWidget(2, \"{\\\"id\\\":5, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":2, \\\"y\\\":2, \\\"label\\\":\\\"Some Text 2\\\", \\\"type\\\":\\\"LCD\\\", \\\"pinType\\\":\\\"DIGITAL\\\", \\\"pin\\\":2}\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(ok(6)));\n\n        clientPair.appClient.createWidget(2, \"{\\\"id\\\":6, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":2, \\\"y\\\":2, \\\"label\\\":\\\"Some Text 2\\\", \\\"type\\\":\\\"LCD\\\", \\\"pinType\\\":\\\"DIGITAL\\\", \\\"pin\\\":2}\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(ok(7)));\n\n        clientPair.appClient.createWidget(2, \"{\\\"id\\\":7, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":2, \\\"y\\\":2, \\\"label\\\":\\\"Some Text 2\\\", \\\"type\\\":\\\"BUTTON\\\", \\\"pinType\\\":\\\"DIGITAL\\\", \\\"pin\\\":2}\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new ResponseMessage(8, ENERGY_LIMIT)));\n\n        clientPair.appClient.deleteDash(2);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(ok(9)));\n\n        clientPair.appClient.send(\"getEnergy\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(10, GET_ENERGY, \"2000\")));\n    }\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/tcp/EventorTest.java",
    "content": "package cc.blynk.integration.tcp;\n\nimport cc.blynk.integration.SingleServerInstancePerTest;\nimport cc.blynk.integration.model.tcp.TestHardClient;\nimport cc.blynk.server.core.model.DataStream;\nimport cc.blynk.server.core.model.Profile;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.enums.WidgetProperty;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.others.eventor.Eventor;\nimport cc.blynk.server.core.model.widgets.others.eventor.Rule;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.action.BaseAction;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.action.SetPinAction;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.action.SetPinActionType;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.action.SetPropertyPinAction;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.action.notification.MailAction;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.action.notification.NotifyAction;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.action.notification.TwitAction;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.condition.BaseCondition;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.condition.ValueChanged;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.condition.number.Between;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.condition.number.Equal;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.condition.number.GreaterThan;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.condition.number.GreaterThanOrEqual;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.condition.number.LessThan;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.condition.number.LessThanOrEqual;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.condition.number.NotBetween;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.condition.number.NotEqual;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.condition.string.StringEqual;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.condition.string.StringNotEqual;\nimport cc.blynk.server.notifications.push.android.AndroidGCMMessage;\nimport cc.blynk.server.notifications.push.enums.Priority;\nimport cc.blynk.utils.NumberUtil;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.ArgumentCaptor;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport static cc.blynk.integration.TestUtil.b;\nimport static cc.blynk.integration.TestUtil.hardware;\nimport static cc.blynk.integration.TestUtil.ok;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE;\nimport static cc.blynk.server.core.protocol.enums.Command.SET_WIDGET_PROPERTY;\nimport static cc.blynk.server.core.protocol.model.messages.MessageFactory.produce;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.eq;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.reset;\nimport static org.mockito.Mockito.timeout;\nimport static org.mockito.Mockito.verify;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/2/2015.\n *\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class EventorTest extends SingleServerInstancePerTest {\n\n    private static Rule buildRule(String s, boolean isActive) {\n        //example \"if V1 > 37 then setpin V2 123\"\n\n        String[] splitted = s.split(\" \");\n\n        //\"V1\"\n        DataStream triggerDataStream = parsePin(splitted[1]);\n                                                       //>                               37\n        BaseCondition ifCondition = resolveCondition(splitted[2], Double.parseDouble(splitted[3]));\n\n        DataStream dataStream = null;\n        String value;\n        try {\n            //V2\n            dataStream = parsePin(splitted[6]);\n            //123\n            value = splitted[7];\n        } catch (Exception e) {\n            value = splitted[6];\n        }\n\n                                            //setpin\n        BaseAction action = resolveAction(splitted[5], dataStream, value);\n\n        return new Rule(triggerDataStream, null, ifCondition, new BaseAction[] { action }, isActive);\n    }\n\n    private static DataStream parsePin(String pinString) {\n        PinType pinType = PinType.getPinType(pinString.charAt(0));\n        short pin = NumberUtil.parsePin(pinString.substring(1));\n        return new DataStream(pin, pinType);\n    }\n\n    private static BaseAction resolveAction(String action, DataStream dataStream, String value) {\n        switch (action) {\n            case \"setpin\" :\n                return new SetPinAction(dataStream.pin, dataStream.pinType, value);\n            case \"notify\" :\n                return new NotifyAction(value);\n            case \"mail\" :\n                return new MailAction(\"Subj\", value);\n            case \"twit\" :\n                return new TwitAction(value);\n\n            default: throw new RuntimeException(\"Not supported action. \" + action);\n        }\n    }\n\n    private static BaseCondition resolveCondition(String conditionString, double value) {\n        switch (conditionString) {\n            case \">\" :\n                return new GreaterThan(value);\n            case \">=\" :\n                return new GreaterThanOrEqual(value);\n            case \"<\" :\n                return new LessThan(value);\n            case \"<=\" :\n                return new LessThanOrEqual(value);\n            case \"=\" :\n                return new Equal(value);\n            case \"!=\" :\n                return new NotEqual(value);\n\n            default: throw new RuntimeException(\"Not supported operation. \" + conditionString);\n        }\n    }\n\n    public static Eventor oneRuleEventor(String ruleString, boolean isActive) {\n        Rule rule = buildRule(ruleString, isActive);\n        return new Eventor(new Rule[] {rule});\n    }\n\n    public static Eventor oneRuleEventor(String ruleString) {\n        Rule rule = buildRule(ruleString, true);\n        return new Eventor(new Rule[] {rule});\n    }\n\n    @Test\n    public void testSimpleRule1() throws Exception {\n        Eventor eventor = oneRuleEventor(\"if v1 > 37 then setpin v2 123\");\n\n        clientPair.appClient.createWidget(1, eventor);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 1 38\");\n        clientPair.appClient.verifyResult(hardware(1, \"1-0 vw 1 38\"));\n        clientPair.hardwareClient.verifyResult(hardware(888, \"vw 2 123\"));\n        clientPair.appClient.verifyResult(hardware(888, \"1-0 vw 2 123\"));\n    }\n\n    @Test\n    public void testInactiveEventsNotTriggered() throws Exception {\n        Eventor eventor = oneRuleEventor(\"if v1 > 37 then setpin v2 123\", false);\n\n        clientPair.appClient.createWidget(1, eventor);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 1 38\");\n        clientPair.appClient.verifyResult(hardware(1, \"1-0 vw 1 38\"));\n        clientPair.hardwareClient.never(hardware(888, \"vw 2 123\"));\n        clientPair.appClient.never(hardware(888, \"1-0 vw 2 123\"));\n    }\n\n    @Test\n    public void testSimpleRule1AndDashUpdatedValue() throws Exception {\n        Eventor eventor = oneRuleEventor(\"if v1 > 37 then setpin v4 123\");\n\n        clientPair.appClient.createWidget(1, eventor);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 1 38\");\n        clientPair.appClient.verifyResult(hardware(1, \"1-0 vw 1 38\"));\n        clientPair.hardwareClient.verifyResult(hardware(888, \"vw 4 123\"));\n        clientPair.appClient.verifyResult(hardware(888, \"1-0 vw 4 123\"));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(1);\n        assertNotNull(profile);\n        OnePinWidget widget = (OnePinWidget) profile.dashBoards[0].findWidgetByPin(0, (short) 4, PinType.VIRTUAL);\n        assertNotNull(widget);\n        assertEquals(\"123\", widget.value);\n    }\n\n    @Test\n    public void testSimpleRule2() throws Exception {\n        Eventor eventor = oneRuleEventor(\"if v1 >= 37 then setpin v2 123\");\n\n        clientPair.appClient.createWidget(1, eventor);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 1 37\");\n        clientPair.appClient.verifyResult(hardware(1, \"1-0 vw 1 37\"));\n        clientPair.hardwareClient.verifyResult(hardware(888, \"vw 2 123\"));\n        clientPair.appClient.verifyResult(hardware(888, \"1-0 vw 2 123\"));\n    }\n\n    @Test\n    public void testSimpleRule3() throws Exception {\n        Eventor eventor = oneRuleEventor(\"if v1 <= 37 then setpin v2 123\");\n\n        clientPair.appClient.createWidget(1, eventor);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 1 37\");\n        clientPair.appClient.verifyResult(hardware(1, \"1-0 vw 1 37\"));\n        clientPair.hardwareClient.verifyResult(hardware(888, \"vw 2 123\"));\n        clientPair.appClient.verifyResult(hardware(888, \"1-0 vw 2 123\"));\n    }\n\n    @Test\n    public void testSimpleRule4() throws Exception {\n        Eventor eventor = oneRuleEventor(\"if v1 = 37 then setpin v2 123\");\n\n        clientPair.appClient.createWidget(1, eventor);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 1 37\");\n        clientPair.appClient.verifyResult(hardware(1, \"1-0 vw 1 37\"));\n        clientPair.hardwareClient.verifyResult(hardware(888, \"vw 2 123\"));\n        clientPair.appClient.verifyResult(hardware(888, \"1-0 vw 2 123\"));\n    }\n\n    @Test\n    public void testSimpleRule5() throws Exception {\n        Eventor eventor = oneRuleEventor(\"if v1 < 37 then setpin v2 123\");\n\n        clientPair.appClient.createWidget(1, eventor);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 1 36\");\n        clientPair.appClient.verifyResult(hardware(1, \"1-0 vw 1 36\"));\n        clientPair.hardwareClient.verifyResult(hardware(888, \"vw 2 123\"));\n        clientPair.appClient.verifyResult(hardware(888, \"1-0 vw 2 123\"));\n    }\n\n    @Test\n    public void testSimpleRule6() throws Exception {\n        Eventor eventor = oneRuleEventor(\"if v1 != 37 then setpin v2 123\");\n\n        clientPair.appClient.createWidget(1, eventor);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 1 36\");\n        clientPair.appClient.verifyResult(hardware(1, \"1-0 vw 1 36\"));\n        clientPair.hardwareClient.verifyResult(hardware(888, \"vw 2 123\"));\n        clientPair.appClient.verifyResult(hardware(888, \"1-0 vw 2 123\"));\n    }\n\n    @Test\n    public void testSimpleRule7() throws Exception {\n        DataStream triggerDataStream = new DataStream((short) 1, PinType.VIRTUAL);\n        DataStream dataStream = new DataStream((short) 2, PinType.VIRTUAL);\n        SetPinAction setPinAction = new SetPinAction(dataStream, \"123\", SetPinActionType.CUSTOM);\n        Rule rule = new Rule(triggerDataStream, null, new Between(10, 12), new BaseAction[] {setPinAction}, true);\n\n        Eventor eventor = new Eventor(new Rule[] {rule});\n\n        clientPair.appClient.createWidget(1, eventor);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 1 11\");\n        clientPair.appClient.verifyResult(hardware(1, \"1-0 vw 1 11\"));\n        clientPair.hardwareClient.verifyResult(hardware(888, \"vw 2 123\"));\n        clientPair.appClient.verifyResult(hardware(888, \"1-0 vw 2 123\"));\n    }\n\n    @Test\n    public void testSimpleRule8() throws Exception {\n        DataStream triggerDataStream = new DataStream((short) 1, PinType.VIRTUAL);\n        DataStream dataStream = new DataStream((short) 2, PinType.VIRTUAL);\n        SetPinAction setPinAction = new SetPinAction(dataStream, \"123\", SetPinActionType.CUSTOM);\n        Rule rule = new Rule(triggerDataStream, null, new NotBetween(10, 12), new BaseAction[] {setPinAction}, true);\n\n        Eventor eventor = new Eventor(new Rule[] {rule});\n\n        clientPair.appClient.createWidget(1, eventor);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 1 9\");\n        clientPair.appClient.verifyResult(hardware(1, \"1-0 vw 1 9\"));\n        clientPair.hardwareClient.verifyResult(hardware(888, \"vw 2 123\"));\n        clientPair.appClient.verifyResult(hardware(888, \"1-0 vw 2 123\"));\n    }\n\n    @Test\n    public void testSimpleRule8Notify() throws Exception {\n        Eventor eventor = oneRuleEventor(\"if v1 = 37 then notify Yo!!!!!\");\n\n        clientPair.appClient.createWidget(1, eventor);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 1 37\");\n        clientPair.appClient.verifyResult(hardware(1, \"1-0 vw 1 37\"));\n\n        ArgumentCaptor<AndroidGCMMessage> objectArgumentCaptor = ArgumentCaptor.forClass(AndroidGCMMessage.class);\n        verify(holder.gcmWrapper, timeout(500).times(1)).send(objectArgumentCaptor.capture(), any(), any());\n        AndroidGCMMessage message = objectArgumentCaptor.getValue();\n\n        String expectedJson = new AndroidGCMMessage(\"token\", Priority.normal, \"Yo!!!!!\", 1).toJson();\n        assertEquals(expectedJson, message.toJson());\n    }\n\n    @Test\n    public void testSimpleRule8NotifyAndFormat() throws Exception {\n        Eventor eventor = oneRuleEventor(\"if v1 = 37 then notify Temperatureis:/pin/.\");\n\n        clientPair.appClient.createWidget(1, eventor);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 1 37\");\n        clientPair.appClient.verifyResult(hardware(1, \"1-0 vw 1 37\"));\n\n        ArgumentCaptor<AndroidGCMMessage> objectArgumentCaptor = ArgumentCaptor.forClass(AndroidGCMMessage.class);\n        verify(holder.gcmWrapper, timeout(500).times(1)).send(objectArgumentCaptor.capture(), any(), any());\n        AndroidGCMMessage message = objectArgumentCaptor.getValue();\n\n        String expectedJson = new AndroidGCMMessage(\"token\", Priority.normal, \"Temperatureis:37.\", 1).toJson();\n        assertEquals(expectedJson, message.toJson());\n    }\n\n    @Test\n    public void testSimpleRule9Twit() throws Exception {\n        Eventor eventor = oneRuleEventor(\"if v1 = 37 then twit Yo!!!!!\");\n\n        clientPair.appClient.createWidget(1, eventor);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 1 37\");\n        clientPair.appClient.verifyResult(hardware(1, \"1-0 vw 1 37\"));\n\n        verify(holder.twitterWrapper, timeout(500)).send(eq(\"token\"), eq(\"secret\"), eq(\"Yo!!!!!\"), any());\n    }\n\n    @Test\n    public void testSimpleRule8Email() throws Exception {\n        Eventor eventor = oneRuleEventor(\"if v1 = 37 then mail Yo!!!!!\");\n\n        clientPair.appClient.createWidget(1, eventor);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":432, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"type\\\":\\\"EMAIL\\\"}\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(ok(2)));\n\n        clientPair.hardwareClient.send(\"hardware vw 1 37\");\n        clientPair.appClient.verifyResult(hardware(1, \"1-0 vw 1 37\"));\n\n        ArgumentCaptor<AndroidGCMMessage> objectArgumentCaptor = ArgumentCaptor.forClass(AndroidGCMMessage.class);\n        verify(holder.mailWrapper, timeout(500).times(1)).sendText(eq(getUserName()), eq(\"Subj\"), eq(\"Yo!!!!!\"));\n    }\n\n    @Test\n    public void testSimpleRule8EmailAndFormat() throws Exception {\n        Eventor eventor = oneRuleEventor(\"if v1 = 37 then mail Yo/pin/!!!!!\");\n\n        clientPair.appClient.createWidget(1, eventor);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":432, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"type\\\":\\\"EMAIL\\\"}\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(ok(2)));\n\n        clientPair.hardwareClient.send(\"hardware vw 1 37\");\n        clientPair.appClient.verifyResult(hardware(1, \"1-0 vw 1 37\"));\n\n        ArgumentCaptor<AndroidGCMMessage> objectArgumentCaptor = ArgumentCaptor.forClass(AndroidGCMMessage.class);\n        verify(holder.mailWrapper, timeout(500).times(1)).sendText(eq(getUserName()), eq(\"Subj\"), eq(\"Yo37!!!!!\"));\n    }\n\n    @Test\n    public void testSimpleRuleCreateUpdateConditionWorks() throws Exception {\n        Eventor eventor = oneRuleEventor(\"if v1 >= 37 then setpin v2 123\");\n\n        clientPair.appClient.createWidget(1, eventor);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 1 37\");\n        clientPair.appClient.verifyResult(hardware(1, \"1-0 vw 1 37\"));\n        clientPair.hardwareClient.verifyResult(hardware(888, \"vw 2 123\"));\n        clientPair.appClient.verifyResult(hardware(888, \"1-0 vw 2 123\"));\n\n        eventor = oneRuleEventor(\"if v1 >= 37 then setpin v2 124\");\n        clientPair.appClient.updateWidget(1, eventor);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(ok(2)));\n\n        clientPair.hardwareClient.send(\"hardware vw 1 36\");\n        clientPair.appClient.verifyResult(hardware(2, \"1-0 vw 1 36\"));\n        verify(clientPair.hardwareClient.responseMock, timeout(500).times(0)).channelRead(any(), eq(produce(888, HARDWARE, b(\"vw 2 124\"))));\n        verify(clientPair.appClient.responseMock, timeout(500).times(0)).channelRead(any(), eq(produce(888, HARDWARE, b(\"1-0 vw 2 124\"))));\n\n        clientPair.hardwareClient.send(\"hardware vw 1 37\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(3, HARDWARE, b(\"1-0 vw 1 37\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(produce(888, HARDWARE, b(\"vw 2 124\"))));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(888, HARDWARE, b(\"1-0 vw 2 124\"))));\n    }\n\n    @Test\n    public void testPinModeForEventorAndSetPinAction() throws Exception {\n        clientPair.appClient.activate(1);\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(produce(1, HARDWARE, b(\"pm 1 out 2 out 3 out 5 out 6 in 7 in 30 in 8 in\"))));\n        reset(clientPair.hardwareClient.responseMock);\n\n        Eventor eventor = oneRuleEventor(\"if v1 > 37 then setpin d9 1\");\n\n        clientPair.appClient.createWidget(1, eventor);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.activate(1);\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(produce(1, HARDWARE, b(\"pm 1 out 2 out 3 out 5 out 6 in 7 in 30 in 8 in 9 out\"))));\n        //reset(clientPair.hardwareClient.responseMock);\n\n        clientPair.hardwareClient.send(\"hardware vw 1 38\");\n        clientPair.appClient.verifyResult(hardware(1, \"1-0 vw 1 38\"));\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(produce(888, HARDWARE, b(\"dw 9 1\"))));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(888, HARDWARE, b(\"1-0 dw 9 1\"))));\n    }\n\n    @Test\n    public void testTriggerOnlyOnceOnCondition() throws Exception {\n        Eventor eventor = oneRuleEventor(\"if v1 < 37 then setpin v2 123\");\n\n        clientPair.appClient.createWidget(1, eventor);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 1 36\");\n        clientPair.appClient.verifyResult(hardware(1, \"1-0 vw 1 36\"));\n        clientPair.hardwareClient.verifyResult(hardware(888, \"vw 2 123\"));\n        clientPair.appClient.verifyResult(hardware(888, \"1-0 vw 2 123\"));\n\n        clientPair.hardwareClient.reset();\n        clientPair.appClient.reset();\n\n        clientPair.hardwareClient.send(\"hardware vw 1 36\");\n        clientPair.appClient.verifyResult(hardware(1, \"1-0 vw 1 36\"));\n        verify(clientPair.hardwareClient.responseMock, timeout(500).times(0)).channelRead(any(), eq(produce(888, HARDWARE, b(\"vw 2 123\"))));\n        verify(clientPair.appClient.responseMock, timeout(500).times(0)).channelRead(any(), eq(produce(888, HARDWARE, b(\"1-0 vw 2 123\"))));\n\n        clientPair.hardwareClient.send(\"hardware vw 1 36\");\n        clientPair.appClient.verifyResult(hardware(2, \"1-0 vw 1 36\"));\n        verify(clientPair.hardwareClient.responseMock, timeout(500).times(0)).channelRead(any(), eq(produce(888, HARDWARE, b(\"vw 2 123\"))));\n        verify(clientPair.appClient.responseMock, timeout(500).times(0)).channelRead(any(), eq(produce(888, HARDWARE, b(\"1-0 vw 2 123\"))));\n\n        clientPair.hardwareClient.send(\"hardware vw 1 38\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(3, HARDWARE, b(\"1-0 vw 1 38\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(500).times(0)).channelRead(any(), eq(produce(888, HARDWARE, b(\"vw 2 123\"))));\n        verify(clientPair.appClient.responseMock, timeout(500).times(0)).channelRead(any(), eq(produce(888, HARDWARE, b(\"1-0 vw 2 123\"))));\n\n        clientPair.hardwareClient.send(\"hardware vw 1 36\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(4, HARDWARE, b(\"1-0 vw 1 36\"))));\n        clientPair.hardwareClient.verifyResult(hardware(888, \"vw 2 123\"));\n        clientPair.appClient.verifyResult(hardware(888, \"1-0 vw 2 123\"));\n    }\n\n    @Test\n    public void testEventorWorksForMultipleHardware() throws Exception {\n        TestHardClient hardClient = new TestHardClient(\"localhost\", properties.getHttpPort());\n        hardClient.start();\n\n        hardClient.login(clientPair.token);\n        hardClient.verifyResult(ok(1));\n        clientPair.appClient.reset();\n\n        Eventor eventor = oneRuleEventor(\"if v1 < 37 then setpin v2 123\");\n\n        clientPair.appClient.createWidget(1, eventor);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 1 36\");\n        clientPair.appClient.verifyResult(hardware(1, \"1-0 vw 1 36\"));\n        clientPair.hardwareClient.verifyResult(hardware(888, \"vw 2 123\"));\n        clientPair.appClient.verifyResult(hardware(888, \"1-0 vw 2 123\"));\n        verify(hardClient.responseMock, timeout(500)).channelRead(any(), eq(produce(888, HARDWARE, b(\"vw 2 123\"))));\n    }\n\n    @Test\n    public void testPinModeForPWMPinForEventorAndSetPinAction() throws Exception {\n        clientPair.appClient.activate(1);\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(produce(1, HARDWARE, b(\"pm 1 out 2 out 3 out 5 out 6 in 7 in 30 in 8 in\"))));\n        reset(clientPair.hardwareClient.responseMock);\n\n        Eventor eventor = oneRuleEventor(\"if v1 > 37 then setpin d9 255\");\n        //here is special case. right now eventor for digital pins supports only LOW/HIGH values\n        //that's why eventor doesn't work with PWM pins, as they handled as analog, where HIGH doesn't work.\n        SetPinAction setPinAction = (SetPinAction) eventor.rules[0].actions[0];\n        DataStream dataStream = setPinAction.dataStream;\n        eventor.rules[0].actions[0] = new SetPinAction(\n                new DataStream(dataStream.pin, true, false, dataStream.pinType, null, 0, 255, null),\n                setPinAction.value,\n                SetPinActionType.CUSTOM\n        );\n\n        clientPair.appClient.createWidget(1, eventor);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.activate(1);\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(produce(1, HARDWARE, b(\"pm 1 out 2 out 3 out 5 out 6 in 7 in 30 in 8 in 9 out\"))));\n\n        clientPair.hardwareClient.send(\"hardware vw 1 38\");\n        clientPair.appClient.verifyResult(hardware(1, \"1-0 vw 1 38\"));\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(produce(888, HARDWARE, b(\"aw 9 255\"))));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(888, HARDWARE, b(\"1-0 aw 9 255\"))));\n    }\n\n    @Test\n    public void testSimpleRule2WorksFromAppSide() throws Exception {\n        Eventor eventor = oneRuleEventor(\"if v1 >= 37 then setpin v2 123\");\n\n        clientPair.appClient.createWidget(1, eventor);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.send(\"hardware 1-0 vw 1 37\");\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(produce(2, HARDWARE, b(\"vw 1 37\"))));\n        clientPair.hardwareClient.verifyResult(hardware(888, \"vw 2 123\"));\n        clientPair.appClient.verifyResult(hardware(888, \"1-0 vw 2 123\"));\n    }\n\n    @Test\n    public void testSimpleRuleWith2Actions() throws Exception {\n        DataStream triggerDataStream = new DataStream((short) 1, PinType.VIRTUAL);\n        Rule rule = new Rule(triggerDataStream, null, new GreaterThan(37),\n                new BaseAction[] {\n                        new SetPinAction((short) 0, PinType.VIRTUAL, \"0\"),\n                        new SetPinAction((short) 1, PinType.VIRTUAL, \"1\")\n                },\n                true);\n\n        Eventor eventor = new Eventor(new Rule[] {rule});\n\n        clientPair.appClient.createWidget(1, eventor);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 1 38\");\n        clientPair.appClient.verifyResult(hardware(1, \"1-0 vw 1 38\"));\n\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(produce(888, HARDWARE, b(\"vw 0 0\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(produce(888, HARDWARE, b(\"vw 1 1\"))));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(888, HARDWARE, b(\"1-0 vw 0 0\"))));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(888, HARDWARE, b(\"1-0 vw 1 1\"))));\n    }\n\n    @Test\n    public void testEventorHasWrongDeviceId() throws Exception {\n        Eventor eventor = oneRuleEventor(\"if v1 != 37 then setpin v2 123\");\n        eventor.deviceId = 1;\n\n        clientPair.appClient.createWidget(1, eventor);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 1 36\");\n        clientPair.appClient.verifyResult(hardware(1, \"1-0 vw 1 36\"));\n        clientPair.hardwareClient.never(hardware(888, \"vw 2 123\"));\n        clientPair.appClient.never(hardware(888, \"1-0 vw 2 123\"));\n    }\n\n    @Test\n    public void testStringEqualsRule() throws Exception {\n        DataStream triggerStream = new DataStream((short) 1, PinType.VIRTUAL);\n        SetPinAction setPinAction = new SetPinAction(new DataStream((short) 2, PinType.VIRTUAL),\n                \"123\", SetPinActionType.CUSTOM);\n\n        Eventor eventor = new Eventor(new Rule[] {\n                new Rule(triggerStream, null, new StringEqual(\"abc\"), new BaseAction[] {setPinAction}, true)\n        });\n\n        clientPair.appClient.createWidget(1, eventor);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 1 abc\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(1, HARDWARE, b(\"1-0 vw 1 abc\"))));\n        clientPair.hardwareClient.verifyResult(hardware(888, \"vw 2 123\"));\n        clientPair.appClient.verifyResult(hardware(888, \"1-0 vw 2 123\"));\n    }\n\n    @Test\n    public void testValueChangedRule() throws Exception {\n        DataStream triggerStream = new DataStream((short) 1, PinType.VIRTUAL);\n        SetPinAction setPinAction = new SetPinAction(new DataStream((short) 2, PinType.VIRTUAL),\n                \"123\", SetPinActionType.CUSTOM);\n\n        Eventor eventor = new Eventor(new Rule[] {\n                new Rule(triggerStream, null, new ValueChanged(\"abc\"), new BaseAction[] {setPinAction}, true)\n        });\n\n        clientPair.appClient.createWidget(1, eventor);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 1 changed\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(1, HARDWARE, b(\"1-0 vw 1 changed\"))));\n        clientPair.hardwareClient.verifyResult(hardware(888, \"vw 2 123\"));\n        clientPair.appClient.verifyResult(hardware(888, \"1-0 vw 2 123\"));\n\n        clientPair.hardwareClient.reset();\n        clientPair.appClient.reset();\n\n        clientPair.hardwareClient.send(\"hardware vw 1 changed\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(1, HARDWARE, b(\"1-0 vw 1 changed\"))));\n        clientPair.hardwareClient.never(hardware(888, \"vw 2 123\"));\n        clientPair.appClient.never(hardware(888, \"1-0 vw 2 123\"));\n    }\n\n    @Test\n    public void testStringNotEqualsRule() throws Exception {\n        DataStream triggerStream = new DataStream((short) 1, PinType.VIRTUAL);\n        SetPinAction setPinAction = new SetPinAction(new DataStream((short) 2, PinType.VIRTUAL),\n                \"123\", SetPinActionType.CUSTOM);\n\n        Eventor eventor = new Eventor(new Rule[] {\n                new Rule(triggerStream, null, new StringNotEqual(\"abc\"), new BaseAction[] {setPinAction}, true)\n        });\n\n        clientPair.appClient.createWidget(1, eventor);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 1 ABC\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(1, HARDWARE, b(\"1-0 vw 1 ABC\"))));\n        clientPair.hardwareClient.verifyResult(hardware(888, \"vw 2 123\"));\n        clientPair.appClient.verifyResult(hardware(888, \"1-0 vw 2 123\"));\n    }\n\n    @Test\n    public void testStringEqualsRuleWrongTrigger() throws Exception {\n        DataStream triggerStream = new DataStream((short) 1, PinType.VIRTUAL);\n        SetPinAction setPinAction = new SetPinAction(new DataStream((short) 2, PinType.VIRTUAL),\n                \"123\", SetPinActionType.CUSTOM);\n\n        Eventor eventor = new Eventor(new Rule[] {\n                new Rule(triggerStream, null, new StringEqual(\"abc\"), new BaseAction[] {setPinAction}, true)\n        });\n\n        clientPair.appClient.createWidget(1, eventor);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 1 ABC\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(1, HARDWARE, b(\"1-0 vw 1 ABC\"))));\n        verify(clientPair.hardwareClient.responseMock, never()).channelRead(any(), eq(produce(888, HARDWARE, b(\"vw 2 123\"))));\n        verify(clientPair.appClient.responseMock, never()).channelRead(any(), eq(produce(888, HARDWARE, b(\"1-0 vw 2 123\"))));\n    }\n\n    @Test\n    public void testSetWidgetPropertyViaEventor() throws Exception {\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(1);\n\n        Widget widget = profile.dashBoards[0].findWidgetByPin(0, (short) 4, PinType.VIRTUAL);\n        assertNotNull(widget);\n        assertEquals(\"Some Text\", widget.label);\n\n        DataStream triggerStream = new DataStream((short) 43, PinType.VIRTUAL);\n        SetPropertyPinAction setPropertyPinAction = new SetPropertyPinAction(new DataStream((short) 4, PinType.VIRTUAL),\n                WidgetProperty.LABEL, \"MyNewLabel\");\n\n        Eventor eventor = new Eventor(new Rule[] {\n                new Rule(triggerStream, null, new StringEqual(\"abc\"), new BaseAction[] {setPropertyPinAction}, true)\n        });\n\n        clientPair.appClient.createWidget(1, eventor);\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.hardwareClient.send(\"hardware vw 43 abc\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(1, HARDWARE, b(\"1-0 vw 43 abc\"))));\n        verify(clientPair.hardwareClient.responseMock, never()).channelRead(any(), eq(produce(888, HARDWARE, b(\"vw 4 label MyNewLabel\"))));\n        verify(clientPair.appClient.responseMock, never()).channelRead(any(), eq(produce(888, HARDWARE, b(\"1-0 4 label MyNewLabel\"))));\n        clientPair.appClient.verifyResult(produce(888, SET_WIDGET_PROPERTY, b(\"1-0 4 label MyNewLabel\")));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        profile = clientPair.appClient.parseProfile(1);\n\n        widget = profile.dashBoards[0].findWidgetByPin(0, (short) 4, PinType.VIRTUAL);\n        assertNotNull(widget);\n        assertEquals(\"MyNewLabel\", widget.label);\n    }\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/tcp/FacebookLoginTest.java",
    "content": "package cc.blynk.integration.tcp;\n\nimport cc.blynk.integration.SingleServerInstancePerTest;\nimport cc.blynk.integration.TestUtil;\nimport cc.blynk.integration.model.tcp.ClientPair;\nimport cc.blynk.integration.model.tcp.TestAppClient;\nimport cc.blynk.integration.model.tcp.TestHardClient;\nimport cc.blynk.server.core.model.DataStream;\nimport cc.blynk.server.core.model.Profile;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.widgets.MultiPinWidget;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport io.netty.channel.ChannelFuture;\nimport org.junit.Ignore;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport static cc.blynk.integration.TestUtil.hardwareConnected;\nimport static cc.blynk.integration.TestUtil.ok;\nimport static cc.blynk.integration.TestUtil.parseProfile;\nimport static cc.blynk.integration.TestUtil.readTestUserProfile;\nimport static cc.blynk.integration.TestUtil.saveProfile;\nimport static org.junit.Assert.assertEquals;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.timeout;\nimport static org.mockito.Mockito.verify;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 28.04.16.\n */\n@RunWith(MockitoJUnitRunner.class)\n@Ignore(\"ignored cause requires token to work properly\")\npublic class FacebookLoginTest extends SingleServerInstancePerTest {\n\n    private final String facebookAuthToken = \"\";\n\n    @Test\n    public void testLoginWorksForNewUser() throws Exception {\n        String host = \"localhost\";\n        String email = \"dima@gmail.com\";\n\n        ClientPair clientPair = TestUtil.initAppAndHardPair(host, properties.getHttpsPort(), properties.getHttpPort(), email, \"1\", \"user_profile_json.txt\", properties, 10000);\n\n        ChannelFuture channelFuture = clientPair.appClient.stop();\n        channelFuture.await();\n\n        TestAppClient appClient = new TestAppClient(properties);\n        appClient.start();\n        appClient.send(\"login \" + email + \"\\0\" + facebookAuthToken + \"\\0\" + \"Android\" + \"\\0\" + \"1.10.4\" + \"\\0\" + \"facebook\");\n        verify(appClient.responseMock, timeout(1000)).channelRead(any(), eq(ok(1)));\n\n        String expected = readTestUserProfile();\n\n        appClient.reset();\n        appClient.send(\"loadProfileGzipped\");\n        verify(appClient.responseMock, timeout(500)).channelRead(any(), any());\n\n        Profile profile = appClient.parseProfile(1);\n        profile.dashBoards[0].updatedAt = 0;\n        assertEquals(expected, profile.toString());\n    }\n\n    @Test\n    public void testFacebookLoginWorksForExistingUser() throws Exception {\n        initFacebookAppAndHardPair(\"localhost\", properties.getHttpsPort(), properties.getHttpPort(), \"dima@gmail.com\", facebookAuthToken);\n    }\n\n    private ClientPair initFacebookAppAndHardPair(String host, int appPort, int hardPort, String user, String facebookAuthToken) throws Exception {\n        TestAppClient appClient = new TestAppClient(host, appPort, properties);\n        TestHardClient hardClient = new TestHardClient(host, hardPort);\n\n        appClient.start();\n        hardClient.start();\n\n        String userProfileString = readTestUserProfile(null);\n        Profile profile = parseProfile(userProfileString);\n\n        int expectedSyncCommandsCount = 0;\n        for (Widget widget : profile.dashBoards[0].widgets) {\n            if (widget instanceof OnePinWidget) {\n                if (((OnePinWidget) widget).makeHardwareBody() != null) {\n                    expectedSyncCommandsCount++;\n                }\n            } else if (widget instanceof MultiPinWidget) {\n                MultiPinWidget multiPinWidget = ((MultiPinWidget) widget);\n                if (multiPinWidget.dataStreams != null) {\n                    for (DataStream dataStream : multiPinWidget.dataStreams) {\n                        if (dataStream.notEmptyAndIsValid()) {\n                            expectedSyncCommandsCount++;\n                        }\n                    }\n                }\n            }\n        }\n\n        int dashId = profile.dashBoards[0].id;\n\n        appClient.send(\"login \" + user + \"\\0\" + facebookAuthToken + \"\\0\" + \"Android\" + \"\\0\" + \"1.10.4\" + \"\\0\" + \"facebook\");\n        verify(appClient.responseMock, timeout(1000)).channelRead(any(), eq(ok(1)));\n        appClient.send(\"addEnergy \" + 10000 + \"\\0\" + \"123456\");\n        verify(appClient.responseMock, timeout(1000)).channelRead(any(), eq(ok(2)));\n\n        saveProfile(appClient, profile.dashBoards);\n\n        appClient.activate(dashId);\n        appClient.getDevice(dashId, 0);\n        Device device = appClient.parseDevice(4 + profile.dashBoards.length + expectedSyncCommandsCount);\n        String token = device.token;\n\n        hardClient.login(token);\n        verify(hardClient.responseMock, timeout(2000)).channelRead(any(), eq(ok(1)));\n        verify(appClient.responseMock, timeout(2000)).channelRead(any(), eq(hardwareConnected(1, String.valueOf(dashId))));\n\n        appClient.reset();\n        hardClient.reset();\n\n        return new ClientPair(appClient, hardClient, token);\n    }\n\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/tcp/HistoryGraphTest.java",
    "content": "package cc.blynk.integration.tcp;\n\nimport cc.blynk.integration.BaseTest;\nimport cc.blynk.integration.SingleServerInstancePerTest;\nimport cc.blynk.integration.model.tcp.TestAppClient;\nimport cc.blynk.server.core.dao.ReportingDiskDao;\nimport cc.blynk.server.core.model.DataStream;\nimport cc.blynk.server.core.model.device.BoardType;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.device.Status;\nimport cc.blynk.server.core.model.device.Tag;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.outputs.graph.AggregationFunctionType;\nimport cc.blynk.server.core.model.widgets.outputs.graph.FontSize;\nimport cc.blynk.server.core.model.widgets.outputs.graph.GraphDataStream;\nimport cc.blynk.server.core.model.widgets.outputs.graph.GraphGranularityType;\nimport cc.blynk.server.core.model.widgets.outputs.graph.GraphPeriod;\nimport cc.blynk.server.core.model.widgets.outputs.graph.GraphType;\nimport cc.blynk.server.core.model.widgets.outputs.graph.Superchart;\nimport cc.blynk.server.core.model.widgets.ui.DeviceSelector;\nimport cc.blynk.server.core.model.widgets.ui.reporting.Report;\nimport cc.blynk.server.core.model.widgets.ui.reporting.ReportingWidget;\nimport cc.blynk.server.core.model.widgets.ui.reporting.source.DeviceReportSource;\nimport cc.blynk.server.core.model.widgets.ui.reporting.source.ReportDataStream;\nimport cc.blynk.server.core.model.widgets.ui.reporting.source.ReportSource;\nimport cc.blynk.server.core.model.widgets.ui.reporting.source.TileTemplateReportSource;\nimport cc.blynk.server.core.model.widgets.ui.reporting.type.OneTimeReport;\nimport cc.blynk.server.core.model.widgets.ui.tiles.DeviceTiles;\nimport cc.blynk.server.core.model.widgets.ui.tiles.TileTemplate;\nimport cc.blynk.server.core.model.widgets.ui.tiles.templates.PageTileTemplate;\nimport cc.blynk.server.core.protocol.model.messages.BinaryMessage;\nimport cc.blynk.server.core.protocol.model.messages.ResponseMessage;\nimport cc.blynk.server.core.protocol.model.messages.common.HardwareMessage;\nimport cc.blynk.server.workers.HistoryGraphUnusedPinDataCleanerWorker;\nimport cc.blynk.server.workers.ReportingTruncateWorker;\nimport cc.blynk.utils.FileUtils;\nimport cc.blynk.utils.ReportingUtil;\nimport org.junit.Before;\nimport org.junit.BeforeClass;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport java.io.BufferedReader;\nimport java.io.DataOutputStream;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.nio.ByteBuffer;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.time.ZoneId;\nimport java.util.concurrent.ThreadLocalRandom;\nimport java.util.concurrent.TimeUnit;\nimport java.util.zip.GZIPInputStream;\n\nimport static cc.blynk.integration.TestUtil.b;\nimport static cc.blynk.integration.TestUtil.createDevice;\nimport static cc.blynk.integration.TestUtil.createTag;\nimport static cc.blynk.integration.TestUtil.illegalCommand;\nimport static cc.blynk.integration.TestUtil.ok;\nimport static cc.blynk.server.core.model.serialization.JsonParser.MAPPER;\nimport static cc.blynk.server.core.model.widgets.outputs.graph.GraphPeriod.ONE_HOUR;\nimport static cc.blynk.server.core.model.widgets.outputs.graph.GraphPeriod.SIX_HOURS;\nimport static cc.blynk.server.core.model.widgets.ui.reporting.ReportOutput.CSV_FILE_PER_DEVICE_PER_PIN;\nimport static cc.blynk.server.core.protocol.enums.Response.NO_DATA;\nimport static java.nio.file.StandardOpenOption.APPEND;\nimport static java.nio.file.StandardOpenOption.CREATE;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\nimport static org.junit.Assert.assertTrue;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.contains;\nimport static org.mockito.Mockito.eq;\nimport static org.mockito.Mockito.timeout;\nimport static org.mockito.Mockito.verify;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/2/2015.\n *\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class HistoryGraphTest extends SingleServerInstancePerTest {\n\n    private static String blynkTempDir;\n\n    @BeforeClass\n    public static void initTempFolder() {\n        blynkTempDir = Paths.get(System.getProperty(\"java.io.tmpdir\"), \"blynk\").toString();\n    }\n\n    @Before\n    public void cleanStorage() {\n        holder.reportingDiskDao.averageAggregator.getMinute().clear();\n        holder.reportingDiskDao.averageAggregator.getHourly().clear();\n        holder.reportingDiskDao.averageAggregator.getDaily().clear();\n        holder.reportingDiskDao.rawDataCacheForGraphProcessor.rawStorage.clear();\n    }\n\n    @Test\n    public void testTooManyDataForGraphWorkWithNewProtocol() throws Exception {\n        TestAppClient appClient = new TestAppClient(properties);\n        appClient.start();\n        appClient.login(getUserName(), \"1\", \"Android\", \"2.18.0\");\n        appClient.verifyResult(ok(1));\n\n        String tempDir = holder.props.getProperty(\"data.folder\");\n\n        Superchart enhancedHistoryGraph = new Superchart();\n        enhancedHistoryGraph.id = 432;\n        enhancedHistoryGraph.width = 8;\n        enhancedHistoryGraph.height = 4;\n        DataStream dataStream1 = new DataStream((short) 8, PinType.DIGITAL);\n        DataStream dataStream2 = new DataStream((short) 9, PinType.DIGITAL);\n        DataStream dataStream3 = new DataStream((short) 10, PinType.DIGITAL);\n        DataStream dataStream4 = new DataStream((short) 11, PinType.DIGITAL);\n        GraphDataStream graphDataStream1 = new GraphDataStream(null, GraphType.LINE, 0, 0, dataStream1, AggregationFunctionType.MAX, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        GraphDataStream graphDataStream2 = new GraphDataStream(null, GraphType.LINE, 0, 0, dataStream2, AggregationFunctionType.MAX, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        GraphDataStream graphDataStream3 = new GraphDataStream(null, GraphType.LINE, 0, 0, dataStream3, AggregationFunctionType.MAX, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        GraphDataStream graphDataStream4 = new GraphDataStream(null, GraphType.LINE, 0, 0, dataStream4, AggregationFunctionType.MAX, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        enhancedHistoryGraph.dataStreams = new GraphDataStream[] {\n                graphDataStream1,\n                graphDataStream2,\n                graphDataStream3,\n                graphDataStream4\n        };\n\n        appClient.createWidget(1, enhancedHistoryGraph);\n        appClient.verifyResult(ok(1));\n        appClient.reset();\n\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n\n        Path pinReportingDataPath1 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.DIGITAL, (short) 8, GraphPeriod.THREE_MONTHS.granularityType));\n        Path pinReportingDataPath2 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.DIGITAL, (short) 9, GraphPeriod.THREE_MONTHS.granularityType));\n        Path pinReportingDataPath3 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.DIGITAL, (short) 10, GraphPeriod.THREE_MONTHS.granularityType));\n        Path pinReportingDataPath4 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.DIGITAL, (short) 11, GraphPeriod.THREE_MONTHS.granularityType));\n\n        for (int i = 0; i < GraphPeriod.THREE_MONTHS.numberOfPoints; i++) {\n            long now = System.currentTimeMillis();\n            FileUtils.write(pinReportingDataPath1, ThreadLocalRandom.current().nextDouble(), now);\n            FileUtils.write(pinReportingDataPath2, ThreadLocalRandom.current().nextDouble(), now);\n            FileUtils.write(pinReportingDataPath3, ThreadLocalRandom.current().nextDouble(), now);\n            FileUtils.write(pinReportingDataPath4, ThreadLocalRandom.current().nextDouble(), now);\n        }\n\n        appClient.reset();\n        appClient.getEnhancedGraphData(1, 432, GraphPeriod.THREE_MONTHS);\n        BinaryMessage graphDataResponse = appClient.getBinaryBody();\n\n        assertNotNull(graphDataResponse);\n        byte[] decompressedGraphData = BaseTest.decompress(graphDataResponse.getBytes());\n        assertNotNull(decompressedGraphData);\n    }\n\n    @Test\n    public void testGetGraphDataForTagAndForEnhancedGraph1StreamWithoutData() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        Tag tag0 = new Tag(100_000, \"Tag1\", new int[] {0,1});\n\n        clientPair.appClient.createTag(1, tag0);\n        String createdTag = clientPair.appClient.getBody(2);\n        Tag tag = JsonParser.parseTag(createdTag, 0);\n        assertNotNull(tag);\n        assertEquals(100_000, tag.id);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(createTag(2, tag)));\n\n\n        String tempDir = holder.props.getProperty(\"data.folder\");\n\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n\n        Path pinReportingDataPath = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.VIRTUAL, (short) 88, GraphPeriod.DAY.granularityType));\n        FileUtils.write(pinReportingDataPath, 1.11D, 1111111);\n        FileUtils.write(pinReportingDataPath, 1.22D, 2222222);\n\n        Superchart enhancedHistoryGraph = new Superchart();\n        enhancedHistoryGraph.id = 432;\n        enhancedHistoryGraph.width = 8;\n        enhancedHistoryGraph.height = 4;\n        DataStream dataStream = new DataStream((short) 88, PinType.VIRTUAL);\n        DataStream dataStream2 = new DataStream((short) 89, PinType.VIRTUAL);\n        GraphDataStream graphDataStream = new GraphDataStream(null, GraphType.LINE, 0, 100_000, dataStream, AggregationFunctionType.MAX, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        GraphDataStream graphDataStream2 = new GraphDataStream(null, GraphType.LINE, 0, 1, dataStream2, AggregationFunctionType.MAX, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        enhancedHistoryGraph.dataStreams = new GraphDataStream[] {\n                graphDataStream,\n                graphDataStream2\n        };\n\n        clientPair.appClient.createWidget(1, enhancedHistoryGraph);\n        clientPair.appClient.verifyResult(ok(3));\n        clientPair.appClient.reset();\n\n        clientPair.appClient.getEnhancedGraphData(1, 432, GraphPeriod.DAY);\n        BinaryMessage graphDataResponse = clientPair.appClient.getBinaryBody();\n\n        assertNotNull(graphDataResponse);\n        byte[] decompressedGraphData = BaseTest.decompress(graphDataResponse.getBytes());\n        ByteBuffer bb = ByteBuffer.wrap(decompressedGraphData);\n\n        assertEquals(1, bb.getInt());\n        assertEquals(2, bb.getInt());\n        assertEquals(1.11D, bb.getDouble(), 0.1);\n        assertEquals(1111111, bb.getLong());\n        assertEquals(1.22D, bb.getDouble(), 0.1);\n        assertEquals(2222222, bb.getLong());\n    }\n\n    @Test\n    public void testGetGraphDataForTagAndForEnhancedGraphMAX() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        Tag tag0 = new Tag(100_000, \"Tag1\", new int[] {0,1});\n\n        clientPair.appClient.createTag(1, tag0);\n        String createdTag = clientPair.appClient.getBody(2);\n        Tag tag = JsonParser.parseTag(createdTag, 0);\n        assertNotNull(tag);\n        assertEquals(100_000, tag.id);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(createTag(2, tag)));\n\n\n        String tempDir = holder.props.getProperty(\"data.folder\");\n\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n\n        Path pinReportingDataPath = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.VIRTUAL, (short) 88, GraphPeriod.DAY.granularityType));\n        FileUtils.write(pinReportingDataPath, 1.11D, 1111111);\n        FileUtils.write(pinReportingDataPath, 1.22D, 2222222);\n\n        Path pinReportingDataPath2 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 1, PinType.VIRTUAL, (short) 88, GraphPeriod.DAY.granularityType));\n        FileUtils.write(pinReportingDataPath2, 1.112D, 1111111);\n        FileUtils.write(pinReportingDataPath2, 1.222D, 2222222);\n\n        Superchart enhancedHistoryGraph = new Superchart();\n        enhancedHistoryGraph.id = 432;\n        enhancedHistoryGraph.width = 8;\n        enhancedHistoryGraph.height = 4;\n        DataStream dataStream = new DataStream((short) 88, PinType.VIRTUAL);\n        GraphDataStream graphDataStream = new GraphDataStream(null, GraphType.LINE, 0, 100_000, dataStream, AggregationFunctionType.MAX, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        enhancedHistoryGraph.dataStreams = new GraphDataStream[] {\n                graphDataStream\n        };\n\n        clientPair.appClient.createWidget(1, enhancedHistoryGraph);\n        clientPair.appClient.verifyResult(ok(3));\n        clientPair.appClient.reset();\n\n        clientPair.appClient.getEnhancedGraphData(1, 432, GraphPeriod.DAY);\n        BinaryMessage graphDataResponse = clientPair.appClient.getBinaryBody();\n\n        assertNotNull(graphDataResponse);\n        byte[] decompressedGraphData = BaseTest.decompress(graphDataResponse.getBytes());\n        ByteBuffer bb = ByteBuffer.wrap(decompressedGraphData);\n\n        assertEquals(1, bb.getInt());\n        assertEquals(2, bb.getInt());\n        assertEquals(1.112D, bb.getDouble(), 0.1);\n        assertEquals(1111111, bb.getLong());\n        assertEquals(1.222D, bb.getDouble(), 0.1);\n        assertEquals(2222222, bb.getLong());\n    }\n\n    @Test\n    public void testGetGraphDataForTagAndForEnhancedGraphMIN() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        Tag tag0 = new Tag(100_000, \"Tag1\", new int[] {0,1});\n\n        clientPair.appClient.createTag(1, tag0);\n        String createdTag = clientPair.appClient.getBody(2);\n        Tag tag = JsonParser.parseTag(createdTag, 0);\n        assertNotNull(tag);\n        assertEquals(100_000, tag.id);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(createTag(2, tag)));\n\n\n        String tempDir = holder.props.getProperty(\"data.folder\");\n\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n\n        Path pinReportingDataPath = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.VIRTUAL, (short) 88, GraphPeriod.DAY.granularityType));\n        FileUtils.write(pinReportingDataPath, 1.11D, 1111111);\n        FileUtils.write(pinReportingDataPath, 1.22D, 2222222);\n\n        Path pinReportingDataPath2 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 1, PinType.VIRTUAL, (short) 88, GraphPeriod.DAY.granularityType));\n        FileUtils.write(pinReportingDataPath2, 1.112D, 1111111);\n        FileUtils.write(pinReportingDataPath2, 1.222D, 2222222);\n\n        Superchart enhancedHistoryGraph = new Superchart();\n        enhancedHistoryGraph.id = 432;\n        enhancedHistoryGraph.width = 8;\n        enhancedHistoryGraph.height = 4;\n        DataStream dataStream = new DataStream((short) 88, PinType.VIRTUAL);\n        GraphDataStream graphDataStream = new GraphDataStream(null, GraphType.LINE, 0, 100_000, dataStream, AggregationFunctionType.MIN, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        enhancedHistoryGraph.dataStreams = new GraphDataStream[] {\n                graphDataStream\n        };\n\n        clientPair.appClient.createWidget(1, enhancedHistoryGraph);\n        clientPair.appClient.verifyResult(ok(3));\n        clientPair.appClient.reset();\n\n        clientPair.appClient.getEnhancedGraphData(1, 432, GraphPeriod.DAY);\n        BinaryMessage graphDataResponse = clientPair.appClient.getBinaryBody();\n\n        assertNotNull(graphDataResponse);\n        byte[] decompressedGraphData = BaseTest.decompress(graphDataResponse.getBytes());\n        ByteBuffer bb = ByteBuffer.wrap(decompressedGraphData);\n\n        assertEquals(1, bb.getInt());\n        assertEquals(2, bb.getInt());\n        assertEquals(1.11D, bb.getDouble(), 0.1);\n        assertEquals(1111111, bb.getLong());\n        assertEquals(1.22D, bb.getDouble(), 0.1);\n        assertEquals(2222222, bb.getLong());\n    }\n\n    @Test\n    public void testGetGraphDataForTagAndForEnhancedGraphSUM() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        Tag tag0 = new Tag(100_000, \"Tag1\", new int[] {0,1});\n\n        clientPair.appClient.createTag(1, tag0);\n        String createdTag = clientPair.appClient.getBody(2);\n        Tag tag = JsonParser.parseTag(createdTag, 0);\n        assertNotNull(tag);\n        assertEquals(100_000, tag.id);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(createTag(2, tag)));\n\n\n        String tempDir = holder.props.getProperty(\"data.folder\");\n\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n\n        Path pinReportingDataPath = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.VIRTUAL, (short) 88, GraphPeriod.DAY.granularityType));\n        FileUtils.write(pinReportingDataPath, 1.11D, 1111111);\n        FileUtils.write(pinReportingDataPath, 1.22D, 2222222);\n\n        Path pinReportingDataPath2 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 1, PinType.VIRTUAL, (short) 88, GraphPeriod.DAY.granularityType));\n        FileUtils.write(pinReportingDataPath2, 1.112D, 1111111);\n        FileUtils.write(pinReportingDataPath2, 1.222D, 2222222);\n\n        Superchart enhancedHistoryGraph = new Superchart();\n        enhancedHistoryGraph.id = 432;\n        enhancedHistoryGraph.width = 8;\n        enhancedHistoryGraph.height = 4;\n        DataStream dataStream = new DataStream((short) 88, PinType.VIRTUAL);\n        GraphDataStream graphDataStream = new GraphDataStream(null, GraphType.LINE, 0, 100_000, dataStream, AggregationFunctionType.SUM, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        enhancedHistoryGraph.dataStreams = new GraphDataStream[] {\n                graphDataStream\n        };\n\n        clientPair.appClient.createWidget(1, enhancedHistoryGraph);\n        clientPair.appClient.verifyResult(ok(3));\n        clientPair.appClient.reset();\n\n        clientPair.appClient.getEnhancedGraphData(1, 432, GraphPeriod.DAY);\n        BinaryMessage graphDataResponse = clientPair.appClient.getBinaryBody();\n\n        assertNotNull(graphDataResponse);\n        byte[] decompressedGraphData = BaseTest.decompress(graphDataResponse.getBytes());\n        ByteBuffer bb = ByteBuffer.wrap(decompressedGraphData);\n\n        assertEquals(1, bb.getInt());\n        assertEquals(2, bb.getInt());\n        assertEquals(2.222D, bb.getDouble(), 0.001);\n        assertEquals(1111111, bb.getLong());\n        assertEquals(2.442D, bb.getDouble(), 0.001);\n        assertEquals(2222222, bb.getLong());\n    }\n\n    @Test\n    public void testGetGraphDataForTagAndForEnhancedGraphAVG() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        Tag tag0 = new Tag(100_000, \"Tag1\", new int[] {0,1});\n\n        clientPair.appClient.createTag(1, tag0);\n        String createdTag = clientPair.appClient.getBody(2);\n        Tag tag = JsonParser.parseTag(createdTag, 0);\n        assertNotNull(tag);\n        assertEquals(100_000, tag.id);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(createTag(2, tag)));\n\n\n        String tempDir = holder.props.getProperty(\"data.folder\");\n\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n\n        Path pinReportingDataPath = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.VIRTUAL, (short) 88, GraphPeriod.DAY.granularityType));\n        FileUtils.write(pinReportingDataPath, 1.11D, 1111111);\n        FileUtils.write(pinReportingDataPath, 1.22D, 2222222);\n\n        Path pinReportingDataPath2 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 1, PinType.VIRTUAL, (short) 88, GraphPeriod.DAY.granularityType));\n        FileUtils.write(pinReportingDataPath2, 1.112D, 1111111);\n        FileUtils.write(pinReportingDataPath2, 1.222D, 2222222);\n\n        Superchart enhancedHistoryGraph = new Superchart();\n        enhancedHistoryGraph.id = 432;\n        enhancedHistoryGraph.width = 8;\n        enhancedHistoryGraph.height = 4;\n        DataStream dataStream = new DataStream((short) 88, PinType.VIRTUAL);\n        GraphDataStream graphDataStream = new GraphDataStream(null, GraphType.LINE, 0, 100_000, dataStream, AggregationFunctionType.AVG, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        enhancedHistoryGraph.dataStreams = new GraphDataStream[] {\n                graphDataStream\n        };\n\n        clientPair.appClient.createWidget(1, enhancedHistoryGraph);\n        clientPair.appClient.verifyResult(ok(3));\n        clientPair.appClient.reset();\n\n        clientPair.appClient.getEnhancedGraphData(1, 432, GraphPeriod.DAY);\n        BinaryMessage graphDataResponse = clientPair.appClient.getBinaryBody();\n\n        assertNotNull(graphDataResponse);\n        byte[] decompressedGraphData = BaseTest.decompress(graphDataResponse.getBytes());\n        ByteBuffer bb = ByteBuffer.wrap(decompressedGraphData);\n\n        assertEquals(1, bb.getInt());\n        assertEquals(2, bb.getInt());\n        assertEquals(1.111D, bb.getDouble(), 0.001);\n        assertEquals(1111111, bb.getLong());\n        assertEquals(1.221D, bb.getDouble(), 0.001);\n        assertEquals(2222222, bb.getLong());\n    }\n\n    @Test\n    public void testGetGraphDataForTagAndForEnhancedGraphMEDIAN() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        Tag tag0 = new Tag(100_000, \"Tag1\", new int[] {0,1});\n\n        clientPair.appClient.createTag(1, tag0);\n        String createdTag = clientPair.appClient.getBody(2);\n        Tag tag = JsonParser.parseTag(createdTag, 0);\n        assertNotNull(tag);\n        assertEquals(100_000, tag.id);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(createTag(2, tag)));\n\n\n        String tempDir = holder.props.getProperty(\"data.folder\");\n\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n\n        Path pinReportingDataPath = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.VIRTUAL, (short) 88, GraphPeriod.DAY.granularityType));\n        FileUtils.write(pinReportingDataPath, 1.11D, 1111111);\n        FileUtils.write(pinReportingDataPath, 1.22D, 2222222);\n\n        Path pinReportingDataPath2 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 1, PinType.VIRTUAL, (short) 88, GraphPeriod.DAY.granularityType));\n        FileUtils.write(pinReportingDataPath2, 1.112D, 1111111);\n        FileUtils.write(pinReportingDataPath2, 1.222D, 2222222);\n\n        Superchart enhancedHistoryGraph = new Superchart();\n        enhancedHistoryGraph.id = 432;\n        enhancedHistoryGraph.width = 8;\n        enhancedHistoryGraph.height = 4;\n        DataStream dataStream = new DataStream((short) 88, PinType.VIRTUAL);\n        GraphDataStream graphDataStream = new GraphDataStream(null, GraphType.LINE, 0, 100_000, dataStream, AggregationFunctionType.MED, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        enhancedHistoryGraph.dataStreams = new GraphDataStream[] {\n                graphDataStream\n        };\n\n        clientPair.appClient.createWidget(1, enhancedHistoryGraph);\n        clientPair.appClient.verifyResult(ok(3));\n        clientPair.appClient.reset();\n\n        clientPair.appClient.getEnhancedGraphData(1, 432, GraphPeriod.DAY);\n        BinaryMessage graphDataResponse = clientPair.appClient.getBinaryBody();\n\n        assertNotNull(graphDataResponse);\n        byte[] decompressedGraphData = BaseTest.decompress(graphDataResponse.getBytes());\n        ByteBuffer bb = ByteBuffer.wrap(decompressedGraphData);\n\n        assertEquals(1, bb.getInt());\n        assertEquals(2, bb.getInt());\n        assertEquals(1.112D, bb.getDouble(), 0.001);\n        assertEquals(1111111, bb.getLong());\n        assertEquals(1.222D, bb.getDouble(), 0.001);\n        assertEquals(2222222, bb.getLong());\n    }\n\n    @Test\n    public void testGetGraphDataForTagAndForEnhancedGraphMEDIANFor3Devices() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        Device device2 = new Device(2, \"My Device\", BoardType.ESP8266);\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        clientPair.appClient.createDevice(1, device2);\n        device = clientPair.appClient.parseDevice(2);\n        assertNotNull(device);\n        assertNotNull(device.token);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(createDevice(2, device)));\n\n        Tag tag0 = new Tag(100_000, \"Tag1\", new int[] {0, 1, 2});\n\n        clientPair.appClient.createTag(1, tag0);\n        String createdTag = clientPair.appClient.getBody(3);\n        Tag tag = JsonParser.parseTag(createdTag, 0);\n        assertNotNull(tag);\n        assertEquals(100_000, tag.id);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(createTag(3, tag)));\n\n\n        String tempDir = holder.props.getProperty(\"data.folder\");\n\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n\n        Path pinReportingDataPath = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.VIRTUAL, (short) 88, GraphPeriod.DAY.granularityType));\n        FileUtils.write(pinReportingDataPath, 1.11D, 1111111);\n        FileUtils.write(pinReportingDataPath, 1.22D, 2222222);\n\n        Path pinReportingDataPath2 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 1, PinType.VIRTUAL, (short) 88, GraphPeriod.DAY.granularityType));\n        FileUtils.write(pinReportingDataPath2, 1.112D, 1111111);\n        FileUtils.write(pinReportingDataPath2, 1.222D, 2222222);\n\n        Path pinReportingDataPath3 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 2, PinType.VIRTUAL, (short) 88, GraphPeriod.DAY.granularityType));\n        FileUtils.write(pinReportingDataPath3, 1.113D, 1111111);\n        FileUtils.write(pinReportingDataPath3, 1.223D, 2222222);\n\n        Superchart enhancedHistoryGraph = new Superchart();\n        enhancedHistoryGraph.id = 432;\n        enhancedHistoryGraph.width = 8;\n        enhancedHistoryGraph.height = 4;\n        DataStream dataStream = new DataStream((short) 88, PinType.VIRTUAL);\n        GraphDataStream graphDataStream = new GraphDataStream(null, GraphType.LINE, 0, 100_000, dataStream, AggregationFunctionType.MED, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        enhancedHistoryGraph.dataStreams = new GraphDataStream[] {\n                graphDataStream\n        };\n\n        clientPair.appClient.createWidget(1, enhancedHistoryGraph);\n        clientPair.appClient.verifyResult(ok(4));\n        clientPair.appClient.reset();\n\n        clientPair.appClient.getEnhancedGraphData(1, 432, GraphPeriod.DAY);\n        BinaryMessage graphDataResponse = clientPair.appClient.getBinaryBody();\n\n        assertNotNull(graphDataResponse);\n        byte[] decompressedGraphData = BaseTest.decompress(graphDataResponse.getBytes());\n        ByteBuffer bb = ByteBuffer.wrap(decompressedGraphData);\n\n        assertEquals(1, bb.getInt());\n        assertEquals(2, bb.getInt());\n        assertEquals(1.112D, bb.getDouble(), 0.001);\n        assertEquals(1111111, bb.getLong());\n        assertEquals(1.222D, bb.getDouble(), 0.001);\n        assertEquals(2222222, bb.getLong());\n    }\n\n    @Test\n    public void testGetGraphDataForEnhancedGraphWithEmptyDataStream() throws Exception {\n        String tempDir = holder.props.getProperty(\"data.folder\");\n\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n\n        Path pinReportingDataPath = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.DIGITAL, (short) 8, GraphGranularityType.HOURLY));\n\n        FileUtils.write(pinReportingDataPath, 1.11D, 1111111);\n        FileUtils.write(pinReportingDataPath, 1.22D, 2222222);\n\n        Superchart enhancedHistoryGraph = new Superchart();\n        enhancedHistoryGraph.id = 432;\n        enhancedHistoryGraph.width = 8;\n        enhancedHistoryGraph.height = 4;\n        GraphDataStream graphDataStream = new GraphDataStream(null, GraphType.LINE, 0, 0, null, null, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        enhancedHistoryGraph.dataStreams = new GraphDataStream[] {\n                graphDataStream\n        };\n\n        clientPair.appClient.createWidget(1, enhancedHistoryGraph);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.getEnhancedGraphData(1, 432, GraphPeriod.DAY);\n\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new ResponseMessage(2, NO_DATA)));\n    }\n\n    @Test\n    public void testGetGraphDataForEnhancedGraph() throws Exception {\n        String tempDir = holder.props.getProperty(\"data.folder\");\n\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n\n        Path pinReportingDataPath = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.DIGITAL, (short) 8, GraphPeriod.ONE_HOUR.granularityType));\n\n        for (int point = 0; point < GraphPeriod.ONE_HOUR.numberOfPoints + 1; point++) {\n            FileUtils.write(pinReportingDataPath, (double) point, 1111111 + point);\n        }\n\n        Superchart enhancedHistoryGraph = new Superchart();\n        enhancedHistoryGraph.id = 432;\n        enhancedHistoryGraph.width = 8;\n        enhancedHistoryGraph.height = 4;\n        DataStream dataStream = new DataStream((short) 8, PinType.DIGITAL);\n        GraphDataStream graphDataStream = new GraphDataStream(null, GraphType.LINE, 0, 0, dataStream, null, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        enhancedHistoryGraph.dataStreams = new GraphDataStream[] {\n                graphDataStream\n        };\n\n        clientPair.appClient.createWidget(1, enhancedHistoryGraph);\n        clientPair.appClient.verifyResult(ok(1));\n        clientPair.appClient.reset();\n\n        clientPair.appClient.getEnhancedGraphData(1, 432, GraphPeriod.ONE_HOUR);\n\n        BinaryMessage graphDataResponse = clientPair.appClient.getBinaryBody();\n\n        assertNotNull(graphDataResponse);\n        byte[] decompressedGraphData = BaseTest.decompress(graphDataResponse.getBytes());\n        ByteBuffer bb = ByteBuffer.wrap(decompressedGraphData);\n\n        assertEquals(1, bb.getInt());\n        assertEquals(60, bb.getInt());\n        for (int point = 1; point < GraphPeriod.ONE_HOUR.numberOfPoints + 1; point++) {\n            assertEquals(point, bb.getDouble(), 0.1);\n            assertEquals(1111111 + point , bb.getLong());\n        }\n    }\n\n    @Test\n    public void testGetGraphDataForEnhancedGraphFor2Streams() throws Exception {\n        String tempDir = holder.props.getProperty(\"data.folder\");\n\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n\n        Path pinReportingDataPath = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.DIGITAL, (short) 8, GraphPeriod.DAY.granularityType));\n\n        FileUtils.write(pinReportingDataPath, 1.11D, 1111111);\n        FileUtils.write(pinReportingDataPath, 1.22D, 2222222);\n\n        Superchart enhancedHistoryGraph = new Superchart();\n        enhancedHistoryGraph.id = 432;\n        enhancedHistoryGraph.width = 8;\n        enhancedHistoryGraph.height = 4;\n        DataStream dataStream = new DataStream((short) 8, PinType.DIGITAL);\n        DataStream dataStream2 = new DataStream((short) 9, PinType.DIGITAL);\n        GraphDataStream graphDataStream = new GraphDataStream(null, GraphType.LINE, 0, 0, dataStream, null, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        GraphDataStream graphDataStream2 = new GraphDataStream(null, GraphType.LINE, 0, 0, dataStream2, null, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        enhancedHistoryGraph.dataStreams = new GraphDataStream[] {\n                graphDataStream,\n                graphDataStream2\n        };\n\n        clientPair.appClient.createWidget(1, enhancedHistoryGraph);\n        clientPair.appClient.verifyResult(ok(1));\n        clientPair.appClient.reset();\n\n        clientPair.appClient.getEnhancedGraphData(1, 432, GraphPeriod.DAY);\n        BinaryMessage graphDataResponse = clientPair.appClient.getBinaryBody();\n\n        assertNotNull(graphDataResponse);\n        byte[] decompressedGraphData = BaseTest.decompress(graphDataResponse.getBytes());\n        ByteBuffer bb = ByteBuffer.wrap(decompressedGraphData);\n\n        assertEquals(1, bb.getInt());\n        assertEquals(2, bb.getInt());\n        assertEquals(1.11D, bb.getDouble(), 0.1);\n        assertEquals(1111111, bb.getLong());\n        assertEquals(1.22D, bb.getDouble(), 0.1);\n        assertEquals(2222222, bb.getLong());\n        assertEquals(0, bb.getInt());\n    }\n\n    @Test\n    public void testGetGraphDataForEnhancedGraphWithWrongDataStream() throws Exception {\n        String tempDir = holder.props.getProperty(\"data.folder\");\n\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n\n        Path pinReportingDataPath = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.DIGITAL, (short) 8, GraphGranularityType.HOURLY));\n\n        FileUtils.write(pinReportingDataPath, 1.11D, 1111111);\n        FileUtils.write(pinReportingDataPath, 1.22D, 2222222);\n\n        Superchart enhancedHistoryGraph = new Superchart();\n        enhancedHistoryGraph.id = 432;\n        enhancedHistoryGraph.width = 8;\n        enhancedHistoryGraph.height = 4;\n        DataStream dataStream = new DataStream((short) 8, null);\n        GraphDataStream graphDataStream = new GraphDataStream(null, GraphType.LINE, 0, 0, dataStream, null, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        enhancedHistoryGraph.dataStreams = new GraphDataStream[] {\n                graphDataStream\n        };\n\n        clientPair.appClient.createWidget(1, enhancedHistoryGraph);\n        clientPair.appClient.verifyResult(ok(1));\n        clientPair.appClient.reset();\n\n        clientPair.appClient.getEnhancedGraphData(1, 432, GraphPeriod.DAY);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new ResponseMessage(1, NO_DATA)));\n    }\n\n    @Test\n    public void makeSureNoReportingWhenNotAGraphPin() throws Exception {\n        Superchart enhancedHistoryGraph = new Superchart();\n        enhancedHistoryGraph.id = 432;\n        enhancedHistoryGraph.width = 8;\n        enhancedHistoryGraph.height = 4;\n        DataStream dataStream = new DataStream((short) 88, PinType.VIRTUAL);\n        GraphDataStream graphDataStream = new GraphDataStream(null, GraphType.LINE, 0, 0, dataStream, null, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        enhancedHistoryGraph.dataStreams = new GraphDataStream[] {\n                graphDataStream\n        };\n\n        clientPair.appClient.createWidget(1, enhancedHistoryGraph);\n        clientPair.appClient.verifyResult(ok(1));\n        clientPair.appClient.reset();\n\n        assertEquals(0, holder.reportingDiskDao.averageAggregator.getMinute().size());\n        assertEquals(0, holder.reportingDiskDao.averageAggregator.getHourly().size());\n        assertEquals(0, holder.reportingDiskDao.averageAggregator.getDaily().size());\n        assertEquals(0, holder.reportingDiskDao.rawDataProcessor.rawStorage.size());\n\n        clientPair.hardwareClient.send(\"hardware vw 89 111\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new HardwareMessage(1, b(\"1-0 vw 89 111\"))));\n\n        assertEquals(0, holder.reportingDiskDao.averageAggregator.getMinute().size());\n        assertEquals(0, holder.reportingDiskDao.averageAggregator.getHourly().size());\n        assertEquals(0, holder.reportingDiskDao.averageAggregator.getDaily().size());\n        assertEquals(0, holder.reportingDiskDao.rawDataProcessor.rawStorage.size());\n    }\n\n    @Test\n    public void makeSureReportingIsPresentWhenGraphAssignedToDevice() throws Exception {\n        Superchart enhancedHistoryGraph = new Superchart();\n        enhancedHistoryGraph.id = 432;\n        enhancedHistoryGraph.width = 8;\n        enhancedHistoryGraph.height = 4;\n        DataStream dataStream = new DataStream((short) 88, PinType.VIRTUAL);\n        GraphDataStream graphDataStream = new GraphDataStream(null, GraphType.LINE, 0, 0, dataStream, null, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        enhancedHistoryGraph.dataStreams = new GraphDataStream[] {\n                graphDataStream\n        };\n\n        clientPair.appClient.createWidget(1, enhancedHistoryGraph);\n        clientPair.appClient.verifyResult(ok(1));\n        clientPair.appClient.reset();\n\n        assertEquals(0, holder.reportingDiskDao.averageAggregator.getMinute().size());\n        assertEquals(0, holder.reportingDiskDao.averageAggregator.getHourly().size());\n        assertEquals(0, holder.reportingDiskDao.averageAggregator.getDaily().size());\n        assertEquals(0, holder.reportingDiskDao.rawDataCacheForGraphProcessor.rawStorage.size());\n        assertEquals(0, holder.reportingDiskDao.rawDataProcessor.rawStorage.size());\n\n        clientPair.hardwareClient.send(\"hardware vw 88 111\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new HardwareMessage(1, b(\"1-0 vw 88 111\"))));\n\n        assertEquals(1, holder.reportingDiskDao.averageAggregator.getMinute().size());\n        assertEquals(1, holder.reportingDiskDao.averageAggregator.getHourly().size());\n        assertEquals(1, holder.reportingDiskDao.averageAggregator.getDaily().size());\n        assertEquals(1, holder.reportingDiskDao.rawDataCacheForGraphProcessor.rawStorage.size());\n        assertEquals(0, holder.reportingDiskDao.rawDataProcessor.rawStorage.size());\n    }\n\n    @Test\n    public void makeSureReportingIsPresentWhenGraphAssignedToDevice2() throws Exception {\n        Superchart enhancedHistoryGraph = new Superchart();\n        enhancedHistoryGraph.id = 432;\n        enhancedHistoryGraph.width = 8;\n        enhancedHistoryGraph.height = 4;\n        //no live\n        enhancedHistoryGraph.selectedPeriods = new GraphPeriod[] {\n                ONE_HOUR, SIX_HOURS\n        };\n        DataStream dataStream = new DataStream((short) 88, PinType.VIRTUAL);\n        GraphDataStream graphDataStream = new GraphDataStream(null, GraphType.LINE, 0, 0, dataStream, null, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        enhancedHistoryGraph.dataStreams = new GraphDataStream[] {\n                graphDataStream\n        };\n\n        clientPair.appClient.createWidget(1, enhancedHistoryGraph);\n        clientPair.appClient.verifyResult(ok(1));\n        clientPair.appClient.reset();\n\n        assertEquals(0, holder.reportingDiskDao.averageAggregator.getMinute().size());\n        assertEquals(0, holder.reportingDiskDao.averageAggregator.getHourly().size());\n        assertEquals(0, holder.reportingDiskDao.averageAggregator.getDaily().size());\n        assertEquals(0, holder.reportingDiskDao.rawDataCacheForGraphProcessor.rawStorage.size());\n        assertEquals(0, holder.reportingDiskDao.rawDataProcessor.rawStorage.size());\n\n        clientPair.hardwareClient.send(\"hardware vw 88 111\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new HardwareMessage(1, b(\"1-0 vw 88 111\"))));\n\n        assertEquals(1, holder.reportingDiskDao.averageAggregator.getMinute().size());\n        assertEquals(1, holder.reportingDiskDao.averageAggregator.getHourly().size());\n        assertEquals(1, holder.reportingDiskDao.averageAggregator.getDaily().size());\n        assertEquals(0, holder.reportingDiskDao.rawDataCacheForGraphProcessor.rawStorage.size());\n        assertEquals(0, holder.reportingDiskDao.rawDataProcessor.rawStorage.size());\n    }\n\n    @Test\n    public void makeSureReportingIsPresentWhenGraphAssignedToDeviceTiles() throws Exception {\n        long widgetId = 21321;\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = widgetId;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(1));\n\n        int[] deviceIds = new int[] {0};\n\n        Superchart enhancedHistoryGraph = new Superchart();\n        enhancedHistoryGraph.id = 432;\n        enhancedHistoryGraph.width = 8;\n        enhancedHistoryGraph.height = 4;\n        GraphDataStream graphDataStream = new GraphDataStream(\n                null, GraphType.LINE, 0, -1,\n                new DataStream((short) 88, PinType.VIRTUAL),\n                AggregationFunctionType.MAX, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        enhancedHistoryGraph.dataStreams = new GraphDataStream[] {\n                graphDataStream\n        };\n\n        TileTemplate tileTemplate = new PageTileTemplate(1,\n                new Widget[]{enhancedHistoryGraph}, deviceIds, \"name\", \"name\", \"iconName\", BoardType.ESP8266, new DataStream((short)1, PinType.VIRTUAL),\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.createTemplate(1, widgetId, tileTemplate);\n        clientPair.appClient.verifyResult(ok(2));\n\n        assertEquals(0, holder.reportingDiskDao.averageAggregator.getMinute().size());\n        assertEquals(0, holder.reportingDiskDao.averageAggregator.getHourly().size());\n        assertEquals(0, holder.reportingDiskDao.averageAggregator.getDaily().size());\n        assertEquals(0, holder.reportingDiskDao.rawDataCacheForGraphProcessor.rawStorage.size());\n        assertEquals(0, holder.reportingDiskDao.rawDataProcessor.rawStorage.size());\n\n        clientPair.hardwareClient.send(\"hardware vw 88 111\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new HardwareMessage(1, b(\"1-0 vw 88 111\"))));\n\n        assertEquals(1, holder.reportingDiskDao.averageAggregator.getMinute().size());\n        assertEquals(1, holder.reportingDiskDao.averageAggregator.getHourly().size());\n        assertEquals(1, holder.reportingDiskDao.averageAggregator.getDaily().size());\n        assertEquals(1, holder.reportingDiskDao.rawDataCacheForGraphProcessor.rawStorage.size());\n        assertEquals(0, holder.reportingDiskDao.rawDataProcessor.rawStorage.size());\n    }\n\n    @Test\n    public void makeSureReportingIsPresentWhenGraphAssignedToDeviceTilesWith2Pins() throws Exception {\n        long widgetId = 21321;\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = widgetId;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(1));\n\n        int[] deviceIds = new int[] {0};\n\n        Superchart enhancedHistoryGraph = new Superchart();\n        enhancedHistoryGraph.id = 432;\n        enhancedHistoryGraph.width = 8;\n        enhancedHistoryGraph.height = 4;\n        GraphDataStream graphDataStream = new GraphDataStream(\n                null, GraphType.LINE, 0, -1,\n                new DataStream((short) 88, PinType.VIRTUAL),\n                AggregationFunctionType.MAX, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        GraphDataStream graphDataStream2 = new GraphDataStream(\n                null, GraphType.LINE, 0, -1,\n                new DataStream((short) 89, PinType.VIRTUAL),\n                AggregationFunctionType.MAX, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        enhancedHistoryGraph.dataStreams = new GraphDataStream[] {\n                graphDataStream,\n                graphDataStream2\n        };\n\n        TileTemplate tileTemplate = new PageTileTemplate(1,\n                new Widget[]{enhancedHistoryGraph}, deviceIds, \"name\", \"name\", \"iconName\", BoardType.ESP8266, new DataStream((short)1, PinType.VIRTUAL),\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.createTemplate(1, widgetId, tileTemplate);\n        clientPair.appClient.verifyResult(ok(2));\n\n        assertEquals(0, holder.reportingDiskDao.averageAggregator.getMinute().size());\n        assertEquals(0, holder.reportingDiskDao.averageAggregator.getHourly().size());\n        assertEquals(0, holder.reportingDiskDao.averageAggregator.getDaily().size());\n        assertEquals(0, holder.reportingDiskDao.rawDataCacheForGraphProcessor.rawStorage.size());\n        assertEquals(0, holder.reportingDiskDao.rawDataProcessor.rawStorage.size());\n\n        clientPair.hardwareClient.send(\"hardware vw 88 111\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new HardwareMessage(1, b(\"1-0 vw 88 111\"))));\n        clientPair.hardwareClient.send(\"hardware vw 89 112\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new HardwareMessage(2, b(\"1-0 vw 89 112\"))));\n\n\n        assertEquals(2, holder.reportingDiskDao.averageAggregator.getMinute().size());\n        assertEquals(2, holder.reportingDiskDao.averageAggregator.getHourly().size());\n        assertEquals(2, holder.reportingDiskDao.averageAggregator.getDaily().size());\n        assertEquals(2, holder.reportingDiskDao.rawDataCacheForGraphProcessor.rawStorage.size());\n        assertEquals(0, holder.reportingDiskDao.rawDataProcessor.rawStorage.size());\n    }\n\n    @Test\n    public void makeSureReportingIsPresentWhenGraphAssignedToDeviceSelector() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        DeviceSelector deviceSelector = new DeviceSelector();\n        deviceSelector.id = 200000;\n        deviceSelector.x = 0;\n        deviceSelector.y = 0;\n        deviceSelector.width = 1;\n        deviceSelector.height = 1;\n        deviceSelector.deviceIds = new int[] {0, 1};\n\n        clientPair.appClient.createWidget(1, deviceSelector);\n        clientPair.appClient.verifyResult(ok(2));\n\n        Superchart superchart = new Superchart();\n        superchart.id = 432;\n        superchart.width = 8;\n        superchart.height = 4;\n        GraphDataStream graphDataStream = new GraphDataStream(\n                null, GraphType.LINE, 0, (int) deviceSelector.id,\n                new DataStream((short) 88, PinType.VIRTUAL),\n                AggregationFunctionType.MAX, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        superchart.dataStreams = new GraphDataStream[] {\n                graphDataStream\n        };\n\n        clientPair.appClient.createWidget(1, superchart);\n        clientPair.appClient.verifyResult(ok(3));\n\n        assertEquals(0, holder.reportingDiskDao.averageAggregator.getMinute().size());\n        assertEquals(0, holder.reportingDiskDao.averageAggregator.getHourly().size());\n        assertEquals(0, holder.reportingDiskDao.averageAggregator.getDaily().size());\n        assertEquals(0, holder.reportingDiskDao.rawDataCacheForGraphProcessor.rawStorage.size());\n        assertEquals(0, holder.reportingDiskDao.rawDataProcessor.rawStorage.size());\n\n        clientPair.hardwareClient.send(\"hardware vw 88 111\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new HardwareMessage(1, b(\"1-0 vw 88 111\"))));\n\n        assertEquals(1, holder.reportingDiskDao.averageAggregator.getMinute().size());\n        assertEquals(1, holder.reportingDiskDao.averageAggregator.getHourly().size());\n        assertEquals(1, holder.reportingDiskDao.averageAggregator.getDaily().size());\n        assertEquals(1, holder.reportingDiskDao.rawDataCacheForGraphProcessor.rawStorage.size());\n        assertEquals(0, holder.reportingDiskDao.rawDataProcessor.rawStorage.size());\n    }\n\n    @Test\n    public void makeSureReportingIsPresentWhenPinAssignedToReporting() throws Exception {\n        ReportingWidget reportingWidget = new ReportingWidget();\n        reportingWidget.id = 432;\n        reportingWidget.width = 8;\n        reportingWidget.height = 4;\n        reportingWidget.reportSources = new ReportSource[] {\n                new DeviceReportSource(\n                        new ReportDataStream[] {new ReportDataStream((short) 88, PinType.VIRTUAL, null, false)},\n                        new int[] {0}\n                )\n        };\n\n        clientPair.appClient.createWidget(1, reportingWidget);\n        clientPair.appClient.verifyResult(ok(1));\n        clientPair.appClient.reset();\n\n        assertEquals(0, holder.reportingDiskDao.averageAggregator.getMinute().size());\n        assertEquals(0, holder.reportingDiskDao.averageAggregator.getHourly().size());\n        assertEquals(0, holder.reportingDiskDao.averageAggregator.getDaily().size());\n        assertEquals(0, holder.reportingDiskDao.rawDataCacheForGraphProcessor.rawStorage.size());\n        assertEquals(0, holder.reportingDiskDao.rawDataProcessor.rawStorage.size());\n\n        clientPair.hardwareClient.send(\"hardware vw 88 111\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new HardwareMessage(1, b(\"1-0 vw 88 111\"))));\n\n        assertEquals(1, holder.reportingDiskDao.averageAggregator.getMinute().size());\n        assertEquals(1, holder.reportingDiskDao.averageAggregator.getHourly().size());\n        assertEquals(1, holder.reportingDiskDao.averageAggregator.getDaily().size());\n        assertEquals(0, holder.reportingDiskDao.rawDataCacheForGraphProcessor.rawStorage.size());\n        assertEquals(0, holder.reportingDiskDao.rawDataProcessor.rawStorage.size());\n    }\n\n    @Test\n    public void makeSureReportingIsPresentWhenPinAssignedToReporting2() throws Exception {\n        ReportingWidget reportingWidget = new ReportingWidget();\n        reportingWidget.id = 432;\n        reportingWidget.width = 8;\n        reportingWidget.height = 4;\n        reportingWidget.reportSources = new ReportSource[] {\n                new TileTemplateReportSource(\n                        new ReportDataStream[] {new ReportDataStream((short) 88, PinType.VIRTUAL, null, false)},\n                        0,\n                        new int[] {0}\n                )\n        };\n\n        clientPair.appClient.createWidget(1, reportingWidget);\n        clientPair.appClient.verifyResult(ok(1));\n        clientPair.appClient.reset();\n\n        assertEquals(0, holder.reportingDiskDao.averageAggregator.getMinute().size());\n        assertEquals(0, holder.reportingDiskDao.averageAggregator.getHourly().size());\n        assertEquals(0, holder.reportingDiskDao.averageAggregator.getDaily().size());\n        assertEquals(0, holder.reportingDiskDao.rawDataCacheForGraphProcessor.rawStorage.size());\n        assertEquals(0, holder.reportingDiskDao.rawDataProcessor.rawStorage.size());\n\n        clientPair.hardwareClient.send(\"hardware vw 88 111\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new HardwareMessage(1, b(\"1-0 vw 88 111\"))));\n\n        assertEquals(1, holder.reportingDiskDao.averageAggregator.getMinute().size());\n        assertEquals(1, holder.reportingDiskDao.averageAggregator.getHourly().size());\n        assertEquals(1, holder.reportingDiskDao.averageAggregator.getDaily().size());\n        assertEquals(0, holder.reportingDiskDao.rawDataCacheForGraphProcessor.rawStorage.size());\n        assertEquals(0, holder.reportingDiskDao.rawDataProcessor.rawStorage.size());\n    }\n\n    @Test\n    public void makeSureReportingIsPresentWhenPinAssignedToReporting3() throws Exception {\n        ReportingWidget reportingWidget = new ReportingWidget();\n        reportingWidget.id = 432;\n        reportingWidget.width = 8;\n        reportingWidget.height = 4;\n        reportingWidget.reportSources = new ReportSource[] {\n                new TileTemplateReportSource(\n                        new ReportDataStream[] {new ReportDataStream((short) 88, PinType.VIRTUAL, null, false),\n                                                new ReportDataStream((short) 89, PinType.VIRTUAL, null, false)},\n                        0,\n                        new int[] {0}\n                )\n        };\n\n        clientPair.appClient.createWidget(1, reportingWidget);\n        clientPair.appClient.verifyResult(ok(1));\n        clientPair.appClient.reset();\n\n        assertEquals(0, holder.reportingDiskDao.averageAggregator.getMinute().size());\n        assertEquals(0, holder.reportingDiskDao.averageAggregator.getHourly().size());\n        assertEquals(0, holder.reportingDiskDao.averageAggregator.getDaily().size());\n        assertEquals(0, holder.reportingDiskDao.rawDataCacheForGraphProcessor.rawStorage.size());\n        assertEquals(0, holder.reportingDiskDao.rawDataProcessor.rawStorage.size());\n\n        clientPair.hardwareClient.send(\"hardware vw 89 111\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new HardwareMessage(1, b(\"1-0 vw 89 111\"))));\n\n        assertEquals(1, holder.reportingDiskDao.averageAggregator.getMinute().size());\n        assertEquals(1, holder.reportingDiskDao.averageAggregator.getHourly().size());\n        assertEquals(1, holder.reportingDiskDao.averageAggregator.getDaily().size());\n        assertEquals(0, holder.reportingDiskDao.rawDataCacheForGraphProcessor.rawStorage.size());\n        assertEquals(0, holder.reportingDiskDao.rawDataProcessor.rawStorage.size());\n    }\n\n    @Test\n    public void testGetLIVEGraphDataForEnhancedGraph() throws Exception {\n        String tempDir = holder.props.getProperty(\"data.folder\");\n\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n\n\n        Superchart enhancedHistoryGraph = new Superchart();\n        enhancedHistoryGraph.id = 432;\n        enhancedHistoryGraph.width = 8;\n        enhancedHistoryGraph.height = 4;\n        DataStream dataStream = new DataStream((short) 88, PinType.VIRTUAL);\n        GraphDataStream graphDataStream = new GraphDataStream(null, GraphType.LINE, 0, 0, dataStream, null, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        enhancedHistoryGraph.dataStreams = new GraphDataStream[] {\n                graphDataStream\n        };\n\n        clientPair.appClient.createWidget(1, enhancedHistoryGraph);\n        clientPair.appClient.verifyResult(ok(1));\n        clientPair.appClient.reset();\n\n        clientPair.appClient.getEnhancedGraphData(1, 432, GraphPeriod.LIVE);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new ResponseMessage(1, NO_DATA)));\n\n        clientPair.hardwareClient.send(\"hardware vw 88 111\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new HardwareMessage(1, b(\"1-0 vw 88 111\"))));\n        clientPair.appClient.reset();\n\n        clientPair.appClient.getEnhancedGraphData(1, 432, GraphPeriod.LIVE);\n        BinaryMessage graphDataResponse = clientPair.appClient.getBinaryBody();\n\n        assertNotNull(graphDataResponse);\n        byte[] decompressedGraphData = BaseTest.decompress(graphDataResponse.getBytes());\n        ByteBuffer bb = ByteBuffer.wrap(decompressedGraphData);\n\n        assertEquals(1, bb.getInt());\n        assertEquals(1, bb.getInt());\n        assertEquals(111D, bb.getDouble(), 0.1);\n        assertEquals(System.currentTimeMillis(), bb.getLong(), 2000);\n\n        for (int i = 1; i <= 60; i++) {\n            clientPair.hardwareClient.send(\"hardware vw 88 \" + i);\n        }\n\n        verify(clientPair.appClient.responseMock, timeout(10000)).channelRead(any(), eq(new HardwareMessage(61, b(\"1-0 vw 88 60\"))));\n        clientPair.appClient.reset();\n\n        clientPair.appClient.getEnhancedGraphData(1, 432, GraphPeriod.LIVE);\n        graphDataResponse = clientPair.appClient.getBinaryBody();\n\n        assertNotNull(graphDataResponse);\n        decompressedGraphData = BaseTest.decompress(graphDataResponse.getBytes());\n        bb = ByteBuffer.wrap(decompressedGraphData);\n\n        assertEquals(1, bb.getInt());\n        assertEquals(60, bb.getInt());\n        for (int i = 1; i <= 60; i++) {\n            assertEquals(i, bb.getDouble(), 0.1);\n            assertEquals(System.currentTimeMillis(), bb.getLong(), 10000);\n        }\n    }\n\n    @Test\n    public void testNoLiveDataWhenNoGraph() throws Exception {\n        String tempDir = holder.props.getProperty(\"data.folder\");\n\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n\n        clientPair.hardwareClient.send(\"hardware vw 88 111\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new HardwareMessage(1, b(\"1-0 vw 88 111\"))));\n\n        Superchart enhancedHistoryGraph = new Superchart();\n        enhancedHistoryGraph.id = 432;\n        enhancedHistoryGraph.width = 8;\n        enhancedHistoryGraph.height = 4;\n        DataStream dataStream = new DataStream((short) 88, PinType.VIRTUAL);\n        GraphDataStream graphDataStream = new GraphDataStream(null, GraphType.LINE, 0, 0, dataStream, null, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        enhancedHistoryGraph.dataStreams = new GraphDataStream[] {\n                graphDataStream\n        };\n\n        clientPair.appClient.createWidget(1, enhancedHistoryGraph);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.getEnhancedGraphData(1, 432, GraphPeriod.LIVE);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new ResponseMessage(2, NO_DATA)));\n    }\n\n    @Test\n    public void testNoLiveDataWhenNoGraph2() throws Exception {\n        String tempDir = holder.props.getProperty(\"data.folder\");\n\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n\n        clientPair.hardwareClient.send(\"hardware vw 88 111\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new HardwareMessage(1, b(\"1-0 vw 88 111\"))));\n\n        Superchart enhancedHistoryGraph = new Superchart();\n        enhancedHistoryGraph.id = 432;\n        enhancedHistoryGraph.width = 8;\n        enhancedHistoryGraph.height = 4;\n        DataStream dataStream = new DataStream((short) 88, PinType.VIRTUAL);\n        GraphDataStream graphDataStream = new GraphDataStream(null, GraphType.LINE, 0, 0, dataStream, null, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        enhancedHistoryGraph.dataStreams = new GraphDataStream[] {\n                graphDataStream\n        };\n\n        clientPair.appClient.createWidget(1, enhancedHistoryGraph);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.getEnhancedGraphData(1, 432, GraphPeriod.LIVE);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new ResponseMessage(2, NO_DATA)));\n\n        clientPair.hardwareClient.send(\"hardware vw 88 111\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new HardwareMessage(2, b(\"1-0 vw 88 111\"))));\n        clientPair.appClient.reset();\n\n        clientPair.appClient.getEnhancedGraphData(1, 432, GraphPeriod.LIVE);\n        BinaryMessage graphDataResponse = clientPair.appClient.getBinaryBody();\n\n        assertNotNull(graphDataResponse);\n        byte[] decompressedGraphData = BaseTest.decompress(graphDataResponse.getBytes());\n        ByteBuffer bb = ByteBuffer.wrap(decompressedGraphData);\n\n        assertEquals(1, bb.getInt());\n        assertEquals(1, bb.getInt());\n        assertEquals(111D, bb.getDouble(), 0.1);\n        assertEquals(System.currentTimeMillis(), bb.getLong(), 2000);\n\n        clientPair.appClient.deleteWidget(1, 432);\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.hardwareClient.send(\"hardware vw 88 111\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new HardwareMessage(3, b(\"1-0 vw 88 111\"))));\n\n        clientPair.appClient.createWidget(1, enhancedHistoryGraph);\n        clientPair.appClient.verifyResult(ok(3));\n\n        clientPair.appClient.send(\"getenhanceddata 1\" + b(\" 432 LIVE\"));\n        clientPair.appClient.reset();\n        graphDataResponse = clientPair.appClient.getBinaryBody();\n\n        assertNotNull(graphDataResponse);\n        decompressedGraphData = BaseTest.decompress(graphDataResponse.getBytes());\n        bb = ByteBuffer.wrap(decompressedGraphData);\n\n        assertEquals(1, bb.getInt());\n        assertEquals(1, bb.getInt());\n        assertEquals(111D, bb.getDouble(), 0.1);\n        assertEquals(System.currentTimeMillis(), bb.getLong(), 2000);\n    }\n\n    @Test\n    public void testGetLIVEGraphDataForEnhancedGraphWithPaging() throws Exception {\n        String tempDir = holder.props.getProperty(\"data.folder\");\n\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n\n\n        Superchart enhancedHistoryGraph = new Superchart();\n        enhancedHistoryGraph.id = 432;\n        enhancedHistoryGraph.width = 8;\n        enhancedHistoryGraph.height = 4;\n        DataStream dataStream = new DataStream((short) 88, PinType.VIRTUAL);\n        GraphDataStream graphDataStream = new GraphDataStream(null, GraphType.LINE, 0, 0, dataStream, null, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        enhancedHistoryGraph.dataStreams = new GraphDataStream[] {\n                graphDataStream\n        };\n\n        clientPair.appClient.createWidget(1, enhancedHistoryGraph);\n        clientPair.appClient.verifyResult(ok(1));\n        clientPair.appClient.reset();\n\n        clientPair.appClient.getEnhancedGraphData(1, 432, GraphPeriod.LIVE);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new ResponseMessage(1, NO_DATA)));\n\n        clientPair.hardwareClient.send(\"hardware vw 88 111\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new HardwareMessage(1, b(\"1-0 vw 88 111\"))));\n        clientPair.appClient.reset();\n\n        clientPair.appClient.getEnhancedGraphData(1, 432, GraphPeriod.LIVE);\n        BinaryMessage graphDataResponse = clientPair.appClient.getBinaryBody();\n\n        assertNotNull(graphDataResponse);\n        byte[] decompressedGraphData = BaseTest.decompress(graphDataResponse.getBytes());\n        ByteBuffer bb = ByteBuffer.wrap(decompressedGraphData);\n\n        assertEquals(1, bb.getInt());\n        assertEquals(1, bb.getInt());\n        assertEquals(111D, bb.getDouble(), 0.1);\n        assertEquals(System.currentTimeMillis(), bb.getLong(), 2000);\n\n        for (int i = 1; i <= 60; i++) {\n            clientPair.hardwareClient.send(\"hardware vw 88 \" + i);\n        }\n\n        verify(clientPair.appClient.responseMock, timeout(10000)).channelRead(any(), eq(new HardwareMessage(61, b(\"1-0 vw 88 60\"))));\n        clientPair.appClient.reset();\n\n        clientPair.appClient.getEnhancedGraphData(1, 432, GraphPeriod.LIVE);\n        graphDataResponse = clientPair.appClient.getBinaryBody();\n\n        assertNotNull(graphDataResponse);\n        decompressedGraphData = BaseTest.decompress(graphDataResponse.getBytes());\n        bb = ByteBuffer.wrap(decompressedGraphData);\n\n        assertEquals(1, bb.getInt());\n        assertEquals(60, bb.getInt());\n        for (int i = 1; i <= 60; i++) {\n            assertEquals(i, bb.getDouble(), 0.1);\n            assertEquals(System.currentTimeMillis(), bb.getLong(), 10000);\n        }\n    }\n\n\n    @Test\n    public void testPagingWorksForGetEnhancedHistoryDataPartialData() throws Exception {\n        String tempDir = holder.props.getProperty(\"data.folder\");\n\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n\n        Path pinReportingDataPath = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.DIGITAL, (short) 8, GraphGranularityType.MINUTE));\n\n        try (DataOutputStream dos = new DataOutputStream(\n                    Files.newOutputStream(pinReportingDataPath, CREATE, APPEND))) {\n            for (int i = 1; i <= 61; i++) {\n                dos.writeDouble(i);\n                dos.writeLong(i * 1000);\n            }\n            dos.flush();\n        }\n\n        Superchart enhancedHistoryGraph = new Superchart();\n        enhancedHistoryGraph.id = 432;\n        enhancedHistoryGraph.width = 8;\n        enhancedHistoryGraph.height = 4;\n        DataStream dataStream = new DataStream((short) 8, PinType.DIGITAL);\n        GraphDataStream graphDataStream = new GraphDataStream(null, GraphType.LINE, 0, 0, dataStream, null, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        enhancedHistoryGraph.dataStreams = new GraphDataStream[] {\n                graphDataStream\n        };\n\n        clientPair.appClient.createWidget(1, enhancedHistoryGraph);\n        clientPair.appClient.verifyResult(ok(1));\n        clientPair.appClient.reset();\n\n        clientPair.appClient.getEnhancedGraphData(1, 432, GraphPeriod.ONE_HOUR, 1);\n        BinaryMessage graphDataResponse = clientPair.appClient.getBinaryBody();\n\n        assertNotNull(graphDataResponse);\n        byte[] decompressedGraphData = BaseTest.decompress(graphDataResponse.getBytes());\n        ByteBuffer bb = ByteBuffer.wrap(decompressedGraphData);\n\n\n        assertEquals(1, bb.getInt());\n        assertEquals(1, bb.getInt());\n        assertEquals(1D, bb.getDouble(), 0.1);\n        assertEquals(1000, bb.getLong());\n    }\n\n    @Test\n    public void testPagingWorksForGetEnhancedHistoryDataFullData() throws Exception {\n        String tempDir = holder.props.getProperty(\"data.folder\");\n\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n\n        Path pinReportingDataPath = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.DIGITAL, (short) 8, GraphGranularityType.MINUTE));\n\n        try (DataOutputStream dos = new DataOutputStream(\n                Files.newOutputStream(pinReportingDataPath, CREATE, APPEND))) {\n            for (int i = 1; i <= 120; i++) {\n                dos.writeDouble(i);\n                dos.writeLong(i * 1000);\n            }\n            dos.flush();\n        }\n\n        Superchart enhancedHistoryGraph = new Superchart();\n        enhancedHistoryGraph.id = 432;\n        enhancedHistoryGraph.width = 8;\n        enhancedHistoryGraph.height = 4;\n        DataStream dataStream = new DataStream((short) 8, PinType.DIGITAL);\n        GraphDataStream graphDataStream = new GraphDataStream(null, GraphType.LINE, 0, 0, dataStream, null, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        enhancedHistoryGraph.dataStreams = new GraphDataStream[] {\n                graphDataStream\n        };\n\n        clientPair.appClient.createWidget(1, enhancedHistoryGraph);\n        clientPair.appClient.verifyResult(ok(1));\n        clientPair.appClient.reset();\n\n        clientPair.appClient.getEnhancedGraphData(1, 432, GraphPeriod.ONE_HOUR, 1);\n        BinaryMessage graphDataResponse = clientPair.appClient.getBinaryBody();\n\n        assertNotNull(graphDataResponse);\n        byte[] decompressedGraphData = BaseTest.decompress(graphDataResponse.getBytes());\n        ByteBuffer bb = ByteBuffer.wrap(decompressedGraphData);\n\n\n        assertEquals(1, bb.getInt());\n        assertEquals(60, bb.getInt());\n        for (int i = 1; i <= 60; i++) {\n            assertEquals(i, bb.getDouble(), 0.1);\n            assertEquals(i * 1000, bb.getLong());\n        }\n    }\n\n    @Test\n    public void testPagingWorksForGetEnhancedHistoryDataFullDataAndSecondPage() throws Exception {\n        String tempDir = holder.props.getProperty(\"data.folder\");\n\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n\n        Path pinReportingDataPath = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.DIGITAL, (short) 8, GraphGranularityType.MINUTE));\n\n        try (DataOutputStream dos = new DataOutputStream(\n                Files.newOutputStream(pinReportingDataPath, CREATE, APPEND))) {\n            for (int i = 1; i <= 120; i++) {\n                dos.writeDouble(i);\n                dos.writeLong(i * 1000);\n            }\n            dos.flush();\n        }\n\n        Superchart enhancedHistoryGraph = new Superchart();\n        enhancedHistoryGraph.id = 432;\n        enhancedHistoryGraph.width = 8;\n        enhancedHistoryGraph.height = 4;\n        DataStream dataStream = new DataStream((short) 8, PinType.DIGITAL);\n        GraphDataStream graphDataStream = new GraphDataStream(null, GraphType.LINE, 0, 0, dataStream, null, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        enhancedHistoryGraph.dataStreams = new GraphDataStream[] {\n                graphDataStream\n        };\n\n        clientPair.appClient.createWidget(1, enhancedHistoryGraph);\n        clientPair.appClient.verifyResult(ok(1));\n        clientPair.appClient.reset();\n\n        clientPair.appClient.getEnhancedGraphData(1, 432, GraphPeriod.ONE_HOUR, 5);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new ResponseMessage(1, NO_DATA)));\n    }\n\n    @Test\n    public void testPagingWorksForGetEnhancedHistoryDataWhenNoData() throws Exception {\n        String tempDir = holder.props.getProperty(\"data.folder\");\n\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n\n        Path pinReportingDataPath = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.DIGITAL, (short) 8, GraphGranularityType.MINUTE));\n\n        try (DataOutputStream dos = new DataOutputStream(\n                Files.newOutputStream(pinReportingDataPath, CREATE, APPEND))) {\n            for (int i = 1; i <= 60; i++) {\n                dos.writeDouble(i);\n                dos.writeLong(i * 1000);\n            }\n            dos.flush();\n        }\n\n        Superchart enhancedHistoryGraph = new Superchart();\n        enhancedHistoryGraph.id = 432;\n        enhancedHistoryGraph.width = 8;\n        enhancedHistoryGraph.height = 4;\n        DataStream dataStream = new DataStream((short) 8, PinType.DIGITAL);\n        GraphDataStream graphDataStream = new GraphDataStream(null, GraphType.LINE, 0, 0, dataStream, null, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        enhancedHistoryGraph.dataStreams = new GraphDataStream[] {\n                graphDataStream\n        };\n\n        clientPair.appClient.createWidget(1, enhancedHistoryGraph);\n        clientPair.appClient.verifyResult(ok(1));\n        clientPair.appClient.reset();\n\n        clientPair.appClient.getEnhancedGraphData(1, 432, GraphPeriod.ONE_HOUR, 1);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new ResponseMessage(1, NO_DATA)));\n    }\n\n    @Test\n    public void testDeleteWorksForEnhancedGraph() throws Exception {\n        Superchart enhancedHistoryGraph = new Superchart();\n        enhancedHistoryGraph.id = 432;\n        enhancedHistoryGraph.width = 8;\n        enhancedHistoryGraph.height = 4;\n        DataStream dataStream = new DataStream((short) 8, PinType.DIGITAL);\n        GraphDataStream graphDataStream = new GraphDataStream(null, GraphType.LINE, 0, 0, dataStream, null, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        enhancedHistoryGraph.dataStreams = new GraphDataStream[] {\n                graphDataStream\n        };\n\n        clientPair.appClient.createWidget(1, enhancedHistoryGraph);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.send(\"deleteEnhancedData 1\\0\" + \"432\");\n        verify(clientPair.appClient.responseMock, timeout(1000)).channelRead(any(), eq(ok(2)));\n        clientPair.appClient.reset();\n\n        clientPair.appClient.getEnhancedGraphData(1, 432, GraphPeriod.DAY);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new ResponseMessage(1, NO_DATA)));\n    }\n\n    private static String getFileNameByMask(String pattern) {\n        File dir = new File(blynkTempDir);\n        File[] files = dir.listFiles((dir1, name) -> name.startsWith(pattern));\n        return latest(files).getName();\n    }\n\n    private static File latest(File[] files) {\n        long lastMod = Long.MIN_VALUE;\n        File choice = null;\n        for (File file : files) {\n            if (file.lastModified() > lastMod) {\n                choice = file;\n                lastMod = file.lastModified();\n            }\n        }\n        return choice;\n    }\n\n    @Test\n    public void testExportDataFromHistoryGraph() throws Exception {\n        clientPair.appClient.send(\"export 1\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(illegalCommand(1)));\n\n        clientPair.appClient.send(\"export 1 666\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(illegalCommand(2)));\n\n        clientPair.appClient.send(\"export 1 1\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(illegalCommand(3)));\n\n        clientPair.appClient.send(\"export 1 191600\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new ResponseMessage(4, NO_DATA)));\n\n        //generate fake reporting data\n        Path userReportDirectory = Paths.get(holder.props.getProperty(\"data.folder\"), \"data\", getUserName());\n        Files.createDirectories(userReportDirectory);\n        Path userReportFile = Paths.get(userReportDirectory.toString(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.ANALOG, (short) 7, GraphGranularityType.MINUTE));\n        FileUtils.write(userReportFile, 1.1, 1L);\n        FileUtils.write(userReportFile, 2.2, 2L);\n\n        clientPair.appClient.send(\"export 1 191600\");\n        verify(holder.mailWrapper, timeout(1000)).sendHtml(eq(getUserName()), eq(\"History graph data for project My Dashboard\"), contains(\"/\" + getUserName() + \"_1_0_a7_\"));\n    }\n\n    @Test\n    public void testGeneratedCSVIsCorrect() throws Exception {\n        //generate fake reporting data\n        Path userReportDirectory = Paths.get(holder.props.getProperty(\"data.folder\"), \"data\", getUserName());\n        Files.createDirectories(userReportDirectory);\n        String filename = ReportingDiskDao.generateFilename(1, 0, PinType.ANALOG, (short) 7, GraphGranularityType.MINUTE);\n        Path userReportFile = Paths.get(userReportDirectory.toString(), filename);\n        FileUtils.write(userReportFile, 1.1, 1L);\n        FileUtils.write(userReportFile, 2.2, 2L);\n\n        clientPair.appClient.send(\"export 1 191600\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        String csvFileName = getFileNameByMask(getUserName() + \"_1_0_a7_\");\n        verify(holder.mailWrapper, timeout(1000)).sendHtml(eq(getUserName()), eq(\"History graph data for project My Dashboard\"), contains(csvFileName));\n\n        try (InputStream fileStream = new FileInputStream(Paths.get(blynkTempDir, csvFileName).toString());\n             InputStream gzipStream = new GZIPInputStream(fileStream);\n             BufferedReader buffered = new BufferedReader(new InputStreamReader(gzipStream))) {\n\n            String[] lineSplit = buffered.readLine().split(\",\");\n            assertEquals(1.1D, Double.parseDouble(lineSplit[0]), 0.001D);\n            assertEquals(1, Long.parseLong(lineSplit[1]));\n            assertEquals(0, Long.parseLong(lineSplit[2]));\n\n            lineSplit = buffered.readLine().split(\",\");\n            assertEquals(2.2D, Double.parseDouble(lineSplit[0]), 0.001D);\n            assertEquals(2, Long.parseLong(lineSplit[1]));\n            assertEquals(0, Long.parseLong(lineSplit[2]));\n        }\n    }\n\n    @Test\n    public void testGeneratedCSVIsCorrectForMultiDevicesNoData() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":200000, \\\"deviceIds\\\":[0,1], \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"DEVICE_SELECTOR\\\"}\");\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.appClient.reset();\n\n        clientPair.appClient.send(\"export 1-200000 191600\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new ResponseMessage(1, NO_DATA)));\n    }\n\n    @Test\n    public void cleanNotUsedPinDataWorksAsExpected() throws Exception {\n        HistoryGraphUnusedPinDataCleanerWorker cleaner = new HistoryGraphUnusedPinDataCleanerWorker(holder.userDao, holder.reportingDiskDao);\n        String tempDir = holder.props.getProperty(\"data.folder\");\n\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n\n        //this file has corresponding history graph\n        Path pinReportingDataPath1 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.ANALOG, (short) 7, GraphGranularityType.HOURLY));\n        FileUtils.write(pinReportingDataPath1, 1.11D, 1111111);\n\n        //those are not\n        Path pinReportingDataPath2 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.DIGITAL, (short) 100, GraphGranularityType.HOURLY));\n        FileUtils.write(pinReportingDataPath2, 1.11D, 1111111);\n\n        Path pinReportingDataPath3 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.DIGITAL, (short) 101, GraphGranularityType.HOURLY));\n        FileUtils.write(pinReportingDataPath3, 1.11D, 1111111);\n\n        Path pinReportingDataPath4 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.DIGITAL, (short) 102, GraphGranularityType.HOURLY));\n        FileUtils.write(pinReportingDataPath4, 1.11D, 1111111);\n\n        assertTrue(Files.exists(pinReportingDataPath1));\n        assertTrue(Files.exists(pinReportingDataPath2));\n        assertTrue(Files.exists(pinReportingDataPath3));\n        assertTrue(Files.exists(pinReportingDataPath4));\n\n        //creating widget just to make user profile \"updated\"\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":200000, \\\"deviceIds\\\":[0,1], \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"DEVICE_SELECTOR\\\"}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        cleaner.run();\n\n        assertTrue(Files.exists(pinReportingDataPath1));\n        assertTrue(Files.notExists(pinReportingDataPath2));\n        assertTrue(Files.notExists(pinReportingDataPath3));\n        assertTrue(Files.notExists(pinReportingDataPath4));\n    }\n\n    @Test\n    public void cleanNotUsedPinDataWorksAsExpectedForSuperChart() throws Exception {\n        HistoryGraphUnusedPinDataCleanerWorker cleaner = new HistoryGraphUnusedPinDataCleanerWorker(holder.userDao, holder.reportingDiskDao);\n\n        Superchart enhancedHistoryGraph = new Superchart();\n        enhancedHistoryGraph.id = 432;\n        enhancedHistoryGraph.width = 8;\n        enhancedHistoryGraph.height = 4;\n        DataStream dataStream1 = new DataStream((short) 8, PinType.DIGITAL);\n        DataStream dataStream2 = new DataStream((short) 9, PinType.DIGITAL);\n        DataStream dataStream3 = new DataStream((short) 10, PinType.DIGITAL);\n        GraphDataStream graphDataStream1 = new GraphDataStream(null, GraphType.LINE, 0, 0, dataStream1, AggregationFunctionType.MAX, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        GraphDataStream graphDataStream2 = new GraphDataStream(null, GraphType.LINE, 0, 0, dataStream2, AggregationFunctionType.MAX, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        GraphDataStream graphDataStream3 = new GraphDataStream(null, GraphType.LINE, 0, 0, dataStream3, AggregationFunctionType.MAX, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        enhancedHistoryGraph.dataStreams = new GraphDataStream[] {\n                graphDataStream1,\n                graphDataStream2,\n                graphDataStream3,\n        };\n\n        clientPair.appClient.createWidget(1, enhancedHistoryGraph);\n        clientPair.appClient.verifyResult(ok(1));\n\n        String tempDir = holder.props.getProperty(\"data.folder\");\n\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n\n        //this file has corresponding history graph\n        Path pinReportingDataPath1 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.DIGITAL, (short) 8, GraphGranularityType.HOURLY));\n        FileUtils.write(pinReportingDataPath1, 1.11D, 1111111);\n\n        Path pinReportingDataPath2 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.DIGITAL, (short) 9, GraphGranularityType.HOURLY));\n        FileUtils.write(pinReportingDataPath2, 1.11D, 1111111);\n\n        Path pinReportingDataPath3 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.DIGITAL, (short) 10, GraphGranularityType.HOURLY));\n        FileUtils.write(pinReportingDataPath3, 1.11D, 1111111);\n\n        //those are not\n        Path pinReportingDataPath4 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.DIGITAL, (short) 11, GraphGranularityType.HOURLY));\n        FileUtils.write(pinReportingDataPath4, 1.11D, 1111111);\n\n        assertTrue(Files.exists(pinReportingDataPath1));\n        assertTrue(Files.exists(pinReportingDataPath2));\n        assertTrue(Files.exists(pinReportingDataPath3));\n        assertTrue(Files.exists(pinReportingDataPath4));\n\n        cleaner.run();\n\n        assertTrue(Files.exists(pinReportingDataPath1));\n        assertTrue(Files.exists(pinReportingDataPath2));\n        assertTrue(Files.exists(pinReportingDataPath3));\n        assertTrue(Files.notExists(pinReportingDataPath4));\n    }\n\n    @Test\n    public void cleanNotUsedPinDataWorksAsExpectedForReportsWidget() throws Exception {\n        HistoryGraphUnusedPinDataCleanerWorker cleaner = new HistoryGraphUnusedPinDataCleanerWorker(holder.userDao, holder.reportingDiskDao);\n\n        ReportingWidget reportingWidget = new ReportingWidget();\n        reportingWidget.id = 432;\n        reportingWidget.width = 8;\n        reportingWidget.height = 4;\n        reportingWidget.reports = new Report[] {\n                new Report(1, \"My One Time Report\",\n                        new ReportSource[] {\n                                new TileTemplateReportSource(\n                                        new ReportDataStream[] {new ReportDataStream((short) 88, PinType.VIRTUAL, null, false),\n                                                new ReportDataStream((short) 89, PinType.VIRTUAL, null, false)},\n                                        0,\n                                        new int[] {0, 1}\n                                )\n                        },\n                        new OneTimeReport(86400), \"test@gmail.com\",\n                        GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE_PER_PIN, null, ZoneId.of(\"UTC\"), 0, 0, null)\n        };\n\n        clientPair.appClient.createWidget(1, reportingWidget);\n        clientPair.appClient.verifyResult(ok(1));\n        clientPair.appClient.reset();\n\n        String tempDir = holder.props.getProperty(\"data.folder\");\n\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n\n        //this file has corresponding history graph\n        Path pinReportingDataPath1 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.VIRTUAL, (short) 88, GraphGranularityType.MINUTE));\n        FileUtils.write(pinReportingDataPath1, 1.11D, 1111111);\n\n        Path pinReportingDataPath2 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.VIRTUAL, (short) 89, GraphGranularityType.MINUTE));\n        FileUtils.write(pinReportingDataPath2, 1.11D, 1111111);\n\n        Path pinReportingDataPath12 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 1, PinType.VIRTUAL, (short) 88, GraphGranularityType.MINUTE));\n        FileUtils.write(pinReportingDataPath12, 1.11D, 1111111);\n\n        Path pinReportingDataPath22 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 1, PinType.VIRTUAL, (short) 89, GraphGranularityType.MINUTE));\n        FileUtils.write(pinReportingDataPath22, 1.11D, 1111111);\n\n        assertTrue(Files.exists(pinReportingDataPath1));\n        assertTrue(Files.exists(pinReportingDataPath2));\n        assertTrue(Files.exists(pinReportingDataPath12));\n        assertTrue(Files.exists(pinReportingDataPath22));\n\n\n        cleaner.run();\n\n        assertTrue(Files.exists(pinReportingDataPath1));\n        assertTrue(Files.exists(pinReportingDataPath2));\n        assertTrue(Files.exists(pinReportingDataPath12));\n        assertTrue(Files.exists(pinReportingDataPath22));\n    }\n\n    @Test\n    public void cleanNotUsedPinDataWorksAsExpectedForSuperChartInDeviceTiles() throws Exception {\n        HistoryGraphUnusedPinDataCleanerWorker cleaner = new HistoryGraphUnusedPinDataCleanerWorker(holder.userDao, holder.reportingDiskDao);\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = 21321;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(1));\n\n        int[] deviceIds = new int[] {1};\n\n        Superchart enhancedHistoryGraph = new Superchart();\n        enhancedHistoryGraph.id = 432;\n        enhancedHistoryGraph.width = 8;\n        enhancedHistoryGraph.height = 4;\n        DataStream dataStream1 = new DataStream((short) 8, PinType.DIGITAL);\n        DataStream dataStream2 = new DataStream((short) 9, PinType.DIGITAL);\n        GraphDataStream graphDataStream1 = new GraphDataStream(null, GraphType.LINE, 0, -1, dataStream1, AggregationFunctionType.MAX, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        GraphDataStream graphDataStream2 = new GraphDataStream(null, GraphType.LINE, 0, -1, dataStream2, AggregationFunctionType.MAX, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        enhancedHistoryGraph.dataStreams = new GraphDataStream[] {\n                graphDataStream1,\n                graphDataStream2,\n        };\n        PageTileTemplate tileTemplate = new PageTileTemplate(1,\n                new Widget[] {\n                        enhancedHistoryGraph\n                },\n                deviceIds, \"123\", \"name\", \"iconName\", BoardType.ESP8266, new DataStream((short) 1, PinType.VIRTUAL),\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.send(\"createTemplate \" + b(\"1 \" + deviceTiles.id + \" \")\n                + MAPPER.writeValueAsString(tileTemplate));\n        clientPair.appClient.verifyResult(ok(2));\n\n        String tempDir = holder.props.getProperty(\"data.folder\");\n\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n\n        //this file has corresponding history graph\n        Path pinReportingDataPath1 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 1, PinType.DIGITAL, (short) 8, GraphGranularityType.HOURLY));\n        FileUtils.write(pinReportingDataPath1, 1.11D, 1111111);\n\n        Path pinReportingDataPath2 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 1, PinType.DIGITAL, (short) 9, GraphGranularityType.HOURLY));\n        FileUtils.write(pinReportingDataPath2, 1.11D, 1111111);\n\n        //those are not\n        Path pinReportingDataPath3 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 1, PinType.DIGITAL, (short) 10, GraphGranularityType.HOURLY));\n        FileUtils.write(pinReportingDataPath3, 1.11D, 1111111);\n\n        Path pinReportingDataPath4 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 1, PinType.DIGITAL, (short) 11, GraphGranularityType.HOURLY));\n        FileUtils.write(pinReportingDataPath4, 1.11D, 1111111);\n\n        assertTrue(Files.exists(pinReportingDataPath1));\n        assertTrue(Files.exists(pinReportingDataPath2));\n        assertTrue(Files.exists(pinReportingDataPath3));\n        assertTrue(Files.exists(pinReportingDataPath4));\n\n        cleaner.run();\n\n        assertTrue(Files.exists(pinReportingDataPath1));\n        assertTrue(Files.exists(pinReportingDataPath2));\n        assertTrue(Files.notExists(pinReportingDataPath3));\n        assertTrue(Files.notExists(pinReportingDataPath4));\n    }\n\n    @Test\n    public void cleanNotUsedPinDataWorksAsExpectedForSuperChartWithDeviceSelector() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":200000, \\\"deviceIds\\\":[0,1], \\\"width\\\":1, \\\"height\\\":1, \\\"value\\\":0, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"DEVICE_SELECTOR\\\"}\");\n        clientPair.appClient.verifyResult(ok(2));\n\n        HistoryGraphUnusedPinDataCleanerWorker cleaner = new HistoryGraphUnusedPinDataCleanerWorker(holder.userDao, holder.reportingDiskDao);\n\n        Superchart enhancedHistoryGraph = new Superchart();\n        enhancedHistoryGraph.id = 432;\n        enhancedHistoryGraph.width = 8;\n        enhancedHistoryGraph.height = 4;\n        DataStream dataStream1 = new DataStream((short) 8, PinType.DIGITAL);\n        DataStream dataStream2 = new DataStream((short) 9, PinType.DIGITAL);\n        DataStream dataStream3 = new DataStream((short) 10, PinType.DIGITAL);\n        GraphDataStream graphDataStream1 = new GraphDataStream(null, GraphType.LINE, 0, 200000, dataStream1, AggregationFunctionType.MAX, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        GraphDataStream graphDataStream2 = new GraphDataStream(null, GraphType.LINE, 0, 200000, dataStream2, AggregationFunctionType.MAX, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        GraphDataStream graphDataStream3 = new GraphDataStream(null, GraphType.LINE, 0, 200000, dataStream3, AggregationFunctionType.MAX, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        enhancedHistoryGraph.dataStreams = new GraphDataStream[] {\n                graphDataStream1,\n                graphDataStream2,\n                graphDataStream3,\n        };\n\n        clientPair.appClient.createWidget(1, enhancedHistoryGraph);\n        clientPair.appClient.verifyResult(ok(3));\n\n        String tempDir = holder.props.getProperty(\"data.folder\");\n\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n\n        //this file has corresponding history graph\n        Path pinReportingDataPath10 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.DIGITAL, (short) 8, GraphGranularityType.HOURLY));\n        FileUtils.write(pinReportingDataPath10, 1.11D, 1111111);\n\n        Path pinReportingDataPath20 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.DIGITAL, (short) 9, GraphGranularityType.HOURLY));\n        FileUtils.write(pinReportingDataPath20, 1.11D, 1111111);\n\n        Path pinReportingDataPath30 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.DIGITAL, (short) 10, GraphGranularityType.HOURLY));\n        FileUtils.write(pinReportingDataPath30, 1.11D, 1111111);\n\n        Path pinReportingDataPath11 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 1, PinType.DIGITAL, (short) 8, GraphGranularityType.HOURLY));\n        FileUtils.write(pinReportingDataPath11, 1.11D, 1111111);\n\n        Path pinReportingDataPath21 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 1, PinType.DIGITAL, (short) 9, GraphGranularityType.HOURLY));\n        FileUtils.write(pinReportingDataPath21, 1.11D, 1111111);\n\n        Path pinReportingDataPath31 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 1, PinType.DIGITAL, (short) 10, GraphGranularityType.HOURLY));\n        FileUtils.write(pinReportingDataPath31, 1.11D, 1111111);\n\n        //those are not\n        Path pinReportingDataPath40 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.DIGITAL, (short) 11, GraphGranularityType.HOURLY));\n        FileUtils.write(pinReportingDataPath40, 1.11D, 1111111);\n\n        Path pinReportingDataPath41 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 1, PinType.DIGITAL, (short) 11, GraphGranularityType.HOURLY));\n        FileUtils.write(pinReportingDataPath41, 1.11D, 1111111);\n\n        //3 files for device 0\n        assertTrue(Files.exists(pinReportingDataPath10));\n        assertTrue(Files.exists(pinReportingDataPath20));\n        assertTrue(Files.exists(pinReportingDataPath30));\n\n        //3 files for device 1\n        assertTrue(Files.exists(pinReportingDataPath11));\n        assertTrue(Files.exists(pinReportingDataPath21));\n        assertTrue(Files.exists(pinReportingDataPath31));\n\n        assertTrue(Files.exists(pinReportingDataPath40));\n        assertTrue(Files.exists(pinReportingDataPath41));\n\n        cleaner.run();\n\n        //3 files for device 0\n        assertTrue(Files.exists(pinReportingDataPath10));\n        assertTrue(Files.exists(pinReportingDataPath20));\n        assertTrue(Files.exists(pinReportingDataPath30));\n\n        //3 files for device 1\n        assertTrue(Files.exists(pinReportingDataPath11));\n        assertTrue(Files.exists(pinReportingDataPath21));\n        assertTrue(Files.exists(pinReportingDataPath31));\n\n        assertTrue(Files.notExists(pinReportingDataPath40));\n        assertTrue(Files.notExists(pinReportingDataPath41));\n    }\n\n    @Test\n    public void truncateReportingDataWorks() throws Exception {\n        ReportingTruncateWorker truncateWorker = new ReportingTruncateWorker(holder.reportingDiskDao,\n                                                                             (int) TimeUnit.MINUTES.toMinutes(10), 45);\n        String tempDir = holder.props.getProperty(\"data.folder\");\n\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n\n        //this file has corresponding history graph\n        Path pinReportingDataPath1 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.ANALOG, (short) 7, GraphGranularityType.MINUTE));\n\n        Path pinReportingDataPath2 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.VIRTUAL, (short) 7, GraphGranularityType.MINUTE));\n        FileUtils.write(pinReportingDataPath2, 1.11D, 1);\n\n        Path pinReportingDataPath3 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.VIRTUAL, (short) 7, GraphGranularityType.HOURLY));\n\n        int STORAGE_PERIOD = 10;\n        //write max amount of data for 1 week + 1 point\n        for (int i = 0; i < STORAGE_PERIOD * 24 * 60 + 1; i++) {\n            FileUtils.write(pinReportingDataPath1, 1.11D, i);\n            FileUtils.write(pinReportingDataPath3, 1.11D, i);\n        }\n\n        assertEquals((STORAGE_PERIOD * 24 * 60 + 1) * ReportingUtil.REPORTING_RECORD_SIZE, Files.size(pinReportingDataPath1));\n        assertEquals(16, Files.size(pinReportingDataPath2));\n        assertEquals((STORAGE_PERIOD * 24 * 60 + 1) * ReportingUtil.REPORTING_RECORD_SIZE, Files.size(pinReportingDataPath3));\n        truncateWorker.run();\n\n        //expecting truncated file here\n        assertEquals(STORAGE_PERIOD * 24 * 60 * ReportingUtil.REPORTING_RECORD_SIZE, Files.size(pinReportingDataPath1));\n        assertEquals(16, Files.size(pinReportingDataPath2));\n        assertEquals((STORAGE_PERIOD * 24 * 60 + 1) * ReportingUtil.REPORTING_RECORD_SIZE, Files.size(pinReportingDataPath3));\n\n        //check truncate is correct\n        ByteBuffer bb = FileUtils.read(pinReportingDataPath1, STORAGE_PERIOD * 24 * 60);\n\n        for (int i = 1; i < STORAGE_PERIOD * 24 * 60 + 1; i++) {\n            assertEquals(1.11D, bb.getDouble(), 0.001D);\n            assertEquals(i, bb.getLong());\n        }\n\n        bb = FileUtils.read(pinReportingDataPath3, STORAGE_PERIOD * 24 * 60 + 1);\n        for (int i = 0; i < STORAGE_PERIOD * 24 * 60 + 1; i++) {\n            assertEquals(1.11D, bb.getDouble(), 0.001D);\n            assertEquals(i, bb.getLong());\n        }\n    }\n\n    @Test\n    public void deleteOldExportFiles() throws Exception {\n        Path csvDir = Paths.get(FileUtils.CSV_DIR);\n        if (Files.notExists(csvDir)) {\n            Files.createDirectories(csvDir);\n        }\n\n        //this file has corresponding history graph\n        Path csvFile = Paths.get(csvDir.toString(), \"123.csv.gz\");\n\n        FileUtils.write(csvFile, 1.11D, 1);\n        assertTrue(Files.exists(csvFile));\n\n        ReportingTruncateWorker truncateWorker = new ReportingTruncateWorker(holder.reportingDiskDao, 0, 0);\n        truncateWorker.run();\n        assertTrue(Files.notExists(csvFile));\n    }\n\n    @Test\n    public void doNotTruncateFileWithCorrectSize() throws Exception {\n        ReportingTruncateWorker truncateWorker = new ReportingTruncateWorker(holder.reportingDiskDao,\n                                                                             (int) TimeUnit.DAYS.toMinutes(10), 45L);\n        String tempDir = holder.props.getProperty(\"data.folder\");\n\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n\n        //this file has corresponding history graph\n        Path pinReportingDataPath1 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.ANALOG, (short) 7, GraphGranularityType.MINUTE));\n\n        //write max amount of data for 1 week + 1 point\n        for (int i = 0; i < 7 * 24 * 60; i++) {\n            FileUtils.write(pinReportingDataPath1, 1.11D, i);\n        }\n\n        assertEquals((7 * 24 * 60) * ReportingUtil.REPORTING_RECORD_SIZE, Files.size(pinReportingDataPath1));\n        truncateWorker.run();\n\n        //expecting truncated file here\n        assertEquals(7 * 24 * 60 * ReportingUtil.REPORTING_RECORD_SIZE, Files.size(pinReportingDataPath1));\n\n        //check no truncate\n        ByteBuffer bb = FileUtils.read(pinReportingDataPath1, 7 * 24 * 60);\n\n        for (int i = 0; i < 7 * 24 * 60; i++) {\n            assertEquals(1.11D, bb.getDouble(), 0.001D);\n            assertEquals(i, bb.getLong());\n        }\n\n        assertTrue(Files.exists(userReportFolder));\n    }\n\n    @Test\n    public void truncateReportingDataDontFailsInEmptyFolder() throws Exception {\n        ReportingTruncateWorker truncateWorker = new ReportingTruncateWorker(holder.reportingDiskDao,\n                                                                             (int) TimeUnit.DAYS.toMinutes(10), 45L);\n        String tempDir = holder.props.getProperty(\"data.folder\");\n\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n\n        truncateWorker.run();\n\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n        truncateWorker.run();\n    }\n\n    @Test\n    public void truncateReportingDataDeletesEmptyFolder() throws Exception {\n        ReportingTruncateWorker truncateWorker = new ReportingTruncateWorker(holder.reportingDiskDao,\n                                                                             (int) TimeUnit.DAYS.toMinutes(10), 45L);\n        String tempDir = holder.props.getProperty(\"data.folder\");\n\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n        truncateWorker.run();\n\n        assertTrue(Files.notExists(userReportFolder));\n    }\n\n    @Test\n    public void testGeneratedCSVIsCorrectForMultiDevicesAndEnhancedGraph() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":200000, \\\"deviceIds\\\":[0,1], \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"DEVICE_SELECTOR\\\"}\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(ok(2)));\n\n        Superchart enhancedHistoryGraph = new Superchart();\n        enhancedHistoryGraph.id = 432;\n        enhancedHistoryGraph.width = 8;\n        enhancedHistoryGraph.height = 4;\n        DataStream dataStream = new DataStream((short) 8, PinType.DIGITAL);\n        GraphDataStream graphDataStream = new GraphDataStream(null, GraphType.LINE, 0, 200_000, dataStream, null, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        enhancedHistoryGraph.dataStreams = new GraphDataStream[] {\n                graphDataStream\n        };\n\n        clientPair.appClient.createWidget(1, enhancedHistoryGraph);\n        clientPair.appClient.verifyResult(ok(3));\n\n        clientPair.appClient.reset();\n\n        //generate fake reporting data\n        Path userReportDirectory = Paths.get(holder.props.getProperty(\"data.folder\"), \"data\", getUserName());\n        Files.createDirectories(userReportDirectory);\n\n        String filename = ReportingDiskDao.generateFilename(1, 0, PinType.DIGITAL, (short) 8, GraphGranularityType.MINUTE);\n        Path userReportFile = Paths.get(userReportDirectory.toString(), filename);\n        FileUtils.write(userReportFile, 1.1, 1L);\n        FileUtils.write(userReportFile, 2.2, 2L);\n\n        filename = ReportingDiskDao.generateFilename(1, 1, PinType.DIGITAL, (short) 8, GraphGranularityType.MINUTE);\n        userReportFile = Paths.get(userReportDirectory.toString(), filename);\n        FileUtils.write(userReportFile, 11.1, 11L);\n        FileUtils.write(userReportFile, 12.2, 12L);\n\n        clientPair.appClient.send(\"export 1 432\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        String csvFileName = getFileNameByMask(getUserName() + \"_1_200000_d8_\");\n        verify(holder.mailWrapper, timeout(1000)).sendHtml(eq(getUserName()), eq(\"History graph data for project My Dashboard\"), contains(csvFileName));\n\n        try (InputStream fileStream = new FileInputStream(Paths.get(blynkTempDir, csvFileName).toString());\n             InputStream gzipStream = new GZIPInputStream(fileStream);\n             BufferedReader buffered = new BufferedReader(new InputStreamReader(gzipStream))) {\n\n            //first device\n            String[] lineSplit = buffered.readLine().split(\",\");\n            assertEquals(1.1D, Double.parseDouble(lineSplit[0]), 0.001D);\n            assertEquals(1, Long.parseLong(lineSplit[1]));\n            assertEquals(0, Long.parseLong(lineSplit[2]));\n\n            lineSplit = buffered.readLine().split(\",\");\n            assertEquals(2.2D, Double.parseDouble(lineSplit[0]), 0.001D);\n            assertEquals(2, Long.parseLong(lineSplit[1]));\n            assertEquals(0, Long.parseLong(lineSplit[2]));\n\n            //second device\n            lineSplit = buffered.readLine().split(\",\");\n            assertEquals(11.1D, Double.parseDouble(lineSplit[0]), 0.001D);\n            assertEquals(11, Long.parseLong(lineSplit[1]));\n            assertEquals(1, Long.parseLong(lineSplit[2]));\n\n            lineSplit = buffered.readLine().split(\",\");\n            assertEquals(12.2D, Double.parseDouble(lineSplit[0]), 0.001D);\n            assertEquals(12, Long.parseLong(lineSplit[1]));\n            assertEquals(1, Long.parseLong(lineSplit[2]));\n        }\n    }\n\n    @Test\n    public void testGeneratedCSVIsCorrectForMultiDevices() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":200000, \\\"deviceIds\\\":[0,1], \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"DEVICE_SELECTOR\\\"}\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(ok(2)));\n\n        Superchart superchart = new Superchart();\n        superchart.id = 191600;\n        superchart.width = 8;\n        superchart.height = 4;\n        DataStream dataStream = new DataStream((short) 7, PinType.ANALOG);\n        GraphDataStream graphDataStream = new GraphDataStream(null, GraphType.LINE, 0,\n                200_000, dataStream, null, 0, null, null, null, 0, 0, false, null, false, false, false, null, 0, false, 0);\n        superchart.dataStreams = new GraphDataStream[] {\n                graphDataStream,\n        };\n\n        clientPair.appClient.updateWidget(1, superchart);\n        clientPair.appClient.verifyResult(ok(3));\n\n        clientPair.appClient.reset();\n\n        //generate fake reporting data\n        Path userReportDirectory = Paths.get(holder.props.getProperty(\"data.folder\"), \"data\", getUserName());\n        Files.createDirectories(userReportDirectory);\n\n        String filename = ReportingDiskDao.generateFilename(1, 0, PinType.ANALOG, (short) 7, GraphGranularityType.MINUTE);\n        Path userReportFile = Paths.get(userReportDirectory.toString(), filename);\n        FileUtils.write(userReportFile, 1.1, 1L);\n        FileUtils.write(userReportFile, 2.2, 2L);\n\n        filename = ReportingDiskDao.generateFilename(1, 1, PinType.ANALOG, (short) 7, GraphGranularityType.MINUTE);\n        userReportFile = Paths.get(userReportDirectory.toString(), filename);\n        FileUtils.write(userReportFile, 11.1, 11L);\n        FileUtils.write(userReportFile, 12.2, 12L);\n\n        clientPair.appClient.send(\"export 1 191600\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        String csvFileName = getFileNameByMask(getUserName() + \"_1_200000_a7_\");\n        verify(holder.mailWrapper, timeout(1000)).sendHtml(eq(getUserName()), eq(\"History graph data for project My Dashboard\"), contains(csvFileName));\n\n        try (InputStream fileStream = new FileInputStream(Paths.get(blynkTempDir, csvFileName).toString());\n             InputStream gzipStream = new GZIPInputStream(fileStream);\n             BufferedReader buffered = new BufferedReader(new InputStreamReader(gzipStream))) {\n\n            //first device\n            String[] lineSplit = buffered.readLine().split(\",\");\n            assertEquals(1.1D, Double.parseDouble(lineSplit[0]), 0.001D);\n            assertEquals(1, Long.parseLong(lineSplit[1]));\n            assertEquals(0, Long.parseLong(lineSplit[2]));\n\n            lineSplit = buffered.readLine().split(\",\");\n            assertEquals(2.2D, Double.parseDouble(lineSplit[0]), 0.001D);\n            assertEquals(2, Long.parseLong(lineSplit[1]));\n            assertEquals(0, Long.parseLong(lineSplit[2]));\n\n            //second device\n            lineSplit = buffered.readLine().split(\",\");\n            assertEquals(11.1D, Double.parseDouble(lineSplit[0]), 0.001D);\n            assertEquals(11, Long.parseLong(lineSplit[1]));\n            assertEquals(1, Long.parseLong(lineSplit[2]));\n\n            lineSplit = buffered.readLine().split(\",\");\n            assertEquals(12.2D, Double.parseDouble(lineSplit[0]), 0.001D);\n            assertEquals(12, Long.parseLong(lineSplit[1]));\n            assertEquals(1, Long.parseLong(lineSplit[2]));\n        }\n    }\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/tcp/LoadBalancingIntegrationTest.java",
    "content": "package cc.blynk.integration.tcp;\n\nimport cc.blynk.integration.BaseTest;\nimport cc.blynk.integration.TestUtil;\nimport cc.blynk.integration.model.tcp.ClientPair;\nimport cc.blynk.integration.model.tcp.TestAppClient;\nimport cc.blynk.integration.model.tcp.TestHardClient;\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.device.BoardType;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.device.Status;\nimport cc.blynk.server.core.protocol.model.messages.ResponseMessage;\nimport cc.blynk.server.servers.BaseServer;\nimport cc.blynk.server.servers.application.MobileAndHttpsServer;\nimport cc.blynk.server.servers.hardware.HardwareAndHttpAPIServer;\nimport cc.blynk.server.workers.ProfileSaverWorker;\nimport cc.blynk.utils.AppNameUtil;\nimport cc.blynk.utils.properties.ServerProperties;\nimport cc.blynk.utils.structure.LRUCache;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Ignore;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport java.util.Collections;\n\nimport static cc.blynk.integration.TestUtil.connectRedirect;\nimport static cc.blynk.integration.TestUtil.createDefaultHolder;\nimport static cc.blynk.integration.TestUtil.createDevice;\nimport static cc.blynk.integration.TestUtil.getServer;\nimport static cc.blynk.integration.TestUtil.illegalCommand;\nimport static cc.blynk.integration.TestUtil.invalidToken;\nimport static cc.blynk.integration.TestUtil.ok;\nimport static cc.blynk.server.core.protocol.enums.Response.DEVICE_NOT_IN_NETWORK;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\nimport static org.junit.Assert.assertNull;\nimport static org.junit.Assert.assertTrue;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.timeout;\nimport static org.mockito.Mockito.verify;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 5/09/2016.\n *\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class LoadBalancingIntegrationTest extends BaseTest {\n\n    private BaseServer appServer1;\n    private BaseServer hardwareServer1;\n    private Holder holder;\n\n    private BaseServer appServer2;\n    private BaseServer hardwareServer2;\n    private int tcpAppPort2;\n    private int plainHardPort2;\n    private Holder holder2;\n    private ServerProperties properties2;\n    private ClientPair clientPair;\n\n    @Before\n    public void init() throws Exception {\n        holder = createDefaultHolder(properties, \"db-test.properties\");;\n        hardwareServer1 = new HardwareAndHttpAPIServer(holder).start();\n        appServer1 = new MobileAndHttpsServer(holder).start();\n\n        properties2 = new ServerProperties(Collections.emptyMap(), \"server2.properties\");\n        properties2.setProperty(\"data.folder\", getDataFolder());\n\n        this.holder2 = createDefaultHolder(properties2, \"db-test.properties\");;\n        hardwareServer2 = new HardwareAndHttpAPIServer(holder2).start();\n        appServer2 = new MobileAndHttpsServer(holder2).start();\n        plainHardPort2 = properties2.getIntProperty(\"http.port\");\n        tcpAppPort2 = properties2.getIntProperty(\"https.port\");\n\n        holder.dbManager.executeSQL(\"DELETE FROM users\");\n        holder.dbManager.executeSQL(\"DELETE FROM forwarding_tokens\");\n        clientPair = initAppAndHardPair(properties.getHttpsPort(), properties.getHttpPort(), properties2);\n    }\n\n    @After\n    public void shutdown() {\n        appServer1.close();\n        hardwareServer1.close();\n\n        appServer2.close();\n        hardwareServer2.close();\n\n        holder.close();\n        holder2.close();\n\n        clientPair.stop();\n    }\n\n    @Test\n    @Ignore\n    //todo fix, local DB was changed, need fix\n    public void test2NewUsersStoredOnDifferentServers() throws Exception {\n        TestAppClient appClient1 =new TestAppClient(properties);\n        appClient1.start();\n\n        String email = \"test_new@gmail.com\";\n        String pass = \"a\";\n        String appName = AppNameUtil.BLYNK;\n\n        appClient1.send(\"getServer \" + email + \"\\0\" + appName);\n        appClient1.verifyResult(getServer(1, \"127.0.0.1\"));\n\n        appClient1.reset();\n\n        ProfileSaverWorker profileSaverWorker = new ProfileSaverWorker(holder.userDao, holder.fileManager, holder.dbManager);\n        ProfileSaverWorker profileSaverWorker2 = new ProfileSaverWorker(holder2.userDao, holder2.fileManager, holder2.dbManager);\n\n        workflowForUser(appClient1, email, pass, appName);\n        profileSaverWorker.run();\n        //waiting for DB update\n        TestUtil.sleep(500);\n\n        assertEquals(\"127.0.0.1\", holder.dbManager.getUserServerIp(email, AppNameUtil.BLYNK));\n\n        TestAppClient appClient2 = new TestAppClient(\"localhost\", tcpAppPort2, properties2);\n        appClient2.start();\n\n        String username2 = \"test2_new@gmail.com\";\n\n        appClient2.send(\"getServer \" + username2 + \"\\0\" + appName);\n        appClient2.verifyResult(getServer(1, \"localhost2\"));\n\n        appClient2.reset();\n\n        workflowForUser(appClient2, username2, pass, appName);\n        profileSaverWorker2.run();\n\n        long tries = 0;\n        String host;\n        //waiting for channel to be closed.\n        //but only limited amount if time\n        while ((host = holder2.dbManager.getUserServerIp(username2, AppNameUtil.BLYNK)) == null && tries < 100) {\n            TestUtil.sleep(10);\n            tries++;\n        }\n\n        assertEquals(\"localhost2\", host);\n    }\n\n    @Test\n    public void testNoGetServerHandlerAfterLogin() throws Exception {\n        TestAppClient appClient1 =new TestAppClient(properties);\n        appClient1.start();\n        workflowForUser(appClient1, \"123@gmail.com\", \"a\", AppNameUtil.BLYNK);\n        appClient1.send(\"getServer \" + \"123@gmail.com\" + \"\\0\" + AppNameUtil.BLYNK);\n        appClient1.neverAfter(500, getServer(1, \"127.0.0.1\"));\n    }\n\n    @Test\n    @Ignore\n    //todo fix, local DB was changed, need fix\n    public void testUserRedirectedToCorrectServer() throws Exception {\n        TestAppClient appClient1 =new TestAppClient(properties);\n        appClient1.start();\n\n        String email = \"test_new@gmail.com\";\n        String pass = \"a\";\n        String appName = AppNameUtil.BLYNK;\n\n        appClient1.send(\"getServer \" + email + \"\\0\" + appName);\n        appClient1.verifyResult(getServer(1, \"127.0.0.1\"));\n\n        appClient1.reset();\n\n        ProfileSaverWorker profileSaverWorker = new ProfileSaverWorker(holder.userDao, holder.fileManager, holder.dbManager);\n\n        workflowForUser(appClient1, email, pass, appName);\n        profileSaverWorker.run();\n        //waiting for DB update\n        TestUtil.sleep(500);\n\n        assertEquals(\"127.0.0.1\", holder.dbManager.getUserServerIp(email, AppNameUtil.BLYNK));\n\n        TestAppClient appClient2 = new TestAppClient(\"localhost\", tcpAppPort2, properties2);\n        appClient2.start();\n\n        appClient2.send(\"getServer \" + email + \"\\0\" + appName);\n        appClient2.verifyResult(getServer(1, \"127.0.0.1\"));\n    }\n\n    @Test\n    public void testCreateFewAccountWithDifferentApp() throws Exception {\n        TestAppClient appClient1 = new TestAppClient(properties);\n        appClient1.start();\n\n        String email = \"test@gmmail.com\";\n        String pass = \"a\";\n        String appName = \"Blynk\";\n\n        appClient1.send(\"getServer\");\n        appClient1.verifyResult(illegalCommand(1));\n\n        appClient1.send(\"getServer \" + email + \"\\0\" + appName);\n        appClient1.verifyResult(getServer(2, \"127.0.0.1\"));\n\n        appClient1.register(email, pass, appName);\n        appClient1.verifyResult(ok(3));\n        appClient1.login(email, pass, \"Android\", \"1.10.4 \" + appName);\n        //we should wait until login finished. Only after that we can send commands\n        appClient1.verifyResult(ok(4));\n\n        appClient1.send(\"getServer \" + email + \"\\0\" + appName);\n        appClient1.never(getServer(5, \"127.0.0.1\"));\n    }\n\n    @Test\n    public void testNoRedirectAsTokenIsWrong() throws Exception {\n        TestHardClient hardClient = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient.start();\n\n        hardClient.login(\"123\");\n        hardClient.verifyResult(invalidToken(1));\n\n        holder.dbManager.assignServerToToken(\"123\", \"127.0.0.1\", \"user\", 0, 0);\n        hardClient.login(\"123\");\n        hardClient.verifyResult(invalidToken(2));\n\n        hardClient.login(\"\\0\");\n        hardClient.verifyResult(invalidToken(3));\n    }\n\n    @Test\n    public void hardwareCreatedAndServerStoredInDB() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResultAfter(500, createDevice(1, device));\n\n        assertEquals(\"127.0.0.1\", holder.dbManager.forwardingTokenDBDao.selectHostByToken(device.token));\n    }\n\n    @Test\n    public void hardwareCreatedAndServerStoredInDBAndDelete() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResultAfter(1000, createDevice(1, device));\n\n        assertEquals(\"127.0.0.1\", holder.dbManager.forwardingTokenDBDao.selectHostByToken(device.token));\n\n        clientPair.appClient.send(\"deleteDevice 1\\0\" + device.id);\n        clientPair.appClient.verifyResultAfter(1000, ok(2));\n\n        assertNull(holder.dbManager.forwardingTokenDBDao.selectHostByToken(device.token));\n    }\n\n    @Test\n    public void redirectForHardwareWorks() throws Exception {\n        String token = \"12345678901234567890123456789012\";\n\n        assertTrue(holder.dbManager.forwardingTokenDBDao.insertTokenHost(\n                token, \"test_host\", getUserName(), 0, 0));\n\n        TestHardClient hardClient = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient.start();\n\n        hardClient.login(token);\n        hardClient.verifyResult(connectRedirect(1, \"test_host \" + tcpHardPort));\n    }\n\n    @Test\n    public void redirectForHardwareWorksWithForce80Port() throws Exception {\n        String token = \"12345678901234567890123456789013\";\n\n        assertTrue(holder.dbManager.forwardingTokenDBDao.insertTokenHost(\n                token, \"test_host\", getUserName(), 0, 0));\n\n        TestHardClient hardClient = new TestHardClient(\"localhost\", plainHardPort2);\n        hardClient.start();\n\n        hardClient.login(token);\n        hardClient.verifyResult(connectRedirect(1, \"test_host \" + 80));\n    }\n\n    @Test\n    public void testInvalidToken() throws Exception {\n        String token = \"1234567890123456789012345678901\";\n\n        assertTrue(holder.dbManager.forwardingTokenDBDao.insertTokenHost(\n                token, \"test_host\", getUserName(), 0, 0));\n\n        TestHardClient hardClient = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient.start();\n\n        hardClient.login(token);\n        verify(hardClient.responseMock, timeout(1000)).channelRead(any(), eq(invalidToken(1)));\n    }\n\n    @Test\n    public void redirectForHardwareWorksFromCache() throws Exception {\n        String token = \"12345678901234567890123456789013\";\n\n        assertTrue(holder.dbManager.forwardingTokenDBDao.insertTokenHost(\n                token, \"test_host\", getUserName(), 0, 0));\n\n        TestHardClient hardClient = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient.start();\n\n        hardClient.login(token);\n        hardClient.verifyResult(connectRedirect(1, \"test_host \" + tcpHardPort));\n\n        holder.dbManager.executeSQL(\"DELETE FROM forwarding_tokens\");\n\n        hardClient.login(token);\n        hardClient.verifyResult(connectRedirect(2, \"test_host \" + tcpHardPort));\n    }\n\n    @Test\n    public void redirectForHardwareDoesntWorkFromInvalidatedCache() throws Exception {\n        String token = \"12345678901234567890123456789012\";\n\n        assertTrue(holder.dbManager.forwardingTokenDBDao.insertTokenHost(\n                token, \"test_host\", getUserName(), 0, 0));\n\n        TestHardClient hardClient = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient.start();\n\n        hardClient.login(token);\n        hardClient.verifyResult(connectRedirect(1, \"test_host \" + tcpHardPort));\n\n        holder.dbManager.executeSQL(\"DELETE FROM forwarding_tokens\");\n\n        hardClient.login(token);\n        hardClient.verifyResult(connectRedirect(2, \"test_host \" + tcpHardPort));\n\n        LRUCache.LOGIN_TOKENS_CACHE.clear();\n\n        hardClient.login(token);\n        hardClient.verifyResult(invalidToken(3));\n\n        assertTrue(holder.dbManager.forwardingTokenDBDao.insertTokenHost(\n                token, \"test_host_2\", getUserName(), 0, 0));\n\n        LRUCache.LOGIN_TOKENS_CACHE.clear();\n\n        hardClient.login(token);\n        hardClient.verifyResult(connectRedirect(4, \"test_host_2 \" + tcpHardPort));\n    }\n\n    private String workflowForUser(TestAppClient appClient, String username, String pass, String appName) throws Exception{\n        appClient.register(username,  pass, appName);\n        appClient.verifyResult(ok(1));\n        appClient.login(username, pass, \"Android\", \"1.10.4 \" + appName);\n        appClient.verifyResult(ok(2));\n\n        DashBoard dash = new DashBoard();\n        dash.id = 1;\n        dash.name = \"test\";\n        appClient.createDash(dash);\n        appClient.verifyResult(ok(3));\n        appClient.activate(1);\n        appClient.verifyResult(new ResponseMessage(4, DEVICE_NOT_IN_NETWORK));\n\n        appClient.reset();\n        appClient.createDevice(1, new Device(0, \"123\", BoardType.ESP8266));\n        Device device = appClient.parseDevice();\n\n        String token = device.token;\n        assertNotNull(token);\n        return token;\n    }\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/tcp/MainWorkflowTest.java",
    "content": "package cc.blynk.integration.tcp;\n\nimport cc.blynk.integration.SingleServerInstancePerTest;\nimport cc.blynk.integration.TestUtil;\nimport cc.blynk.integration.model.tcp.TestAppClient;\nimport cc.blynk.integration.model.tcp.TestHardClient;\nimport cc.blynk.server.core.dao.ReportingDiskDao;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.DashboardSettings;\nimport cc.blynk.server.core.model.Profile;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.BoardType;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.enums.Theme;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.model.serialization.View;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.controls.Button;\nimport cc.blynk.server.core.model.widgets.controls.Step;\nimport cc.blynk.server.core.model.widgets.notifications.Twitter;\nimport cc.blynk.server.core.model.widgets.others.Player;\nimport cc.blynk.server.core.model.widgets.outputs.graph.GraphGranularityType;\nimport cc.blynk.server.core.model.widgets.ui.TimeInput;\nimport cc.blynk.server.core.protocol.model.messages.ResponseMessage;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.server.notifications.mail.QrHolder;\nimport cc.blynk.utils.AppNameUtil;\nimport cc.blynk.utils.FileUtils;\nimport cc.blynk.utils.SHA256Util;\nimport io.netty.channel.ChannelFuture;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.ArgumentCaptor;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.time.ZoneId;\nimport java.util.List;\n\nimport static cc.blynk.integration.TestUtil.appIsOutdated;\nimport static cc.blynk.integration.TestUtil.b;\nimport static cc.blynk.integration.TestUtil.createDevice;\nimport static cc.blynk.integration.TestUtil.deviceOffline;\nimport static cc.blynk.integration.TestUtil.hardware;\nimport static cc.blynk.integration.TestUtil.hardwareConnected;\nimport static cc.blynk.integration.TestUtil.illegalCommand;\nimport static cc.blynk.integration.TestUtil.illegalCommandBody;\nimport static cc.blynk.integration.TestUtil.notAllowed;\nimport static cc.blynk.integration.TestUtil.ok;\nimport static cc.blynk.integration.TestUtil.parseProfile;\nimport static cc.blynk.integration.TestUtil.readTestUserProfile;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_ENERGY;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE_CONNECTED;\nimport static cc.blynk.server.core.protocol.enums.Response.DEVICE_NOT_IN_NETWORK;\nimport static cc.blynk.server.core.protocol.enums.Response.INVALID_TOKEN;\nimport static cc.blynk.server.core.protocol.enums.Response.NOTIFICATION_INVALID_BODY;\nimport static cc.blynk.server.core.protocol.enums.Response.NOTIFICATION_NOT_AUTHORIZED;\nimport static cc.blynk.server.core.protocol.enums.Response.NO_ACTIVE_DASHBOARD;\nimport static cc.blynk.server.core.protocol.enums.Response.QUOTA_LIMIT;\nimport static cc.blynk.server.core.protocol.enums.Response.USER_ALREADY_REGISTERED;\nimport static cc.blynk.server.core.protocol.enums.Response.USER_NOT_AUTHENTICATED;\nimport static cc.blynk.server.core.protocol.model.messages.MessageFactory.produce;\nimport static org.junit.Assert.assertArrayEquals;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertFalse;\nimport static org.junit.Assert.assertNotEquals;\nimport static org.junit.Assert.assertNotNull;\nimport static org.junit.Assert.assertNull;\nimport static org.junit.Assert.assertTrue;\nimport static org.mockito.ArgumentMatchers.contains;\nimport static org.mockito.Mockito.after;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.eq;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.timeout;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/2/2015.\n *\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class MainWorkflowTest extends SingleServerInstancePerTest {\n\n    @Test\n    public void testCloneForLocalServerWithNoDB() throws Exception  {\n        assertFalse(holder.dbManager.isDBEnabled());\n\n        clientPair.appClient.send(\"getCloneCode 1\");\n        String token = clientPair.appClient.getBody();\n        assertNotNull(token);\n        assertEquals(32, token.length());\n\n        clientPair.appClient.send(\"getProjectByCloneCode \" + token);\n        DashBoard dashBoard = clientPair.appClient.parseDash(2);\n        assertEquals(\"My Dashboard\", dashBoard.name);\n    }\n\n    @Test\n    public void testResetEmail() throws Exception {\n        String userName = getUserName();\n        TestAppClient appClient = new TestAppClient(properties);\n        appClient.start();\n\n        appClient.send(\"resetPass start \" + userName + \" \" + AppNameUtil.BLYNK);\n        appClient.verifyResult(ok(1));\n\n        appClient.send(\"resetPass start \" + userName + \" \" + AppNameUtil.BLYNK);\n        appClient.verifyResult(notAllowed(2));\n\n        String token = holder.tokensPool.getTokens().entrySet().iterator().next().getKey();\n        verify(holder.mailWrapper).sendWithAttachment(eq(userName), eq(\"Password restoration for your Blynk account.\"), contains(\"http://blynk-cloud.com/restore?token=\" + token), any(QrHolder.class));\n\n        appClient.send(\"resetPass verify 123\");\n        appClient.verifyResult(notAllowed(3));\n\n        appClient.send(\"resetPass verify \" + token);\n        appClient.verifyResult(ok(4));\n\n        appClient.send(\"resetPass reset \" + token + \" \" + SHA256Util.makeHash(\"2\", userName));\n        appClient.verifyResult(ok(5));\n        //verify(holder.mailWrapper).sendHtml(eq(userName), eq(\"Your new password on Blynk\"), contains(\"You have changed your password on Blynk. Please, keep it in your records so you don't forget it.\"));\n\n        appClient.login(userName, \"1\");\n        appClient.verifyResult(new ResponseMessage(6, USER_NOT_AUTHENTICATED));\n\n        appClient.login(userName, \"2\");\n        appClient.verifyResult(ok(7));\n    }\n\n    @Test\n    public void testResetEmail2() throws Exception {\n        String userName = getUserName();\n        TestAppClient appClient = new TestAppClient(properties);\n        appClient.start();\n\n        appClient.send(\"resetPass start \" + userName + \" \" + AppNameUtil.BLYNK);\n        appClient.verifyResult(ok(1));\n\n        appClient.send(\"resetPass start \" + userName + \" \" + AppNameUtil.BLYNK);\n        appClient.verifyResult(notAllowed(2));\n\n        String token = holder.tokensPool.getTokens().entrySet().iterator().next().getKey();\n        verify(holder.mailWrapper).sendWithAttachment(eq(userName), eq(\"Password restoration for your Blynk account.\"), contains(\"http://blynk-cloud.com/restore?token=\" + token), any(QrHolder.class));\n\n        appClient.send(\"resetPass verify 123\");\n        appClient.verifyResult(notAllowed(3));\n\n        appClient.send(\"resetPass verify \" + token);\n        appClient.verifyResult(ok(4));\n\n        appClient.send(\"resetPass reset \" + token + \" \" + SHA256Util.makeHash(\"2\", userName));\n        appClient.verifyResult(ok(5));\n        //verify(holder.mailWrapper).sendHtml(eq(userName), eq(\"Your new password on Blynk\"), contains(\"You have changed your password on Blynk. Please, keep it in your records so you don't forget it.\"));\n\n        appClient.login(userName, \"1\");\n        appClient.verifyResult(new ResponseMessage(6, USER_NOT_AUTHENTICATED));\n\n        appClient.login(userName, \"2\");\n        appClient.verifyResult(ok(7));\n    }\n\n    @Test\n    public void registrationAllowedOnlyOncePerConnection() throws Exception {\n        TestAppClient appClient = new TestAppClient(properties);\n\n        appClient.start();\n\n        appClient.register(\"test1@test.com\", \"1\", AppNameUtil.BLYNK);\n        appClient.verifyResult(ok(1));\n\n        appClient.register(\"test2@test.com\", \"1\", AppNameUtil.BLYNK);\n        appClient.verifyResult(notAllowed(2));\n\n        assertTrue(appClient.isClosed());\n    }\n\n    @Test\n    public void createBasicProfile() throws Exception {\n        TestAppClient appClient = new TestAppClient(properties);\n\n        appClient.start();\n\n        String username = incrementAndGetUserName();\n\n        appClient.register(username, \"1\", AppNameUtil.BLYNK);\n        appClient.verifyResult(ok(1));\n\n        appClient.login(username, \"1\", \"Android\", \"RC13\");\n        appClient.verifyResult(ok(2));\n\n        appClient.createDash(\"{\\\"id\\\":1, \\\"createdAt\\\":1, \\\"name\\\":\\\"test board\\\"}\");\n        appClient.verifyResult(ok(3));\n\n        appClient.createWidget(1, \"{\\\"id\\\":1, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"BUTTON\\\", \\\"pinType\\\":\\\"DIGITAL\\\", \\\"pin\\\":1}\");\n        appClient.verifyResult(ok(4));\n\n        appClient.reset();\n\n        appClient.send(\"loadProfileGzipped\");\n        Profile profile = appClient.parseProfile(1);\n        profile.dashBoards[0].updatedAt = 0;\n\n        assertEquals(\"{\\\"dashBoards\\\":[{\\\"id\\\":1,\\\"parentId\\\":-1,\\\"isPreview\\\":false,\\\"name\\\":\\\"test board\\\",\\\"createdAt\\\":1,\\\"updatedAt\\\":0,\\\"widgets\\\":[{\\\"type\\\":\\\"BUTTON\\\",\\\"id\\\":1,\\\"x\\\":0,\\\"y\\\":0,\\\"color\\\":0,\\\"width\\\":1,\\\"height\\\":1,\\\"tabId\\\":0,\\\"label\\\":\\\"Some Text\\\",\\\"isDefaultColor\\\":false,\\\"deviceId\\\":0,\\\"pinType\\\":\\\"DIGITAL\\\",\\\"pin\\\":1,\\\"pwmMode\\\":false,\\\"rangeMappingOn\\\":false,\\\"min\\\":0.0,\\\"max\\\":0.0,\\\"pushMode\\\":false}],\\\"theme\\\":\\\"Blynk\\\",\\\"keepScreenOn\\\":false,\\\"isAppConnectedOn\\\":false,\\\"isNotificationsOff\\\":false,\\\"isShared\\\":false,\\\"isActive\\\":false,\\\"widgetBackgroundOn\\\":false,\\\"color\\\":-1,\\\"isDefaultColor\\\":true}]}\", profile.toString());\n\n        appClient.createWidget(1, \"{\\\"id\\\":2, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":2, \\\"y\\\":2, \\\"label\\\":\\\"Some Text 2\\\", \\\"type\\\":\\\"BUTTON\\\", \\\"pinType\\\":\\\"DIGITAL\\\", \\\"pin\\\":2}\");\n        appClient.verifyResult(ok(2));\n\n        appClient.reset();\n\n        appClient.send(\"loadProfileGzipped\");\n        profile = appClient.parseProfile(1);\n        profile.dashBoards[0].updatedAt = 0;\n        assertEquals(\"{\\\"dashBoards\\\":[{\\\"id\\\":1,\\\"parentId\\\":-1,\\\"isPreview\\\":false,\\\"name\\\":\\\"test board\\\",\\\"createdAt\\\":1,\\\"updatedAt\\\":0,\\\"widgets\\\":[{\\\"type\\\":\\\"BUTTON\\\",\\\"id\\\":1,\\\"x\\\":0,\\\"y\\\":0,\\\"color\\\":0,\\\"width\\\":1,\\\"height\\\":1,\\\"tabId\\\":0,\\\"label\\\":\\\"Some Text\\\",\\\"isDefaultColor\\\":false,\\\"deviceId\\\":0,\\\"pinType\\\":\\\"DIGITAL\\\",\\\"pin\\\":1,\\\"pwmMode\\\":false,\\\"rangeMappingOn\\\":false,\\\"min\\\":0.0,\\\"max\\\":0.0,\\\"pushMode\\\":false},{\\\"type\\\":\\\"BUTTON\\\",\\\"id\\\":2,\\\"x\\\":2,\\\"y\\\":2,\\\"color\\\":0,\\\"width\\\":1,\\\"height\\\":1,\\\"tabId\\\":0,\\\"label\\\":\\\"Some Text 2\\\",\\\"isDefaultColor\\\":false,\\\"deviceId\\\":0,\\\"pinType\\\":\\\"DIGITAL\\\",\\\"pin\\\":2,\\\"pwmMode\\\":false,\\\"rangeMappingOn\\\":false,\\\"min\\\":0.0,\\\"max\\\":0.0,\\\"pushMode\\\":false}],\\\"theme\\\":\\\"Blynk\\\",\\\"keepScreenOn\\\":false,\\\"isAppConnectedOn\\\":false,\\\"isNotificationsOff\\\":false,\\\"isShared\\\":false,\\\"isActive\\\":false,\\\"widgetBackgroundOn\\\":false,\\\"color\\\":-1,\\\"isDefaultColor\\\":true}]}\", profile.toString());\n\n        appClient.updateWidget(1, \"{\\\"id\\\":2, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":2, \\\"y\\\":2, \\\"label\\\":\\\"new label\\\", \\\"type\\\":\\\"BUTTON\\\", \\\"pinType\\\":\\\"DIGITAL\\\", \\\"pin\\\":3}\\\"\");\n        appClient.verifyResult(ok(2));\n\n        appClient.reset();\n\n        appClient.send(\"loadProfileGzipped\");\n        profile = appClient.parseProfile(1);\n        profile.dashBoards[0].updatedAt = 0;\n        assertEquals(\"{\\\"dashBoards\\\":[{\\\"id\\\":1,\\\"parentId\\\":-1,\\\"isPreview\\\":false,\\\"name\\\":\\\"test board\\\",\\\"createdAt\\\":1,\\\"updatedAt\\\":0,\\\"widgets\\\":[{\\\"type\\\":\\\"BUTTON\\\",\\\"id\\\":1,\\\"x\\\":0,\\\"y\\\":0,\\\"color\\\":0,\\\"width\\\":1,\\\"height\\\":1,\\\"tabId\\\":0,\\\"label\\\":\\\"Some Text\\\",\\\"isDefaultColor\\\":false,\\\"deviceId\\\":0,\\\"pinType\\\":\\\"DIGITAL\\\",\\\"pin\\\":1,\\\"pwmMode\\\":false,\\\"rangeMappingOn\\\":false,\\\"min\\\":0.0,\\\"max\\\":0.0,\\\"pushMode\\\":false},{\\\"type\\\":\\\"BUTTON\\\",\\\"id\\\":2,\\\"x\\\":2,\\\"y\\\":2,\\\"color\\\":0,\\\"width\\\":1,\\\"height\\\":1,\\\"tabId\\\":0,\\\"label\\\":\\\"new label\\\",\\\"isDefaultColor\\\":false,\\\"deviceId\\\":0,\\\"pinType\\\":\\\"DIGITAL\\\",\\\"pin\\\":3,\\\"pwmMode\\\":false,\\\"rangeMappingOn\\\":false,\\\"min\\\":0.0,\\\"max\\\":0.0,\\\"pushMode\\\":false}],\\\"theme\\\":\\\"Blynk\\\",\\\"keepScreenOn\\\":false,\\\"isAppConnectedOn\\\":false,\\\"isNotificationsOff\\\":false,\\\"isShared\\\":false,\\\"isActive\\\":false,\\\"widgetBackgroundOn\\\":false,\\\"color\\\":-1,\\\"isDefaultColor\\\":true}]}\", profile.toString());\n\n        appClient.deleteWidget(1, 3);\n        appClient.verifyResult(illegalCommand(2));\n\n        appClient.deleteWidget(1, 1);\n        appClient.verifyResult(ok(3));\n\n        appClient.deleteWidget(1, 2);\n        appClient.verifyResult(ok(4));\n\n        appClient.reset();\n\n        appClient.send(\"loadProfileGzipped\");\n        profile = appClient.parseProfile(1);\n        profile.dashBoards[0].updatedAt = 0;\n        assertEquals(\"{\\\"dashBoards\\\":[{\\\"id\\\":1,\\\"parentId\\\":-1,\\\"isPreview\\\":false,\\\"name\\\":\\\"test board\\\",\\\"createdAt\\\":1,\\\"updatedAt\\\":0,\\\"theme\\\":\\\"Blynk\\\",\\\"keepScreenOn\\\":false,\\\"isAppConnectedOn\\\":false,\\\"isNotificationsOff\\\":false,\\\"isShared\\\":false,\\\"isActive\\\":false,\\\"widgetBackgroundOn\\\":false,\\\"color\\\":-1,\\\"isDefaultColor\\\":true}]}\", profile.toString());\n    }\n\n    @Test\n    public void testNoEmptyPMCommands() throws Exception {\n        TestAppClient appClient = new TestAppClient(properties);\n\n        appClient.start();\n\n        String username = incrementAndGetUserName();\n\n        appClient.register(username, \"1\", AppNameUtil.BLYNK);\n        appClient.verifyResult(ok(1));\n\n        appClient.login(username, \"1\", \"Android\", \"RC13\");\n        appClient.verifyResult(ok(2));\n\n        appClient.createDash(\"{\\\"id\\\":1, \\\"createdAt\\\":1, \\\"name\\\":\\\"test board\\\"}\");\n        appClient.verifyResult(ok(3));\n\n        appClient.createWidget(1, \"{\\\"id\\\":1, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"BUTTON\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":1}\");\n        appClient.verifyResult(ok(4));\n\n        Device device = new Device();\n        device.id = 1;\n        device.name = \"123\";\n        device.boardType = BoardType.ESP32_Dev_Board;\n        appClient.createDevice(1, device);\n        device = appClient.parseDevice(5);\n\n        assertNotNull(device);\n        assertNotNull(device.token);\n        appClient.verifyResult(createDevice(5, device));\n\n        appClient.activate(1);\n        appClient.verifyResult(new ResponseMessage(6, DEVICE_NOT_IN_NETWORK));\n\n        TestHardClient hardClient = new TestHardClient(\"localhost\", properties.getHttpPort());\n        hardClient.start();\n\n        hardClient.login(device.token);\n        hardClient.verifyResult(ok(1));\n        hardClient.never(hardware(1, \"pm\"));\n        appClient.verifyResult(new StringMessage(1, HARDWARE_CONNECTED, \"1-1\"));\n\n        appClient.activate(1);\n        appClient.verifyResult(ok(7));\n        hardClient.never(hardware(1, \"pm\"));\n    }\n\n    @Test\n    public void doNotAllowUsersWithQuestionMark() throws Exception {\n        TestAppClient appClient = new TestAppClient(properties);\n\n        appClient.start();\n\n        appClient.register(\"te?st@test.com\", \"1\", AppNameUtil.BLYNK);\n        appClient.verifyResult(illegalCommand(1));\n    }\n\n    @Test\n    public void createDashWithDevices() throws Exception {\n        TestAppClient appClient = new TestAppClient(properties);\n\n        appClient.start();\n\n        appClient.register(\"test@test.com\", \"1\", AppNameUtil.BLYNK);\n        appClient.verifyResult(ok(1));\n\n        appClient.login(\"test@test.com\", \"1\", \"Android\", \"RC13\");\n        appClient.verifyResult(ok(2));\n\n        DashBoard dash = new DashBoard();\n        dash.id = 1;\n        dash.name = \"AAAa\";\n        Device device = new Device();\n        device.id = 0;\n        device.name = \"123\";\n        dash.devices = new Device[] {device};\n\n        appClient.createDash(\"no_token\\0\" + dash.toString());\n        appClient.verifyResult(ok(3));\n\n        appClient.send(\"getDevices 1\");\n\n        Device[] devices = appClient.parseDevices(4);\n        assertNotNull(devices);\n        assertEquals(1, devices.length);\n        assertEquals(0, devices[0].id);\n        assertEquals(\"123\", devices[0].name);\n        assertNull(devices[0].token);\n    }\n\n    @Test\n    public void testRegisterWithAnotherApp() throws Exception {\n        TestAppClient appClient = new TestAppClient(properties);\n\n        appClient.start();\n\n        appClient.register(getUserName(), \"1\", \"MyApp\");\n        appClient.verifyResult(ok(1));\n\n        appClient.login(getUserName(), \"1\", \"Android\", \"1.13.3\", \"MyApp\");\n        appClient.verifyResult(ok(2));\n\n        appClient.createDash(\"{\\\"id\\\":1, \\\"createdAt\\\":1, \\\"name\\\":\\\"test board\\\"}\");\n        appClient.verifyResult(ok(3));\n\n        appClient.createWidget(1, \"{\\\"id\\\":1, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"BUTTON\\\", \\\"pinType\\\":\\\"DIGITAL\\\", \\\"pin\\\":1}\");\n        appClient.verifyResult(ok(4));\n\n        appClient.reset();\n\n        appClient.send(\"loadProfileGzipped\");\n        Profile profile = appClient.parseProfile(1);\n        profile.dashBoards[0].updatedAt = 0;\n\n        assertEquals(\"{\\\"dashBoards\\\":[{\\\"id\\\":1,\\\"parentId\\\":-1,\\\"isPreview\\\":false,\\\"name\\\":\\\"test board\\\",\\\"createdAt\\\":1,\\\"updatedAt\\\":0,\\\"widgets\\\":[{\\\"type\\\":\\\"BUTTON\\\",\\\"id\\\":1,\\\"x\\\":0,\\\"y\\\":0,\\\"color\\\":0,\\\"width\\\":1,\\\"height\\\":1,\\\"tabId\\\":0,\\\"label\\\":\\\"Some Text\\\",\\\"isDefaultColor\\\":false,\\\"deviceId\\\":0,\\\"pinType\\\":\\\"DIGITAL\\\",\\\"pin\\\":1,\\\"pwmMode\\\":false,\\\"rangeMappingOn\\\":false,\\\"min\\\":0.0,\\\"max\\\":0.0,\\\"pushMode\\\":false}],\\\"theme\\\":\\\"Blynk\\\",\\\"keepScreenOn\\\":false,\\\"isAppConnectedOn\\\":false,\\\"isNotificationsOff\\\":false,\\\"isShared\\\":false,\\\"isActive\\\":false,\\\"widgetBackgroundOn\\\":false,\\\"color\\\":-1,\\\"isDefaultColor\\\":true}]}\", profile.toString());\n    }\n\n    @Test\n    public void testDoubleLogin() throws Exception {\n        clientPair.hardwareClient.login(getUserName() + \" 1\");\n        clientPair.hardwareClient.verifyResult(new ResponseMessage(1, USER_ALREADY_REGISTERED));\n    }\n\n    @Test\n    public void testDoubleLogin2() throws Exception {\n        TestHardClient newHardwareClient = new TestHardClient(\"localhost\", properties.getHttpPort());\n        newHardwareClient.start();\n        newHardwareClient.login(clientPair.token);\n        newHardwareClient.login(clientPair.token);\n        newHardwareClient.verifyResult(ok(1));\n        newHardwareClient.verifyResult(new ResponseMessage(2, USER_ALREADY_REGISTERED));\n    }\n\n    @Test\n    public void sendCommandBeforeLogin() {\n        TestHardClient newHardwareClient = new TestHardClient(\"localhost\", properties.getHttpPort());\n        newHardwareClient.start();\n        newHardwareClient.send(\"hardware vw 1 1\");\n\n        long tries = 0;\n        while(!newHardwareClient.isClosed() && tries < 10) {\n            TestUtil.sleep(100);\n            tries++;\n        }\n        assertTrue(newHardwareClient.isClosed());\n    }\n\n    @Test\n    public void testForwardBluetoothFromAppWorks() throws Exception {\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":743, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":2, \\\"y\\\":2, \\\"label\\\":\\\"Some Text 2\\\", \\\"type\\\":\\\"STEP\\\", \\\"pwmMode\\\":true, \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":67}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.send(\"hardwareBT 1-0 vw 67 100\");\n        verify(clientPair.hardwareClient.responseMock, after(500).never()).channelRead(any(), eq(hardware(2, \"1-0 vw 67 100\")));\n\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(2);\n        assertNotNull(profile);\n        Widget widget = profile.dashBoards[0].findWidgetByPin(0, (short) 67, PinType.VIRTUAL);\n        assertNotNull(widget);\n        assertTrue(widget instanceof Step);\n        assertEquals(\"100\", ((OnePinWidget) widget).value);\n    }\n\n    @Test\n    public void testValueForPWMPinForStteperIsAccepted() throws Exception {\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":743, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":2, \\\"y\\\":2, \\\"label\\\":\\\"Some Text 2\\\", \\\"type\\\":\\\"STEP\\\", \\\"pwmMode\\\":true, \\\"pinType\\\":\\\"DIGITAL\\\", \\\"pin\\\":24}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.send(\"hardware 1 aw 24 100\");\n        clientPair.hardwareClient.verifyResult(hardware(2, \"aw 24 100\"));\n\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(2);\n        assertNotNull(profile);\n        Widget widget = profile.dashBoards[0].findWidgetByPin(0, (short) 24, PinType.DIGITAL);\n        assertNotNull(widget);\n        assertTrue(widget instanceof Step);\n        assertEquals(\"100\", ((OnePinWidget) widget).value);\n    }\n\n    @Test\n    public void testSendInvalidVirtualPin() throws Exception {\n        clientPair.hardwareClient.send(\"hardware vw 256 100\");\n        clientPair.hardwareClient.verifyResult(illegalCommand(1));\n    }\n\n    @Test\n    public void testSendInvalidVirtualPin2() throws Exception {\n        clientPair.hardwareClient.send(\"hardware vw -1 100\");\n        clientPair.hardwareClient.verifyResult(illegalCommand(1));\n    }\n\n    @Test\n    public void testSendValidVirtualPin() throws Exception {\n        clientPair.hardwareClient.send(\"hardware vw 0 100\");\n        clientPair.hardwareClient.send(\"hardware vw 255 100\");\n        clientPair.hardwareClient.never(illegalCommand(1));\n        clientPair.hardwareClient.never(illegalCommand(2));\n    }\n\n    @Test\n    public void testNoEnergyDrainForBusinessApps() throws Exception {\n        TestAppClient appClient = new TestAppClient(properties);\n\n        appClient.start();\n\n        appClient.register(\"test@test.com\", \"1\", \"MyApp\");\n        appClient.verifyResult(ok(1));\n\n        appClient.login(\"test@test.com\", \"1\", \"Android\", \"1.13.3\", \"MyApp\");\n        appClient.verifyResult(ok(2));\n\n        appClient.createDash(\"{\\\"id\\\":2, \\\"createdAt\\\":1458856800001, \\\"name\\\":\\\"test board\\\"}\");\n        appClient.verifyResult(ok(3));\n\n        appClient.send(\"getEnergy\");\n        appClient.verifyResult(produce(4, GET_ENERGY, \"2000\"));\n\n        appClient.createWidget(2, \"{\\\"id\\\":2, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":2, \\\"y\\\":2, \\\"label\\\":\\\"Some Text 2\\\", \\\"type\\\":\\\"LCD\\\", \\\"pinType\\\":\\\"DIGITAL\\\", \\\"pin\\\":2}\");\n        appClient.verifyResult(ok(5));\n\n        appClient.createWidget(2, \"{\\\"id\\\":3, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":2, \\\"y\\\":2, \\\"label\\\":\\\"Some Text 2\\\", \\\"type\\\":\\\"LCD\\\", \\\"pinType\\\":\\\"DIGITAL\\\", \\\"pin\\\":2}\");\n        appClient.verifyResult(ok(6));\n\n        appClient.createWidget(2, \"{\\\"id\\\":4, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":2, \\\"y\\\":2, \\\"label\\\":\\\"Some Text 2\\\", \\\"type\\\":\\\"LCD\\\", \\\"pinType\\\":\\\"DIGITAL\\\", \\\"pin\\\":2}\");\n        appClient.verifyResult(ok(7));\n\n        appClient.createWidget(2, \"{\\\"id\\\":5, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":2, \\\"y\\\":2, \\\"label\\\":\\\"Some Text 2\\\", \\\"type\\\":\\\"LCD\\\", \\\"pinType\\\":\\\"DIGITAL\\\", \\\"pin\\\":2}\");\n        appClient.verifyResult(ok(8));\n\n        appClient.createWidget(2, \"{\\\"id\\\":6, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":2, \\\"y\\\":2, \\\"label\\\":\\\"Some Text 2\\\", \\\"type\\\":\\\"LCD\\\", \\\"pinType\\\":\\\"DIGITAL\\\", \\\"pin\\\":2}\");\n        appClient.verifyResult(ok(9));\n\n        appClient.createWidget(2, \"{\\\"id\\\":7, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":2, \\\"y\\\":2, \\\"label\\\":\\\"Some Text 2\\\", \\\"type\\\":\\\"BUTTON\\\", \\\"pinType\\\":\\\"DIGITAL\\\", \\\"pin\\\":2}\");\n        appClient.verifyResult(ok(10));\n    }\n\n    @Test\n    public void testPingCommandWorks() throws Exception {\n        clientPair.appClient.send(\"ping\");\n        clientPair.appClient.verifyResult(ok(1));\n    }\n\n    @Test\n    public void testAddAndRemoveTabs() throws Exception {\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(1);\n        assertEquals(16, profile.dashBoards[0].widgets.length);\n\n        clientPair.appClient.send(\"getEnergy\");\n        clientPair.appClient.verifyResult(produce(2, GET_ENERGY, \"7500\"));\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":100, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"tabs\\\":[{\\\"label\\\":\\\"tab 1\\\"}, {\\\"label\\\":\\\"tab 2\\\"}, {\\\"label\\\":\\\"tab 3\\\"}], \\\"type\\\":\\\"TABS\\\"}\");\n        clientPair.appClient.verifyResult(ok(3));\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":101, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":15, \\\"y\\\":0, \\\"tabId\\\":1, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"BUTTON\\\", \\\"pinType\\\":\\\"DIGITAL\\\", \\\"pin\\\":18}\");\n        clientPair.appClient.verifyResult(ok(4));\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":102, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":5, \\\"y\\\":0, \\\"tabId\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"BUTTON\\\", \\\"pinType\\\":\\\"DIGITAL\\\", \\\"pin\\\":17}\");\n        clientPair.appClient.verifyResult(ok(5));\n\n        clientPair.appClient.send(\"getEnergy\");\n        clientPair.appClient.verifyResult(produce(6, GET_ENERGY, \"7100\"));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        profile = clientPair.appClient.parseProfile(1);\n        assertEquals(19, profile.dashBoards[0].widgets.length);\n\n        clientPair.appClient.deleteWidget(1, 100);\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.appClient.send(\"getEnergy\");\n        clientPair.appClient.verifyResult(produce(3, GET_ENERGY, \"7300\"));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        profile = clientPair.appClient.parseProfile(1);\n        assertEquals(17, profile.dashBoards[0].widgets.length);\n        assertNotNull(profile.dashBoards[0].findWidgetByPin(0, (short) 17, PinType.DIGITAL));\n    }\n\n    @Test\n    public void testAddAndUpdateTabs() throws Exception {\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(1);\n        assertEquals(16, profile.dashBoards[0].widgets.length);\n\n        clientPair.appClient.send(\"getEnergy\");\n        clientPair.appClient.verifyResult(produce(2, GET_ENERGY, \"7500\"));\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":100, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"tabs\\\":[{\\\"label\\\":\\\"tab 1\\\"}, {\\\"label\\\":\\\"tab 2\\\"}, {\\\"label\\\":\\\"tab 3\\\"}], \\\"type\\\":\\\"TABS\\\"}\");\n        clientPair.appClient.verifyResult(ok(3));\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":101, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":15, \\\"y\\\":0, \\\"tabId\\\":1, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"BUTTON\\\", \\\"pinType\\\":\\\"DIGITAL\\\", \\\"pin\\\":18}\");\n        clientPair.appClient.verifyResult(ok(4));\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":102, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":5, \\\"y\\\":0, \\\"tabId\\\":2, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"BUTTON\\\", \\\"pinType\\\":\\\"DIGITAL\\\", \\\"pin\\\":17}\");\n        clientPair.appClient.verifyResult(ok(5));\n\n        clientPair.appClient.send(\"getEnergy\");\n        clientPair.appClient.verifyResult(produce(6, GET_ENERGY, \"7100\"));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        profile = clientPair.appClient.parseProfile(1);\n        assertEquals(19, profile.dashBoards[0].widgets.length);\n\n        clientPair.appClient.updateWidget(1, \"{\\\"id\\\":100, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"tabs\\\":[{\\\"label\\\":\\\"tab 1\\\"}, {\\\"label\\\":\\\"tab 2\\\"}], \\\"type\\\":\\\"TABS\\\"}\");\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.appClient.send(\"getEnergy\");\n        clientPair.appClient.verifyResult(produce(3, GET_ENERGY, \"7300\"));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        profile = clientPair.appClient.parseProfile(1);\n        assertEquals(18, profile.dashBoards[0].widgets.length);\n        assertNull(profile.dashBoards[0].findWidgetByPin(0, (short) 17, PinType.DIGITAL));\n        assertNotNull(profile.dashBoards[0].findWidgetByPin(0, (short) 18, PinType.DIGITAL));\n    }\n\n    @Test\n    public void testPurchaseEnergy() throws Exception {\n        clientPair.appClient.send(\"addEnergy \" + \"1000\" + \"\\0\" + \"5262996016779471529.4493624392154338\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(notAllowed(1)));\n\n        clientPair.appClient.send(\"addEnergy \" + \"1000\" + \"\\0\" + \"A3B93EE9-BC65-499E-A660-F2A84F2AF1FC\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(ok(2)));\n\n        clientPair.appClient.send(\"addEnergy \" + \"1000\" + \"\\0\" + \"com.blynk.energy.280001461578468247\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(notAllowed(3)));\n\n        clientPair.appClient.send(\"addEnergy \" + \"1000\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(notAllowed(4)));\n\n        clientPair.appClient.send(\"addEnergy \" + \"1000\" + \"\\0\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(notAllowed(5)));\n\n        clientPair.appClient.send(\"addEnergy \" + \"1000\" + \"\\0\" + \"150000195113772\");\n        clientPair.appClient.verifyResult(ok(6));\n\n        clientPair.appClient.send(\"addEnergy \" + \"1000\" + \"\\0\" + \"1370-3990-1414-55681\");\n        clientPair.appClient.verifyResult(ok(7));\n    }\n\n    @Test\n    public void testApplicationPingCommandOk() throws Exception {\n        clientPair.appClient.send(\"ping\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.reset();\n\n        clientPair.appClient.send(\"ping\");\n        clientPair.appClient.verifyResult(ok(1));\n    }\n\n    @Test\n    public void testHardPingCommandOk() throws Exception {\n        clientPair.hardwareClient.send(\"ping\");\n        clientPair.hardwareClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"ping\");\n        clientPair.hardwareClient.verifyResult(ok(2));\n    }\n\n    @Test\n    public void testDashCommands() throws Exception {\n        clientPair.appClient.updateDash(\"{\\\"id\\\":10, \\\"name\\\":\\\"test board update\\\"}\");\n        clientPair.appClient.verifyResult(illegalCommand(1));\n\n        clientPair.appClient.createDash(\"{\\\"id\\\":10, \\\"name\\\":\\\"test board\\\"}\");\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.appClient.createDash(\"{\\\"id\\\":10, \\\"name\\\":\\\"test board\\\"}\");\n        clientPair.appClient.verifyResult(notAllowed(3));\n\n        clientPair.appClient.updateDash(\"{\\\"id\\\":10, \\\"name\\\":\\\"test board update\\\"}\");\n        clientPair.appClient.verifyResult(ok(4));\n\n        clientPair.hardwareClient.send(\"ping\");\n\n        clientPair.appClient.deleteDash(1);\n        clientPair.appClient.verifyResult(ok(5));\n\n        clientPair.appClient.deleteDash(1);\n        clientPair.appClient.verifyResult(illegalCommand(6));\n        clientPair.appClient.verifyResult(deviceOffline(0, \"1-0\"));\n        clientPair.appClient.reset();\n\n        Profile responseProfile;\n        DashBoard responseDash;\n\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        responseProfile = clientPair.appClient.parseProfile(1);\n        responseProfile.dashBoards[0].updatedAt = 0;\n        responseProfile.dashBoards[0].createdAt = 0;\n        assertEquals(\"{\\\"dashBoards\\\":[{\\\"id\\\":10,\\\"parentId\\\":-1,\\\"isPreview\\\":false,\\\"name\\\":\\\"test board update\\\",\\\"createdAt\\\":0,\\\"updatedAt\\\":0,\\\"theme\\\":\\\"Blynk\\\",\\\"keepScreenOn\\\":false,\\\"isAppConnectedOn\\\":false,\\\"isNotificationsOff\\\":false,\\\"isShared\\\":false,\\\"isActive\\\":false,\\\"widgetBackgroundOn\\\":false,\\\"color\\\":-1,\\\"isDefaultColor\\\":true}]}\", responseProfile.toString());\n\n        clientPair.appClient.send(\"loadProfileGzipped 10\");\n        responseDash = clientPair.appClient.parseDash(2);\n        responseDash.updatedAt = 0;\n        responseDash.createdAt = 0;\n        assertEquals(\"{\\\"id\\\":10,\\\"parentId\\\":-1,\\\"isPreview\\\":false,\\\"name\\\":\\\"test board update\\\",\\\"createdAt\\\":0,\\\"updatedAt\\\":0,\\\"theme\\\":\\\"Blynk\\\",\\\"keepScreenOn\\\":false,\\\"isAppConnectedOn\\\":false,\\\"isNotificationsOff\\\":false,\\\"isShared\\\":false,\\\"isActive\\\":false,\\\"widgetBackgroundOn\\\":false,\\\"color\\\":-1,\\\"isDefaultColor\\\":true}\", responseDash.toString());\n\n        clientPair.appClient.send(\"loadProfileGzipped 1\");\n        clientPair.appClient.verifyResult(illegalCommand(3));\n\n        clientPair.appClient.activate(10);\n        clientPair.appClient.verifyResult(new ResponseMessage(4, DEVICE_NOT_IN_NETWORK));\n\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        responseProfile = clientPair.appClient.parseProfile(5);\n        responseProfile.dashBoards[0].updatedAt = 0;\n        responseProfile.dashBoards[0].createdAt = 0;\n        String expectedProfile = \"{\\\"dashBoards\\\":[{\\\"id\\\":10,\\\"parentId\\\":-1,\\\"isPreview\\\":false,\\\"name\\\":\\\"test board update\\\",\\\"createdAt\\\":0,\\\"updatedAt\\\":0,\\\"theme\\\":\\\"Blynk\\\",\\\"keepScreenOn\\\":false,\\\"isAppConnectedOn\\\":false,\\\"isNotificationsOff\\\":false,\\\"isShared\\\":false,\\\"isActive\\\":true,\\\"widgetBackgroundOn\\\":false,\\\"color\\\":-1,\\\"isDefaultColor\\\":true}]}\";\n        assertEquals(expectedProfile, responseProfile.toString());\n\n        clientPair.appClient.updateDash(\"{\\\"id\\\":10,\\\"name\\\":\\\"test board update\\\",\\\"keepScreenOn\\\":false,\\\"isShared\\\":false,\\\"isActive\\\":false}\");\n        clientPair.appClient.verifyResult(ok(6));\n\n        expectedProfile = \"{\\\"dashBoards\\\":[{\\\"id\\\":10,\\\"parentId\\\":-1,\\\"isPreview\\\":false,\\\"name\\\":\\\"test board update\\\",\\\"createdAt\\\":0,\\\"updatedAt\\\":0,\\\"theme\\\":\\\"Blynk\\\",\\\"keepScreenOn\\\":false,\\\"isAppConnectedOn\\\":false,\\\"isNotificationsOff\\\":false,\\\"isShared\\\":false,\\\"isActive\\\":true,\\\"widgetBackgroundOn\\\":false,\\\"color\\\":-1,\\\"isDefaultColor\\\":true}]}\";\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        responseProfile = clientPair.appClient.parseProfile(7);\n        responseProfile.dashBoards[0].updatedAt = 0;\n        responseProfile.dashBoards[0].createdAt = 0;\n        assertEquals(expectedProfile, responseProfile.toString());\n    }\n\n    @Test\n    public void testHardwareChannelClosedOnDashRemoval() throws Exception {\n        String username = getUserName();\n        String tempDir = holder.props.getProperty(\"data.folder\");\n        Path userReportFolder = Paths.get(tempDir, \"data\", username);\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n\n        Path pinReportingDataPath10 = Paths.get(tempDir, \"data\", username,\n                ReportingDiskDao.generateFilename(1, 0, PinType.DIGITAL, (short) 8, GraphGranularityType.MINUTE));\n        Path pinReportingDataPath11 = Paths.get(tempDir, \"data\", username,\n                ReportingDiskDao.generateFilename(1, 0, PinType.DIGITAL, (short) 8, GraphGranularityType.HOURLY));\n        Path pinReportingDataPath12 = Paths.get(tempDir, \"data\", username,\n                ReportingDiskDao.generateFilename(1, 0, PinType.DIGITAL, (short) 8, GraphGranularityType.DAILY));\n        Path pinReportingDataPath13 = Paths.get(tempDir, \"data\", username,\n                ReportingDiskDao.generateFilename(1, 0, PinType.VIRTUAL, (short) 9, GraphGranularityType.DAILY));\n\n        FileUtils.write(pinReportingDataPath10, 1.11D, 1111111);\n        FileUtils.write(pinReportingDataPath11, 1.11D, 1111111);\n        FileUtils.write(pinReportingDataPath12, 1.11D, 1111111);\n        FileUtils.write(pinReportingDataPath13, 1.11D, 1111111);\n\n        clientPair.appClient.deleteDash(1);\n        clientPair.appClient.verifyResult(ok(1));\n\n        assertTrue(clientPair.hardwareClient.isClosed());\n        assertTrue(Files.notExists(pinReportingDataPath10));\n        assertTrue(Files.notExists(pinReportingDataPath11));\n        assertTrue(Files.notExists(pinReportingDataPath12));\n        assertTrue(Files.notExists(pinReportingDataPath13));\n    }\n\n    @Test\n    public void testGetTokenWorksWithNewFormats() throws Exception {\n        clientPair.appClient.createDash(\"{\\\"id\\\":10, \\\"name\\\":\\\"test board\\\"}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.createDevice(10, new Device(2, \"123\", BoardType.ESP8266));\n        Device device = clientPair.appClient.parseDevice(2);\n        String token = device.token;\n        assertNotNull(token);\n\n        clientPair.appClient.getDevice(10, 2);\n        Device device2 = clientPair.appClient.parseDevice(3);\n        assertNotNull(device2);\n        assertEquals(token, device2.token);\n\n        clientPair.appClient.getDevice(10, 2);\n        device2 = clientPair.appClient.parseDevice(4);\n        assertNotNull(device2);\n        assertEquals(token, device2.token);\n\n        clientPair.appClient.createDash(\"{\\\"id\\\":11, \\\"name\\\":\\\"test board\\\"}\");\n        clientPair.appClient.verifyResult(ok(5));\n\n        clientPair.appClient.getDevice(11, 0);\n        clientPair.appClient.verifyResult(illegalCommandBody(6));\n    }\n\n    @Test\n    public void deleteDashDeletesTokensAlso() throws Exception {\n        clientPair.appClient.createDash(\"{\\\"id\\\":10, \\\"name\\\":\\\"test board\\\"}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.createDevice(10, new Device(2, \"123\", BoardType.ESP8266));\n        Device device = clientPair.appClient.parseDevice();\n        String token = device.token;\n        assertNotNull(token);\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"getShareToken 10\");\n        String sharedToken = clientPair.appClient.getBody();\n        assertNotNull(sharedToken);\n\n        clientPair.appClient.deleteDash(10);\n        clientPair.appClient.verifyResult(ok(2));\n\n        TestHardClient newHardClient = new TestHardClient(\"localhost\", properties.getHttpPort());\n        newHardClient.start();\n        newHardClient.login(token);\n        newHardClient.verifyResult(new ResponseMessage(1, INVALID_TOKEN));\n\n        TestAppClient newAppClient = new TestAppClient(properties);\n        newAppClient.start();\n        newAppClient.send(\"shareLogin \" + getUserName() + \" \" + sharedToken + \" Android 24\");\n\n        newAppClient.verifyResult(notAllowed(1));\n    }\n\n    @Test\n    public void loadGzippedProfile() throws Exception{\n        Profile expectedProfile = JsonParser.parseProfileFromString(readTestUserProfile());\n\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(1);\n\n        profile.dashBoards[0].updatedAt = 0;\n\n        expectedProfile.dashBoards[0].devices = null;\n        profile.dashBoards[0].devices = null;\n\n        assertEquals(expectedProfile.toString(), profile.toString());\n    }\n\n    @Test\n    public void settingsUpdateCommand() throws Exception{\n        DashboardSettings settings = new DashboardSettings(\"New Name\",\n                true, Theme.BlynkLight, true, true, false, false, 0, false);\n\n        clientPair.appClient.send(\"updateSettings 1\\0\" + JsonParser.toJson(settings));\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(2);\n        DashBoard dashBoard = profile.dashBoards[0];\n        assertNotNull(dashBoard);\n        assertEquals(settings.name, dashBoard.name);\n        assertEquals(settings.isAppConnectedOn, dashBoard.isAppConnectedOn);\n        assertEquals(settings.isNotificationsOff, dashBoard.isNotificationsOff);\n        assertEquals(settings.isShared, dashBoard.isShared);\n        assertEquals(settings.keepScreenOn, dashBoard.keepScreenOn);\n        assertEquals(settings.theme, dashBoard.theme);\n    }\n\n    @Test\n    public void testSendUnicodeChar() throws Exception {\n        clientPair.hardwareClient.send(\"hardware vw 1 °F\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(hardware(1, \"1-0 vw 1 °F\")));\n    }\n\n    @Test\n    public void testAppSendAnyHardCommandAndBack() throws Exception {\n        clientPair.appClient.send(\"hardware 1 dw 1 1\");\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(hardware(1, \"dw 1 1\")));\n\n        clientPair.hardwareClient.send(\"hardware ar 2\");\n        verify(clientPair.hardwareClient.responseMock, after(500).never()).channelRead(any(), eq(hardware(2, \"ar 2\")));\n    }\n\n    @Test\n    public void testAppNoActiveDashForHard() throws Exception {\n        clientPair.hardwareClient.send(\"hardware aw 1 1\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(hardware(1, \"1-0 aw 1 1\")));\n\n        clientPair.appClient.deactivate(1);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware aw 1 1\");\n        verify(clientPair.appClient.responseMock, after(500).never()).channelRead(any(), eq(new ResponseMessage(2, NO_ACTIVE_DASHBOARD)));\n    }\n\n    @Test\n    public void testHardwareSendsWrongCommand() throws Exception {\n        clientPair.hardwareClient.send(\"hardware aw 1 \");\n        clientPair.hardwareClient.verifyResult(illegalCommand(1));\n\n        clientPair.hardwareClient.send(\"hardware aw 1\");\n        clientPair.hardwareClient.verifyResult(illegalCommand(2));\n    }\n\n    @Test\n    public void testAppChangeActiveDash() throws Exception {\n        clientPair.hardwareClient.send(\"hardware aw 1 1\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(hardware(1, \"1-0 aw 1 1\")));\n\n        clientPair.appClient.deactivate(1);\n        clientPair.appClient.verifyResult(ok(1));\n\n        Profile newProfile = parseProfile(readTestUserProfile(\"user_profile_json_3_dashes.txt\"));\n        clientPair.appClient.createDash(newProfile.dashBoards[1]);\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.hardwareClient.send(\"hardware aw 1 1\");\n        verify(clientPair.appClient.responseMock, after(500).never()).channelRead(any(), eq(new ResponseMessage(2, NO_ACTIVE_DASHBOARD)));\n\n        clientPair.appClient.activate(2);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new ResponseMessage(3, DEVICE_NOT_IN_NETWORK)));\n\n        clientPair.hardwareClient.send(\"hardware aw 1 1\");\n        verify(clientPair.appClient.responseMock, after(500).never()).channelRead(any(), eq(new ResponseMessage(3, NO_ACTIVE_DASHBOARD)));\n\n        clientPair.appClient.activate(1);\n        clientPair.appClient.verifyResult(ok(4));\n\n        clientPair.hardwareClient.send(\"hardware aw 1 1\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(hardware(4, \"1-0 aw 1 1\")));\n    }\n\n    @Test\n    public void testActive2AndDeactivate1() throws Exception {\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", properties.getHttpPort());\n        hardClient2.start();\n\n        Profile newProfile = parseProfile(readTestUserProfile(\"user_profile_json_3_dashes.txt\"));\n        clientPair.appClient.createDash(newProfile.dashBoards[1]);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.activate(1);\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.appClient.activate(2);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new ResponseMessage(3, DEVICE_NOT_IN_NETWORK)));\n\n        clientPair.appClient.reset();\n\n        clientPair.appClient.createDevice(2, new Device(2, \"123\", BoardType.ESP8266));\n        Device device = clientPair.appClient.parseDevice();\n        String token2 = device.token;\n        hardClient2.login(token2);\n        hardClient2.verifyResult(ok(1));\n\n        clientPair.appClient.reset();\n\n        clientPair.hardwareClient.send(\"hardware aw 1 1\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(hardware(1, \"1-0 aw 1 1\")));\n\n        hardClient2.send(\"hardware aw 1 1\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(hardware(2, \"2-\" + device.id + \" aw 1 1\")));\n\n\n        clientPair.appClient.deactivate(1);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware aw 1 1\");\n        verify(clientPair.appClient.responseMock, after(500).never()).channelRead(any(), eq(new ResponseMessage(2, NO_ACTIVE_DASHBOARD)));\n\n        hardClient2.send(\"hardware aw 1 1\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(hardware(3, \"2-\" + device.id + \" aw 1 1\")));\n        hardClient2.stop().awaitUninterruptibly();\n    }\n\n    @Test\n    public void testActivateWorkflow() throws Exception {\n        clientPair.appClient.activate(2);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(illegalCommand(1)));\n\n        clientPair.appClient.deactivate(2);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(illegalCommand(2)));\n\n        clientPair.appClient.send(\"hardware 1 ar 1 1\");\n        verify(clientPair.appClient.responseMock, never()).channelRead(any(), eq(ok(3)));\n\n        clientPair.appClient.activate(1);\n        clientPair.appClient.verifyResult(ok(4));\n    }\n\n    @Test\n    public void testTweetNotWorks() throws Exception {\n        clientPair.hardwareClient.send(\"tweet\");\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(new ResponseMessage(1, NOTIFICATION_INVALID_BODY)));\n\n        clientPair.hardwareClient.send(\"tweet \");\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(new ResponseMessage(2, NOTIFICATION_INVALID_BODY)));\n\n        StringBuilder a = new StringBuilder();\n        for (int i = 0; i < 141; i++) {\n            a.append(\"a\");\n        }\n\n        clientPair.hardwareClient.send(\"tweet \" + a);\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(new ResponseMessage(3, NOTIFICATION_INVALID_BODY)));\n\n        clientPair.appClient.deactivate(1);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"tweet yo\");\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(new ResponseMessage(4, NOTIFICATION_NOT_AUTHORIZED)));\n    }\n\n    @Test\n    public void testSmsWorks() throws Exception {\n        clientPair.hardwareClient.send(\"sms\");\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(new ResponseMessage(1, NOTIFICATION_INVALID_BODY)));\n\n        //no sms widget\n        clientPair.hardwareClient.send(\"sms yo\");\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(new ResponseMessage(2, NOTIFICATION_NOT_AUTHORIZED)));\n\n        //adding sms widget\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":432, \\\"width\\\":1, \\\"height\\\":1, \\\"to\\\":\\\"3809683423423\\\", \\\"x\\\":0, \\\"y\\\":0, \\\"type\\\":\\\"SMS\\\"}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"sms yo\");\n        verify(holder.smsWrapper, timeout(500)).send(eq(\"3809683423423\"), eq(\"yo\"));\n        clientPair.hardwareClient.verifyResult(ok(3));\n\n        clientPair.hardwareClient.send(\"sms yo\");\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(new ResponseMessage(4, QUOTA_LIMIT)));\n    }\n\n    @Test\n    public void testTweetWorks() throws Exception {\n        clientPair.hardwareClient.send(\"tweet yo\");\n        verify(holder.twitterWrapper, timeout(500)).send(eq(\"token\"), eq(\"secret\"), eq(\"yo\"), any());\n\n        clientPair.hardwareClient.send(\"tweet yo\");\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(new ResponseMessage(2, QUOTA_LIMIT)));\n    }\n\n    @Test\n    public void testPlayerUpdateWorksAsExpected() throws Exception {\n        clientPair.appClient.createWidget(1, \"{\\\"type\\\":\\\"PLAYER\\\",\\\"id\\\":99, \\\"pin\\\":99, \\\"pinType\\\":\\\"VIRTUAL\\\", \" +\n                \"\\\"x\\\":0,\\\"y\\\":0,\\\"width\\\":1,\\\"height\\\":1}\");\n\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.send(\"hardware 1 vw 99 play\");\n        verify(clientPair.hardwareClient.responseMock, timeout(500).times(1)).channelRead(any(), eq(hardware(2, \"vw 99 play\")));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(1);\n        Player player = (Player) profile.dashBoards[0].findWidgetByPin(0, (short) 99, PinType.VIRTUAL);\n        assertNotNull(player);\n        assertTrue(player.isOnPlay);\n\n        clientPair.appClient.send(\"hardware 1 vw 99 stop\");\n        verify(clientPair.hardwareClient.responseMock, timeout(500).times(1)).channelRead(any(), eq(hardware(2, \"vw 99 stop\")));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        profile = clientPair.appClient.parseProfile(1);\n        player = (Player) profile.dashBoards[0].findWidgetByPin(0, (short) 99, PinType.VIRTUAL);\n        assertNotNull(player);\n        assertFalse(player.isOnPlay);\n    }\n\n    @Test\n    public void testTimeInputUpdateWorksAsExpected() throws Exception {\n        clientPair.appClient.createWidget(1, \"{\\\"type\\\":\\\"TIME_INPUT\\\",\\\"id\\\":99, \\\"pin\\\":99, \\\"pinType\\\":\\\"VIRTUAL\\\", \" +\n                \"\\\"x\\\":0,\\\"y\\\":0,\\\"width\\\":1,\\\"height\\\":1}\");\n\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.send(\"hardware 1 vw \" + b(\"99 82800 82860 Europe/Kiev 1\"));\n        verify(clientPair.hardwareClient.responseMock, timeout(500).times(1)).channelRead(any(), eq(hardware(2, \"vw 99 82800 82860 Europe/Kiev 1\")));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(1);\n        TimeInput timeInput = (TimeInput) profile.dashBoards[0].findWidgetByPin(0, (short) 99, PinType.VIRTUAL);\n        assertNotNull(timeInput);\n        assertEquals(82800, timeInput.startAt);\n        assertEquals(82860, timeInput.stopAt);\n        assertEquals(ZoneId.of(\"Europe/Kiev\"), timeInput.tzName);\n        assertArrayEquals(new int[] {1}, timeInput.days);\n\n\n        clientPair.appClient.send(\"hardware 1 vw \" + b(\"99 82800 82860 Europe/Kiev \"));\n        verify(clientPair.hardwareClient.responseMock, timeout(500).times(1)).channelRead(any(), eq(hardware(2, \"vw 99 82800 82860 Europe/Kiev \")));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        profile = clientPair.appClient.parseProfile(1);\n        timeInput = (TimeInput) profile.dashBoards[0].findWidgetByPin(0, (short) 99, PinType.VIRTUAL);\n        assertNotNull(timeInput);\n        assertEquals(82800, timeInput.startAt);\n        assertEquals(82860, timeInput.stopAt);\n        assertEquals(ZoneId.of(\"Europe/Kiev\"), timeInput.tzName);\n        assertNull(timeInput.days);\n\n        clientPair.appClient.send(\"hardware 1 vw \" + b(\"99 82800  Europe/Kiev \"));\n        verify(clientPair.hardwareClient.responseMock, timeout(500).times(1)).channelRead(any(), eq(hardware(2, \"vw 99 82800  Europe/Kiev \")));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        profile = clientPair.appClient.parseProfile(1);\n        timeInput = (TimeInput) profile.dashBoards[0].findWidgetByPin(0, (short) 99, PinType.VIRTUAL);\n        assertNotNull(timeInput);\n        assertEquals(82800, timeInput.startAt);\n        assertEquals(-1, timeInput.stopAt);\n        assertEquals(ZoneId.of(\"Europe/Kiev\"), timeInput.tzName);\n        assertNull(timeInput.days);\n\n        clientPair.appClient.send(\"hardware 1 vw \" + b(\"99 82800  Europe/Kiev 1,2,3,4\"));\n        verify(clientPair.hardwareClient.responseMock, timeout(500).times(1)).channelRead(any(), eq(hardware(2, \"vw 99 82800  Europe/Kiev 1,2,3,4\")));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        profile = clientPair.appClient.parseProfile(1);\n        timeInput = (TimeInput) profile.dashBoards[0].findWidgetByPin(0, (short) 99, PinType.VIRTUAL);\n        assertNotNull(timeInput);\n        assertEquals(82800, timeInput.startAt);\n        assertEquals(-1, timeInput.stopAt);\n        assertEquals(ZoneId.of(\"Europe/Kiev\"), timeInput.tzName);\n        assertArrayEquals(new int[]{1,2,3,4}, timeInput.days);\n\n        clientPair.appClient.send(\"hardware 1 vw \" + b(\"99   Europe/Kiev 1,2,3,4\"));\n        verify(clientPair.hardwareClient.responseMock, timeout(500).times(1)).channelRead(any(), eq(hardware(2, \"vw 99   Europe/Kiev 1,2,3,4\")));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        profile = clientPair.appClient.parseProfile(1);\n        timeInput = (TimeInput) profile.dashBoards[0].findWidgetByPin(0, (short) 99, PinType.VIRTUAL);\n        assertNotNull(timeInput);\n        assertEquals(-1, timeInput.startAt);\n        assertEquals(-1, timeInput.stopAt);\n        assertEquals(ZoneId.of(\"Europe/Kiev\"), timeInput.tzName);\n        assertArrayEquals(new int[]{1,2,3,4}, timeInput.days);\n\n        clientPair.appClient.send(\"hardware 1 vw \" + b(\"99 82800 82800 Europe/Kiev  10800\"));\n        verify(clientPair.hardwareClient.responseMock, timeout(500).times(1)).channelRead(any(), eq(hardware(2, \"vw 99 82800 82800 Europe/Kiev  10800\")));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        profile = clientPair.appClient.parseProfile(1);\n        timeInput = (TimeInput) profile.dashBoards[0].findWidgetByPin(0, (short) 99, PinType.VIRTUAL);\n        assertNotNull(timeInput);\n        assertEquals(82800, timeInput.startAt);\n        assertEquals(82800, timeInput.stopAt);\n        assertEquals(ZoneId.of(\"Europe/Kiev\"), timeInput.tzName);\n        assertNull(timeInput.days);\n\n        clientPair.appClient.send(\"hardware 1 vw \" + b(\"99 ss sr Europe/Kiev  10800\"));\n        verify(clientPair.hardwareClient.responseMock, timeout(500).times(1)).channelRead(any(), eq(hardware(2, \"vw 99 ss sr Europe/Kiev  10800\")));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        profile = clientPair.appClient.parseProfile(1);\n        timeInput = (TimeInput) profile.dashBoards[0].findWidgetByPin(0, (short) 99, PinType.VIRTUAL);\n        assertNotNull(timeInput);\n        assertEquals(-2, timeInput.startAt);\n        assertEquals(-3, timeInput.stopAt);\n        assertEquals(ZoneId.of(\"Europe/Kiev\"), timeInput.tzName);\n        assertNull(timeInput.days);\n    }\n\n    @Test\n    public void testTimeInputUpdateWorksAsExpectedFromHardSide() throws Exception {\n        clientPair.appClient.createWidget(1, \"{\\\"type\\\":\\\"TIME_INPUT\\\",\\\"orgId\\\":99, \\\"pin\\\":99, \\\"pinType\\\":\\\"VIRTUAL\\\", \" +\n                \"\\\"x\\\":0,\\\"y\\\":0,\\\"width\\\":1,\\\"height\\\":1}\");\n\n        clientPair.appClient.verifyResult(ok(1));\n        clientPair.appClient.reset();\n\n        clientPair.hardwareClient.send(\"hardware vw \" + b(\"99 82800 82860 Europe/Kiev 1\"));\n        verify(clientPair.appClient.responseMock, timeout(500).times(1)).channelRead(any(), eq(hardware(1, \"1-0 vw 99 82800 82860 Europe/Kiev 1\")));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(1);\n        TimeInput timeInput = (TimeInput) profile.dashBoards[0].findWidgetByPin(0, (short) 99, PinType.VIRTUAL);\n        assertNotNull(timeInput);\n        assertEquals(82800, timeInput.startAt);\n        assertEquals(82860, timeInput.stopAt);\n        assertEquals(ZoneId.of(\"Europe/Kiev\"), timeInput.tzName);\n        assertArrayEquals(new int[] {1}, timeInput.days);\n\n\n        clientPair.hardwareClient.send(\"hardware vw \" + b(\"99 82800 82860 Europe/Kiev \"));\n        verify(clientPair.appClient.responseMock, timeout(500).times(1)).channelRead(any(), eq(hardware(2, \"1-0 vw 99 82800 82860 Europe/Kiev \")));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        profile = clientPair.appClient.parseProfile(1);\n        timeInput = (TimeInput) profile.dashBoards[0].findWidgetByPin(0, (short) 99, PinType.VIRTUAL);\n        assertNotNull(timeInput);\n        assertEquals(82800, timeInput.startAt);\n        assertEquals(82860, timeInput.stopAt);\n        assertEquals(ZoneId.of(\"Europe/Kiev\"), timeInput.tzName);\n        assertNull(timeInput.days);\n\n        clientPair.hardwareClient.send(\"hardware vw \" + b(\"99 82800  Europe/Kiev \"));\n        verify(clientPair.appClient.responseMock, timeout(500).times(1)).channelRead(any(), eq(hardware(3, \"1-0 vw 99 82800  Europe/Kiev \")));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        profile = (clientPair.appClient.parseProfile(1));\n        timeInput = (TimeInput) profile.dashBoards[0].findWidgetByPin(0, (short) 99, PinType.VIRTUAL);\n        assertNotNull(timeInput);\n        assertEquals(82800, timeInput.startAt);\n        assertEquals(-1, timeInput.stopAt);\n        assertEquals(ZoneId.of(\"Europe/Kiev\"), timeInput.tzName);\n        assertNull(timeInput.days);\n\n        clientPair.hardwareClient.send(\"hardware vw \" + b(\"99 82800  Europe/Kiev 1,2,3,4\"));\n        verify(clientPair.appClient.responseMock, timeout(500).times(1)).channelRead(any(), eq(hardware(4, \"1-0 vw 99 82800  Europe/Kiev 1,2,3,4\")));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        profile = (clientPair.appClient.parseProfile(1));\n        timeInput = (TimeInput) profile.dashBoards[0].findWidgetByPin(0, (short) 99, PinType.VIRTUAL);\n        assertNotNull(timeInput);\n        assertEquals(82800, timeInput.startAt);\n        assertEquals(-1, timeInput.stopAt);\n        assertEquals(ZoneId.of(\"Europe/Kiev\"), timeInput.tzName);\n        assertArrayEquals(new int[]{1,2,3,4}, timeInput.days);\n\n        clientPair.hardwareClient.send(\"hardware vw \" + b(\"99   Europe/Kiev 1,2,3,4\"));\n        verify(clientPair.appClient.responseMock, timeout(500).times(1)).channelRead(any(), eq(hardware(5, \"1-0 vw 99   Europe/Kiev 1,2,3,4\")));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        profile = (clientPair.appClient.parseProfile(1));\n        timeInput = (TimeInput) profile.dashBoards[0].findWidgetByPin(0, (short) 99, PinType.VIRTUAL);\n        assertNotNull(timeInput);\n        assertEquals(-1, timeInput.startAt);\n        assertEquals(-1, timeInput.stopAt);\n        assertEquals(ZoneId.of(\"Europe/Kiev\"), timeInput.tzName);\n        assertArrayEquals(new int[]{1,2,3,4}, timeInput.days);\n\n        clientPair.hardwareClient.send(\"hardware vw \" + b(\"99 82800 82800 Europe/Kiev  10800\"));\n        verify(clientPair.appClient.responseMock, timeout(500).times(1)).channelRead(any(), eq(hardware(6, \"1-0 vw 99 82800 82800 Europe/Kiev  10800\")));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        profile = clientPair.appClient.parseProfile(1);\n        timeInput = (TimeInput) profile.dashBoards[0].findWidgetByPin(0, (short) 99, PinType.VIRTUAL);\n        assertNotNull(timeInput);\n        assertEquals(82800, timeInput.startAt);\n        assertEquals(82800, timeInput.stopAt);\n        assertEquals(ZoneId.of(\"Europe/Kiev\"), timeInput.tzName);\n        assertNull(timeInput.days);\n\n        clientPair.hardwareClient.send(\"hardware vw \" + b(\"99 ss sr Europe/Kiev  10800\"));\n        verify(clientPair.appClient.responseMock, timeout(500).times(1)).channelRead(any(), eq(hardware(7, \"1-0 vw 99 ss sr Europe/Kiev  10800\")));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        profile = clientPair.appClient.parseProfile(1);\n        timeInput = (TimeInput) profile.dashBoards[0].findWidgetByPin(0, (short) 99, PinType.VIRTUAL);\n        assertNotNull(timeInput);\n        assertEquals(-2, timeInput.startAt);\n        assertEquals(-3, timeInput.stopAt);\n        assertEquals(ZoneId.of(\"Europe/Kiev\"), timeInput.tzName);\n        assertNull(timeInput.days);\n    }\n\n    @Test\n    public void testWrongCommandForAggregation() throws Exception {\n        clientPair.hardwareClient.send(\"hardware vw 10 aaaa\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(hardware(1, \"1-0 vw 10 aaaa\")));\n    }\n\n    @Test\n    public void testWrongPin() throws Exception {\n        clientPair.hardwareClient.send(\"hardware vw x aaaa\");\n        clientPair.hardwareClient.verifyResult(illegalCommand(1));\n    }\n\n    @Test\n    public void testAppSendWAwWorks() throws Exception {\n        String body = \"aw 8 333\";\n        clientPair.hardwareClient.send(\"hardware \" + body);\n\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(hardware(1, \"1-0 aw 8 333\")));\n    }\n\n    @Test\n    public void testClosedConnectionWhenNotLogged() throws Exception {\n        TestAppClient appClient2 = new TestAppClient(properties);\n        appClient2.start();\n        appClient2.getDevice(1, 0);\n        verify(appClient2.responseMock, after(600).never()).channelRead(any(), any());\n        assertTrue(appClient2.isClosed());\n\n        appClient2.login(getUserName(), \"1\", \"Android\", \"1RC7\");\n        verify(appClient2.responseMock, after(200).never()).channelRead(any(), any());\n    }\n\n    @Test\n    public void testRefreshTokenClosesExistingConnections() throws Exception {\n        clientPair.appClient.send(\"refreshToken 1\");\n        clientPair.appClient.verifyResult(deviceOffline(0, \"1-0\"));\n        assertTrue(clientPair.hardwareClient.isClosed());\n    }\n\n    @Test\n    public void testSendPinModeCommandWhenHardwareGoesOnline() throws Exception {\n        ChannelFuture channelFuture = clientPair.hardwareClient.stop();\n        channelFuture.await();\n\n        assertTrue(channelFuture.isDone());\n\n        String body = \"vw 13 1\";\n        clientPair.appClient.send(\"hardware 1 \" + body);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new ResponseMessage(1, DEVICE_NOT_IN_NETWORK)));\n\n        TestHardClient hardClient = new TestHardClient(\"localhost\", properties.getHttpPort());\n        hardClient.start();\n        hardClient.login(clientPair.token);\n        verify(hardClient.responseMock, timeout(1000)).channelRead(any(), eq(ok(1)));\n\n        verify(hardClient.responseMock, timeout(500)).channelRead(any(), eq(hardware(1, \"pm 1 out 2 out 3 out 5 out 6 in 7 in 30 in 8 in\")));\n        verify(hardClient.responseMock, times(2)).channelRead(any(), any());\n        hardClient.stop().awaitUninterruptibly();\n    }\n\n    @Test\n    public void testSendGeneratedPinModeCommandWhenHardwareGoesOnline() throws Exception {\n        ChannelFuture channelFuture = clientPair.hardwareClient.stop();\n        channelFuture.awaitUninterruptibly();\n\n        assertTrue(channelFuture.isDone());\n\n        clientPair.appClient.send(\"hardware 1 vw 1 1\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new ResponseMessage(1, DEVICE_NOT_IN_NETWORK)));\n\n        TestHardClient hardClient = new TestHardClient(\"localhost\", properties.getHttpPort());\n        hardClient.start();\n        hardClient.login(clientPair.token);\n        verify(hardClient.responseMock, timeout(1000)).channelRead(any(), eq(ok(1)));\n\n        String expectedBody = \"pm 1 out 2 out 3 out 5 out 6 in 7 in 30 in 8 in\";\n        verify(hardClient.responseMock, timeout(500)).channelRead(any(), eq(hardware(1, expectedBody)));\n        verify(hardClient.responseMock, times(2)).channelRead(any(), any());\n        hardClient.stop().awaitUninterruptibly();\n    }\n\n    @Test\n    public void testSendHardwareCommandToNotActiveDashboard() throws Exception {\n        clientPair.appClient.createDash(\"{\\\"id\\\":2,\\\"name\\\":\\\"My Dashboard2\\\"}\");\n        verify(clientPair.appClient.responseMock, timeout(1000)).channelRead(any(), eq(ok(1)));\n        clientPair.appClient.reset();\n\n        clientPair.appClient.createDevice(2, new Device(2, \"123\", BoardType.ESP8266));\n        Device device = clientPair.appClient.parseDevice();\n\n        clientPair.appClient.reset();\n\n        //connecting separate hardware to non active dashboard\n        TestHardClient nonActiveDashHardClient = new TestHardClient(\"localhost\", properties.getHttpPort());\n        nonActiveDashHardClient.start();\n        nonActiveDashHardClient.login(device.token);\n        verify(nonActiveDashHardClient.responseMock, timeout(2000)).channelRead(any(), eq(ok(1)));\n        nonActiveDashHardClient.reset();\n\n\n        //sending hardware command from hardware that has no active dashboard\n        nonActiveDashHardClient.send(\"hardware aw 1 1\");\n        //verify(nonActiveDashHardClient.responseMock, timeout(1000)).channelRead(any(), eq(new ResponseMessage(1, NO_ACTIVE_DASHBOARD)));\n        verify(clientPair.appClient.responseMock, timeout(1000).times(1)).channelRead(any(), any());\n        verify(clientPair.appClient.responseMock, timeout(1000).times(1)).channelRead(any(), eq(hardwareConnected(1, \"2-\" + device.id)));\n\n        clientPair.hardwareClient.send(\"hardware aw 1 1\");\n        verify(clientPair.hardwareClient.responseMock, after(1000).never()).channelRead(any(), any());\n        verify(clientPair.appClient.responseMock, timeout(1000)).channelRead(any(), eq(hardware(1, \"1-0 aw 1 1\")));\n        nonActiveDashHardClient.stop().awaitUninterruptibly();\n    }\n\n    @Test\n    public void testConnectAppAndHardwareAndSendCommands() throws Exception {\n        for (int i = 0; i < 100; i++) {\n            clientPair.appClient.send(\"hardware 1 aw 1 1\");\n        }\n\n        verify(clientPair.hardwareClient.responseMock, timeout(500).times(100)).channelRead(any(), any());\n    }\n\n    @Test\n    public void testTryReachQuotaLimit() throws Exception {\n        String body = \"aw 100 100\";\n\n        //within 1 second sending more messages than default limit 100.\n        for (int i = 0; i < 200; i++) {\n            clientPair.hardwareClient.send(\"hardware \" + body);\n            TestUtil.sleep(5);\n        }\n\n        ArgumentCaptor<ResponseMessage> objectArgumentCaptor = ArgumentCaptor.forClass(ResponseMessage.class);\n        verify(clientPair.hardwareClient.responseMock, timeout(1000)).channelRead(any(), objectArgumentCaptor.capture());\n        List<ResponseMessage> arguments = objectArgumentCaptor.getAllValues();\n        ResponseMessage responseMessage = arguments.get(0);\n        assertTrue(responseMessage.id > 100);\n\n        //at least 100 iterations should be\n        for (int i = 0; i < 100; i++) {\n            verify(clientPair.appClient.responseMock).channelRead(any(), eq(hardware(i+1, \"1-0 \" + body)));\n        }\n\n        clientPair.appClient.reset();\n        clientPair.hardwareClient.reset();\n\n        //check no more accepted\n        for (int i = 0; i < 10; i++) {\n            clientPair.hardwareClient.send(\"hardware \" + body);\n            TestUtil.sleep(9);\n        }\n\n        verify(clientPair.hardwareClient.responseMock, never()).channelRead(any(), eq(new ResponseMessage(1, QUOTA_LIMIT)));\n        verify(clientPair.appClient.responseMock, never()).channelRead(any(), eq(hardware(1, body)));\n    }\n\n    @Test\n    public void testCreateProjectWithDevicesGeneratesNewTokens() throws Exception {\n        DashBoard dashBoard = new DashBoard();\n        dashBoard.id = 2;\n        dashBoard.name = \"Test Dash\";\n\n        Device device = new Device();\n        device.id = 1;\n        device.name = \"MyDevice\";\n        device.token = \"aaa\";\n        dashBoard.devices = new Device[] {\n                device\n        };\n\n        clientPair.appClient.createDash(dashBoard);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.send(\"getDevices 2\");\n\n        Device[] devices = clientPair.appClient.parseDevices(2);\n        assertNotNull(devices);\n        assertEquals(1, devices.length);\n        assertEquals(1, devices[0].id);\n        assertEquals(\"MyDevice\", devices[0].name);\n        assertNotEquals(\"aaa\", devices[0].token);\n    }\n\n    @Test\n    public void testButtonStateInPWMModeIsStored() throws Exception {\n        clientPair.appClient.createWidget(1, \"{\\\"type\\\":\\\"BUTTON\\\",\\\"id\\\":1000,\\\"x\\\":0,\\\"y\\\":0,\\\"color\\\":616861439,\\\"width\\\":2,\\\"height\\\":2,\\\"label\\\":\\\"Relay\\\",\\\"pinType\\\":\\\"DIGITAL\\\",\\\"pin\\\":18,\\\"pwmMode\\\":true,\\\"rangeMappingOn\\\":false,\\\"min\\\":0,\\\"max\\\":0,\\\"value\\\":\\\"1\\\",\\\"pushMode\\\":false}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.send(\"hardware 1 aw 18 1032\");\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(hardware(2, \"aw 18 1032\")));\n\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(2);\n        Widget widget = profile.dashBoards[0].findWidgetByPin(0, (short) 18, PinType.DIGITAL);\n        assertNotNull(widget);\n        assertEquals(\"1032\", ((Button) widget).value);\n    }\n\n    @Test\n    public void testTwoWidgetsOnTheSamePin() throws Exception {\n        clientPair.appClient.createWidget(1, \"{\\\"type\\\":\\\"BUTTON\\\",\\\"id\\\":1000,\\\"x\\\":0,\\\"y\\\":0,\\\"color\\\":616861439,\\\"width\\\":2,\\\"height\\\":2,\\\"label\\\":\\\"Relay\\\",\\\"pinType\\\":\\\"VIRTUAL\\\",\\\"pin\\\":37,\\\"pwmMode\\\":true,\\\"rangeMappingOn\\\":false,\\\"min\\\":0,\\\"max\\\":0,\\\"pushMode\\\":false}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.createWidget(1, \"{\\\"type\\\":\\\"BUTTON\\\",\\\"id\\\":1001,\\\"x\\\":0,\\\"y\\\":0,\\\"color\\\":616861439,\\\"width\\\":2,\\\"height\\\":2,\\\"label\\\":\\\"Relay\\\",\\\"pinType\\\":\\\"VIRTUAL\\\",\\\"pin\\\":37,\\\"pwmMode\\\":true,\\\"rangeMappingOn\\\":false,\\\"min\\\":0,\\\"max\\\":0,\\\"pushMode\\\":false}\");\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.appClient.send(\"hardware 1 vw 37 10\");\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(hardware(3, \"vw 37 10\")));\n\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(3);\n\n        int counter = 0;\n        for (Widget widget : profile.dashBoards[0].widgets) {\n            if (widget.isSame(0, (short) 37, PinType.VIRTUAL)) {\n                counter++;\n                assertEquals(\"10\", ((OnePinWidget) widget).value);\n            }\n        }\n        assertEquals(2, counter);\n\n        clientPair.hardwareClient.send(\"hardware vw 37 11\");\n        clientPair.appClient.verifyResult(hardware(1, \"1-0 vw 37 11\"));\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        profile = clientPair.appClient.parseProfile(5);\n        counter = 0;\n        for (Widget widget : profile.dashBoards[0].widgets) {\n            if (widget.isSame(0, (short) 37, PinType.VIRTUAL)) {\n                counter++;\n                assertEquals(\"11\", ((OnePinWidget) widget).value);\n            }\n        }\n        assertEquals(2, counter);\n    }\n\n    @Test\n    public void testButtonStateInPWMModeIsStoredWithUIHack() throws Exception {\n        clientPair.appClient.createWidget(1, \"{\\\"type\\\":\\\"BUTTON\\\",\\\"id\\\":1000,\\\"x\\\":0,\\\"y\\\":0,\\\"color\\\":616861439,\\\"width\\\":2,\\\"height\\\":2,\\\"label\\\":\\\"Relay\\\",\\\"pinType\\\":\\\"DIGITAL\\\",\\\"pin\\\":18,\\\"pwmMode\\\":true,\\\"rangeMappingOn\\\":false,\\\"min\\\":0,\\\"max\\\":0,\\\"value\\\":\\\"1\\\",\\\"pushMode\\\":false}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.send(\"hardware 1 dw 18 1\");\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(hardware(2, \"dw 18 1\")));\n\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(2);\n        Widget widget = profile.dashBoards[0].findWidgetByPin(0, (short) 18, PinType.DIGITAL);\n        assertNotNull(widget);\n        assertEquals(\"1\", ((Button) widget).value);\n    }\n\n    @Test\n    public void testOutdatedAppNotificationAlertWorks() throws Exception {\n        TestAppClient appClient = new TestAppClient(properties);\n        appClient.start();\n        appClient.login(getUserName(), \"1\", \"Android\", \"1.1.1\");\n        appClient.verifyResult(ok(1));\n        verify(appClient.responseMock, timeout(500)).channelRead(any(), eq(\n                appIsOutdated(1,\n                        \"Your app is outdated. Please update to the latest app version. \" +\n                                \"Ignoring this notice may affect your projects.\")));\n    }\n\n    @Test\n    public void testOutdatedAppNotificationNotTriggered() throws Exception {\n        TestAppClient appClient = new TestAppClient(properties);\n        appClient.start();\n        appClient.login(getUserName(), \"1\", \"Android\", \"1.1.2\");\n        appClient.verifyResult(ok(1));\n        verify(appClient.responseMock, never()).channelRead(any(), eq(\n                appIsOutdated(1,\n                        \"Your app is outdated. Please update to the latest app version. \" +\n                                \"Ignoring this notice may affect your projects.\")));\n    }\n\n    @Test\n    public void newUserReceivesGrettingEmailAndNoIPLogged() throws Exception {\n        TestAppClient appClient1 = new TestAppClient(properties);\n        appClient1.start();\n\n        appClient1.register(\"test@blynk.cc\", \"a\", \"Blynk\");\n        appClient1.verifyResult(ok(1));\n\n        User user = holder.userDao.getByName(\"test@blynk.cc\", \"Blynk\");\n        assertNull(user.lastLoggedIP);\n\n        verify(holder.mailWrapper).sendHtml(eq(\"test@blynk.cc\"), eq(\"Get started with Blynk\"), contains(\"Welcome to Blynk, a platform to build your next awesome IOT project.\"));\n\n        appClient1.login(\"test@blynk.cc\", \"a\");\n        appClient1.verifyResult(ok(2));\n\n        user = holder.userDao.getByName(\"test@blynk.cc\", \"Blynk\");\n        assertNull(user.lastLoggedIP);\n    }\n\n    @Test\n    public void test() throws Exception {\n            Twitter twitter = new Twitter();\n            twitter.secret = \"123\";\n            twitter.token = \"124\";\n\n            DashBoard dash = new DashBoard();\n            dash.sharedToken = \"ffffffffffffffffffffffffffff\";\n            dash.widgets = new Widget[] {\n                    twitter\n            };\n\n            System.out.println(JsonParser.init().writerFor(DashBoard.class).writeValueAsString(dash));\n            System.out.println(JsonParser.init().writerFor(DashBoard.class).withView(View.PublicOnly.class).writeValueAsString(dash));\n    }\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/tcp/MultiAppTest.java",
    "content": "package cc.blynk.integration.tcp;\n\nimport cc.blynk.integration.SingleServerInstancePerTest;\nimport cc.blynk.integration.model.tcp.TestAppClient;\nimport cc.blynk.integration.model.tcp.TestHardClient;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.device.BoardType;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.protocol.model.messages.ResponseMessage;\nimport cc.blynk.server.core.protocol.model.messages.common.HardwareMessage;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport static cc.blynk.integration.TestUtil.b;\nimport static cc.blynk.integration.TestUtil.hardwareConnected;\nimport static cc.blynk.integration.TestUtil.ok;\nimport static cc.blynk.server.core.protocol.enums.Response.DEVICE_NOT_IN_NETWORK;\nimport static org.junit.Assert.assertNotNull;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.eq;\nimport static org.mockito.Mockito.timeout;\nimport static org.mockito.Mockito.verify;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 5/09/2016.\n *\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class MultiAppTest extends SingleServerInstancePerTest {\n\n    @Test\n    public void testCreateFewAccountWithDifferentApp() throws Exception {\n        TestAppClient appClient1 = new TestAppClient(properties);\n        appClient1.start();\n        TestAppClient appClient2 = new TestAppClient(properties);\n        appClient2.start();\n\n        String token1 = workflowForUser(appClient1, \"test@blynk.cc\", \"a\", \"testapp1\");\n        String token2 = workflowForUser(appClient2, \"test@blynk.cc\", \"a\", \"testapp2\");\n\n        appClient1.reset();\n        appClient2.reset();\n\n        TestHardClient hardClient1 = new TestHardClient(\"localhost\", properties.getHttpPort());\n        hardClient1.start();\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", properties.getHttpPort());\n        hardClient2.start();\n\n        hardClient1.login(token1);\n        verify(hardClient1.responseMock, timeout(2000)).channelRead(any(), eq(ok(1)));\n        verify(appClient1.responseMock, timeout(2000)).channelRead(any(), eq(hardwareConnected(1, \"1-0\")));\n        hardClient2.login(token2);\n        verify(hardClient2.responseMock, timeout(2000)).channelRead(any(), eq(ok(1)));\n        verify(appClient2.responseMock, timeout(2000)).channelRead(any(), eq(hardwareConnected(1, \"1-0\")));\n\n        hardClient1.send(\"hardware vw 1 100\");\n        verify(appClient1.responseMock, timeout(2000)).channelRead(any(), eq(new HardwareMessage(2, b(\"1-0 vw 1 100\"))));\n        verify(appClient2.responseMock, timeout(500).times(0)).channelRead(any(), eq(new HardwareMessage(1, b(\"1-0 vw 1 100\"))));\n\n        appClient1.reset();\n        appClient2.reset();\n\n        hardClient2.send(\"hardware vw 1 100\");\n        verify(appClient2.responseMock, timeout(2000)).channelRead(any(), eq(new HardwareMessage(2, b(\"1-0 vw 1 100\"))));\n        verify(appClient1.responseMock, timeout(500).times(0)).channelRead(any(), eq(new HardwareMessage(1, b(\"1-0 vw 1 100\"))));\n\n    }\n\n    private String workflowForUser(TestAppClient appClient, String email, String pass, String appName) throws Exception{\n        appClient.register(email, pass, appName);\n        verify(appClient.responseMock, timeout(1000)).channelRead(any(), eq(ok(1)));\n        appClient.login(email, pass, \"Android\", \"1.10.4\", appName);\n        //we should wait until login finished. Only after that we can send commands\n        verify(appClient.responseMock, timeout(1000)).channelRead(any(), eq(ok(2)));\n\n        DashBoard dash = new DashBoard();\n        dash.id = 1;\n        dash.name = \"test\";\n        appClient.createDash(dash);\n        verify(appClient.responseMock, timeout(1000)).channelRead(any(), eq(ok(3)));\n        appClient.activate(1);\n        verify(appClient.responseMock, timeout(1000)).channelRead(any(), eq(new ResponseMessage(4, DEVICE_NOT_IN_NETWORK)));\n\n        appClient.reset();\n        appClient.createDevice(1, new Device(0, \"123\", BoardType.ESP8266));\n        Device device = appClient.parseDevice();\n\n        String token = device.token;\n        assertNotNull(token);\n        return token;\n    }\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/tcp/NotificationsLogicTest.java",
    "content": "package cc.blynk.integration.tcp;\n\nimport cc.blynk.integration.SingleServerInstancePerTest;\nimport cc.blynk.integration.model.tcp.TestAppClient;\nimport cc.blynk.integration.model.tcp.TestHardClient;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.Profile;\nimport cc.blynk.server.core.model.device.BoardType;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.notifications.Notification;\nimport cc.blynk.server.notifications.push.android.AndroidGCMMessage;\nimport cc.blynk.server.notifications.push.enums.Priority;\nimport cc.blynk.utils.AppNameUtil;\nimport io.netty.channel.ChannelFuture;\nimport org.junit.BeforeClass;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.ArgumentCaptor;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport java.util.Map;\n\nimport static cc.blynk.integration.TestUtil.createDevice;\nimport static cc.blynk.integration.TestUtil.deviceOffline;\nimport static cc.blynk.integration.TestUtil.hardwareConnected;\nimport static cc.blynk.integration.TestUtil.notAllowed;\nimport static cc.blynk.integration.TestUtil.ok;\nimport static cc.blynk.integration.TestUtil.parseProfile;\nimport static cc.blynk.integration.TestUtil.readTestUserProfile;\nimport static cc.blynk.integration.TestUtil.sleep;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\nimport static org.junit.Assert.assertTrue;\nimport static org.mockito.Mockito.after;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.eq;\nimport static org.mockito.Mockito.timeout;\nimport static org.mockito.Mockito.verify;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/2/2015.\n *\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class NotificationsLogicTest extends SingleServerInstancePerTest {\n\n    private static int tcpHardPort;\n\n    @BeforeClass\n    public static void initPort() {\n        tcpHardPort = properties.getHttpPort();\n    }\n\n    @Test\n    public void addPushTokenWrongInput()  throws Exception  {\n        TestAppClient appClient = new TestAppClient(properties);\n\n        appClient.start();\n\n        appClient.register(\"test@test.com\", \"1\", AppNameUtil.BLYNK);\n        appClient.verifyResult(ok(1));\n\n        appClient.login(\"test@test.com\", \"1\", \"Android\", \"RC13\");\n        appClient.verifyResult(ok(2));\n\n        appClient.createDash(\"{\\\"id\\\":1, \\\"createdAt\\\":1, \\\"name\\\":\\\"test board\\\"}\");\n        appClient.verifyResult(ok(3));\n\n        appClient.send(\"addPushToken 1\\0uid\\0token\");\n        verify(appClient.responseMock, timeout(500)).channelRead(any(), eq(notAllowed(4)));\n    }\n\n    @Test\n    public void addPushTokenWorksForAndroid() throws Exception {\n        clientPair.appClient.send(\"addPushToken 1\\0uid1\\0token1\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(2);\n\n        Notification notification = profile.getDashById(1).getNotificationWidget();\n        assertNotNull(notification);\n        assertEquals(2, notification.androidTokens.size());\n        assertEquals(0, notification.iOSTokens.size());\n\n        assertTrue(notification.androidTokens.containsKey(\"uid1\"));\n        assertTrue(notification.androidTokens.containsValue(\"token1\"));\n    }\n\n    @Test\n    public void addPushTokenNotOverridedOnProfileSave() throws Exception {\n        clientPair.appClient.send(\"addPushToken 1\\0uid1\\0token1\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(2);\n\n        Notification notification = profile.getDashById(1).getNotificationWidget();\n        assertNotNull(notification);\n        assertEquals(2, notification.androidTokens.size());\n        assertEquals(0, notification.iOSTokens.size());\n\n        assertTrue(notification.androidTokens.containsKey(\"uid1\"));\n        assertTrue(notification.androidTokens.containsValue(\"token1\"));\n\n        clientPair.appClient.updateDash(profile.getDashById(1));\n        clientPair.appClient.verifyResult(ok(3));\n\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        profile = clientPair.appClient.parseProfile(4);\n\n        notification = profile.getDashById(1).getNotificationWidget();\n        assertNotNull(notification);\n        assertEquals(2, notification.androidTokens.size());\n        assertEquals(0, notification.iOSTokens.size());\n\n        assertTrue(notification.androidTokens.containsKey(\"uid1\"));\n        assertTrue(notification.androidTokens.containsValue(\"token1\"));\n    }\n\n    @Test\n    public void addPushTokenWorksForIos() throws Exception {\n        TestAppClient appClient = new TestAppClient(properties);\n\n        appClient.start();\n\n        appClient.login(getUserName(), \"1\", \"iOS\", \"1.10.2\");\n        appClient.verifyResult(ok(1));\n\n        appClient.send(\"addPushToken 1\\0uid2\\0token2\");\n        appClient.verifyResult(ok(2));\n\n        appClient.reset();\n\n        appClient.send(\"loadProfileGzipped\");\n        Profile profile = appClient.parseProfile(1);\n\n        Notification notification = profile.getDashById(1).getNotificationWidget();\n        assertNotNull(notification);\n        assertEquals(1, notification.androidTokens.size());\n        assertEquals(1, notification.iOSTokens.size());\n        Map.Entry<String, String> entry = notification.iOSTokens.entrySet().iterator().next();\n        assertEquals(\"uid2\", entry.getKey());\n        assertEquals(\"token2\", entry.getValue());\n    }\n\n    @Test\n    public void testHardwareDeviceWentOffline() throws Exception {\n        Profile profile = parseProfile(readTestUserProfile());\n        Notification notification = profile.getDashById(1).getNotificationWidget();\n        notification.notifyWhenOffline = false;\n\n        clientPair.appClient.updateDash(profile.getDashById(1));\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.stop();\n        clientPair.appClient.verifyResult(deviceOffline(0, \"1-0\"));\n    }\n\n    @Test\n    public void testHardwareDeviceWentOfflineForSecondDeviceSameToken() throws Exception {\n        Profile profile = parseProfile(readTestUserProfile());\n        Notification notification = profile.getDashById(1).getNotificationWidget();\n        notification.notifyWhenOffline = false;\n        clientPair.appClient.updateDash(profile.getDashById(1));\n        clientPair.appClient.verifyResult(ok(1));\n        clientPair.appClient.reset();\n\n        TestHardClient newHardClient = new TestHardClient(\"localhost\", tcpHardPort);\n        newHardClient.start();\n        newHardClient.login(clientPair.token);\n        verify(newHardClient.responseMock, timeout(500)).channelRead(any(), eq(ok(1)));\n\n        newHardClient.stop();\n        verify(clientPair.appClient.responseMock, timeout(1500)).channelRead(any(), eq(deviceOffline(0, \"1-0\")));\n    }\n\n    @Test\n    public void testHardwareDeviceWentOfflineForSecondDeviceNewToken() throws Exception {\n        Profile profile = parseProfile(readTestUserProfile());\n        Notification notification = profile.getDashById(1).getNotificationWidget();\n        notification.notifyWhenOffline = false;\n        clientPair.appClient.updateDash(profile.getDashById(1));\n        clientPair.appClient.verifyResult(ok(1));\n        clientPair.appClient.reset();\n\n        Device device1 = new Device(1, \"Name\", BoardType.ESP8266);\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        clientPair.appClient.getDevice(1, 1);\n        device = clientPair.appClient.parseDevice(2);\n\n        TestHardClient newHardClient = new TestHardClient(\"localhost\", tcpHardPort);\n        newHardClient.start();\n        newHardClient.login(device.token);\n        newHardClient.verifyResult(ok(1));\n        clientPair.appClient.verifyResult(hardwareConnected(1, \"1-\" + device.id));\n\n        newHardClient.stop();\n        clientPair.appClient.verifyResult(deviceOffline(0, \"1-\" + device.id));\n    }\n\n    @Test\n    public void testHardwareDeviceWentOfflineAndPushWorks() throws Exception {\n        Profile profile = parseProfile(readTestUserProfile());\n        Notification notification = profile.getDashById(1).getNotificationWidget();\n        notification.notifyWhenOffline = true;\n\n        clientPair.appClient.updateDash(profile.getDashById(1));\n        clientPair.appClient.verifyResult(ok(1));\n\n        ChannelFuture channelFuture = clientPair.hardwareClient.stop();\n        channelFuture.await();\n\n        ArgumentCaptor<AndroidGCMMessage> objectArgumentCaptor = ArgumentCaptor.forClass(AndroidGCMMessage.class);\n        verify(holder.gcmWrapper, timeout(500).times(1)).send(objectArgumentCaptor.capture(), any(), any());\n        AndroidGCMMessage message = objectArgumentCaptor.getValue();\n\n        String expectedJson = new AndroidGCMMessage(\"token\", Priority.normal, \"Your My Device went offline.\", 1).toJson();\n        assertEquals(expectedJson, message.toJson());\n    }\n\n    @Test\n    public void testNotifWidgetOverrideProjectSetting() throws Exception {\n        Profile profile = parseProfile(readTestUserProfile());\n        DashBoard dashBoard = profile.getDashById(1);\n        dashBoard.isNotificationsOff = true;\n\n        Notification notification = dashBoard.getNotificationWidget();\n        notification.notifyWhenOffline = true;\n\n        clientPair.appClient.updateDash(dashBoard);\n        clientPair.appClient.verifyResult(ok(1));\n\n        ChannelFuture channelFuture = clientPair.hardwareClient.stop();\n        channelFuture.await();\n\n        ArgumentCaptor<AndroidGCMMessage> objectArgumentCaptor = ArgumentCaptor.forClass(AndroidGCMMessage.class);\n        verify(holder.gcmWrapper, timeout(500).times(1)).send(objectArgumentCaptor.capture(), any(), any());\n        AndroidGCMMessage message = objectArgumentCaptor.getValue();\n\n        String expectedJson = new AndroidGCMMessage(\"token\", Priority.normal, \"Your My Device went offline.\", 1).toJson();\n        assertEquals(expectedJson, message.toJson());\n\n        clientPair.appClient.never(deviceOffline(0, \"1-0\"));\n    }\n\n    @Test\n    public void testNoOfflineNotifsExpected() throws Exception {\n        Profile profile = parseProfile(readTestUserProfile());\n        DashBoard dashBoard = profile.getDashById(1);\n        dashBoard.isNotificationsOff = true;\n\n        Notification notification = dashBoard.getNotificationWidget();\n        notification.notifyWhenOffline = false;\n\n        clientPair.appClient.updateDash(dashBoard);\n        clientPair.appClient.verifyResult(ok(1));\n\n        ChannelFuture channelFuture = clientPair.hardwareClient.stop();\n        channelFuture.await();\n\n        verify(holder.gcmWrapper, after(500).never()).send(any(), any(), any());\n        clientPair.appClient.never(deviceOffline(0, \"1-0\"));\n    }\n\n    @Test\n    public void testOfflineNotifsExpectedButNotPush() throws Exception {\n        Profile profile = parseProfile(readTestUserProfile());\n        DashBoard dashBoard = profile.getDashById(1);\n        dashBoard.isNotificationsOff = false;\n\n        Notification notification = dashBoard.getNotificationWidget();\n        notification.notifyWhenOffline = false;\n\n        clientPair.appClient.updateDash(dashBoard);\n        clientPair.appClient.verifyResult(ok(1));\n\n        ChannelFuture channelFuture = clientPair.hardwareClient.stop();\n        channelFuture.await();\n\n        verify(holder.gcmWrapper, after(500).never()).send(any(), any(), any());\n        clientPair.appClient.verifyResult(deviceOffline(0, \"1-0\"));\n    }\n\n    @Test\n    public void testHardwareDeviceWentOfflineAndPushNotWorksForLogoutUser() throws Exception {\n        Profile profile = parseProfile(readTestUserProfile());\n        Notification notification = profile.getDashById(1).getNotificationWidget();\n        notification.notifyWhenOffline = true;\n\n        clientPair.appClient.updateDash(profile.getDashById(1));\n        clientPair.appClient.send(\"logout\");\n        clientPair.appClient.verifyResult(ok(1));\n        clientPair.appClient.verifyResult(ok(2));\n\n        ChannelFuture channelFuture = clientPair.hardwareClient.stop();\n        channelFuture.await();\n\n        verify(holder.gcmWrapper, after(500).never()).send(any(), any(), any());\n\n        clientPair.appClient.send(\"logout\");\n        verify(clientPair.appClient.responseMock, after(500).never()).channelRead(any(), eq(ok(3)));\n    }\n\n    @Test\n    public void testHardwareDeviceWentOfflineAndPushNotWorksForLogoutUserWithUID() throws Exception {\n        Profile profile = parseProfile(readTestUserProfile());\n        Notification notification = profile.getDashById(1).getNotificationWidget();\n        notification.notifyWhenOffline = true;\n\n        clientPair.appClient.updateDash(profile.getDashById(1));\n        clientPair.appClient.send(\"logout uid\");\n        clientPair.appClient.verifyResult(ok(1));\n        clientPair.appClient.verifyResult(ok(2));\n\n        ChannelFuture channelFuture = clientPair.hardwareClient.stop();\n        channelFuture.await();\n\n        verify(holder.gcmWrapper, after(500).never()).send(any(), any(), any());\n\n        clientPair.appClient.send(\"logout\");\n        verify(clientPair.appClient.responseMock, after(500).never()).channelRead(any(), eq(ok(3)));\n    }\n\n    @Test\n    public void testHardwareDeviceWentOfflineAndPushNotWorksForLogoutUserWithWrongUID() throws Exception {\n        Profile profile = parseProfile(readTestUserProfile());\n        Notification notification = profile.getDashById(1).getNotificationWidget();\n        notification.notifyWhenOffline = true;\n\n        clientPair.appClient.updateDash(profile.getDashById(1));\n        clientPair.appClient.send(\"logout uidxxx\");\n        clientPair.appClient.verifyResult(ok(1));\n        clientPair.appClient.verifyResult(ok(2));\n\n        ChannelFuture channelFuture = clientPair.hardwareClient.stop();\n        channelFuture.await();\n\n        verify(holder.gcmWrapper, timeout(500)).send(any(), any(), eq(\"uid\"));\n    }\n\n    @Test\n    public void testHardwareDeviceWentOfflineAndPushNotWorksForLogoutUser2() throws Exception {\n        Profile profile = parseProfile(readTestUserProfile());\n        Notification notification = profile.getDashById(1).getNotificationWidget();\n        notification.notifyWhenOffline = true;\n\n        clientPair.appClient.updateDash(profile.getDashById(1));\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.getDevice(1, 0);\n        Device device = clientPair.appClient.parseDevice(2);\n\n        clientPair.appClient.send(\"logout\");\n        clientPair.appClient.verifyResult(ok(3));\n\n        clientPair.hardwareClient.stop().await();\n\n        verify(holder.gcmWrapper, after(500).never()).send(any(), any(), any());\n\n        TestAppClient appClient = new TestAppClient(properties);\n        appClient.start();\n        appClient.login(getUserName(), \"1\", \"Android\", \"1.10.4\");\n        appClient.verifyResult(ok(1));\n\n        TestHardClient hardClient = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient.start();\n\n        hardClient.login(device.token);\n        hardClient.verifyResult(ok(1));\n\n        appClient.send(\"addPushToken 1\\0uid\\0token\");\n        appClient.verifyResult(ok(2));\n\n        hardClient.stop().await();\n\n        ArgumentCaptor<AndroidGCMMessage> objectArgumentCaptor = ArgumentCaptor.forClass(AndroidGCMMessage.class);\n        verify(holder.gcmWrapper, timeout(500).times(1)).send(objectArgumentCaptor.capture(), any(), eq(\"uid\"));\n        AndroidGCMMessage message = objectArgumentCaptor.getValue();\n\n        String expectedJson = new AndroidGCMMessage(\"token\", Priority.normal, \"Your My Device went offline.\", 1).toJson();\n        assertEquals(expectedJson, message.toJson());\n    }\n\n    @Test\n    public void testLoginWith2AppsAndLogoutFrom1() throws Exception {\n        Profile profile = parseProfile(readTestUserProfile());\n        Notification notification = profile.getDashById(1).getNotificationWidget();\n        notification.notifyWhenOffline = true;\n\n        clientPair.appClient.updateDash(profile.getDashById(1));\n        clientPair.appClient.verifyResult(ok(1));\n\n        TestAppClient appClient = new TestAppClient(properties);\n        appClient.start();\n        appClient.login(getUserName(), \"1\", \"Android\", \"1.10.4\");\n        appClient.verifyResult(ok(1));\n\n        appClient.send(\"addPushToken 1\\0uid2\\0token2\");\n        appClient.verifyResult(ok(2));\n\n        clientPair.appClient.send(\"logout uid\");\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.hardwareClient.stop().await();\n\n        ArgumentCaptor<AndroidGCMMessage> objectArgumentCaptor = ArgumentCaptor.forClass(AndroidGCMMessage.class);\n        verify(holder.gcmWrapper, timeout(500).times(1)).send(objectArgumentCaptor.capture(), any(), eq(\"uid2\"));\n        AndroidGCMMessage message = objectArgumentCaptor.getValue();\n\n        String expectedJson = new AndroidGCMMessage(\"token2\", Priority.normal, \"Your My Device went offline.\", 1).toJson();\n        assertEquals(expectedJson, message.toJson());\n    }\n\n    @Test\n    public void testLoginWith2AppsAndLogoutFrom2() throws Exception {\n        Profile profile = parseProfile(readTestUserProfile());\n        Notification notification = profile.getDashById(1).getNotificationWidget();\n        notification.notifyWhenOffline = true;\n\n        clientPair.appClient.updateDash(profile.getDashById(1));\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.getDevice(1, 0);\n        Device device = clientPair.appClient.parseDevice(2);\n\n        TestAppClient appClient = new TestAppClient(properties);\n        appClient.start();\n        appClient.login(getUserName(), \"1\", \"Android\", \"1.10.4\");\n        appClient.verifyResult(ok(1));\n\n        appClient.send(\"addPushToken 1\\0uid2\\0token2\");\n        appClient.verifyResult(ok(2));\n\n        clientPair.appClient.send(\"logout\");\n        clientPair.appClient.verifyResult(ok(3));\n\n        clientPair.hardwareClient.stop().await();\n\n        ArgumentCaptor<AndroidGCMMessage> objectArgumentCaptor = ArgumentCaptor.forClass(AndroidGCMMessage.class);\n        verify(holder.gcmWrapper, after(500).never()).send(objectArgumentCaptor.capture(), any(), eq(\"uid2\"));\n    }\n\n    @Test\n    public void testLoginWithSharedAppAndLogoutFrom() throws Exception {\n        Profile profile = parseProfile(readTestUserProfile());\n        Notification notification = profile.getDashById(1).getNotificationWidget();\n        notification.notifyWhenOffline = true;\n\n        clientPair.appClient.updateDash(profile.getDashById(1));\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"getShareToken 1\");\n\n        String token = clientPair.appClient.getBody();\n\n        TestAppClient appClient = new TestAppClient(properties);\n        appClient.start();\n        appClient.send(\"shareLogin \" + getUserName() + \" \" + token + \" Android 24\");\n\n        appClient.send(\"addPushToken 1\\0uid2\\0token2\");\n        appClient.verifyResult(ok(2));\n\n        appClient.send(\"logout uid2\");\n        appClient.verifyResult(ok(3));\n\n        clientPair.hardwareClient.stop().await();\n\n        ArgumentCaptor<AndroidGCMMessage> objectArgumentCaptor = ArgumentCaptor.forClass(AndroidGCMMessage.class);\n        verify(holder.gcmWrapper, after(500).never()).send(objectArgumentCaptor.capture(), any(), eq(\"uid2\"));\n    }\n\n    @Test\n    public void testHardwareDeviceWentOfflineAndPushDelayedWorks() throws Exception {\n        Profile profile = parseProfile(readTestUserProfile());\n        Notification notification = profile.getDashById(1).getNotificationWidget();\n        notification.notifyWhenOffline = true;\n        notification.notifyWhenOfflineIgnorePeriod = 1000;\n\n        long now = System.currentTimeMillis();\n\n        clientPair.appClient.updateDash(profile.getDashById(1));\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.stop();\n\n        ArgumentCaptor<AndroidGCMMessage> objectArgumentCaptor = ArgumentCaptor.forClass(AndroidGCMMessage.class);\n\n        verify(holder.gcmWrapper, timeout(2000).times(1)).send(objectArgumentCaptor.capture(), any(), any());\n        AndroidGCMMessage message = objectArgumentCaptor.getValue();\n        assertTrue(System.currentTimeMillis() - now > notification.notifyWhenOfflineIgnorePeriod );\n\n        String expectedJson = new AndroidGCMMessage(\"token\", Priority.normal, \"Your My Device went offline.\", 1).toJson();\n        assertEquals(expectedJson, message.toJson());\n        clientPair.appClient.verifyResult(deviceOffline(0, \"1-0\"));\n    }\n\n    @Test\n    public void testHardwareDeviceWentOfflineAndPushDelayedNotTriggeredDueToReconnect() throws Exception {\n        Profile profile = parseProfile(readTestUserProfile());\n        Notification notification = profile.getDashById(1).getNotificationWidget();\n        notification.notifyWhenOffline = true;\n        notification.notifyWhenOfflineIgnorePeriod = 1000;\n\n        clientPair.appClient.updateDash(profile.getDashById(1));\n        clientPair.appClient.verifyResult(ok(1));\n\n        ChannelFuture channelFuture = clientPair.hardwareClient.stop();\n        channelFuture.await();\n\n        clientPair.appClient.getDevice(1, 0);\n        Device device = clientPair.appClient.parseDevice(2);\n\n        TestHardClient newHardClient = new TestHardClient(\"localhost\", tcpHardPort);\n        newHardClient.start();\n        newHardClient.login(device.token);\n        newHardClient.verifyResult(ok(1));\n\n        ArgumentCaptor<AndroidGCMMessage> objectArgumentCaptor = ArgumentCaptor.forClass(AndroidGCMMessage.class);\n        verify(holder.gcmWrapper, after(1500).never()).send(objectArgumentCaptor.capture(), any(), any());\n    }\n\n    @Test\n    public void testCreateNewNotificationWidget() throws Exception  {\n        clientPair.appClient.deleteWidget(1, 9);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":9, \\\"x\\\":1, \\\"y\\\":1, \\\"width\\\":1, \\\"height\\\":1, \\\"type\\\":\\\"NOTIFICATION\\\", \\\"notifyWhenOfflineIgnorePeriod\\\":0, \\\"priority\\\":\\\"high\\\", \\\"notifyWhenOffline\\\":true}\");\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.appClient.send(\"addPushToken 1\\0uid1\\0token1\");\n        clientPair.appClient.verifyResult(ok(3));\n\n        clientPair.appClient.updateWidget(1, \"{\\\"id\\\":9, \\\"x\\\":1, \\\"y\\\":1, \\\"width\\\":1, \\\"height\\\":1, \\\"type\\\":\\\"NOTIFICATION\\\", \\\"notifyWhenOfflineIgnorePeriod\\\":0, \\\"priority\\\":\\\"high\\\", \\\"notifyWhenOffline\\\":false}\");\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.hardwareClient.send(\"push 123\");\n\n        ArgumentCaptor<AndroidGCMMessage> objectArgumentCaptor = ArgumentCaptor.forClass(AndroidGCMMessage.class);\n\n        verify(holder.gcmWrapper, timeout(500).times(1)).send(objectArgumentCaptor.capture(), any(), any());\n        AndroidGCMMessage message = objectArgumentCaptor.getValue();\n\n        String expectedJson = new AndroidGCMMessage(\"token1\", Priority.high, \"123\", 1).toJson();\n        assertEquals(expectedJson, message.toJson());\n    }\n\n    @Test\n    public void testPushWhenHardwareOffline() throws Exception {\n        ChannelFuture channelFuture = clientPair.hardwareClient.stop();\n        channelFuture.await();\n\n        ArgumentCaptor<AndroidGCMMessage> objectArgumentCaptor = ArgumentCaptor.forClass(AndroidGCMMessage.class);\n        verify(holder.gcmWrapper, timeout(750).times(1)).send(objectArgumentCaptor.capture(), any(), any());\n        AndroidGCMMessage message = objectArgumentCaptor.getValue();\n\n        String expectedJson = new AndroidGCMMessage(\"token\", Priority.normal, \"Your My Device went offline.\", 1).toJson();\n        assertEquals(expectedJson, message.toJson());\n    }\n\n    @Test\n    public void testPushHandler() throws Exception {\n        clientPair.hardwareClient.send(\"push Yo!\");\n\n        ArgumentCaptor<AndroidGCMMessage> objectArgumentCaptor = ArgumentCaptor.forClass(AndroidGCMMessage.class);\n        verify(holder.gcmWrapper, timeout(500).times(1)).send(objectArgumentCaptor.capture(), any(), any());\n        AndroidGCMMessage message = objectArgumentCaptor.getValue();\n\n        String expectedJson = new AndroidGCMMessage(\"token\", Priority.normal, \"Yo!\", 1).toJson();\n        assertEquals(expectedJson, message.toJson());\n    }\n\n    @Test\n    public void testPushHandlerWithPlaceHolder() throws Exception {\n        clientPair.hardwareClient.send(\"push Yo {DEVICE_NAME}!\");\n\n        ArgumentCaptor<AndroidGCMMessage> objectArgumentCaptor = ArgumentCaptor.forClass(AndroidGCMMessage.class);\n        verify(holder.gcmWrapper, timeout(500).times(1)).send(objectArgumentCaptor.capture(), any(), any());\n        AndroidGCMMessage message = objectArgumentCaptor.getValue();\n\n        String expectedJson = new AndroidGCMMessage(\"token\", Priority.normal, \"Yo My Device!\", 1).toJson();\n        assertEquals(expectedJson, message.toJson());\n    }\n\n    @Test\n    public void testOfflineMessageIsSentToBothApps()  throws Exception  {\n        TestAppClient appClient = new TestAppClient(properties);\n        appClient.start();\n\n        appClient.login(getUserName(), \"1\", \"iOS\", \"1.10.2\");\n        appClient.verifyResult(ok(1));\n\n        clientPair.appClient.deleteWidget(1, 9);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.stop();\n        clientPair.appClient.verifyResult(deviceOffline(0, \"1-0\"));\n        appClient.verifyResult(deviceOffline(0, \"1-0\"));\n    }\n\n    @Test\n    public void multipleAccountsOnTheSameDevice() throws Exception {\n        TestAppClient appClient = new TestAppClient(properties);\n        appClient.start();\n\n        appClient.login(getUserName(), \"1\", \"iOS\", \"1.10.2\");\n        appClient.verifyResult(ok(1));\n\n        appClient.send(\"addPushToken 1\\0uid\\0token\");\n        appClient.verifyResult(ok(2));\n\n        appClient.send(\"loadProfileGzipped\");\n        Profile profile = appClient.parseProfile(3);\n\n        Notification notification = profile.getDashById(1).getNotificationWidget();\n        assertNotNull(notification);\n        assertEquals(1, notification.androidTokens.size());\n        assertEquals(1, notification.iOSTokens.size());\n\n        appClient.reset();\n\n        TestAppClient appClient2 = new TestAppClient(properties);\n        appClient2.start();\n\n        appClient2.register(\"testuser@test.com\", \"1\", AppNameUtil.BLYNK);\n        appClient2.verifyResult(ok(1));\n\n        appClient2.login(\"testuser@test.com\", \"1\", \"iOS\", \"1.10.2\");\n        appClient2.verifyResult(ok(2));\n\n        DashBoard dash = new DashBoard();\n        dash.id = 5;\n        dash.name = \"test\";\n        Notification notif = new Notification();\n        notif.x = 1;\n        notif.y = 1;\n        notif.width = 1;\n        notif.height = 1;\n        notif.id = 22;\n        dash.widgets = new Widget[] {\n                notif\n        };\n        dash.activate();\n        appClient2.createDash(dash);\n        appClient2.verifyResult(ok(3));\n\n        appClient2.send(\"addPushToken 5\\0uid\\0token222\");\n        appClient2.verifyResult(ok(4));\n\n        appClient2.send(\"loadProfileGzipped\");\n        profile = appClient2.parseProfile(5);\n\n        notification = profile.getDashById(5).getNotificationWidget();\n        assertNotNull(notification);\n        assertEquals(0, notification.androidTokens.size());\n        assertEquals(1, notification.iOSTokens.size());\n        appClient2.reset();\n\n        //waiting for another thread to remove the duplicate\n        sleep(500);\n\n        appClient.send(\"loadProfileGzipped\");\n        profile = appClient.parseProfile(1);\n        notification = profile.getDashById(1).getNotificationWidget();\n        assertNotNull(notification);\n        assertEquals(1, notification.androidTokens.size());\n        assertEquals(0, notification.iOSTokens.size());\n\n        appClient2.send(\"loadProfileGzipped\");\n        profile = appClient2.parseProfile(1);\n        notification = profile.getDashById(5).getNotificationWidget();\n        assertNotNull(notification);\n        assertEquals(0, notification.androidTokens.size());\n        assertEquals(1, notification.iOSTokens.size());\n    }\n\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/tcp/OfflineNotificationTest.java",
    "content": "package cc.blynk.integration.tcp;\n\nimport cc.blynk.integration.SingleServerInstancePerTest;\nimport cc.blynk.integration.TestUtil;\nimport cc.blynk.integration.model.tcp.TestAppClient;\nimport cc.blynk.integration.model.tcp.TestHardClient;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.DashboardSettings;\nimport cc.blynk.server.core.model.Profile;\nimport cc.blynk.server.core.model.device.BoardType;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.device.Status;\nimport cc.blynk.server.core.model.enums.Theme;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport org.junit.BeforeClass;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport static cc.blynk.integration.TestUtil.b;\nimport static cc.blynk.integration.TestUtil.createDevice;\nimport static cc.blynk.integration.TestUtil.deviceOffline;\nimport static cc.blynk.integration.TestUtil.ok;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\nimport static org.junit.Assert.assertTrue;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/2/2015.\n *\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class OfflineNotificationTest extends SingleServerInstancePerTest {\n\n    private static int tcpHardPort;\n\n    @BeforeClass\n    public static void initPort() {\n        tcpHardPort = properties.getHttpPort();\n    }\n\n    @Override\n    protected String changeProfileTo() {\n        return \"user_profile_json_empty_dash.txt\";\n    }\n\n    @Test\n    public void testOfflineTimingIsCorrectForMultipleDevices() throws Exception {\n        Device device2 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device2.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device2);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        clientPair.appClient.send(\"getDevices 1\");\n\n        Device[] devices = clientPair.appClient.parseDevices(2);\n        assertNotNull(devices);\n        assertEquals(2, devices.length);\n\n        assertEquals(1, devices[1].id);\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient2.start();\n        hardClient2.login(devices[1].token);\n        hardClient2.verifyResult(ok(1));\n\n        hardClient2.send(\"internal \" + b(\"ver 0.3.1 h-beat 10 buff-in 256 dev Arduino cpu ATmega328P con W5100\"));\n        hardClient2.verifyResult(ok(2));\n\n        clientPair.hardwareClient.stop();\n\n        clientPair.appClient.verifyResult(deviceOffline(0, \"1-0\"));\n        clientPair.appClient.never(deviceOffline(0, \"1-1\"));\n\n        clientPair.appClient.reset();\n        hardClient2.stop();\n\n        clientPair.appClient.verifyResult(deviceOffline(0, b(\"1-1\")));\n        clientPair.appClient.never(deviceOffline(0, b(\"1-0\")));\n    }\n\n    @Test\n    public void testOfflineTimingIsCorrectForMultipleDevices2() throws Exception {\n        Device device2 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device2.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device2);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        clientPair.appClient.send(\"getDevices 1\");\n\n        Device[] devices = clientPair.appClient.parseDevices(2);\n        assertNotNull(devices);\n        assertEquals(2, devices.length);\n\n        assertEquals(1, devices[1].id);\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient2.start();\n        hardClient2.login(devices[1].token);\n        hardClient2.verifyResult(ok(1));\n\n        hardClient2.send(\"internal \" + b(\"ver 0.3.1 h-beat 1 buff-in 256 dev Arduino cpu ATmega328P con W5100\"));\n        hardClient2.verifyResult(ok(2));\n\n        clientPair.hardwareClient.stop();\n        hardClient2.stop();\n\n        clientPair.appClient.verifyResult(deviceOffline(0, b(\"1-0\")));\n        clientPair.appClient.verifyResult(deviceOffline(0, b(\"1-1\")));\n    }\n\n    @Test\n    public void testTurnOffNotifications() throws Exception{\n        DashboardSettings settings = new DashboardSettings(\"New Name\",\n                true, Theme.BlynkLight, true, true, true, false, 0, false);\n\n        clientPair.appClient.send(\"updateSettings 1\\0\" + JsonParser.toJson(settings));\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(2);\n        DashBoard dashBoard = profile.dashBoards[0];\n        assertNotNull(dashBoard);\n        assertEquals(settings.name, dashBoard.name);\n        assertEquals(settings.isAppConnectedOn, dashBoard.isAppConnectedOn);\n        assertEquals(settings.isNotificationsOff, dashBoard.isNotificationsOff);\n        assertTrue(dashBoard.isNotificationsOff);\n        assertEquals(settings.isShared, dashBoard.isShared);\n        assertEquals(settings.keepScreenOn, dashBoard.keepScreenOn);\n        assertEquals(settings.theme, dashBoard.theme);\n        assertEquals(settings.widgetBackgroundOn, dashBoard.widgetBackgroundOn);\n\n        clientPair.hardwareClient.stop();\n\n        clientPair.appClient.neverAfter(500, deviceOffline(0, \"1-0\"));\n\n        Device device2 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device2.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device2);\n        Device device = clientPair.appClient.parseDevice(3);\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(3, device));\n\n        clientPair.appClient.send(\"getDevices 1\");\n\n        Device[] devices = clientPair.appClient.parseDevices(4);\n        assertNotNull(devices);\n        assertEquals(2, devices.length);\n\n        assertEquals(1, devices[1].id);\n\n        settings = new DashboardSettings(\"New Name\",\n                true, Theme.BlynkLight, true, true, false, false, 0, false);\n        clientPair.appClient.send(\"updateSettings 1\\0\" + JsonParser.toJson(settings));\n        clientPair.appClient.verifyResult(ok(5));\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient2.start();\n        hardClient2.login(devices[1].token);\n        hardClient2.verifyResult(ok(1));\n        hardClient2.stop();\n\n        clientPair.appClient.verifyResult(deviceOffline(0, \"1-1\"));\n    }\n\n    @Test\n    public void testTurnOffNotificationsAndNoDevices() throws Exception{\n        DashboardSettings settings = new DashboardSettings(\"New Name\",\n                true, Theme.BlynkLight, true, true, true, false, 0, false);\n\n        clientPair.appClient.send(\"updateSettings 1\\0\" + JsonParser.toJson(settings));\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.stop();\n        clientPair.appClient.neverAfter(500, deviceOffline(0, \"1-0\"));\n\n        clientPair.appClient.activate(1);\n        clientPair.appClient.verifyResult(ok(2));\n    }\n\n    @Test\n    public void deviceGoesOfflineAfterBeingIdle() throws Exception {\n        Device device2 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device2.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device2);\n        clientPair.appClient.send(\"getDevices 1\");\n\n        Device[] devices = clientPair.appClient.parseDevices(2);\n        assertNotNull(devices);\n        assertEquals(2, devices.length);\n\n        assertEquals(1, devices[1].id);\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient2.start();\n        hardClient2.login(devices[1].token);\n        hardClient2.verifyResult(ok(1));\n\n        hardClient2.send(\"internal \" + b(\"ver 0.3.1 h-beat 1 buff-in 256 dev Arduino cpu ATmega328P con W5100\"));\n        hardClient2.verifyResult(ok(2));\n\n        //just waiting 2.5 secs so server trigger idle event\n        TestUtil.sleep(2500);\n\n        clientPair.appClient.verifyResult(deviceOffline(0, b(\"1-1\")));\n    }\n\n    @Test\n    public void sessionDisconnectChangeState() throws Exception {\n        Device device2 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device2.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device2);\n        clientPair.appClient.send(\"getDevices 1\");\n\n        Device[] devices = clientPair.appClient.parseDevices(2);\n        assertNotNull(devices);\n        assertEquals(2, devices.length);\n\n        assertEquals(1, devices[1].id);\n        assertEquals(0, devices[1].disconnectTime);\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient2.start();\n        hardClient2.login(devices[1].token);\n        hardClient2.verifyResult(ok(1));\n\n        holder.sessionDao.close();\n\n        TestAppClient testAppClient = new TestAppClient(properties);\n        testAppClient.start();\n        testAppClient.login(getUserName(), \"1\");\n        testAppClient.verifyResult(ok(1));\n\n        testAppClient.send(\"getDevices 1\");\n        devices = testAppClient.parseDevices(2);\n        assertNotNull(devices);\n        assertEquals(2, devices.length);\n        assertEquals(1, devices[1].id);\n        assertEquals(System.currentTimeMillis(), devices[1].disconnectTime, 5000);\n    }\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/tcp/PortUnificationTest.java",
    "content": "package cc.blynk.integration.tcp;\n\nimport cc.blynk.integration.SingleServerInstancePerTest;\nimport cc.blynk.integration.model.tcp.TestAppClient;\nimport cc.blynk.integration.model.tcp.TestHardClient;\nimport cc.blynk.server.core.model.device.BoardType;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.utils.AppNameUtil;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport static cc.blynk.integration.TestUtil.ok;\nimport static org.junit.Assert.assertNotNull;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/2/2015.\n *\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class PortUnificationTest extends SingleServerInstancePerTest {\n\n    @Test\n    public void testAppConectsOk() throws Exception {\n        int appPort = properties.getHttpsPort();\n        TestAppClient appClient = new TestAppClient(\"localhost\", appPort, properties);\n        appClient.start();\n\n        appClient.register(incrementAndGetUserName(), \"1\", AppNameUtil.BLYNK);\n        appClient.login(getUserName(), \"1\", \"Android\", \"1.10.4\");\n        appClient.verifyResult(ok(1));\n        appClient.verifyResult(ok(2));\n    }\n\n    @Test\n    public void testHardwareConnectsOk() throws Exception {\n        int appPort = properties.getHttpsPort();\n        TestAppClient appClient = new TestAppClient(\"localhost\", appPort, properties);\n        appClient.start();\n\n        appClient.register(incrementAndGetUserName(), \"1\", AppNameUtil.BLYNK);\n        appClient.login(getUserName(), \"1\", \"Android\", \"1.10.4\");\n        appClient.createDash(\"{\\\"id\\\":1, \\\"createdAt\\\":1, \\\"name\\\":\\\"test board\\\"}\");\n\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        appClient.createDevice(1, device1);\n\n        appClient.verifyResult(ok(1));\n        appClient.verifyResult(ok(2));\n        appClient.verifyResult(ok(3));\n\n        Device device = appClient.parseDevice(4);\n        assertNotNull(device);\n        assertNotNull(device.token);\n\n        int hardwarePort = properties.getHttpPort();\n        TestHardClient hardClient = new TestHardClient(\"localhost\", hardwarePort);\n        hardClient.start();\n\n        hardClient.login(device.token);\n        hardClient.verifyResult(ok(1));\n    }\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/tcp/PublishingPreviewFlow.java",
    "content": "package cc.blynk.integration.tcp;\n\nimport cc.blynk.integration.SingleServerInstancePerTestWithDB;\nimport cc.blynk.integration.model.tcp.TestAppClient;\nimport cc.blynk.integration.model.tcp.TestHardClient;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.Profile;\nimport cc.blynk.server.core.model.auth.App;\nimport cc.blynk.server.core.model.device.BoardType;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.device.Status;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.enums.ProvisionType;\nimport cc.blynk.server.core.model.enums.Theme;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.notifications.Notification;\nimport cc.blynk.server.core.model.widgets.notifications.Twitter;\nimport cc.blynk.server.core.model.widgets.outputs.Gauge;\nimport cc.blynk.server.core.model.widgets.outputs.graph.FontSize;\nimport cc.blynk.server.core.model.widgets.ui.TimeInput;\nimport cc.blynk.server.core.model.widgets.ui.tiles.DeviceTiles;\nimport cc.blynk.server.core.model.widgets.ui.tiles.TileTemplate;\nimport cc.blynk.server.core.model.widgets.ui.tiles.templates.PageTileTemplate;\nimport cc.blynk.server.db.model.FlashedToken;\nimport cc.blynk.server.notifications.mail.QrHolder;\nimport cc.blynk.utils.AppNameUtil;\nimport net.glxn.qrgen.core.image.ImageType;\nimport net.glxn.qrgen.javase.QRCode;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport java.sql.Connection;\nimport java.sql.ResultSet;\nimport java.sql.Statement;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport static cc.blynk.integration.TestUtil.appSync;\nimport static cc.blynk.integration.TestUtil.b;\nimport static cc.blynk.integration.TestUtil.createDevice;\nimport static cc.blynk.integration.TestUtil.deviceOffline;\nimport static cc.blynk.integration.TestUtil.hardware;\nimport static cc.blynk.integration.TestUtil.hardwareConnected;\nimport static cc.blynk.integration.TestUtil.illegalCommand;\nimport static cc.blynk.integration.TestUtil.notAllowed;\nimport static cc.blynk.integration.TestUtil.ok;\nimport static cc.blynk.integration.TestUtil.readTestUserProfile;\nimport static cc.blynk.integration.TestUtil.sleep;\nimport static cc.blynk.server.core.model.serialization.JsonParser.MAPPER;\nimport static cc.blynk.utils.properties.Placeholders.DYNAMIC_SECTION;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertFalse;\nimport static org.junit.Assert.assertNotNull;\nimport static org.junit.Assert.assertNull;\nimport static org.junit.Assert.assertTrue;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.eq;\nimport static org.mockito.Mockito.timeout;\nimport static org.mockito.Mockito.verify;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/2/2015.\n *\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class PublishingPreviewFlow extends SingleServerInstancePerTestWithDB {\n\n    @Before\n    public void deleteTable() throws Exception {\n        holder.dbManager.executeSQL(\"DELETE FROM flashed_tokens\");\n    }\n\n    @Test\n    public void testGetProjectByToken() throws Exception {\n        App appObj = new App(null, Theme.BlynkLight,\n                ProvisionType.STATIC,\n                0, false, \"AppPreview\", \"myIcon\", new int[] {1});\n        clientPair.appClient.createApp(appObj);\n        App appFromApi = clientPair.appClient.parseApp(1);\n        assertNotNull(appFromApi);\n        assertNotNull(appFromApi.id);\n        clientPair.appClient.reset();\n\n        clientPair.appClient.send(\"getDevices 1\");\n        Device[] devices = clientPair.appClient.parseDevices();\n        assertEquals(1, devices.length);\n\n        clientPair.appClient.send(\"emailQr 1\\0\" + appFromApi.id);\n        clientPair.appClient.verifyResult(ok(2));\n\n        QrHolder[] qrHolders = makeQRs(devices, 1);\n        StringBuilder sb = new StringBuilder();\n        qrHolders[0].attach(sb);\n        verify(holder.mailWrapper, timeout(500)).sendWithAttachment(eq(getUserName()), eq(\"AppPreview\" + \" - App details\"), eq(holder.textHolder.staticMailBody.replace(\"{project_name}\", \"My Dashboard\").replace(DYNAMIC_SECTION, sb.toString())), eq(qrHolders));\n\n        clientPair.appClient.send(\"getProjectByToken \" + qrHolders[0].token);\n        DashBoard dashBoard = clientPair.appClient.parseDash(3);\n        assertNotNull(dashBoard);\n        assertEquals(1, dashBoard.id);\n    }\n\n    @Test\n    public void testSendStaticEmailForAppPublish() throws Exception {\n        App appObj = new App(null, Theme.BlynkLight,\n                ProvisionType.STATIC,\n                0, false, \"AppPreview\", \"myIcon\", new int[] {1});\n        clientPair.appClient.createApp(appObj);\n        App app = clientPair.appClient.parseApp(1);\n        assertNotNull(app);\n        assertNotNull(app.id);\n        clientPair.appClient.reset();\n\n        clientPair.appClient.send(\"getDevices 1\");\n        Device[] devices = clientPair.appClient.parseDevices();\n        assertEquals(1, devices.length);\n\n        clientPair.appClient.send(\"emailQr 1\\0\" + app.id);\n        clientPair.appClient.verifyResult(ok(2));\n\n        QrHolder[] qrHolders = makeQRs(devices, 1);\n        StringBuilder sb = new StringBuilder();\n        qrHolders[0].attach(sb);\n        verify(holder.mailWrapper, timeout(500)).sendWithAttachment(eq(getUserName()), eq(\"AppPreview\" + \" - App details\"), eq(holder.textHolder.staticMailBody.replace(\"{project_name}\", \"My Dashboard\").replace(DYNAMIC_SECTION, sb.toString())), eq(qrHolders));\n\n        FlashedToken flashedToken = holder.dbManager.selectFlashedToken(qrHolders[0].token);\n        assertNotNull(flashedToken);\n        assertEquals(flashedToken.appId, app.id);\n        assertEquals(1, flashedToken.dashId);\n        assertEquals(0, flashedToken.deviceId);\n        assertEquals(qrHolders[0].token, flashedToken.token);\n        assertFalse(flashedToken.isActivated);\n    }\n\n    @Test\n    public void testSendDynamicEmailForAppPublish() throws Exception {\n        App appObj = new App(null, Theme.BlynkLight,\n                ProvisionType.DYNAMIC,\n                0, false, \"AppPreview\", \"myIcon\", new int[] {1});\n        clientPair.appClient.createApp(appObj);\n        App app = clientPair.appClient.parseApp(1);\n        assertNotNull(app);\n        assertNotNull(app.id);\n        clientPair.appClient.reset();\n\n        clientPair.appClient.send(\"getDevices 1\");\n        Device[] devices = clientPair.appClient.parseDevices();\n        assertEquals(1, devices.length);\n\n        clientPair.appClient.send(\"emailQr 1\\0\" + app.id);\n        clientPair.appClient.verifyResult(ok(2));\n\n        FlashedToken flashedToken = getFlashedTokenByDevice();\n        assertNotNull(flashedToken);\n        QrHolder qrHolder = new QrHolder(1, -1, null, flashedToken.token, QRCode.from(flashedToken.token).to(ImageType.JPG).stream().toByteArray());\n\n        verify(holder.mailWrapper, timeout(500)).sendWithAttachment(eq(getUserName()), eq(\"AppPreview\" + \" - App details\"), eq(holder.textHolder.dynamicMailBody.replace(\"{project_name}\", \"My Dashboard\")), eq(qrHolder));\n    }\n\n    @Test\n    public void testSendDynamicEmailForAppPublishAndNoDevices() throws Exception {\n        App appObj = new App(null, Theme.BlynkLight,\n                ProvisionType.DYNAMIC,\n                0, false, \"AppPreview\", \"myIcon\", new int[] {1});\n        clientPair.appClient.createApp(appObj);\n        App app = clientPair.appClient.parseApp(1);\n        assertNotNull(app);\n        assertNotNull(app.id);\n        clientPair.appClient.reset();\n\n        clientPair.appClient.deleteDevice(1, 0);\n        clientPair.appClient.verifyResult(ok(1));\n        clientPair.appClient.verifyResult(deviceOffline(0, \"1-0\"));\n        clientPair.appClient.reset();\n\n        clientPair.appClient.send(\"getDevices 1\");\n        Device[] devices = clientPair.appClient.parseDevices(1);\n        assertEquals(0, devices.length);\n\n        clientPair.appClient.send(\"emailQr 1\\0\" + app.id);\n        clientPair.appClient.verifyResult(ok(2));\n\n        FlashedToken flashedToken = getFlashedTokenByDevice();\n        assertNotNull(flashedToken);\n        QrHolder qrHolder = new QrHolder(1, -1, null, flashedToken.token, QRCode.from(flashedToken.token).to(ImageType.JPG).stream().toByteArray());\n\n        verify(holder.mailWrapper, timeout(500)).sendWithAttachment(eq(getUserName()), eq(\"AppPreview\" + \" - App details\"), eq(holder.textHolder.dynamicMailBody.replace(\"{project_name}\", \"My Dashboard\")), eq(qrHolder));\n    }\n\n    @Test\n    public void testSendDynamicEmailForAppPublishWithFewDevices() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n        device1.status = Status.OFFLINE;\n\n        clientPair.appClient.createDevice(1, device1);\n        device1 = clientPair.appClient.parseDevice();\n        assertNotNull(device1);\n        assertEquals(1, device1.id);\n\n        clientPair.appClient.send(\"createApp {\\\"theme\\\":\\\"Blynk\\\",\\\"provisionType\\\":\\\"DYNAMIC\\\",\\\"color\\\":0,\\\"name\\\":\\\"AppPreview\\\",\\\"icon\\\":\\\"myIcon\\\",\\\"projectIds\\\":[1]}\");\n        App app = clientPair.appClient.parseApp(2);\n        assertNotNull(app);\n        assertNotNull(app.id);\n        clientPair.appClient.reset();\n\n        clientPair.appClient.send(\"getDevices 1\");\n        Device[] devices = clientPair.appClient.parseDevices();\n        assertEquals(2, devices.length);\n\n        clientPair.appClient.send(\"emailQr 1\\0\" + app.id);\n        clientPair.appClient.verifyResult(ok(2));\n\n        FlashedToken flashedToken = getFlashedTokenByDevice();\n        assertNotNull(flashedToken);\n        QrHolder qrHolder = new QrHolder(1, -1, null, flashedToken.token, QRCode.from(flashedToken.token).to(ImageType.JPG).stream().toByteArray());\n\n        verify(holder.mailWrapper, timeout(500)).sendWithAttachment(eq(getUserName()), eq(\"AppPreview\" + \" - App details\"), eq(holder.textHolder.dynamicMailBody.replace(\"{project_name}\", \"My Dashboard\")), eq(qrHolder));\n    }\n\n    @Test\n    public void testFaceEditNotAllowedHasNoChild() throws Exception {\n        clientPair.appClient.send(\"updateFace 1\");\n        clientPair.appClient.verifyResult(notAllowed(1));\n    }\n\n    @Test\n    public void testFaceUpdateWorks() throws Exception {\n        DashBoard dashBoard = new DashBoard();\n        dashBoard.id = 10;\n        dashBoard.parentId = 1;\n        dashBoard.isPreview = true;\n        dashBoard.name = \"Face Edit Test\";\n\n        clientPair.appClient.createDash(dashBoard);\n\n        Device device0 = new Device(0, \"My Dashboard\", BoardType.Arduino_UNO);\n        device0.status = Status.ONLINE;\n\n        clientPair.appClient.createDevice(10, device0);\n        Device device = clientPair.appClient.parseDevice(2);\n        assertNotNull(device);\n        assertNotNull(device.token);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(createDevice(2, device)));\n\n        clientPair.appClient.send(\"createApp {\\\"theme\\\":\\\"Blynk\\\",\\\"provisionType\\\":\\\"STATIC\\\",\\\"color\\\":0,\\\"name\\\":\\\"AppPreview\\\",\\\"icon\\\":\\\"myIcon\\\",\\\"projectIds\\\":[10]}\");\n        App app = clientPair.appClient.parseApp(3);\n        assertNotNull(app);\n        assertNotNull(app.id);\n\n\n        clientPair.appClient.send(\"emailQr 10\\0\" + app.id);\n        clientPair.appClient.verifyResult(ok(4));\n\n        QrHolder[] qrHolders = makeQRs(new Device[] {device}, 10);\n        StringBuilder sb = new StringBuilder();\n        qrHolders[0].attach(sb);\n        verify(holder.mailWrapper, timeout(500)).sendWithAttachment(eq(getUserName()), eq(\"AppPreview\" + \" - App details\"), eq(holder.textHolder.staticMailBody.replace(\"{project_name}\", \"Face Edit Test\").replace(DYNAMIC_SECTION, sb.toString())), eq(qrHolders));\n\n        TestAppClient appClient2 = new TestAppClient(properties);\n        appClient2.start();\n\n        appClient2.register(\"test@blynk.cc\", \"a\", app.id);\n        verify(appClient2.responseMock, timeout(1000)).channelRead(any(), eq(ok(1)));\n\n        appClient2.login(\"test@blynk.cc\", \"a\", \"Android\", \"1.10.4\", app.id);\n        verify(appClient2.responseMock, timeout(1000)).channelRead(any(), eq(ok(2)));\n\n        appClient2.send(\"loadProfileGzipped\");\n        Profile profile = appClient2.parseProfile(3);\n        assertEquals(1, profile.dashBoards.length);\n        dashBoard = profile.dashBoards[0];\n        assertNotNull(dashBoard);\n        assertEquals(1, dashBoard.id);\n        assertEquals(1, dashBoard.parentId);\n        assertEquals(0, dashBoard.widgets.length);\n\n        clientPair.appClient.send(\"updateFace 1\");\n        clientPair.appClient.verifyResult(ok(5));\n        assertTrue(appClient2.isClosed());\n\n        appClient2 = new TestAppClient(properties);\n        appClient2.start();\n        appClient2.login(\"test@blynk.cc\", \"a\", \"Android\", \"1.10.4\", app.id);\n        verify(appClient2.responseMock, timeout(1000)).channelRead(any(), eq(ok(1)));\n\n        appClient2.send(\"loadProfileGzipped\");\n        profile = appClient2.parseProfile(2);\n        assertEquals(1, profile.dashBoards.length);\n        dashBoard = profile.dashBoards[0];\n        assertNotNull(dashBoard);\n        assertEquals(1, dashBoard.id);\n        assertEquals(1, dashBoard.parentId);\n        assertEquals(16, dashBoard.widgets.length);\n    }\n\n    @Test\n    public void testUpdateFaceDoesntEraseExistingDeviceTiles() throws Exception {\n        DashBoard dashBoard = new DashBoard();\n        dashBoard.id = 10;\n        dashBoard.parentId = 1;\n        dashBoard.isPreview = true;\n        dashBoard.name = \"Face Edit Test\";\n\n        clientPair.appClient.createDash(dashBoard);\n\n        Device device0 = new Device(0, \"My Dashboard\", BoardType.ESP8266);\n        device0.status = Status.ONLINE;\n\n        clientPair.appClient.createDevice(10, device0);\n        Device device = clientPair.appClient.parseDevice(2);\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(2, device));\n\n        clientPair.appClient.send(\"createApp {\\\"theme\\\":\\\"Blynk\\\",\\\"provisionType\\\":\\\"STATIC\\\",\\\"color\\\":0,\\\"name\\\":\\\"AppPreview\\\",\\\"icon\\\":\\\"myIcon\\\",\\\"projectIds\\\":[10]}\");\n        App app = clientPair.appClient.parseApp(3);\n        assertNotNull(app);\n        assertNotNull(app.id);\n\n\n        long widgetId = 21321;\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = widgetId;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        //creating manually widget for child project\n        clientPair.appClient.createWidget(10, deviceTiles);\n        clientPair.appClient.verifyResult(ok(4));\n\n        TileTemplate tileTemplate = new PageTileTemplate(1,\n                null, null, \"123\", \"name\", \"iconName\", BoardType.ESP8266, null,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.send(\"createTemplate \" + b(\"10 \" + widgetId + \" \")\n                + MAPPER.writeValueAsString(tileTemplate));\n        clientPair.appClient.verifyResult(ok(5));\n\n        //creating manually widget for parent project\n        clientPair.appClient.send(\"addEnergy \" + \"10000\" + \"\\0\" + \"1370-3990-1414-55681\");\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(7));\n\n        tileTemplate = new PageTileTemplate(1,\n                null, null, \"123\", \"name\", \"iconName\", BoardType.ESP8266, null,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.send(\"createTemplate \" + b(\"1 \" + widgetId + \" \")\n                + MAPPER.writeValueAsString(tileTemplate));\n        clientPair.appClient.verifyResult(ok(8));\n\n\n        clientPair.appClient.send(\"emailQr 10\\0\" + app.id);\n        clientPair.appClient.verifyResult(ok(9));\n\n        QrHolder[] qrHolders = makeQRs(new Device[] {device}, 10);\n        StringBuilder sb = new StringBuilder();\n        qrHolders[0].attach(sb);\n        verify(holder.mailWrapper, timeout(500)).sendWithAttachment(eq(getUserName()), eq(\"AppPreview\" + \" - App details\"), eq(holder.textHolder.staticMailBody.replace(\"{project_name}\", \"Face Edit Test\").replace(DYNAMIC_SECTION, sb.toString())), eq(qrHolders));\n\n        TestAppClient appClient2 = new TestAppClient(properties);\n        appClient2.start();\n\n        appClient2.register(\"test@blynk.cc\", \"a\", app.id);\n        appClient2.verifyResult(ok(1));\n\n        appClient2.login(\"test@blynk.cc\", \"a\", \"Android\", \"1.10.4\", app.id);\n        appClient2.verifyResult(ok(2));\n\n        appClient2.send(\"loadProfileGzipped\");\n        Profile profile = appClient2.parseProfile(3);\n        assertEquals(1, profile.dashBoards.length);\n        dashBoard = profile.dashBoards[0];\n        assertNotNull(dashBoard);\n        assertEquals(1, dashBoard.id);\n        assertEquals(1, dashBoard.parentId);\n        assertEquals(1, dashBoard.widgets.length);\n        assertTrue(dashBoard.widgets[0] instanceof DeviceTiles);\n        deviceTiles = (DeviceTiles) dashBoard.getWidgetById(widgetId);\n        assertNotNull(deviceTiles.tiles);\n        assertNotNull(deviceTiles.templates);\n        assertEquals(0, deviceTiles.tiles.length);\n        assertEquals(1, deviceTiles.templates.length);\n\n        tileTemplate = new PageTileTemplate(1,\n                null, new int[] {0}, \"123\", \"name\", \"iconName\", BoardType.ESP8266, null,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n        appClient2.send(\"updateTemplate \" + b(\"1 \" + widgetId + \" \")\n                + MAPPER.writeValueAsString(tileTemplate));\n        appClient2.verifyResult(ok(4));\n\n\n        clientPair.appClient.send(\"updateFace 1\");\n        clientPair.appClient.verifyResult(ok(10));\n        assertTrue(appClient2.isClosed());\n\n        appClient2 = new TestAppClient(properties);\n        appClient2.start();\n        appClient2.login(\"test@blynk.cc\", \"a\", \"Android\", \"1.10.4\", app.id);\n        verify(appClient2.responseMock, timeout(1000)).channelRead(any(), eq(ok(1)));\n\n        appClient2.send(\"loadProfileGzipped\");\n        profile = appClient2.parseProfile(2);\n        assertEquals(1, profile.dashBoards.length);\n        dashBoard = profile.dashBoards[0];\n        assertNotNull(dashBoard);\n        assertEquals(1, dashBoard.id);\n        assertEquals(1, dashBoard.parentId);\n        assertEquals(17, dashBoard.widgets.length);\n        deviceTiles = (DeviceTiles) dashBoard.getWidgetById(widgetId);\n        assertNotNull(deviceTiles);\n        assertNotNull(deviceTiles.tiles);\n        assertNotNull(deviceTiles.templates);\n        assertEquals(1, deviceTiles.tiles.length);\n        assertEquals(0, deviceTiles.tiles[0].deviceId);\n        assertEquals(1, deviceTiles.tiles[0].templateId);\n        assertEquals(1, deviceTiles.templates.length);\n        assertNotNull(deviceTiles.templates[0].deviceIds);\n        assertEquals(1, deviceTiles.templates[0].deviceIds.length);\n    }\n\n    @Test\n    public void testDeviceTilesAreNotCopiedFromParentProjectOnCreationAndFaceUpdate() throws Exception {\n        DashBoard dashBoard = new DashBoard();\n        dashBoard.id = 10;\n        dashBoard.parentId = 1;\n        dashBoard.isPreview = true;\n        dashBoard.name = \"Face Edit Test\";\n\n        clientPair.appClient.createDash(dashBoard);\n\n        Device device0 = new Device(0, \"My Dashboard\", BoardType.Arduino_UNO);\n        clientPair.appClient.createDevice(10, device0);\n        device0 = clientPair.appClient.parseDevice(2);\n        clientPair.appClient.verifyResult(createDevice(2, device0));\n\n        Device device2 = new Device(2, \"My Dashboard\", BoardType.Arduino_UNO);\n        clientPair.appClient.createDevice(10, device2);\n        device2 = clientPair.appClient.parseDevice(3);\n        clientPair.appClient.verifyResult(createDevice(3, device2));\n\n        clientPair.appClient.send(\"createApp {\\\"theme\\\":\\\"Blynk\\\",\\\"provisionType\\\":\\\"STATIC\\\",\\\"color\\\":0,\\\"name\\\":\\\"AppPreview\\\",\\\"icon\\\":\\\"myIcon\\\",\\\"projectIds\\\":[10]}\");\n        App app = clientPair.appClient.parseApp(4);\n        assertNotNull(app);\n        assertNotNull(app.id);\n\n\n        long widgetId = 21321;\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = widgetId;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        //creating manually widget for child project\n        clientPair.appClient.createWidget(10, deviceTiles);\n        clientPair.appClient.verifyResult(ok(5));\n\n        TileTemplate tileTemplate = new PageTileTemplate(1,\n                null, new int[] {2}, \"123\", \"name\", \"iconName\", BoardType.ESP8266, null,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.send(\"createTemplate \" + b(\"10 \" + widgetId + \" \")\n                + MAPPER.writeValueAsString(tileTemplate));\n        clientPair.appClient.verifyResult(ok(6));\n\n        clientPair.appClient.createWidget(10, \"{\\\"id\\\":155, \\\"deviceId\\\":0, \\\"frequency\\\":400, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"GAUGE\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":100}\");\n        clientPair.appClient.verifyResult(ok(7));\n\n        TestAppClient appClient2 = new TestAppClient(properties);\n        appClient2.start();\n\n        appClient2.register(\"test@blynk.cc\", \"a\", app.id);\n        appClient2.verifyResult(ok(1));\n\n        appClient2.login(\"test@blynk.cc\", \"a\", \"Android\", \"1.10.4\", app.id);\n        appClient2.verifyResult(ok(2));\n\n        appClient2.send(\"loadProfileGzipped\");\n        Profile profile = appClient2.parseProfile(3);\n        assertEquals(1, profile.dashBoards.length);\n        dashBoard = profile.dashBoards[0];\n        assertNotNull(dashBoard);\n        assertEquals(1, dashBoard.id);\n        assertEquals(1, dashBoard.parentId);\n        assertEquals(1, dashBoard.devices.length);\n        assertEquals(0, dashBoard.devices[0].id);\n        assertEquals(2, dashBoard.widgets.length);\n        assertTrue(dashBoard.widgets[0] instanceof DeviceTiles);\n        deviceTiles = (DeviceTiles) dashBoard.getWidgetById(widgetId);\n        assertNotNull(deviceTiles.tiles);\n        assertNotNull(deviceTiles.templates);\n        assertEquals(0, deviceTiles.tiles.length);\n        assertEquals(1, deviceTiles.templates.length);\n    }\n\n    @Test\n    public void testChildProjectDoesntGetParentValues() throws Exception {\n        DashBoard dashBoard = new DashBoard();\n        dashBoard.id = 10;\n        dashBoard.parentId = 1;\n        dashBoard.isPreview = true;\n        dashBoard.name = \"Face Edit Test\";\n\n        clientPair.appClient.createDash(dashBoard);\n\n        Device device0 = new Device(0, \"My Dashboard\", BoardType.Arduino_UNO);\n        clientPair.appClient.createDevice(10, device0);\n        device0 = clientPair.appClient.parseDevice(2);\n        clientPair.appClient.verifyResult(createDevice(2, device0));\n\n        Device device2 = new Device(2, \"My Dashboard\", BoardType.Arduino_UNO);\n        clientPair.appClient.createDevice(10, device2);\n        device2 = clientPair.appClient.parseDevice(3);\n        clientPair.appClient.verifyResult(createDevice(3, device2));\n\n        clientPair.appClient.send(\"createApp {\\\"theme\\\":\\\"Blynk\\\",\\\"provisionType\\\":\\\"STATIC\\\",\\\"color\\\":0,\\\"name\\\":\\\"AppPreview\\\",\\\"icon\\\":\\\"myIcon\\\",\\\"projectIds\\\":[10]}\");\n        App app = clientPair.appClient.parseApp(4);\n        assertNotNull(app);\n        assertNotNull(app.id);\n\n\n        long widgetId = 21321;\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = widgetId;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        //creating manually widget for child project\n        clientPair.appClient.createWidget(10, deviceTiles);\n        clientPair.appClient.verifyResult(ok(5));\n\n        TileTemplate tileTemplate = new PageTileTemplate(1,\n                null, new int[] {2}, \"123\", \"name\", \"iconName\", BoardType.ESP8266, null,\n                false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.send(\"createTemplate \" + b(\"10 \" + widgetId + \" \")\n                + MAPPER.writeValueAsString(tileTemplate));\n        clientPair.appClient.verifyResult(ok(6));\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":155, \\\"value\\\":\\\"data\\\", \\\"deviceId\\\":0, \\\"frequency\\\":400, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"GAUGE\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":100}\");\n        clientPair.appClient.verifyResult(ok(7));\n\n        TestAppClient appClient2 = new TestAppClient(properties);\n        appClient2.start();\n\n        appClient2.register(\"test@blynk.cc\", \"a\", app.id);\n        appClient2.verifyResult(ok(1));\n\n        appClient2.login(\"test@blynk.cc\", \"a\", \"Android\", \"1.10.4\", app.id);\n        appClient2.verifyResult(ok(2));\n\n        appClient2.send(\"loadProfileGzipped\");\n        Profile profile = appClient2.parseProfile(3);\n        assertEquals(1, profile.dashBoards.length);\n        dashBoard = profile.dashBoards[0];\n        assertNotNull(dashBoard);\n        assertEquals(1, dashBoard.id);\n        assertEquals(1, dashBoard.parentId);\n        assertEquals(1, dashBoard.widgets.length);\n        assertTrue(dashBoard.widgets[0] instanceof DeviceTiles);\n        deviceTiles = (DeviceTiles) dashBoard.getWidgetById(widgetId);\n        assertNotNull(deviceTiles.tiles);\n        assertNotNull(deviceTiles.templates);\n        assertEquals(0, deviceTiles.tiles.length);\n        assertEquals(1, deviceTiles.templates.length);\n        Gauge gauge = (Gauge) dashBoard.getWidgetById(155);\n        assertNull(gauge);\n\n        clientPair.appClient.send(\"updateFace 1\");\n        clientPair.appClient.verifyResult(ok(8));\n\n        if (!appClient2.isClosed()) {\n            sleep(300);\n        }\n        assertTrue(appClient2.isClosed());\n\n        appClient2 = new TestAppClient(properties);\n        appClient2.start();\n\n        appClient2.login(\"test@blynk.cc\", \"a\", \"Android\", \"1.10.4\", app.id);\n        appClient2.verifyResult(ok(1));\n\n        appClient2.send(\"loadProfileGzipped\");\n        profile = appClient2.parseProfile(2);\n        assertEquals(1, profile.dashBoards.length);\n        dashBoard = profile.dashBoards[0];\n        gauge = (Gauge) dashBoard.getWidgetById(155);\n        assertNotNull(gauge);\n        assertNull(gauge.value);\n\n        //one more time, to check another branch\n        clientPair.appClient.send(\"updateFace 1\");\n        clientPair.appClient.verifyResult(ok(9));\n\n        assertTrue(appClient2.isClosed());\n\n        appClient2 = new TestAppClient(properties);\n        appClient2.start();\n\n        appClient2.login(\"test@blynk.cc\", \"a\", \"Android\", \"1.10.4\", app.id);\n        appClient2.verifyResult(ok(1));\n\n        appClient2.send(\"loadProfileGzipped\");\n        profile = appClient2.parseProfile(2);\n        assertEquals(1, profile.dashBoards.length);\n        dashBoard = profile.dashBoards[0];\n        gauge = (Gauge) dashBoard.getWidgetById(155);\n        assertNotNull(gauge);\n        assertNull(gauge.value);\n    }\n\n    @Test\n    public void testFaceEditForRestrictiveFields() throws Exception {\n        Profile profile = JsonParser.parseProfileFromString(readTestUserProfile());\n\n        DashBoard dashBoard = profile.dashBoards[0];\n        dashBoard.id = 10;\n        dashBoard.parentId = 1;\n        dashBoard.isPreview = true;\n        dashBoard.name = \"Face Edit Test\";\n        dashBoard.devices = null;\n\n        clientPair.appClient.createDash(dashBoard);\n        clientPair.appClient.verifyResult(ok(1));\n\n        Device device0 = new Device(0, \"My Device\", BoardType.Arduino_UNO);\n        device0.status = Status.ONLINE;\n\n        clientPair.appClient.createDevice(10, device0);\n        Device device = clientPair.appClient.parseDevice(2);\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(2, device));\n\n        clientPair.appClient.send(\"createApp {\\\"theme\\\":\\\"Blynk\\\",\\\"provisionType\\\":\\\"STATIC\\\",\\\"color\\\":0,\\\"name\\\":\\\"AppPreview\\\",\\\"icon\\\":\\\"myIcon\\\",\\\"projectIds\\\":[10]}\");\n        App app = clientPair.appClient.parseApp(3);\n        assertNotNull(app);\n        assertNotNull(app.id);\n\n\n        clientPair.appClient.send(\"emailQr 10\\0\" + app.id);\n        clientPair.appClient.verifyResult(ok(4));\n\n        QrHolder[] qrHolders = makeQRs(new Device[] {device}, 10);\n        StringBuilder sb = new StringBuilder();\n        qrHolders[0].attach(sb);\n        verify(holder.mailWrapper, timeout(500)).sendWithAttachment(eq(getUserName()), eq(\"AppPreview\" + \" - App details\"), eq(holder.textHolder.staticMailBody.replace(\"{project_name}\", \"Face Edit Test\").replace(DYNAMIC_SECTION, sb.toString())), eq(qrHolders));\n\n        TestAppClient appClient2 = new TestAppClient(properties);\n        appClient2.start();\n\n        appClient2.register(\"test@blynk.cc\", \"a\", app.id);\n        appClient2.verifyResult(ok(1));\n\n        appClient2.login(\"test@blynk.cc\", \"a\", \"Android\", \"1.10.4\", app.id);\n        appClient2.verifyResult(ok(2));\n\n        appClient2.send(\"loadProfileGzipped\");\n        profile = appClient2.parseProfile(3);\n        assertEquals(1, profile.dashBoards.length);\n        dashBoard = profile.dashBoards[0];\n        assertNotNull(dashBoard);\n        assertEquals(1, dashBoard.id);\n        assertEquals(1, dashBoard.parentId);\n        assertEquals(16, dashBoard.widgets.length);\n\n        clientPair.appClient.send(\"addPushToken 1\\0uid1\\0token1\");\n        clientPair.appClient.verifyResult(ok(5));\n\n        clientPair.appClient.updateWidget(1, \"{\\\"id\\\":10, \\\"height\\\":2, \\\"width\\\":1, \\\"x\\\":22, \\\"y\\\":23, \\\"username\\\":\\\"pupkin@gmail.com\\\", \\\"token\\\":\\\"token\\\", \\\"secret\\\":\\\"secret\\\", \\\"type\\\":\\\"TWITTER\\\"}\");\n        clientPair.appClient.verifyResult(ok(6));\n\n        clientPair.appClient.send(\"updateFace 1\");\n        clientPair.appClient.verifyResult(ok(7));\n\n        assertTrue(appClient2.isClosed());\n\n        appClient2 = new TestAppClient(properties);\n        appClient2.start();\n        appClient2.login(\"test@blynk.cc\", \"a\", \"Android\", \"1.10.4\", app.id);\n        verify(appClient2.responseMock, timeout(1000)).channelRead(any(), eq(ok(1)));\n\n        appClient2.send(\"loadProfileGzipped\");\n        profile = appClient2.parseProfile(2);\n        assertEquals(1, profile.dashBoards.length);\n        dashBoard = profile.dashBoards[0];\n        assertNotNull(dashBoard);\n        assertEquals(1, dashBoard.id);\n        assertEquals(1, dashBoard.parentId);\n        assertEquals(16, dashBoard.widgets.length);\n        Notification notification = dashBoard.getNotificationWidget();\n        assertEquals(0, notification.androidTokens.size());\n        assertEquals(0, notification.iOSTokens.size());\n        Twitter twitter = dashBoard.getTwitterWidget();\n        assertNull(twitter.username);\n        assertNull(twitter.token);\n        assertNull(twitter.secret);\n        assertEquals(22, twitter.x);\n        assertEquals(23, twitter.y);\n    }\n\n    @Test\n    public void testDeleteWorksForPreviewApp() throws Exception {\n        clientPair.appClient.send(\"createApp {\\\"theme\\\":\\\"Blynk\\\",\\\"provisionType\\\":\\\"STATIC\\\",\\\"color\\\":0,\\\"name\\\":\\\"AppPreview\\\",\\\"icon\\\":\\\"myIcon\\\",\\\"projectIds\\\":[1]}\");\n        App app = clientPair.appClient.parseApp(1);\n        assertNotNull(app);\n        assertNotNull(app.id);\n        clientPair.appClient.reset();\n\n        clientPair.appClient.send(\"getDevices 1\");\n        Device[] devices = clientPair.appClient.parseDevices();\n        assertEquals(1, devices.length);\n\n        clientPair.appClient.send(\"emailQr 1\\0\" + app.id);\n        clientPair.appClient.verifyResult(ok(2));\n\n        QrHolder[] qrHolders = makeQRs(devices, 1);\n\n        StringBuilder sb = new StringBuilder();\n        qrHolders[0].attach(sb);\n        verify(holder.mailWrapper, timeout(500)).sendWithAttachment(eq(getUserName()), eq(\"AppPreview\" + \" - App details\"), eq(holder.textHolder.staticMailBody.replace(\"{project_name}\", \"My Dashboard\").replace(DYNAMIC_SECTION, sb.toString())), eq(qrHolders));\n\n        clientPair.appClient.send(\"loadProfileGzipped \" + qrHolders[0].token + \" \" + qrHolders[0].dashId + \" \" + getUserName());\n\n        DashBoard dashBoard = clientPair.appClient.parseDash(3);\n        assertNotNull(dashBoard);\n        assertNotNull(dashBoard.devices);\n        assertNull(dashBoard.devices[0].token);\n        assertNull(dashBoard.devices[0].lastLoggedIP);\n        assertEquals(0, dashBoard.devices[0].disconnectTime);\n        assertEquals(Status.OFFLINE, dashBoard.devices[0].status);\n\n        dashBoard.id = 2;\n        dashBoard.parentId = 1;\n        dashBoard.isPreview = true;\n\n        clientPair.appClient.createDash(dashBoard);\n        clientPair.appClient.verifyResult(ok(4));\n\n        clientPair.appClient.deleteDash(2);\n        clientPair.appClient.verifyResult(ok(5));\n\n        clientPair.appClient.send(\"loadProfileGzipped 1\");\n        dashBoard = clientPair.appClient.parseDash(6);\n        assertNotNull(dashBoard);\n        assertEquals(1, dashBoard.id);\n\n        clientPair.appClient.send(\"loadProfileGzipped 2\");\n        clientPair.appClient.verifyResult(illegalCommand(7));\n    }\n\n    @Test\n    public void testDeleteWorksForParentOfPreviewApp() throws Exception {\n        clientPair.appClient.send(\"createApp {\\\"theme\\\":\\\"Blynk\\\",\\\"provisionType\\\":\\\"STATIC\\\",\\\"color\\\":0,\\\"name\\\":\\\"AppPreview\\\",\\\"icon\\\":\\\"myIcon\\\",\\\"projectIds\\\":[1]}\");\n        App app = clientPair.appClient.parseApp(1);\n        assertNotNull(app);\n        assertNotNull(app.id);\n        clientPair.appClient.reset();\n\n        clientPair.appClient.send(\"getDevices 1\");\n        Device[] devices = clientPair.appClient.parseDevices();\n        assertEquals(1, devices.length);\n\n        clientPair.appClient.send(\"emailQr 1\\0\" + app.id);\n        clientPair.appClient.verifyResult(ok(2));\n\n        QrHolder[] qrHolders = makeQRs(devices, 1);\n\n        StringBuilder sb = new StringBuilder();\n        qrHolders[0].attach(sb);\n        verify(holder.mailWrapper, timeout(500)).sendWithAttachment(eq(getUserName()), eq(\"AppPreview\" + \" - App details\"), eq(holder.textHolder.staticMailBody.replace(\"{project_name}\", \"My Dashboard\").replace(DYNAMIC_SECTION, sb.toString())), eq(qrHolders));\n\n        clientPair.appClient.send(\"loadProfileGzipped \" + qrHolders[0].token + \" \" + qrHolders[0].dashId + \" \" + getUserName());\n\n        DashBoard dashBoard = clientPair.appClient.parseDash(3);\n        assertNotNull(dashBoard);\n        assertNotNull(dashBoard.devices);\n        assertNull(dashBoard.devices[0].token);\n        assertNull(dashBoard.devices[0].lastLoggedIP);\n        assertEquals(0, dashBoard.devices[0].disconnectTime);\n        assertEquals(Status.OFFLINE, dashBoard.devices[0].status);\n\n        dashBoard.id = 2;\n        dashBoard.parentId = 1;\n        dashBoard.isPreview = true;\n\n        clientPair.appClient.createDash(dashBoard);\n        clientPair.appClient.verifyResult(ok(4));\n\n        clientPair.appClient.deleteDash(1);\n        clientPair.appClient.verifyResult(ok(5));\n        clientPair.appClient.verifyResult(deviceOffline(0, \"1-0\"));\n        clientPair.appClient.reset();\n\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(1);\n        assertNotNull(profile);\n        assertNotNull(profile.dashBoards);\n        assertEquals(1, profile.dashBoards.length);\n\n        clientPair.appClient.send(\"loadProfileGzipped 2\");\n        String response = clientPair.appClient.getBody(2);\n        assertNotNull(response);\n    }\n\n    @Test\n    public void testExportedAppFlowWithOneDynamicTest() throws Exception {\n        clientPair.appClient.send(\"createApp {\\\"theme\\\":\\\"Blynk\\\",\\\"provisionType\\\":\\\"DYNAMIC\\\",\\\"color\\\":0,\\\"name\\\":\\\"My App\\\",\\\"icon\\\":\\\"myIcon\\\",\\\"projectIds\\\":[1]}\");\n        App app = clientPair.appClient.parseApp(1);\n        assertNotNull(app);\n        assertNotNull(app.id);\n\n        TestAppClient appClient2 = new TestAppClient(properties);\n        appClient2.start();\n\n        appClient2.register(\"test@blynk.cc\", \"a\", app.id);\n        appClient2.verifyResult(ok(1));\n\n        appClient2.login(\"test@blynk.cc\", \"a\", \"Android\", \"1.10.4\", app.id);\n        appClient2.verifyResult(ok(2));\n\n        appClient2.send(\"loadProfileGzipped 1\");\n        DashBoard dashBoard = appClient2.parseDash(3);\n        assertNotNull(dashBoard);\n\n        Device device = dashBoard.devices[0];\n        assertNotNull(device);\n        assertNotNull(device.token);\n\n        TestHardClient hardClient1 = new TestHardClient(\"localhost\", properties.getHttpPort());\n        hardClient1.start();\n\n        hardClient1.login(device.token);\n        hardClient1.verifyResult(ok(1));\n        appClient2.verifyResult(hardwareConnected(1, \"1-0\"));\n\n        hardClient1.send(\"hardware vw 1 100\");\n        appClient2.verifyResult(hardware(2, \"1-0 vw 1 100\"));\n    }\n\n    @Test\n    public void testFullDynamicAppFlow() throws Exception {\n        clientPair.appClient.send(\"createApp {\\\"theme\\\":\\\"Blynk\\\",\\\"provisionType\\\":\\\"DYNAMIC\\\",\\\"color\\\":0,\\\"name\\\":\\\"My App\\\",\\\"icon\\\":\\\"myIcon\\\",\\\"projectIds\\\":[1]}\");\n        App app = clientPair.appClient.parseApp(1);\n        assertNotNull(app);\n        assertNotNull(app.id);\n\n        clientPair.hardwareClient.send(\"hardware dw 1 abc\");\n        clientPair.hardwareClient.send(\"hardware vw 77 123\");\n\n        clientPair.appClient.verifyResult(hardware(1, \"1-0 dw 1 abc\"));\n        clientPair.appClient.verifyResult(hardware(2, \"1-0 vw 77 123\"));\n\n        clientPair.appClient.send(\"loadProfileGzipped 1\");\n        DashBoard dashBoard = clientPair.appClient.parseDash(4);\n        assertNotNull(dashBoard);\n        assertNotNull(dashBoard.pinsStorage);\n        assertEquals(0, dashBoard.pinsStorage.size());\n        Widget w = dashBoard.findWidgetByPin(0, (short) 1, PinType.DIGITAL);\n        assertNotNull(w);\n        assertEquals(\"abc\", ((OnePinWidget) w).value);\n\n        clientPair.appClient.reset();\n\n        clientPair.appClient.send(\"getDevices 1\");\n        Device[] devices = clientPair.appClient.parseDevices();\n        assertEquals(1, devices.length);\n\n        clientPair.appClient.send(\"emailQr 1\\0\" + app.id);\n        clientPair.appClient.verifyResult(ok(2));\n\n        QrHolder[] qrHolders = makeQRs(devices, 1);\n\n        TestAppClient appClient2 = new TestAppClient(properties);\n        appClient2.start();\n\n        appClient2.register(\"test@blynk.cc\", \"a\", app.id);\n        appClient2.verifyResult(ok(1));\n\n        appClient2.login(\"test@blynk.cc\", \"a\", \"Android\", \"1.10.4\", app.id);\n        appClient2.verifyResult(ok(2));\n\n        appClient2.send(\"loadProfileGzipped \" + qrHolders[0].token + \"\\0\" + 1 + \"\\0\" + getUserName() + \"\\0\" + AppNameUtil.BLYNK);\n        dashBoard = appClient2.parseDash(3);\n        assertNotNull(dashBoard);\n        assertNotNull(dashBoard.pinsStorage);\n        assertTrue(dashBoard.pinsStorage.isEmpty());\n        w = dashBoard.findWidgetByPin(0, (short) 1, PinType.DIGITAL);\n        assertNotNull(w);\n        assertNull(((OnePinWidget) w).value);\n\n        Device device = dashBoard.devices[0];\n        assertNotNull(device);\n        assertNull(device.token);\n\n        appClient2.reset();\n        appClient2.getDevice(1, device.id);\n        device = appClient2.parseDevice();\n\n        TestHardClient hardClient1 = new TestHardClient(\"localhost\", properties.getHttpPort());\n        hardClient1.start();\n\n        hardClient1.login(device.token);\n        hardClient1.verifyResult(ok(1));\n        appClient2.verifyResult(hardwareConnected(1, \"1-0\"));\n\n        hardClient1.send(\"hardware vw 1 100\");\n        appClient2.verifyResult(hardware(2, \"1-0 vw 1 100\"));\n    }\n\n    @Test\n    public void testTimeInputInTheFaceAndDeviceTilesIsNotErasedByParentFaceUpdate() throws Exception {\n        DashBoard dashBoard = new DashBoard();\n        dashBoard.id = 10;\n        dashBoard.parentId = 1;\n        dashBoard.isPreview = true;\n        dashBoard.name = \"Face Edit Test\";\n\n        clientPair.appClient.createDash(dashBoard);\n\n        Device device0 = new Device(0, \"My Dashboard\", BoardType.Arduino_UNO);\n        clientPair.appClient.createDevice(dashBoard.id, device0);\n        device0 = clientPair.appClient.parseDevice(2);\n        clientPair.appClient.verifyResult(createDevice(2, device0));\n\n        Device device2 = new Device(2, \"My Dashboard\", BoardType.Arduino_UNO);\n        clientPair.appClient.createDevice(dashBoard.id, device2);\n        device2 = clientPair.appClient.parseDevice(3);\n        clientPair.appClient.verifyResult(createDevice(3, device2));\n\n        clientPair.appClient.send(\"createApp {\\\"theme\\\":\\\"Blynk\\\",\\\"provisionType\\\":\\\"STATIC\\\",\\\"color\\\":0,\\\"name\\\":\\\"AppPreview\\\",\\\"icon\\\":\\\"myIcon\\\",\\\"projectIds\\\":[10]}\");\n        App app = clientPair.appClient.parseApp(4);\n        assertNotNull(app);\n        assertNotNull(app.id);\n\n\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = 21321;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        //creating manually widget for child project\n        clientPair.appClient.createWidget(dashBoard.id, deviceTiles);\n        clientPair.appClient.verifyResult(ok(5));\n\n        TileTemplate tileTemplate = new PageTileTemplate(1,\n                                                         null, new int[] {2}, \"123\", \"name\", \"iconName\", BoardType.ESP8266, null,\n                                                         false, null, null, null, 0, 0, FontSize.LARGE, false, 2);\n\n        clientPair.appClient.send(\"createTemplate \" + b(\"10 \" + deviceTiles.id + \" \")\n                                          + MAPPER.writeValueAsString(tileTemplate));\n        clientPair.appClient.verifyResult(ok(6));\n\n        TimeInput timeInput = new TimeInput();\n        timeInput.width = 2;\n        timeInput.height = 1;\n        timeInput.id = 333;\n        timeInput.pin = 77;\n        timeInput.pinType = PinType.VIRTUAL;\n        clientPair.appClient.createWidget(dashBoard.id, deviceTiles.id, tileTemplate.id, timeInput);\n        clientPair.appClient.verifyResult(ok(7));\n\n        TestAppClient appClient2 = new TestAppClient(properties);\n        appClient2.start();\n\n        appClient2.register(\"test@blynk.cc\", \"a\", app.id);\n        appClient2.verifyResult(ok(1));\n\n        appClient2.login(\"test@blynk.cc\", \"a\", \"Android\", \"1.10.4\", app.id);\n        appClient2.verifyResult(ok(2));\n\n        appClient2.send(\"loadProfileGzipped\");\n        Profile profile = appClient2.parseProfile(3);\n        assertEquals(1, profile.dashBoards.length);\n        dashBoard = profile.dashBoards[0];\n        assertNotNull(dashBoard);\n        assertEquals(1, dashBoard.id);\n        assertEquals(1, dashBoard.parentId);\n        assertEquals(1, dashBoard.widgets.length);\n        assertTrue(dashBoard.widgets[0] instanceof DeviceTiles);\n        deviceTiles = (DeviceTiles) dashBoard.getWidgetById(deviceTiles.id);\n        assertNotNull(deviceTiles.tiles);\n        assertNotNull(deviceTiles.templates);\n        assertEquals(0, deviceTiles.tiles.length);\n        assertEquals(1, deviceTiles.templates.length);\n        timeInput = (TimeInput) deviceTiles.getWidgetById(timeInput.id);\n        assertNotNull(timeInput);\n        assertNull(timeInput.value);\n\n        Device provisionedDevice = new Device();\n        provisionedDevice.id = 0;\n        provisionedDevice.name = \"123\";\n        provisionedDevice.boardType = BoardType.ESP8266;\n        appClient2.createDevice(1, provisionedDevice);\n        provisionedDevice = appClient2.parseDevice(4);\n        assertNotNull(provisionedDevice);\n\n        appClient2.send(\"hardware 1-0 vw \" + b(\"77 82800 82860 Europe/Kiev 1\"));\n\n        appClient2.send(\"loadProfileGzipped\");\n        profile = appClient2.parseProfile(6);\n        assertEquals(1, profile.dashBoards.length);\n        dashBoard = profile.dashBoards[0];\n        assertNotNull(dashBoard);\n        assertEquals(1, dashBoard.id);\n        assertEquals(1, dashBoard.parentId);\n        assertEquals(1, dashBoard.widgets.length);\n        assertTrue(dashBoard.widgets[0] instanceof DeviceTiles);\n        deviceTiles = (DeviceTiles) dashBoard.getWidgetById(deviceTiles.id);\n        assertNotNull(deviceTiles.tiles);\n        assertNotNull(deviceTiles.templates);\n        assertEquals(0, deviceTiles.tiles.length);\n        assertEquals(1, deviceTiles.templates.length);\n        timeInput = (TimeInput) deviceTiles.getWidgetById(timeInput.id);\n        assertNotNull(timeInput);\n        assertNull(timeInput.value);\n\n        appClient2.sync(dashBoard.id, provisionedDevice.id);\n        appClient2.verifyResult(ok(7));\n        appClient2.verifyResult(appSync(1111, \"1-0 vw 77 82800 82860 Europe/Kiev 1\"));\n\n        clientPair.appClient.send(\"updateFace 1\");\n        clientPair.appClient.verifyResult(ok(8));\n\n        if (!appClient2.isClosed()) {\n            sleep(300);\n        }\n        assertTrue(appClient2.isClosed());\n\n        appClient2 = new TestAppClient(properties);\n        appClient2.start();\n\n        appClient2.login(\"test@blynk.cc\", \"a\", \"Android\", \"1.10.4\", app.id);\n        appClient2.verifyResult(ok(1));\n\n        appClient2.sync(dashBoard.id, provisionedDevice.id);\n        appClient2.verifyResult(ok(2));\n        appClient2.verifyResult(appSync(1111, \"1-0 vw 77 82800 82860 Europe/Kiev 1\"));\n    }\n\n    private QrHolder[] makeQRs(Device[] devices, int dashId) throws Exception {\n        QrHolder[] qrHolders = new QrHolder[devices.length];\n\n        List<FlashedToken> flashedTokens = getAllTokens();\n\n        int i = 0;\n        for (Device device : devices) {\n            String newToken = flashedTokens.get(i).token;\n            qrHolders[i] = new QrHolder(dashId, device.id, device.name, newToken, QRCode.from(newToken).to(ImageType.JPG).stream().toByteArray());\n            i++;\n        }\n\n        return qrHolders;\n    }\n\n    private FlashedToken getFlashedTokenByDevice() throws Exception {\n        List<FlashedToken> flashedTokens = getAllTokens();\n\n        int i = 0;\n        for (FlashedToken flashedToken : flashedTokens) {\n            if (-1 == flashedToken.deviceId) {\n                return flashedTokens.get(i);\n            }\n\n        }\n        return null;\n    }\n\n    private List<FlashedToken> getAllTokens() throws Exception {\n        List<FlashedToken> list = new ArrayList<>();\n        try (Connection connection = holder.dbManager.getConnection();\n             Statement statement = connection.createStatement();\n             ResultSet rs = statement.executeQuery(\"select * from flashed_tokens\")) {\n\n            if (rs.next()) {\n                list.add(new FlashedToken(rs.getString(\"token\"), rs.getString(\"app_name\"),\n                        rs.getString(\"email\"), rs.getInt(\"project_id\"), rs.getInt(\"device_id\"),\n                        rs.getBoolean(\"is_activated\"), rs.getDate(\"ts\")\n                ));\n            }\n            connection.commit();\n        }\n        return list;\n    }\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/tcp/ReadingWorkflowTest.java",
    "content": "package cc.blynk.integration.tcp;\n\nimport cc.blynk.integration.SingleServerInstancePerTest;\nimport cc.blynk.integration.model.tcp.TestHardClient;\nimport cc.blynk.server.core.model.device.BoardType;\nimport cc.blynk.server.core.model.device.Device;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.BeforeClass;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.ScheduledExecutorService;\nimport java.util.concurrent.TimeUnit;\n\nimport static cc.blynk.integration.TestUtil.b;\nimport static cc.blynk.integration.TestUtil.createDevice;\nimport static cc.blynk.integration.TestUtil.ok;\nimport static cc.blynk.server.core.model.widgets.FrequencyWidget.READING_MSG_ID;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE;\nimport static cc.blynk.server.core.protocol.model.messages.MessageFactory.produce;\nimport static org.junit.Assert.assertNotNull;\nimport static org.mockito.Mockito.after;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.eq;\nimport static org.mockito.Mockito.timeout;\nimport static org.mockito.Mockito.verify;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/2/2015.\n *\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class ReadingWorkflowTest extends SingleServerInstancePerTest {\n\n    private static int tcpHardPort;\n    private ScheduledExecutorService ses;\n\n    @BeforeClass\n    public static void initPort() {\n        tcpHardPort = properties.getHttpPort();\n    }\n\n    @Before\n    public void initSES() {\n        ses = Executors.newScheduledThreadPool(1);\n    }\n\n    @After\n    public void closeSES() {\n        ses.shutdownNow();\n    }\n\n    @Test\n    public void testReadingCommandNotAcceptedAnymoreFromApp() throws Exception {\n        clientPair.appClient.send(\"hardware 1 ar 7\");\n        verify(clientPair.hardwareClient.responseMock, after(500).never()).channelRead(any(), any());\n    }\n\n    @Test\n    public void testServerSendReadingCommandWithReadingWorkerEnabled() throws Exception {\n        ses.scheduleAtFixedRate(holder.readingWidgetsWorker, 0, 1000, TimeUnit.MILLISECONDS);\n        verify(clientPair.hardwareClient.responseMock, timeout(1000)).channelRead(any(), eq(produce(READING_MSG_ID, HARDWARE, b(\"ar 7\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(1000)).channelRead(any(), eq(produce(READING_MSG_ID, HARDWARE, b(\"ar 30\"))));\n    }\n\n    @Test\n    public void testServerSendReadingCommandCorrectly() throws Exception {\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":155, \\\"frequency\\\":400, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"GAUGE\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":100}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        ses.scheduleAtFixedRate(holder.readingWidgetsWorker, 0, 500, TimeUnit.MILLISECONDS);\n\n        verify(clientPair.hardwareClient.responseMock, after(600).times(2)).channelRead(any(), eq(produce(READING_MSG_ID, HARDWARE, b(\"vr 100\"))));\n    }\n\n    @Test\n    public void testServerDontSendReadingCommandsForNonActiveDash() throws Exception {\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":155, \\\"frequency\\\":400, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"GAUGE\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":100}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.deactivate(1);\n        clientPair.appClient.verifyResult(ok(2));\n\n        ses.scheduleAtFixedRate(holder.readingWidgetsWorker, 0, 500, TimeUnit.MILLISECONDS);\n\n        verify(clientPair.hardwareClient.responseMock, after(1000).never()).channelRead(any(), any());\n    }\n\n    @Test\n    public void testSendReadCommandsForLCD() throws Exception {\n        clientPair.appClient.createWidget(1, \"{\\\"type\\\":\\\"LCD\\\",\\\"id\\\":1923810267,\\\"x\\\":0,\\\"y\\\":6,\\\"color\\\":600084223,\\\"width\\\":8,\\\"height\\\":2,\\\"tabId\\\":0,\\\"\" +\n                \"pins\\\":[\" +\n                \"{\\\"pin\\\":100,\\\"pinType\\\":\\\"VIRTUAL\\\",\\\"pwmMode\\\":false,\\\"rangeMappingOn\\\":false,\\\"min\\\":0,\\\"max\\\":1023},\" +\n                \"{\\\"pin\\\":101,\\\"pinType\\\":\\\"VIRTUAL\\\",\\\"pwmMode\\\":false,\\\"rangeMappingOn\\\":false,\\\"min\\\":0,\\\"max\\\":1023}],\" +\n                \"\\\"advancedMode\\\":false,\\\"textLight\\\":false,\\\"textLightOn\\\":false,\\\"frequency\\\":900}\");\n\n        clientPair.appClient.verifyResult(ok(1));\n\n        ses.scheduleAtFixedRate(holder.readingWidgetsWorker, 0, 1000, TimeUnit.MILLISECONDS);\n\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(produce(READING_MSG_ID, HARDWARE, b(\"vr 100\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(produce(READING_MSG_ID, HARDWARE, b(\"vr 101\"))));\n\n        clientPair.hardwareClient.reset();\n\n        verify(clientPair.hardwareClient.responseMock, timeout(1500)).channelRead(any(), eq(produce(READING_MSG_ID, HARDWARE, b(\"vr 100\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(1500)).channelRead(any(), eq(produce(READING_MSG_ID, HARDWARE, b(\"vr 101\"))));\n    }\n\n    @Test\n    public void testSendReadForMultipleDevices() throws Exception {\n        Device device2 = new Device(2, \"My Device\", BoardType.ESP8266);\n\n        clientPair.appClient.createDevice(1, device2);\n        device2 = clientPair.appClient.parseDevice();\n\n        assertNotNull(device2);\n        assertNotNull(device2.token);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(createDevice(1, device2)));\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient2.start();\n\n        hardClient2.login(device2.token);\n        hardClient2.verifyResult(ok(1));\n        clientPair.appClient.reset();\n\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":155, \\\"deviceId\\\":0, \\\"frequency\\\":400, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"GAUGE\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":100}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":156, \\\"deviceId\\\":2, \\\"frequency\\\":400, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"GAUGE\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":101}\");\n        clientPair.appClient.verifyResult(ok(2));\n\n        ses.scheduleAtFixedRate(holder.readingWidgetsWorker, 0, 500, TimeUnit.MILLISECONDS);\n\n        verify(clientPair.hardwareClient.responseMock, timeout(750)).channelRead(any(), eq(produce(READING_MSG_ID, HARDWARE, b(\"vr 100\"))));\n        verify(hardClient2.responseMock, timeout(750)).channelRead(any(), eq(produce(READING_MSG_ID, HARDWARE, b(\"vr 101\"))));\n    }\n\n    @Test\n    public void testSendReadForDeviceSelector() throws Exception {\n        Device device2 = new Device(2, \"My Device\", BoardType.ESP8266);\n\n        clientPair.appClient.createDevice(1, device2);\n        device2 = clientPair.appClient.parseDevice();\n\n        assertNotNull(device2);\n        assertNotNull(device2.token);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(createDevice(1, device2)));\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient2.start();\n\n        hardClient2.login(device2.token);\n        hardClient2.verifyResult(ok(1));\n        clientPair.appClient.reset();\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":200000, \\\"width\\\":1, \\\"value\\\":2, \\\"height\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"DEVICE_SELECTOR\\\"}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":155, \\\"deviceId\\\":200000, \\\"frequency\\\":400, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"GAUGE\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":100}\");\n        clientPair.appClient.verifyResult(ok(2));\n\n        ses.scheduleAtFixedRate(holder.readingWidgetsWorker, 0, 500, TimeUnit.MILLISECONDS);\n\n        verify(hardClient2.responseMock, timeout(500)).channelRead(any(), eq(produce(READING_MSG_ID, HARDWARE, b(\"vr 100\"))));\n        verify(clientPair.hardwareClient.responseMock, after(100).never()).channelRead(any(), eq(produce(READING_MSG_ID, HARDWARE, b(\"vr 100\"))));\n\n        clientPair.hardwareClient.reset();\n        hardClient2.reset();\n\n        clientPair.appClient.send(\"hardware 1 vu 200000 0\");\n\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(produce(READING_MSG_ID, HARDWARE, b(\"vr 100\"))));\n        verify(hardClient2.responseMock, after(100).never()).channelRead(any(), eq(produce(READING_MSG_ID, HARDWARE, b(\"vr 100\"))));\n    }\n\n    @Test\n    public void testSendReadForMultipleDevices2() throws Exception {\n        Device device2 = new Device(2, \"My Device\", BoardType.ESP8266);\n\n        clientPair.appClient.createDevice(1, device2);\n        device2 = clientPair.appClient.parseDevice();\n\n        assertNotNull(device2);\n        assertNotNull(device2.token);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(createDevice(1, device2)));\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient2.start();\n\n        hardClient2.login(device2.token);\n        hardClient2.verifyResult(ok(1));\n        clientPair.appClient.reset();\n\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":155, \\\"deviceId\\\":0, \\\"frequency\\\":400, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"GAUGE\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":100}\");\n        clientPair.appClient.verifyResult(ok(1));\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":156, \\\"deviceId\\\":0, \\\"frequency\\\":400, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"GAUGE\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":101}\");\n        clientPair.appClient.verifyResult(ok(2));\n\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":157, \\\"deviceId\\\":2, \\\"frequency\\\":400, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"GAUGE\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":102}\");\n        clientPair.appClient.verifyResult(ok(3));\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":158, \\\"deviceId\\\":2, \\\"frequency\\\":400, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"GAUGE\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":103}\");\n        clientPair.appClient.verifyResult(ok(4));\n\n        ses.scheduleAtFixedRate(holder.readingWidgetsWorker, 0, 500, TimeUnit.MILLISECONDS);\n\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(produce(READING_MSG_ID, HARDWARE, b(\"vr 100\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(produce(READING_MSG_ID, HARDWARE, b(\"vr 101\"))));\n        verify(hardClient2.responseMock, timeout(500)).channelRead(any(), eq(produce(READING_MSG_ID, HARDWARE, b(\"vr 102\"))));\n        verify(hardClient2.responseMock, timeout(500)).channelRead(any(), eq(produce(READING_MSG_ID, HARDWARE, b(\"vr 103\"))));\n\n        hardClient2.reset();\n        clientPair.hardwareClient.reset();\n\n        verify(clientPair.hardwareClient.responseMock, timeout(1000)).channelRead(any(), eq(produce(READING_MSG_ID, HARDWARE, b(\"vr 100\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(1000)).channelRead(any(), eq(produce(READING_MSG_ID, HARDWARE, b(\"vr 101\"))));\n        verify(hardClient2.responseMock, timeout(500)).channelRead(any(), eq(produce(READING_MSG_ID, HARDWARE, b(\"vr 102\"))));\n        verify(hardClient2.responseMock, timeout(500)).channelRead(any(), eq(produce(READING_MSG_ID, HARDWARE, b(\"vr 103\"))));\n\n        clientPair.appClient.deactivate(1);\n\n        hardClient2.reset();\n        clientPair.hardwareClient.reset();\n\n        verify(clientPair.hardwareClient.responseMock, after(500).never()).channelRead(any(), eq(produce(READING_MSG_ID, HARDWARE, b(\"vr 100\"))));\n        verify(clientPair.hardwareClient.responseMock, after(500).never()).channelRead(any(), eq(produce(READING_MSG_ID, HARDWARE, b(\"vr 101\"))));\n        verify(hardClient2.responseMock, after(500).never()).channelRead(any(), eq(produce(READING_MSG_ID, HARDWARE, b(\"vr 102\"))));\n        verify(hardClient2.responseMock, after(500).never()).channelRead(any(), eq(produce(READING_MSG_ID, HARDWARE, b(\"vr 103\"))));\n    }\n\n    @Test\n    public void testSendReadOnlyForOnlineApp() throws Exception {\n        Device device2 = new Device(2, \"My Device\", BoardType.ESP8266);\n\n        clientPair.appClient.createDevice(1, device2);\n\n        device2 = clientPair.appClient.parseDevice();\n        assertNotNull(device2);\n        assertNotNull(device2.token);\n        verify(clientPair.appClient.responseMock, timeout(1000)).channelRead(any(), eq(createDevice(1, device2)));\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", tcpHardPort);\n        hardClient2.start();\n\n        hardClient2.login(device2.token);\n        hardClient2.verifyResult(ok(1));\n        clientPair.appClient.reset();\n\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":155, \\\"deviceId\\\":0, \\\"frequency\\\":400, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"GAUGE\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":100}\");\n        verify(clientPair.appClient.responseMock, timeout(1000)).channelRead(any(), eq(ok(1)));\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":156, \\\"deviceId\\\":2, \\\"frequency\\\":400, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"GAUGE\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":101}\");\n        verify(clientPair.appClient.responseMock, timeout(1000)).channelRead(any(), eq(ok(2)));\n\n        clientPair.appClient.stop().await();\n        ses.scheduleAtFixedRate(holder.readingWidgetsWorker, 0, 500, TimeUnit.MILLISECONDS);\n\n        verify(clientPair.hardwareClient.responseMock, after(1000).never()).channelRead(any(), eq(produce(READING_MSG_ID, HARDWARE, b(\"vr 100\"))));\n        verify(hardClient2.responseMock, after(1000).never()).channelRead(any(), eq(produce(READING_MSG_ID, HARDWARE, b(\"vr 101\"))));\n    }\n\n}"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/tcp/RegistrationLimitCheckTest.java",
    "content": "package cc.blynk.integration.tcp;\n\nimport cc.blynk.integration.SingleServerInstancePerTest;\nimport cc.blynk.integration.model.tcp.TestAppClient;\nimport cc.blynk.utils.AppNameUtil;\nimport org.junit.Test;\n\nimport static cc.blynk.integration.TestUtil.notAllowed;\nimport static cc.blynk.integration.TestUtil.ok;\n\npublic class RegistrationLimitCheckTest extends SingleServerInstancePerTest {\n\n    @Test\n    public void registrationLimitCheck() throws Exception {\n        for (int i = 0; i < 100; i++) {\n            TestAppClient appClient = new TestAppClient(properties);\n            appClient.start();\n            appClient.register(incrementAndGetUserName(), \"1\", AppNameUtil.BLYNK);\n            appClient.verifyResult(ok(1));\n            appClient.stop();\n        }\n\n        TestAppClient appClient = new TestAppClient(properties);\n        appClient.start();\n        appClient.register(incrementAndGetUserName(), \"1\", AppNameUtil.BLYNK);\n        appClient.verifyResult(notAllowed(1));\n    }\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/tcp/ReportingTest.java",
    "content": "package cc.blynk.integration.tcp;\n\nimport cc.blynk.integration.BaseTest;\nimport cc.blynk.integration.model.tcp.ClientPair;\nimport cc.blynk.server.core.dao.ReportingDiskDao;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.device.BoardType;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.model.widgets.outputs.graph.GraphGranularityType;\nimport cc.blynk.server.core.model.widgets.ui.reporting.Format;\nimport cc.blynk.server.core.model.widgets.ui.reporting.Report;\nimport cc.blynk.server.core.model.widgets.ui.reporting.ReportResult;\nimport cc.blynk.server.core.model.widgets.ui.reporting.ReportingWidget;\nimport cc.blynk.server.core.model.widgets.ui.reporting.source.ReportDataStream;\nimport cc.blynk.server.core.model.widgets.ui.reporting.source.ReportSource;\nimport cc.blynk.server.core.model.widgets.ui.reporting.source.TileTemplateReportSource;\nimport cc.blynk.server.core.model.widgets.ui.reporting.type.DailyReport;\nimport cc.blynk.server.core.model.widgets.ui.reporting.type.DayOfMonth;\nimport cc.blynk.server.core.model.widgets.ui.reporting.type.MonthlyReport;\nimport cc.blynk.server.core.model.widgets.ui.reporting.type.OneTimeReport;\nimport cc.blynk.server.core.model.widgets.ui.reporting.type.ReportDurationType;\nimport cc.blynk.server.core.protocol.model.messages.ResponseMessage;\nimport cc.blynk.server.servers.BaseServer;\nimport cc.blynk.server.servers.application.MobileAndHttpsServer;\nimport cc.blynk.server.servers.hardware.HardwareAndHttpAPIServer;\nimport cc.blynk.utils.AppNameUtil;\nimport cc.blynk.utils.FileUtils;\nimport org.asynchttpclient.AsyncHttpClient;\nimport org.asynchttpclient.DefaultAsyncHttpClient;\nimport org.asynchttpclient.DefaultAsyncHttpClientConfig;\nimport org.asynchttpclient.Response;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport java.io.BufferedInputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.time.Instant;\nimport java.time.LocalDate;\nimport java.time.LocalTime;\nimport java.time.ZoneId;\nimport java.time.format.DateTimeFormatter;\nimport java.util.Enumeration;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.zip.ZipEntry;\nimport java.util.zip.ZipFile;\n\nimport static cc.blynk.integration.TestUtil.illegalCommand;\nimport static cc.blynk.integration.TestUtil.illegalCommandBody;\nimport static cc.blynk.integration.TestUtil.ok;\nimport static cc.blynk.integration.TestUtil.sleep;\nimport static cc.blynk.server.core.model.widgets.ui.reporting.ReportOutput.CSV_FILE_PER_DEVICE;\nimport static cc.blynk.server.core.model.widgets.ui.reporting.ReportOutput.CSV_FILE_PER_DEVICE_PER_PIN;\nimport static cc.blynk.server.core.model.widgets.ui.reporting.ReportOutput.MERGED_CSV;\nimport static cc.blynk.server.core.model.widgets.ui.reporting.ReportResult.EXPIRED;\nimport static cc.blynk.server.core.model.widgets.ui.reporting.ReportResult.OK;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_ENERGY;\nimport static cc.blynk.server.core.protocol.enums.Response.QUOTA_LIMIT;\nimport static cc.blynk.server.core.protocol.model.messages.MessageFactory.produce;\nimport static java.nio.charset.StandardCharsets.UTF_16;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\nimport static org.junit.Assert.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.reset;\nimport static org.mockito.Mockito.timeout;\nimport static org.mockito.Mockito.verify;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/2/2015.\n *\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class ReportingTest extends BaseTest {\n\n    private BaseServer appServer;\n    private BaseServer hardwareServer;\n    private ClientPair clientPair;\n\n    @Before\n    public void init() throws Exception {\n        this.hardwareServer = new HardwareAndHttpAPIServer(holder).start();\n        this.appServer = new MobileAndHttpsServer(holder).start();\n\n        this.clientPair = initAppAndHardPair();\n        reset(holder.mailWrapper);\n    }\n\n    @After\n    public void shutdown() {\n        this.appServer.close();\n        this.hardwareServer.close();\n        this.clientPair.stop();\n    }\n\n    @Test\n    public void testDeleteAllDeviceData() throws Exception {\n        Device device1 = new Device(2, \"My Device2\", BoardType.ESP8266);\n        clientPair.appClient.createDevice(1, device1);\n\n        String tempDir = holder.props.getProperty(\"data.folder\");\n\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n\n        Path pinReportingDataPath10 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.DIGITAL, (short) 8, GraphGranularityType.MINUTE));\n        Path pinReportingDataPath11 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.DIGITAL, (short) 8, GraphGranularityType.HOURLY));\n        Path pinReportingDataPath12 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.DIGITAL, (short) 8, GraphGranularityType.DAILY));\n\n\n        Path pinReportingDataPath20 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 2, PinType.DIGITAL, (short) 8, GraphGranularityType.MINUTE));\n\n        FileUtils.write(pinReportingDataPath10, 1.11D, 1111111);\n        FileUtils.write(pinReportingDataPath11, 1.11D, 1111111);\n        FileUtils.write(pinReportingDataPath12, 1.11D, 1111111);\n        FileUtils.write(pinReportingDataPath20, 1.22D, 2222222);\n\n        clientPair.appClient.send(\"deleteDeviceData 1-*\");\n        clientPair.appClient.verifyResult(ok(2));\n\n        assertTrue(Files.notExists(pinReportingDataPath10));\n        assertTrue(Files.notExists(pinReportingDataPath11));\n        assertTrue(Files.notExists(pinReportingDataPath12));\n        assertTrue(Files.notExists(pinReportingDataPath20));\n    }\n\n    @Test\n    public void testDeleteDeviceDataFor1Device() throws Exception {\n        Device device1 = new Device(2, \"My Device2\", BoardType.ESP8266);\n        clientPair.appClient.createDevice(1, device1);\n\n        String tempDir = holder.props.getProperty(\"data.folder\");\n\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n\n        Path pinReportingDataPath10 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.DIGITAL, (short) 8, GraphGranularityType.MINUTE));\n        Path pinReportingDataPath11 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.DIGITAL, (short) 8, GraphGranularityType.HOURLY));\n        Path pinReportingDataPath12 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.DIGITAL, (short) 8, GraphGranularityType.DAILY));\n\n\n        Path pinReportingDataPath20 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 2, PinType.DIGITAL, (short) 8, GraphGranularityType.MINUTE));\n\n        FileUtils.write(pinReportingDataPath10, 1.11D, 1111111);\n        FileUtils.write(pinReportingDataPath11, 1.11D, 1111111);\n        FileUtils.write(pinReportingDataPath12, 1.11D, 1111111);\n        FileUtils.write(pinReportingDataPath20, 1.22D, 2222222);\n\n        clientPair.appClient.deleteDeviceData(1, 2);\n        clientPair.appClient.verifyResult(ok(2));\n\n        assertTrue(Files.exists(pinReportingDataPath10));\n        assertTrue(Files.exists(pinReportingDataPath11));\n        assertTrue(Files.exists(pinReportingDataPath12));\n        assertTrue(Files.notExists(pinReportingDataPath20));\n    }\n\n    @Test\n    public void testDeleteDeviceDataForSpecificPin() throws Exception {\n        Device device1 = new Device(2, \"My Device2\", BoardType.ESP8266);\n        clientPair.appClient.createDevice(1, device1);\n\n        String tempDir = holder.props.getProperty(\"data.folder\");\n\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n\n        Path pinReportingDataPath10 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.DIGITAL, (short) 8, GraphGranularityType.MINUTE));\n        Path pinReportingDataPath11 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.DIGITAL, (short) 8, GraphGranularityType.HOURLY));\n        Path pinReportingDataPath12 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.DIGITAL, (short) 8, GraphGranularityType.DAILY));\n        Path pinReportingDataPath13 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.VIRTUAL, (short) 9, GraphGranularityType.DAILY));\n\n\n        Path pinReportingDataPath20 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 2, PinType.DIGITAL, (short) 8, GraphGranularityType.MINUTE));\n\n        FileUtils.write(pinReportingDataPath10, 1.11D, 1111111);\n        FileUtils.write(pinReportingDataPath11, 1.11D, 1111111);\n        FileUtils.write(pinReportingDataPath12, 1.11D, 1111111);\n        FileUtils.write(pinReportingDataPath13, 1.11D, 1111111);\n        FileUtils.write(pinReportingDataPath20, 1.22D, 2222222);\n\n        clientPair.appClient.deleteDeviceData(1, 2, \"d8\");\n        clientPair.appClient.verifyResult(ok(2));\n\n        assertTrue(Files.exists(pinReportingDataPath10));\n        assertTrue(Files.exists(pinReportingDataPath11));\n        assertTrue(Files.exists(pinReportingDataPath12));\n        assertTrue(Files.exists(pinReportingDataPath13));\n        assertTrue(Files.notExists(pinReportingDataPath20));\n\n        clientPair.appClient.deleteDeviceData(1, 0, \"d8\", \"v9\");\n        clientPair.appClient.verifyResult(ok(3));\n\n        assertTrue(Files.notExists(pinReportingDataPath10));\n        assertTrue(Files.notExists(pinReportingDataPath11));\n        assertTrue(Files.notExists(pinReportingDataPath12));\n        assertTrue(Files.notExists(pinReportingDataPath13));\n        assertTrue(Files.notExists(pinReportingDataPath20));\n    }\n\n    @Test\n    public void createReportCRUD() throws Exception {\n        ReportDataStream reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, \"Temperature\", true);\n        ReportSource reportSource = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream},\n                1,\n                new int[] {0}\n        );\n\n        ReportingWidget reportingWidget = new ReportingWidget();\n        reportingWidget.height = 1;\n        reportingWidget.width = 1;\n        reportingWidget.reportSources = new ReportSource[] {\n                reportSource\n        };\n\n        clientPair.appClient.send(\"getEnergy\");\n        clientPair.appClient.verifyResult(produce(1, GET_ENERGY, \"7500\"));\n\n        clientPair.appClient.createWidget(1, reportingWidget);\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.appClient.send(\"getEnergy\");\n        clientPair.appClient.verifyResult(produce(3, GET_ENERGY, \"7500\"));\n\n        Report report = new Report(1, \"My One Time Report\",\n                new ReportSource[] {reportSource},\n                new OneTimeReport(TimeUnit.DAYS.toMillis(1)), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE, null, ZoneId.of(\"UTC\"), 0, 0, null);\n\n        clientPair.appClient.createReport(1, report);\n        report = clientPair.appClient.parseReportFromResponse(4);\n        assertNotNull(report);\n\n        clientPair.appClient.send(\"getEnergy\");\n        clientPair.appClient.verifyResult(produce(5, GET_ENERGY, \"4600\"));\n\n        report = new Report(1, \"Updated\",\n                new ReportSource[] {reportSource},\n                new OneTimeReport(TimeUnit.DAYS.toMillis(1)), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE, null, ZoneId.of(\"UTC\"), 0, 0, null);\n\n        clientPair.appClient.updateReport(1, report);\n        report = clientPair.appClient.parseReportFromResponse(6);\n        assertNotNull(report);\n        assertEquals(\"Updated\", report.name);\n\n        clientPair.appClient.deleteReport(1, report.id);\n        clientPair.appClient.verifyResult(ok(7));\n\n        clientPair.appClient.send(\"getEnergy\");\n        clientPair.appClient.verifyResult(produce(8, GET_ENERGY, \"7500\"));\n    }\n\n    @Test\n    public void testDailyReportIsTriggered() throws Exception {\n        ReportDataStream reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, \"Temperature\", true);\n        ReportSource reportSource = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream},\n                1,\n                new int[] {0}\n        );\n\n        ReportingWidget reportingWidget = new ReportingWidget();\n        reportingWidget.height = 1;\n        reportingWidget.width = 1;\n        reportingWidget.reportSources = new ReportSource[] {\n                reportSource\n        };\n\n        clientPair.appClient.createWidget(1, reportingWidget);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.send(\"addEnergy \" + \"100000\" + \"\\0\" + \"1370-3990-1414-55681\");\n        clientPair.appClient.verifyResult(ok(2));\n\n        //a bit upfront\n        long now = System.currentTimeMillis() + 1500;\n\n        Report report = new Report(1, \"DailyReport\",\n                new ReportSource[] {reportSource},\n                new DailyReport(now, ReportDurationType.INFINITE, 0, 0), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE, null, ZoneId.of(\"UTC\"), 0, 0, null);\n        clientPair.appClient.createReport(1, report);\n\n        report = clientPair.appClient.parseReportFromResponse(3);\n        assertNotNull(report);\n        assertEquals(now, report.nextReportAt, 3000);\n\n        Report report2 = new Report(2, \"DailyReport2\",\n                new ReportSource[] {reportSource},\n                new DailyReport(now, ReportDurationType.INFINITE, now, now), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE, null, ZoneId.of(\"UTC\"), 0, 0, null);\n        clientPair.appClient.createReport(1, report2);\n\n        report = clientPair.appClient.parseReportFromResponse(4);\n        assertNotNull(report);\n        assertEquals(now, report.nextReportAt, 3000);\n\n        //expecting now is ignored as duration is INFINITE\n        Report report3 = new Report(3, \"DailyReport3\",\n                new ReportSource[] {reportSource},\n                new DailyReport(now, ReportDurationType.INFINITE, now + 86400_000, now + 86400_000), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE, null, ZoneId.of(\"UTC\"), 0, 0, null);\n        clientPair.appClient.createReport(1, report3);\n\n        report = clientPair.appClient.parseReportFromResponse(5);\n        assertNotNull(report);\n        assertEquals(System.currentTimeMillis(), report.nextReportAt, 3000);\n\n        //now date is greater than end date, such reports are not accepted.\n        Report report4 = new Report(4, \"DailyReport4\",\n                new ReportSource[] {reportSource},\n                new DailyReport(now, ReportDurationType.INFINITE, now + 86400_000, now), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE, null, ZoneId.of(\"UTC\"), 0, 0, null);\n        clientPair.appClient.createReport(1, report4);\n\n        clientPair.appClient.verifyResult(illegalCommand(6));\n\n        //trigger date is tomorrow\n        Report report5 = new Report(5, \"DailyReport5\",\n                new ReportSource[] {reportSource},\n                new DailyReport(now, ReportDurationType.CUSTOM, now + 86400_000, now + 86400_000), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE, null, ZoneId.of(\"UTC\"), 0, 0, null);\n        clientPair.appClient.createReport(1, report5);\n\n        report = clientPair.appClient.parseReportFromResponse(7);\n        assertNotNull(report);\n        assertEquals(now + 86400_000, report.nextReportAt, 3000);\n\n        //report wit the same id is not allowed\n        Report report6 = new Report(5, \"DailyReport6\",\n                new ReportSource[] {reportSource},\n                new DailyReport(now, ReportDurationType.CUSTOM, now + 86400_000, now + 86400_000), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE, null, ZoneId.of(\"UTC\"), 0, 0, null);\n        clientPair.appClient.createReport(1, report6);\n\n        clientPair.appClient.verifyResult(illegalCommand(8));\n\n        int tries = 0;\n        while (holder.reportScheduler.getCompletedTaskCount() < 3 && tries < 20) {\n            sleep(100);\n            tries++;\n        }\n\n        verify(holder.mailWrapper, never()).sendReportEmail(eq(\"test@gmail.com\"), eq(\"DailyReport\"), any(), any());\n        verify(holder.mailWrapper, never()).sendReportEmail(eq(\"test@gmail.com\"), eq(\"DailyReport2\"), any(), any());\n        verify(holder.mailWrapper, never()).sendReportEmail(eq(\"test@gmail.com\"), eq(\"DailyReport3\"), any(), any());\n        assertEquals(3, holder.reportScheduler.getCompletedTaskCount());\n        assertEquals(7, holder.reportScheduler.getTaskCount());\n    }\n\n    @Test\n    public void testReportIdRemovedFromScheduler() throws Exception {\n        ReportDataStream reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, \"Temperature\", true);\n        ReportSource reportSource = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream},\n                1,\n                new int[] {0}\n        );\n\n        ReportingWidget reportingWidget = new ReportingWidget();\n        reportingWidget.height = 1;\n        reportingWidget.width = 1;\n        reportingWidget.reportSources = new ReportSource[] {\n                reportSource\n        };\n\n        clientPair.appClient.createWidget(1, reportingWidget);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.send(\"addEnergy \" + \"100000\" + \"\\0\" + \"1370-3990-1414-55681\");\n        clientPair.appClient.verifyResult(ok(2));\n\n        //a bit upfront\n        long now = System.currentTimeMillis() + 1000;\n\n        Report report = new Report(1, \"DailyReport\",\n                new ReportSource[] {reportSource},\n                new DailyReport(now, ReportDurationType.INFINITE, 0, 0), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE, null, ZoneId.of(\"UTC\"), 0, 0, null);\n        clientPair.appClient.createReport(1, report);\n\n        report = clientPair.appClient.parseReportFromResponse(3);\n        assertNotNull(report);\n        assertEquals(System.currentTimeMillis(), report.nextReportAt, 3000);\n\n        Report report2 = new Report(2, \"DailyReport2\",\n                new ReportSource[] {reportSource},\n                new DailyReport(now, ReportDurationType.INFINITE, now, now), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE, null, ZoneId.of(\"UTC\"), 0, 0, null);\n        clientPair.appClient.createReport(1, report2);\n\n        report2 = clientPair.appClient.parseReportFromResponse(4);\n        assertNotNull(report2);\n        assertEquals(System.currentTimeMillis(), report2.nextReportAt, 3000);\n\n        int tries = 0;\n        while (holder.reportScheduler.getCompletedTaskCount() < 2 && tries < 20) {\n            sleep(100);\n            tries++;\n        }\n\n        verify(holder.mailWrapper, never()).sendReportEmail(eq(\"test@gmail.com\"), eq(\"DailyReport\"), any(), any());\n        verify(holder.mailWrapper, never()).sendReportEmail(eq(\"test@gmail.com\"), eq(\"DailyReport2\"), any(), any());\n        assertEquals(2, holder.reportScheduler.getCompletedTaskCount());\n        assertEquals(4, holder.reportScheduler.getTaskCount());\n\n        clientPair.appClient.send(\"loadProfileGzipped 1\");\n        DashBoard dashBoard = clientPair.appClient.parseDash(5);\n        assertNotNull(dashBoard);\n        reportingWidget = dashBoard.getReportingWidget();\n        assertNotNull(reportingWidget);\n\n        assertEquals(ReportResult.NO_DATA, reportingWidget.reports[0].lastRunResult);\n        assertEquals(ReportResult.NO_DATA, reportingWidget.reports[1].lastRunResult);\n\n        clientPair.appClient.deleteReport(1, 1);\n        clientPair.appClient.verifyResult(ok(6));\n\n        assertEquals(3, holder.reportScheduler.getTaskCount());\n\n        clientPair.appClient.deleteReport(1, 2);\n        clientPair.appClient.verifyResult(ok(7));\n\n        assertEquals(2, holder.reportScheduler.getCompletedTaskCount());\n        assertEquals(2, holder.reportScheduler.getTaskCount());\n        assertEquals(0, holder.reportScheduler.map.size());\n    }\n\n    @Test\n    public void testReportIdRemovedFromSchedulerWhenDashIsRemoved() throws Exception {\n        ReportDataStream reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, \"Temperature\", true);\n        ReportSource reportSource = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream},\n                1,\n                new int[] {0}\n        );\n\n        ReportingWidget reportingWidget = new ReportingWidget();\n        reportingWidget.height = 1;\n        reportingWidget.width = 1;\n        reportingWidget.reportSources = new ReportSource[] {\n                reportSource\n        };\n\n        clientPair.appClient.createWidget(1, reportingWidget);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.send(\"addEnergy \" + \"100000\" + \"\\0\" + \"1370-3990-1414-55681\");\n        clientPair.appClient.verifyResult(ok(2));\n\n        //a bit upfront\n        long now = System.currentTimeMillis() + 1000;\n\n        Report report = new Report(1, \"DailyReport\",\n                new ReportSource[] {reportSource},\n                new DailyReport(now, ReportDurationType.INFINITE, 0, 0), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE, null, ZoneId.of(\"UTC\"), 0, 0, null);\n        clientPair.appClient.createReport(1, report);\n\n        report = clientPair.appClient.parseReportFromResponse(3);\n        assertNotNull(report);\n        assertEquals(System.currentTimeMillis(), report.nextReportAt, 3000);\n\n        Report report2 = new Report(2, \"DailyReport2\",\n                new ReportSource[] {reportSource},\n                new DailyReport(now, ReportDurationType.INFINITE, now, now), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE, null, ZoneId.of(\"UTC\"), 0, 0, null);\n        clientPair.appClient.createReport(1, report2);\n\n        report2 = clientPair.appClient.parseReportFromResponse(4);\n        assertNotNull(report2);\n        assertEquals(System.currentTimeMillis(), report2.nextReportAt, 3000);\n\n        int tries = 0;\n        while (holder.reportScheduler.getCompletedTaskCount() < 2 && tries < 20) {\n            sleep(100);\n            tries++;\n        }\n\n        verify(holder.mailWrapper, never()).sendReportEmail(eq(\"test@gmail.com\"), eq(\"DailyReport\"), any(), any());\n        verify(holder.mailWrapper, never()).sendReportEmail(eq(\"test@gmail.com\"), eq(\"DailyReport2\"), any(), any());\n        assertEquals(2, holder.reportScheduler.getCompletedTaskCount());\n        assertEquals(4, holder.reportScheduler.getTaskCount());\n\n        clientPair.appClient.deleteDash(1);\n        clientPair.appClient.verifyResult(ok(5));\n\n        assertEquals(2, holder.reportScheduler.getCompletedTaskCount());\n        assertEquals(2, holder.reportScheduler.getTaskCount());\n        assertEquals(0, holder.reportScheduler.map.size());\n    }\n\n    @Test\n    public void testDailyReportWithSinglePointIsTriggeredAndNullName() throws Exception {\n        String tempDir = holder.props.getProperty(\"data.folder\");\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n        Path pinReportingDataPath10 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.VIRTUAL, (short) 1, GraphGranularityType.MINUTE));\n        long pointNow = System.currentTimeMillis();\n        FileUtils.write(pinReportingDataPath10, 1.11D, pointNow);\n\n        ReportDataStream reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, \"Temperature\", true);\n        ReportSource reportSource = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream},\n                1,\n                new int[] {0}\n        );\n\n        ReportingWidget reportingWidget = new ReportingWidget();\n        reportingWidget.height = 1;\n        reportingWidget.width = 1;\n        reportingWidget.reportSources = new ReportSource[] {\n                reportSource\n        };\n\n        clientPair.appClient.createWidget(1, reportingWidget);\n        clientPair.appClient.verifyResult(ok(1));\n\n        //a bit upfront\n        long now = System.currentTimeMillis() + 1000;\n        LocalTime localTime = LocalTime.ofInstant(Instant.ofEpochMilli(now), ZoneId.of(\"UTC\"));\n        localTime = LocalTime.of(localTime.getHour(), localTime.getMinute());\n\n        Report report = new Report(1, null,\n                new ReportSource[] {reportSource},\n                new DailyReport(now, ReportDurationType.INFINITE, 0, 0), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE_PER_PIN,\n                Format.ISO_SIMPLE, ZoneId.of(\"UTC\"), 0, 0, null);\n        clientPair.appClient.createReport(1, report);\n\n        report = clientPair.appClient.parseReportFromResponse(2);\n        assertNotNull(report);\n        assertEquals(System.currentTimeMillis(), report.nextReportAt, 5000);\n\n        String date = LocalDate.now(report.tzName).toString();\n        String filename = getUserName() + \"_Blynk_\" + report.id + \"_\" + date + \".zip\";\n        String downloadUrl = \"http://127.0.0.1:18080/\" + filename;\n        verify(holder.mailWrapper, timeout(3000)).sendReportEmail(eq(\"test@gmail.com\"),\n                eq(\"Your daily Report is ready\"),\n                eq(downloadUrl),\n                eq(\"Report name: Report<br>Period: Daily, at \" + localTime));\n        sleep(200);\n        assertEquals(1, holder.reportScheduler.getCompletedTaskCount());\n        assertEquals(2, holder.reportScheduler.getTaskCount());\n\n        Path result = Paths.get(FileUtils.CSV_DIR,\n                getUserName() + \"_\" + AppNameUtil.BLYNK + \"_\" + report.id + \"_\" + date + \".zip\");\n        assertTrue(Files.exists(result));\n        String resultCsvString = readStringFromFirstZipEntry(result);\n        String[] split = resultCsvString.split(\"[,\\n]\");\n        String nowFormatted = DateTimeFormatter\n                .ofPattern(Format.ISO_SIMPLE.pattern)\n                .withZone(ZoneId.of(\"UTC\"))\n                .format(Instant.ofEpochMilli(pointNow));\n        assertEquals(nowFormatted, split[0]);\n        assertEquals(1.11D, Double.parseDouble(split[1]), 0.0001);\n\n        AsyncHttpClient httpclient = new DefaultAsyncHttpClient(\n                new DefaultAsyncHttpClientConfig.Builder()\n                        .setUserAgent(null)\n                        .setKeepAlive(true)\n                        .build()\n        );\n        Future<Response> f = httpclient.prepareGet(downloadUrl).execute();\n        Response response = f.get();\n        assertEquals(200, response.getStatusCode());\n        assertEquals(\"application/zip\", response.getContentType());\n        httpclient.close();\n    }\n\n    @Test\n    public void testDailyReportWith24PointsCorrectlyFetched() throws Exception {\n        String tempDir = holder.props.getProperty(\"data.folder\");\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n        Path pinReportingDataPath10 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.VIRTUAL, (short) 1, GraphGranularityType.HOURLY));\n        long pointNow = System.currentTimeMillis();\n        long pointNowTruncated = (pointNow / GraphGranularityType.HOURLY.period) * GraphGranularityType.HOURLY.period;\n        pointNowTruncated -= TimeUnit.DAYS.toMillis(1);\n        for (int i = 0; i < 24; i++) {\n            FileUtils.write(pinReportingDataPath10, i, pointNowTruncated + TimeUnit.HOURS.toMillis(i));\n        }\n\n        ReportDataStream reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, \"Temperature\", true);\n        ReportSource reportSource = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream},\n                1,\n                new int[] {0}\n        );\n\n        ReportingWidget reportingWidget = new ReportingWidget();\n        reportingWidget.height = 1;\n        reportingWidget.width = 1;\n        reportingWidget.reportSources = new ReportSource[] {\n                reportSource\n        };\n\n        clientPair.appClient.createWidget(1, reportingWidget);\n        clientPair.appClient.verifyResult(ok(1));\n\n        //a bit upfront\n        long now = pointNow + 1000;\n        LocalTime localTime = LocalTime.ofInstant(Instant.ofEpochMilli(now), ZoneId.of(\"UTC\"));\n        localTime = LocalTime.of(localTime.getHour(), localTime.getMinute());\n\n        Report report = new Report(1, \"DailyReport\",\n                new ReportSource[] {reportSource},\n                new DailyReport(now, ReportDurationType.INFINITE, 0, 0), \"test@gmail.com\",\n                GraphGranularityType.HOURLY, true, CSV_FILE_PER_DEVICE_PER_PIN,\n                Format.ISO_SIMPLE, ZoneId.of(\"UTC\"), 0, 0, null);\n        clientPair.appClient.createReport(1, report);\n\n        report = clientPair.appClient.parseReportFromResponse(2);\n        assertNotNull(report);\n        assertEquals(System.currentTimeMillis(), report.nextReportAt, 3500);\n\n        String date = LocalDate.now(report.tzName).toString();\n        String filename = getUserName() + \"_Blynk_\" + report.id + \"_\" + date + \".zip\";\n        String downloadUrl = \"http://127.0.0.1:18080/\" + filename;\n        verify(holder.mailWrapper, timeout(3000)).sendReportEmail(eq(\"test@gmail.com\"),\n                eq(\"Your daily DailyReport is ready\"),\n                eq(downloadUrl),\n                eq(\"Report name: DailyReport<br>Period: Daily, at \" + localTime));\n        sleep(200);\n        assertEquals(1, holder.reportScheduler.getCompletedTaskCount());\n        assertEquals(2, holder.reportScheduler.getTaskCount());\n\n        Path result = Paths.get(FileUtils.CSV_DIR,\n                getUserName() + \"_\" + AppNameUtil.BLYNK + \"_\" + report.id + \"_\" + date + \".zip\");\n        assertTrue(Files.exists(result));\n        String resultCsvString = readStringFromFirstZipEntry(result);\n        String[] split = resultCsvString.split(\"\\n\");\n        assertEquals(24, split.length);\n    }\n\n    @Test\n    public void testFinalFileNameCSVPerDevicePerPin() throws Exception {\n        Device device1 = new Device(2, \"My Device2 with big name\", BoardType.ESP8266);\n        clientPair.appClient.createDevice(1, device1);\n\n        String tempDir = holder.props.getProperty(\"data.folder\");\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n        Path pinReportingDataPath10 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.VIRTUAL, (short) 1, GraphGranularityType.MINUTE));\n        long pointNow = System.currentTimeMillis();\n        FileUtils.write(pinReportingDataPath10, 1.11D, pointNow);\n\n        Path pinReportingDataPath20 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 2, PinType.VIRTUAL, (short) 1, GraphGranularityType.MINUTE));\n        FileUtils.write(pinReportingDataPath20, 1.11D, pointNow);\n\n        ReportDataStream reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, \"Temperature,yes\", true);\n        ReportSource reportSource = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream},\n                1,\n                new int[] {0, 2}\n        );\n\n        ReportingWidget reportingWidget = new ReportingWidget();\n        reportingWidget.height = 1;\n        reportingWidget.width = 1;\n        reportingWidget.reportSources = new ReportSource[] {\n                reportSource\n        };\n\n        clientPair.appClient.createWidget(1, reportingWidget);\n        clientPair.appClient.verifyResult(ok(2));\n\n        //a bit upfront\n        long now = System.currentTimeMillis() + 1000;\n        LocalTime localTime = LocalTime.ofInstant(Instant.ofEpochMilli(now), ZoneId.of(\"UTC\"));\n        localTime = LocalTime.of(localTime.getHour(), localTime.getMinute());\n\n        Report report = new Report(1, \"DailyReport\",\n                new ReportSource[] {reportSource},\n                new DailyReport(now, ReportDurationType.INFINITE, 0, 0), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE_PER_PIN,\n                Format.ISO_SIMPLE, ZoneId.of(\"UTC\"), 0, 0, null);\n        clientPair.appClient.createReport(1, report);\n\n        report = clientPair.appClient.parseReportFromResponse(3);\n        assertNotNull(report);\n        assertEquals(System.currentTimeMillis(), report.nextReportAt, 3000);\n\n        String date = LocalDate.now(report.tzName).toString();\n        String filename = getUserName() + \"_Blynk_\" + report.id + \"_\" + date + \".zip\";\n        String downloadUrl = \"http://127.0.0.1:18080/\" + filename;\n        verify(holder.mailWrapper, timeout(3000)).sendReportEmail(eq(\"test@gmail.com\"),\n                eq(\"Your daily DailyReport is ready\"),\n                eq(downloadUrl),\n                eq(\"Report name: DailyReport<br>Period: Daily, at \" + localTime));\n        sleep(200);\n        assertEquals(1, holder.reportScheduler.getCompletedTaskCount());\n        assertEquals(2, holder.reportScheduler.getTaskCount());\n\n        Path result = Paths.get(FileUtils.CSV_DIR,\n                getUserName() + \"_\" + AppNameUtil.BLYNK + \"_\" + report.id + \"_\" + date + \".zip\");\n        assertTrue(Files.exists(result));\n        ZipFile zipFile = new ZipFile(result.toString());\n\n        Enumeration<? extends ZipEntry> entries = zipFile.entries();\n        assertTrue(entries.hasMoreElements());\n\n        ZipEntry entry = entries.nextElement();\n        assertNotNull(entry);\n        assertEquals(\"MyDevice_0_Temperatureyes.csv\", entry.getName());\n\n        ZipEntry entry2 = entries.nextElement();\n        assertNotNull(entry2);\n        assertEquals(\"MyDevice2withbig_2_Temperatureyes.csv\", entry2.getName());\n\n    }\n\n    @Test\n    public void testFinalFileNameCSVPerDevice() throws Exception {\n        Device device1 = new Device(2, \"My Device2 with big name\", BoardType.ESP8266);\n        clientPair.appClient.createDevice(1, device1);\n\n        String tempDir = holder.props.getProperty(\"data.folder\");\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n        Path pinReportingDataPath10 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.VIRTUAL, (short) 1, GraphGranularityType.MINUTE));\n        long pointNow = System.currentTimeMillis();\n        FileUtils.write(pinReportingDataPath10, 1.11D, pointNow);\n\n        Path pinReportingDataPath20 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 2, PinType.VIRTUAL, (short) 1, GraphGranularityType.MINUTE));\n        FileUtils.write(pinReportingDataPath20, 1.11D, pointNow);\n\n        ReportDataStream reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, null, true);\n        ReportSource reportSource = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream},\n                1,\n                new int[] {0, 2}\n        );\n\n        ReportingWidget reportingWidget = new ReportingWidget();\n        reportingWidget.height = 1;\n        reportingWidget.width = 1;\n        reportingWidget.reportSources = new ReportSource[] {\n                reportSource\n        };\n\n        clientPair.appClient.createWidget(1, reportingWidget);\n        clientPair.appClient.verifyResult(ok(2));\n\n        //a bit upfront\n        long now = System.currentTimeMillis() + 1000;\n        LocalTime localTime = LocalTime.ofInstant(Instant.ofEpochMilli(now), ZoneId.of(\"UTC\"));\n        localTime = LocalTime.of(localTime.getHour(), localTime.getMinute());\n\n        Report report = new Report(1, \"DailyReport\",\n                new ReportSource[] {reportSource},\n                new DailyReport(now, ReportDurationType.INFINITE, 0, 0), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE,\n                Format.ISO_SIMPLE, ZoneId.of(\"UTC\"), 0, 0, null);\n        clientPair.appClient.createReport(1, report);\n\n        report = clientPair.appClient.parseReportFromResponse(3);\n        assertNotNull(report);\n        assertEquals(System.currentTimeMillis(), report.nextReportAt, 3000);\n\n        String date = LocalDate.now(report.tzName).toString();\n        String filename = getUserName() + \"_Blynk_\" + report.id + \"_\" + date + \".zip\";\n        String downloadUrl = \"http://127.0.0.1:18080/\" + filename;\n        verify(holder.mailWrapper, timeout(3000)).sendReportEmail(eq(\"test@gmail.com\"),\n                eq(\"Your daily DailyReport is ready\"),\n                eq(downloadUrl),\n                eq(\"Report name: DailyReport<br>Period: Daily, at \" + localTime));\n        sleep(200);\n        assertEquals(1, holder.reportScheduler.getCompletedTaskCount());\n        assertEquals(2, holder.reportScheduler.getTaskCount());\n\n        Path result = Paths.get(FileUtils.CSV_DIR,\n                getUserName() + \"_\" + AppNameUtil.BLYNK + \"_\" + report.id + \"_\" + date + \".zip\");\n        assertTrue(Files.exists(result));\n        ZipFile zipFile = new ZipFile(result.toString());\n\n        Enumeration<? extends ZipEntry> entries = zipFile.entries();\n        assertTrue(entries.hasMoreElements());\n\n        ZipEntry entry = entries.nextElement();\n        assertNotNull(entry);\n        assertEquals(\"MyDevice_0.csv\", entry.getName());\n\n        ZipEntry entry2 = entries.nextElement();\n        assertNotNull(entry2);\n        assertEquals(\"MyDevice2withbig_2.csv\", entry2.getName());\n\n        String resultCsvString = readStringFromFirstZipEntry(result);\n        assertNotNull(resultCsvString);\n\n        String nowFormatted = DateTimeFormatter\n                .ofPattern(Format.ISO_SIMPLE.pattern)\n                .withZone(ZoneId.of(\"UTC\"))\n                .format(Instant.ofEpochMilli(pointNow));\n        assertEquals(resultCsvString, nowFormatted + \",v1,1.11\\n\");\n    }\n\n    @Test\n    public void testFinalFileNameCSVPerDeviceUtf8() throws Exception {\n        Device device1 = new Device(2, \"My Device2 with big name\", BoardType.ESP8266);\n        clientPair.appClient.createDevice(1, device1);\n\n        String tempDir = holder.props.getProperty(\"data.folder\");\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n        Path pinReportingDataPath10 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.VIRTUAL, (short) 1, GraphGranularityType.MINUTE));\n        long pointNow = System.currentTimeMillis();\n        FileUtils.write(pinReportingDataPath10, 1.11D, pointNow);\n\n        Path pinReportingDataPath20 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 2, PinType.VIRTUAL, (short) 1, GraphGranularityType.MINUTE));\n        FileUtils.write(pinReportingDataPath20, 1.11D, pointNow);\n\n        ReportDataStream reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, \"Мій датастрім\", true);\n        ReportSource reportSource = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream},\n                1,\n                new int[] {0, 2}\n        );\n\n        ReportingWidget reportingWidget = new ReportingWidget();\n        reportingWidget.height = 1;\n        reportingWidget.width = 1;\n        reportingWidget.reportSources = new ReportSource[] {\n                reportSource\n        };\n\n        clientPair.appClient.createWidget(1, reportingWidget);\n        clientPair.appClient.verifyResult(ok(2));\n\n        //a bit upfront\n        long now = System.currentTimeMillis() + 1000;\n        LocalTime localTime = LocalTime.ofInstant(Instant.ofEpochMilli(now), ZoneId.of(\"UTC\"));\n        localTime = LocalTime.of(localTime.getHour(), localTime.getMinute());\n\n        Report report = new Report(1, \"DailyReport\",\n                new ReportSource[] {reportSource},\n                new DailyReport(now, ReportDurationType.INFINITE, 0, 0), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE,\n                Format.ISO_SIMPLE, ZoneId.of(\"UTC\"), 0, 0, null);\n        clientPair.appClient.createReport(1, report);\n\n        report = clientPair.appClient.parseReportFromResponse(3);\n        assertNotNull(report);\n        assertEquals(System.currentTimeMillis(), report.nextReportAt, 3000);\n\n        String date = LocalDate.now(report.tzName).toString();\n        String filename = getUserName() + \"_Blynk_\" + report.id + \"_\" + date + \".zip\";\n        String downloadUrl = \"http://127.0.0.1:18080/\" + filename;\n        verify(holder.mailWrapper, timeout(3000)).sendReportEmail(eq(\"test@gmail.com\"),\n                eq(\"Your daily DailyReport is ready\"),\n                eq(downloadUrl),\n                eq(\"Report name: DailyReport<br>Period: Daily, at \" + localTime));\n        sleep(200);\n        assertEquals(1, holder.reportScheduler.getCompletedTaskCount());\n        assertEquals(2, holder.reportScheduler.getTaskCount());\n\n        Path result = Paths.get(FileUtils.CSV_DIR,\n                getUserName() + \"_\" + AppNameUtil.BLYNK + \"_\" + report.id + \"_\" + date + \".zip\");\n        assertTrue(Files.exists(result));\n        ZipFile zipFile = new ZipFile(result.toString());\n\n        Enumeration<? extends ZipEntry> entries = zipFile.entries();\n        assertTrue(entries.hasMoreElements());\n\n        ZipEntry entry = entries.nextElement();\n        assertNotNull(entry);\n        assertEquals(\"MyDevice_0.csv\", entry.getName());\n\n        ZipEntry entry2 = entries.nextElement();\n        assertNotNull(entry2);\n        assertEquals(\"MyDevice2withbig_2.csv\", entry2.getName());\n\n        String resultCsvString = readStringFromFirstZipEntry(result);\n        assertNotNull(resultCsvString);\n\n        String nowFormatted = DateTimeFormatter\n                .ofPattern(Format.ISO_SIMPLE.pattern)\n                .withZone(ZoneId.of(\"UTC\"))\n                .format(Instant.ofEpochMilli(pointNow));\n        assertEquals(nowFormatted + \",Мій датастрім,1.11\\n\", resultCsvString);\n    }\n\n    @Test\n    public void testFinalFileNameCSVPerDevice2() throws Exception {\n        Device device1 = new Device(2, \"My Device2 with big name\", BoardType.ESP8266);\n        clientPair.appClient.createDevice(1, device1);\n\n        String tempDir = holder.props.getProperty(\"data.folder\");\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n        Path pinReportingDataPath10 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.VIRTUAL, (short) 1, GraphGranularityType.MINUTE));\n        Path pinReportingDataPath20 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.VIRTUAL, (short) 2, GraphGranularityType.MINUTE));\n        long pointNow = System.currentTimeMillis();\n        FileUtils.write(pinReportingDataPath10, 1.11D, pointNow);\n        FileUtils.write(pinReportingDataPath20, 1.12D, pointNow);\n\n        Path pinReportingDataPath12 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 2, PinType.VIRTUAL, (short) 1, GraphGranularityType.MINUTE));\n        Path pinReportingDataPath22 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 2, PinType.VIRTUAL, (short) 2, GraphGranularityType.MINUTE));\n        FileUtils.write(pinReportingDataPath12, 1.13D, pointNow);\n        FileUtils.write(pinReportingDataPath22, 1.14D, pointNow);\n\n        ReportDataStream reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, null, true);\n        ReportDataStream reportDataStream2 = new ReportDataStream((short) 2, PinType.VIRTUAL, null, true);\n        ReportSource reportSource = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream, reportDataStream2},\n                1,\n                new int[] {0, 2}\n        );\n\n        ReportingWidget reportingWidget = new ReportingWidget();\n        reportingWidget.height = 1;\n        reportingWidget.width = 1;\n        reportingWidget.reportSources = new ReportSource[] {\n                reportSource\n        };\n\n        clientPair.appClient.createWidget(1, reportingWidget);\n        clientPair.appClient.verifyResult(ok(2));\n\n        //a bit upfront\n        long now = System.currentTimeMillis() + 1000;\n        LocalTime localTime = LocalTime.ofInstant(Instant.ofEpochMilli(now), ZoneId.of(\"UTC\"));\n        localTime = LocalTime.of(localTime.getHour(), localTime.getMinute());\n\n        Report report = new Report(1, \"DailyReport\",\n                new ReportSource[] {reportSource},\n                new DailyReport(now, ReportDurationType.INFINITE, 0, 0), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE,\n                Format.ISO_SIMPLE, ZoneId.of(\"UTC\"), 0, 0, null);\n        clientPair.appClient.createReport(1, report);\n\n        report = clientPair.appClient.parseReportFromResponse(3);\n        assertNotNull(report);\n        assertEquals(System.currentTimeMillis(), report.nextReportAt, 3000);\n\n        String date = LocalDate.now(report.tzName).toString();\n        String filename = getUserName() + \"_Blynk_\" + report.id + \"_\" + date + \".zip\";\n        String downloadUrl = \"http://127.0.0.1:18080/\" + filename;\n        verify(holder.mailWrapper, timeout(3000)).sendReportEmail(eq(\"test@gmail.com\"),\n                eq(\"Your daily DailyReport is ready\"),\n                eq(downloadUrl),\n                eq(\"Report name: DailyReport<br>Period: Daily, at \" + localTime));\n        sleep(200);\n        assertEquals(1, holder.reportScheduler.getCompletedTaskCount());\n        assertEquals(2, holder.reportScheduler.getTaskCount());\n\n        Path result = Paths.get(FileUtils.CSV_DIR,\n                getUserName() + \"_\" + AppNameUtil.BLYNK + \"_\" + report.id + \"_\" + date + \".zip\");\n        assertTrue(Files.exists(result));\n        ZipFile zipFile = new ZipFile(result.toString());\n\n        Enumeration<? extends ZipEntry> entries = zipFile.entries();\n        assertTrue(entries.hasMoreElements());\n\n        ZipEntry entry = entries.nextElement();\n        assertNotNull(entry);\n        assertEquals(\"MyDevice_0.csv\", entry.getName());\n\n        ZipEntry entry2 = entries.nextElement();\n        assertNotNull(entry2);\n        assertEquals(\"MyDevice2withbig_2.csv\", entry2.getName());\n\n        String nowFormatted = DateTimeFormatter\n                .ofPattern(Format.ISO_SIMPLE.pattern)\n                .withZone(ZoneId.of(\"UTC\"))\n                .format(Instant.ofEpochMilli(pointNow));\n\n        String resultCsvString = readStringFromZipEntry(zipFile, entry);\n        assertNotNull(resultCsvString);\n        assertEquals(resultCsvString, nowFormatted + \",v1,1.11\\n\" + nowFormatted + \",v2,1.12\\n\");\n\n        String resultCsvString2 = readStringFromZipEntry(zipFile, entry2);\n        assertNotNull(resultCsvString2);\n        assertEquals(resultCsvString2, nowFormatted + \",v1,1.13\\n\" + nowFormatted + \",v2,1.14\\n\");\n    }\n\n    @Test\n    public void testFinalFileNameCSVPerDeviceUnicode() throws Exception {\n        Device device1 = new Device(2, \"Мій девайс\", BoardType.ESP8266);\n        clientPair.appClient.createDevice(1, device1);\n\n        String tempDir = holder.props.getProperty(\"data.folder\");\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n        long pointNow = System.currentTimeMillis();\n\n        Path pinReportingDataPath12 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 2, PinType.VIRTUAL, (short) 1, GraphGranularityType.MINUTE));\n        Path pinReportingDataPath22 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 2, PinType.VIRTUAL, (short) 2, GraphGranularityType.MINUTE));\n        FileUtils.write(pinReportingDataPath12, 1.13D, pointNow);\n        FileUtils.write(pinReportingDataPath22, 1.14D, pointNow);\n\n        ReportDataStream reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, null, true);\n        ReportDataStream reportDataStream2 = new ReportDataStream((short) 2, PinType.VIRTUAL, null, true);\n        ReportSource reportSource = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream, reportDataStream2},\n                1,\n                new int[] {2}\n        );\n\n        ReportingWidget reportingWidget = new ReportingWidget();\n        reportingWidget.height = 1;\n        reportingWidget.width = 1;\n        reportingWidget.reportSources = new ReportSource[] {\n                reportSource\n        };\n\n        clientPair.appClient.createWidget(1, reportingWidget);\n        clientPair.appClient.verifyResult(ok(2));\n\n        //a bit upfront\n        long now = System.currentTimeMillis() + 1000;\n        LocalTime localTime = LocalTime.ofInstant(Instant.ofEpochMilli(now), ZoneId.of(\"UTC\"));\n        localTime = LocalTime.of(localTime.getHour(), localTime.getMinute());\n\n        Report report = new Report(1, \"DailyReport\",\n                new ReportSource[] {reportSource},\n                new DailyReport(now, ReportDurationType.INFINITE, 0, 0), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE,\n                Format.ISO_SIMPLE, ZoneId.of(\"UTC\"), 0, 0, null);\n        clientPair.appClient.createReport(1, report);\n\n        report = clientPair.appClient.parseReportFromResponse(3);\n        assertNotNull(report);\n        assertEquals(System.currentTimeMillis(), report.nextReportAt, 3000);\n\n        String date = LocalDate.now(report.tzName).toString();\n        String filename = getUserName() + \"_Blynk_\" + report.id + \"_\" + date + \".zip\";\n        String downloadUrl = \"http://127.0.0.1:18080/\" + filename;\n        verify(holder.mailWrapper, timeout(3000)).sendReportEmail(eq(\"test@gmail.com\"),\n                eq(\"Your daily DailyReport is ready\"),\n                eq(downloadUrl),\n                eq(\"Report name: DailyReport<br>Period: Daily, at \" + localTime));\n        sleep(200);\n        assertEquals(1, holder.reportScheduler.getCompletedTaskCount());\n        assertEquals(2, holder.reportScheduler.getTaskCount());\n\n        Path result = Paths.get(FileUtils.CSV_DIR,\n                getUserName() + \"_\" + AppNameUtil.BLYNK + \"_\" + report.id + \"_\" + date + \".zip\");\n        assertTrue(Files.exists(result));\n        ZipFile zipFile = new ZipFile(result.toString());\n\n        Enumeration<? extends ZipEntry> entries = zipFile.entries();\n        assertTrue(entries.hasMoreElements());\n\n        ZipEntry entry = entries.nextElement();\n        assertNotNull(entry);\n        assertEquals(\"Мійдевайс_2.csv\", entry.getName());\n\n        String nowFormatted = DateTimeFormatter\n                .ofPattern(Format.ISO_SIMPLE.pattern)\n                .withZone(ZoneId.of(\"UTC\"))\n                .format(Instant.ofEpochMilli(pointNow));\n\n        String resultCsvString = readStringFromZipEntry(zipFile, entry);\n        assertNotNull(resultCsvString);\n        assertEquals(nowFormatted + \",v1,1.13\\n\" + nowFormatted + \",v2,1.14\\n\", resultCsvString);\n    }\n\n    @Test\n    public void testFinalFileNameCSVPerDevice2DataStreamWithName() throws Exception {\n        Device device1 = new Device(2, \"My Device2 with big name\", BoardType.ESP8266);\n        clientPair.appClient.createDevice(1, device1);\n\n        String tempDir = holder.props.getProperty(\"data.folder\");\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n        Path pinReportingDataPath10 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.VIRTUAL, (short) 1, GraphGranularityType.MINUTE));\n        Path pinReportingDataPath20 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.VIRTUAL, (short) 2, GraphGranularityType.MINUTE));\n        long pointNow = System.currentTimeMillis();\n        FileUtils.write(pinReportingDataPath10, 1.11D, pointNow);\n        FileUtils.write(pinReportingDataPath20, 1.12D, pointNow);\n\n        Path pinReportingDataPath12 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 2, PinType.VIRTUAL, (short) 1, GraphGranularityType.MINUTE));\n        Path pinReportingDataPath22 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 2, PinType.VIRTUAL, (short) 2, GraphGranularityType.MINUTE));\n        FileUtils.write(pinReportingDataPath12, 1.13D, pointNow);\n        FileUtils.write(pinReportingDataPath22, 1.14D, pointNow);\n\n        ReportDataStream reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, \"Temperature\", true);\n        ReportDataStream reportDataStream2 = new ReportDataStream((short) 2, PinType.VIRTUAL, \"Humidity\", true);\n        ReportSource reportSource = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream, reportDataStream2},\n                1,\n                new int[] {0, 2}\n        );\n\n        ReportingWidget reportingWidget = new ReportingWidget();\n        reportingWidget.height = 1;\n        reportingWidget.width = 1;\n        reportingWidget.reportSources = new ReportSource[] {\n                reportSource\n        };\n\n        clientPair.appClient.createWidget(1, reportingWidget);\n        clientPair.appClient.verifyResult(ok(2));\n\n        //a bit upfront\n        long now = System.currentTimeMillis() + 1000;\n        LocalTime localTime = LocalTime.ofInstant(Instant.ofEpochMilli(now), ZoneId.of(\"UTC\"));\n        localTime = LocalTime.of(localTime.getHour(), localTime.getMinute());\n\n        Report report = new Report(1, \"DailyReport\",\n                new ReportSource[] {reportSource},\n                new DailyReport(now, ReportDurationType.INFINITE, 0, 0), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE,\n                Format.ISO_SIMPLE, ZoneId.of(\"UTC\"), 0, 0, null);\n        clientPair.appClient.createReport(1, report);\n\n        report = clientPair.appClient.parseReportFromResponse(3);\n        assertNotNull(report);\n        assertEquals(System.currentTimeMillis(), report.nextReportAt, 2000);\n\n        String date = LocalDate.now(report.tzName).toString();\n        String filename = getUserName() + \"_Blynk_\" + report.id + \"_\" + date + \".zip\";\n        String downloadUrl = \"http://127.0.0.1:18080/\" + filename;\n        verify(holder.mailWrapper, timeout(3000)).sendReportEmail(eq(\"test@gmail.com\"),\n                eq(\"Your daily DailyReport is ready\"),\n                eq(downloadUrl),\n                eq(\"Report name: DailyReport<br>Period: Daily, at \" + localTime));\n        sleep(200);\n        assertEquals(1, holder.reportScheduler.getCompletedTaskCount());\n        assertEquals(2, holder.reportScheduler.getTaskCount());\n\n        Path result = Paths.get(FileUtils.CSV_DIR,\n                getUserName() + \"_\" + AppNameUtil.BLYNK + \"_\" + report.id + \"_\" + date + \".zip\");\n        assertTrue(Files.exists(result));\n        ZipFile zipFile = new ZipFile(result.toString());\n\n        Enumeration<? extends ZipEntry> entries = zipFile.entries();\n        assertTrue(entries.hasMoreElements());\n\n        ZipEntry entry = entries.nextElement();\n        assertNotNull(entry);\n        assertEquals(\"MyDevice_0.csv\", entry.getName());\n\n        ZipEntry entry2 = entries.nextElement();\n        assertNotNull(entry2);\n        assertEquals(\"MyDevice2withbig_2.csv\", entry2.getName());\n\n        String nowFormatted = DateTimeFormatter\n                .ofPattern(Format.ISO_SIMPLE.pattern)\n                .withZone(ZoneId.of(\"UTC\"))\n                .format(Instant.ofEpochMilli(pointNow));\n\n        String resultCsvString = readStringFromZipEntry(zipFile, entry);\n        assertNotNull(resultCsvString);\n        assertEquals(resultCsvString, nowFormatted + \",Temperature,1.11\\n\" + nowFormatted + \",Humidity,1.12\\n\");\n\n        String resultCsvString2 = readStringFromZipEntry(zipFile, entry2);\n        assertNotNull(resultCsvString2);\n        assertEquals(resultCsvString2, nowFormatted + \",Temperature,1.13\\n\" + nowFormatted + \",Humidity,1.14\\n\");\n    }\n\n    @Test\n    public void testFinalFileNameCSVMerged2DataStreamWithName() throws Exception {\n        Device device1 = new Device(2, \"My Device2 with big name\", BoardType.ESP8266);\n        clientPair.appClient.createDevice(1, device1);\n\n        String tempDir = holder.props.getProperty(\"data.folder\");\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n        Path pinReportingDataPath10 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.VIRTUAL, (short) 1, GraphGranularityType.MINUTE));\n        Path pinReportingDataPath20 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.VIRTUAL, (short) 2, GraphGranularityType.MINUTE));\n        long pointNow = System.currentTimeMillis();\n        FileUtils.write(pinReportingDataPath10, 1.11D, pointNow);\n        FileUtils.write(pinReportingDataPath20, 1.12D, pointNow);\n\n        Path pinReportingDataPath12 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 2, PinType.VIRTUAL, (short) 1, GraphGranularityType.MINUTE));\n        Path pinReportingDataPath22 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 2, PinType.VIRTUAL, (short) 2, GraphGranularityType.MINUTE));\n        FileUtils.write(pinReportingDataPath12, 1.13D, pointNow);\n        FileUtils.write(pinReportingDataPath22, 1.14D, pointNow);\n\n        ReportDataStream reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, \"Temperature\", true);\n        ReportDataStream reportDataStream2 = new ReportDataStream((short) 2, PinType.VIRTUAL, \"Humidity\", true);\n        ReportSource reportSource = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream, reportDataStream2},\n                1,\n                new int[] {0, 2}\n        );\n\n        ReportingWidget reportingWidget = new ReportingWidget();\n        reportingWidget.height = 1;\n        reportingWidget.width = 1;\n        reportingWidget.reportSources = new ReportSource[] {\n                reportSource\n        };\n\n        clientPair.appClient.createWidget(1, reportingWidget);\n        clientPair.appClient.verifyResult(ok(2));\n\n        //a bit upfront\n        long now = System.currentTimeMillis() + 1000;\n        LocalTime localTime = LocalTime.ofInstant(Instant.ofEpochMilli(now), ZoneId.of(\"UTC\"));\n        localTime = LocalTime.of(localTime.getHour(), localTime.getMinute());\n\n        Report report = new Report(1, \"DailyReport\",\n                new ReportSource[] {reportSource},\n                new DailyReport(now, ReportDurationType.INFINITE, 0, 0), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, MERGED_CSV,\n                Format.ISO_SIMPLE, ZoneId.of(\"UTC\"), 0, 0, null);\n        clientPair.appClient.createReport(1, report);\n\n        report = clientPair.appClient.parseReportFromResponse(3);\n        assertNotNull(report);\n        assertEquals(System.currentTimeMillis(), report.nextReportAt, 2000);\n\n        String date = LocalDate.now(report.tzName).toString();\n        String filename = getUserName() + \"_Blynk_\" + report.id + \"_\" + date + \".zip\";\n        String downloadUrl = \"http://127.0.0.1:18080/\" + filename;\n        verify(holder.mailWrapper, timeout(3000)).sendReportEmail(eq(\"test@gmail.com\"),\n                eq(\"Your daily DailyReport is ready\"),\n                eq(downloadUrl),\n                eq(\"Report name: DailyReport<br>Period: Daily, at \" + localTime));\n        sleep(200);\n        assertEquals(1, holder.reportScheduler.getCompletedTaskCount());\n        assertEquals(2, holder.reportScheduler.getTaskCount());\n\n        Path result = Paths.get(FileUtils.CSV_DIR,\n                getUserName() + \"_\" + AppNameUtil.BLYNK + \"_\" + report.id + \"_\" + date + \".zip\");\n        assertTrue(Files.exists(result));\n        ZipFile zipFile = new ZipFile(result.toString());\n\n        Enumeration<? extends ZipEntry> entries = zipFile.entries();\n        assertTrue(entries.hasMoreElements());\n\n        ZipEntry entry = entries.nextElement();\n        assertNotNull(entry);\n        assertEquals(\"DailyReport.csv\", entry.getName());\n\n        String nowFormatted = DateTimeFormatter\n                .ofPattern(Format.ISO_SIMPLE.pattern)\n                .withZone(ZoneId.of(\"UTC\"))\n                .format(Instant.ofEpochMilli(pointNow));\n\n        String resultCsvString = readStringFromZipEntry(zipFile, entry);\n        assertNotNull(resultCsvString);\n        assertEquals(resultCsvString, nowFormatted + \",Temperature,My Device,1.11\\n\" + nowFormatted + \",Humidity,My Device,1.12\\n\"\n                +                     nowFormatted + \",Temperature,My Device2 with big name,1.13\\n\" + nowFormatted + \",Humidity,My Device2 with big name,1.14\\n\");\n    }\n\n    @Test\n    public void testFinalFileNameCSVMerged2DataStreamWithNameCorrectEscaping() throws Exception {\n        Device device1 = new Device(2, \"My Device2 with \\\"big\\\" name\", BoardType.ESP8266);\n        clientPair.appClient.createDevice(1, device1);\n\n        String tempDir = holder.props.getProperty(\"data.folder\");\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n        Path pinReportingDataPath10 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.VIRTUAL, (short) 1, GraphGranularityType.MINUTE));\n        Path pinReportingDataPath20 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.VIRTUAL, (short) 2, GraphGranularityType.MINUTE));\n        long pointNow = System.currentTimeMillis();\n        FileUtils.write(pinReportingDataPath10, 1.11D, pointNow);\n        FileUtils.write(pinReportingDataPath20, 1.12D, pointNow);\n\n        Path pinReportingDataPath12 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 2, PinType.VIRTUAL, (short) 1, GraphGranularityType.MINUTE));\n        Path pinReportingDataPath22 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 2, PinType.VIRTUAL, (short) 2, GraphGranularityType.MINUTE));\n        FileUtils.write(pinReportingDataPath12, 1.13D, pointNow);\n        FileUtils.write(pinReportingDataPath22, 1.14D, pointNow);\n\n        ReportDataStream reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, \"Temperature\", true);\n        ReportDataStream reportDataStream2 = new ReportDataStream((short) 2, PinType.VIRTUAL, \"Humidity\", true);\n        ReportSource reportSource = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream, reportDataStream2},\n                1,\n                new int[] {0, 2}\n        );\n\n        ReportingWidget reportingWidget = new ReportingWidget();\n        reportingWidget.height = 1;\n        reportingWidget.width = 1;\n        reportingWidget.reportSources = new ReportSource[] {\n                reportSource\n        };\n\n        clientPair.appClient.createWidget(1, reportingWidget);\n        clientPair.appClient.verifyResult(ok(2));\n\n        //a bit upfront\n        long now = System.currentTimeMillis() + 1000;\n        LocalTime localTime = LocalTime.ofInstant(Instant.ofEpochMilli(now), ZoneId.of(\"UTC\"));\n        localTime = LocalTime.of(localTime.getHour(), localTime.getMinute());\n\n        Report report = new Report(1, \"DailyReport\",\n                new ReportSource[] {reportSource},\n                new DailyReport(now, ReportDurationType.INFINITE, 0, 0), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, MERGED_CSV,\n                Format.ISO_SIMPLE, ZoneId.of(\"UTC\"), 0, 0, null);\n        clientPair.appClient.createReport(1, report);\n\n        report = clientPair.appClient.parseReportFromResponse(3);\n        assertNotNull(report);\n        assertEquals(System.currentTimeMillis(), report.nextReportAt, 2000);\n\n        String date = LocalDate.now(report.tzName).toString();\n        String filename = getUserName() + \"_Blynk_\" + report.id + \"_\" + date + \".zip\";\n        String downloadUrl = \"http://127.0.0.1:18080/\" + filename;\n        verify(holder.mailWrapper, timeout(3000)).sendReportEmail(eq(\"test@gmail.com\"),\n                eq(\"Your daily DailyReport is ready\"),\n                eq(downloadUrl),\n                eq(\"Report name: DailyReport<br>Period: Daily, at \" + localTime));\n        sleep(200);\n        assertEquals(1, holder.reportScheduler.getCompletedTaskCount());\n        assertEquals(2, holder.reportScheduler.getTaskCount());\n\n        Path result = Paths.get(FileUtils.CSV_DIR,\n                getUserName() + \"_\" + AppNameUtil.BLYNK + \"_\" + report.id + \"_\" + date + \".zip\");\n        assertTrue(Files.exists(result));\n        ZipFile zipFile = new ZipFile(result.toString());\n\n        Enumeration<? extends ZipEntry> entries = zipFile.entries();\n        assertTrue(entries.hasMoreElements());\n\n        ZipEntry entry = entries.nextElement();\n        assertNotNull(entry);\n        assertEquals(\"DailyReport.csv\", entry.getName());\n\n        String nowFormatted = DateTimeFormatter\n                .ofPattern(Format.ISO_SIMPLE.pattern)\n                .withZone(ZoneId.of(\"UTC\"))\n                .format(Instant.ofEpochMilli(pointNow));\n\n        String resultCsvString = readStringFromZipEntry(zipFile, entry);\n        assertNotNull(resultCsvString);\n        assertEquals(resultCsvString, nowFormatted + \",Temperature,My Device,1.11\\n\" + nowFormatted + \",Humidity,My Device,1.12\\n\"\n                +                     nowFormatted + \",Temperature,\\\"My Device2 with \\\"\\\"big\\\"\\\" name\\\",1.13\\n\" + nowFormatted + \",Humidity,\\\"My Device2 with \\\"\\\"big\\\"\\\" name\\\",1.14\\n\");\n    }\n\n    @Test\n    public void testFinalFileNameCSVMerged2DataStreamWithNameCorrectEscaping2() throws Exception {\n        Device device1 = new Device(2, \"My Device2 with, big name\", BoardType.ESP8266);\n        clientPair.appClient.createDevice(1, device1);\n\n        String tempDir = holder.props.getProperty(\"data.folder\");\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n        Path pinReportingDataPath10 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.VIRTUAL, (short) 1, GraphGranularityType.MINUTE));\n        Path pinReportingDataPath20 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.VIRTUAL, (short) 2, GraphGranularityType.MINUTE));\n        long pointNow = System.currentTimeMillis();\n        FileUtils.write(pinReportingDataPath10, 1.11D, pointNow);\n        FileUtils.write(pinReportingDataPath20, 1.12D, pointNow);\n\n        Path pinReportingDataPath12 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 2, PinType.VIRTUAL, (short) 1, GraphGranularityType.MINUTE));\n        Path pinReportingDataPath22 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 2, PinType.VIRTUAL, (short) 2, GraphGranularityType.MINUTE));\n        FileUtils.write(pinReportingDataPath12, 1.13D, pointNow);\n        FileUtils.write(pinReportingDataPath22, 1.14D, pointNow);\n\n        ReportDataStream reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, \"Temperature\", true);\n        ReportDataStream reportDataStream2 = new ReportDataStream((short) 2, PinType.VIRTUAL, \"Humidity\", true);\n        ReportSource reportSource = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream, reportDataStream2},\n                1,\n                new int[] {0, 2}\n        );\n\n        ReportingWidget reportingWidget = new ReportingWidget();\n        reportingWidget.height = 1;\n        reportingWidget.width = 1;\n        reportingWidget.reportSources = new ReportSource[] {\n                reportSource\n        };\n\n        clientPair.appClient.createWidget(1, reportingWidget);\n        clientPair.appClient.verifyResult(ok(2));\n\n        //a bit upfront\n        long now = System.currentTimeMillis() + 1000;\n        LocalTime localTime = LocalTime.ofInstant(Instant.ofEpochMilli(now), ZoneId.of(\"UTC\"));\n        localTime = LocalTime.of(localTime.getHour(), localTime.getMinute());\n\n        Report report = new Report(1, \"DailyReport\",\n                new ReportSource[] {reportSource},\n                new DailyReport(now, ReportDurationType.INFINITE, 0, 0), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, MERGED_CSV,\n                Format.ISO_SIMPLE, ZoneId.of(\"UTC\"), 0, 0, null);\n        clientPair.appClient.createReport(1, report);\n\n        report = clientPair.appClient.parseReportFromResponse(3);\n        assertNotNull(report);\n        assertEquals(System.currentTimeMillis(), report.nextReportAt, 5000);\n\n        String date = LocalDate.now(report.tzName).toString();\n        String filename = getUserName() + \"_Blynk_\" + report.id + \"_\" + date + \".zip\";\n        String downloadUrl = \"http://127.0.0.1:18080/\" + filename;\n        verify(holder.mailWrapper, timeout(3000)).sendReportEmail(eq(\"test@gmail.com\"),\n                eq(\"Your daily DailyReport is ready\"),\n                eq(downloadUrl),\n                eq(\"Report name: DailyReport<br>Period: Daily, at \" + localTime));\n        sleep(200);\n        assertEquals(1, holder.reportScheduler.getCompletedTaskCount());\n        assertEquals(2, holder.reportScheduler.getTaskCount());\n\n        Path result = Paths.get(FileUtils.CSV_DIR,\n                getUserName() + \"_\" + AppNameUtil.BLYNK + \"_\" + report.id + \"_\" + date + \".zip\");\n        assertTrue(Files.exists(result));\n        ZipFile zipFile = new ZipFile(result.toString());\n\n        Enumeration<? extends ZipEntry> entries = zipFile.entries();\n        assertTrue(entries.hasMoreElements());\n\n        ZipEntry entry = entries.nextElement();\n        assertNotNull(entry);\n        assertEquals(\"DailyReport.csv\", entry.getName());\n\n        String nowFormatted = DateTimeFormatter\n                .ofPattern(Format.ISO_SIMPLE.pattern)\n                .withZone(ZoneId.of(\"UTC\"))\n                .format(Instant.ofEpochMilli(pointNow));\n\n        String resultCsvString = readStringFromZipEntry(zipFile, entry);\n        assertNotNull(resultCsvString);\n        assertEquals(resultCsvString, nowFormatted + \",Temperature,My Device,1.11\\n\" + nowFormatted + \",Humidity,My Device,1.12\\n\"\n                +                     nowFormatted + \",Temperature,\\\"My Device2 with, big name\\\",1.13\\n\" + nowFormatted + \",Humidity,\\\"My Device2 with, big name\\\",1.14\\n\");\n    }\n\n    @Test\n    public void testDailyReportWithSinglePointIsTriggeredAndExpired() throws Exception {\n        String tempDir = holder.props.getProperty(\"data.folder\");\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n        Path pinReportingDataPath10 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.VIRTUAL, (short) 1, GraphGranularityType.MINUTE));\n        long pointNow = System.currentTimeMillis();\n        FileUtils.write(pinReportingDataPath10, 1.11D, pointNow);\n\n        ReportDataStream reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, null, true);\n        ReportSource reportSource = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream},\n                1,\n                new int[] {0}\n        );\n\n        ReportingWidget reportingWidget = new ReportingWidget();\n        reportingWidget.id = 222222;\n        reportingWidget.height = 1;\n        reportingWidget.width = 1;\n        reportingWidget.reportSources = new ReportSource[] {\n                reportSource\n        };\n\n        clientPair.appClient.createWidget(1, reportingWidget);\n        clientPair.appClient.verifyResult(ok(1));\n\n        //a bit upfront\n        long now = System.currentTimeMillis() + 1500;\n\n        Report report = new Report(1, \"DailyReport\",\n                new ReportSource[] {reportSource},\n                new DailyReport(now, ReportDurationType.CUSTOM, now, now), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE_PER_PIN,\n                Format.ISO_SIMPLE, ZoneId.of(\"UTC\"), 0, 0, null);\n        clientPair.appClient.createReport(1, report);\n\n        report = clientPair.appClient.parseReportFromResponse(2);\n        assertNotNull(report);\n        assertEquals(System.currentTimeMillis(), report.nextReportAt, 3000);\n\n        String date = LocalDate.now(report.tzName).toString();\n        String filename = getUserName() + \"_Blynk_\" + report.id + \"_\" + date + \".zip\";\n        verify(holder.mailWrapper, timeout(3000)).sendReportEmail(eq(\"test@gmail.com\"),\n                eq(\"Your daily DailyReport is ready\"),\n                eq(\"http://127.0.0.1:18080/\" + filename),\n                any());\n        sleep(200);\n        assertEquals(1, holder.reportScheduler.getCompletedTaskCount());\n        assertEquals(1, holder.reportScheduler.getTaskCount());\n\n        Path result = Paths.get(FileUtils.CSV_DIR,\n                getUserName() + \"_\" + AppNameUtil.BLYNK + \"_\" + report.id + \"_\" + date + \".zip\");\n        assertTrue(Files.exists(result));\n        String resultCsvString = readStringFromFirstZipEntry(result);\n        String[] split = resultCsvString.split(\"[,\\n]\");\n        String nowFormatted = DateTimeFormatter\n                .ofPattern(Format.ISO_SIMPLE.pattern)\n                .withZone(ZoneId.of(\"UTC\"))\n                .format(Instant.ofEpochMilli(pointNow));\n        assertEquals(nowFormatted, split[0]);\n        assertEquals(1.11D, Double.parseDouble(split[1]), 0.0001);\n\n        clientPair.appClient.getWidget(1, 222222);\n        ReportingWidget reportingWidget2 = (ReportingWidget) JsonParser.parseWidget(clientPair.appClient.getBody(3), 0);\n        assertNotNull(reportingWidget2);\n        assertEquals(EXPIRED, reportingWidget2.reports[0].lastRunResult);\n    }\n\n    @Test\n    public void testOneTimeReportIsTriggered() throws Exception {\n        String tempDir = holder.props.getProperty(\"data.folder\");\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n        Path pinReportingDataPath10 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.VIRTUAL, (short) 1, GraphGranularityType.MINUTE));\n        long now = System.currentTimeMillis();\n        FileUtils.write(pinReportingDataPath10, 1.11D, now);\n\n        ReportDataStream reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, \"Temperature\", true);\n        ReportSource reportSource = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream},\n                1,\n                new int[] {0}\n        );\n\n        ReportingWidget reportingWidget = new ReportingWidget();\n        reportingWidget.height = 1;\n        reportingWidget.width = 1;\n        reportingWidget.reportSources = new ReportSource[] {\n                reportSource\n        };\n\n        clientPair.appClient.createWidget(1, reportingWidget);\n        clientPair.appClient.verifyResult(ok(1));\n\n        Report report = new Report(1, \"OneTime Report\",\n                new ReportSource[] {reportSource},\n                new OneTimeReport(TimeUnit.DAYS.toMillis(1)), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE_PER_PIN,\n                Format.ISO_SIMPLE, ZoneId.of(\"UTC\"), 0, 0, null);\n\n        clientPair.appClient.createReport(1, report);\n        report = clientPair.appClient.parseReportFromResponse(2);\n        assertNotNull(report);\n        assertEquals(0, report.nextReportAt);\n        assertEquals(0, report.lastReportAt);\n\n        verify(holder.mailWrapper, never()).sendReportEmail(eq(\"test@gmail.com\"),\n                any(),\n                any(),\n                any());\n\n        clientPair.appClient.exportReport(1, 1);\n        report = clientPair.appClient.parseReportFromResponse(3);\n        assertNotNull(report);\n        assertEquals(0, report.nextReportAt);\n        assertEquals(System.currentTimeMillis(), report.lastReportAt, 2000);\n        assertEquals(OK, report.lastRunResult);\n\n        String date = LocalDate.now(report.tzName).toString();\n        String filename = getUserName() + \"_Blynk_\" + report.id + \"_\" + date + \".zip\";\n        verify(holder.mailWrapper, timeout(3000)).sendReportEmail(eq(\"test@gmail.com\"),\n                eq(\"Your one time OneTime Report is ready\"),\n                eq(\"http://127.0.0.1:18080/\" + filename),\n                eq(\"Report name: OneTime Report<br>Period: One time\"));\n        sleep(200);\n        assertEquals(1, holder.reportScheduler.getCompletedTaskCount());\n        assertEquals(1, holder.reportScheduler.getTaskCount());\n        assertEquals(0, holder.reportScheduler.map.size());\n        assertEquals(0, holder.reportScheduler.getActiveCount());\n\n        Path result = Paths.get(FileUtils.CSV_DIR,\n                getUserName() + \"_\" + AppNameUtil.BLYNK + \"_\" + report.id + \"_\" + date + \".zip\");\n        assertTrue(Files.exists(result));\n        String resultCsvString = readStringFromFirstZipEntry(result);\n        assertNotNull(resultCsvString);\n        String[] split = resultCsvString.split(\"[,\\n]\");\n        String nowFormatted = DateTimeFormatter\n                .ofPattern(Format.ISO_SIMPLE.pattern)\n                .withZone(ZoneId.of(\"UTC\"))\n                .format(Instant.ofEpochMilli(now));\n        assertEquals(nowFormatted, split[0]);\n        assertEquals(1.11D, Double.parseDouble(split[1]), 0.0001);\n    }\n\n    @Test\n    public void testOneTimeReportWithWrongSources() throws Exception {\n        String tempDir = holder.props.getProperty(\"data.folder\");\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n        Path pinReportingDataPath10 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.VIRTUAL, (short) 1, GraphGranularityType.MINUTE));\n        long now = System.currentTimeMillis();\n        FileUtils.write(pinReportingDataPath10, 1.11D, now);\n\n        ReportDataStream reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, \"Temperature\", true);\n        ReportSource reportSource = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream},\n                1,\n                new int[] {0}\n        );\n        ReportSource reportSource2 = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream},\n                1,\n                new int[] {0}\n        );\n\n        ReportingWidget reportingWidget = new ReportingWidget();\n        reportingWidget.height = 1;\n        reportingWidget.width = 1;\n        reportingWidget.reportSources = new ReportSource[] {\n                reportSource\n        };\n\n        clientPair.appClient.createWidget(1, reportingWidget);\n        clientPair.appClient.verifyResult(ok(1));\n\n        Report report = new Report(1, \"OneTime Report\",\n                new ReportSource[] {reportSource, reportSource2},\n                new OneTimeReport(TimeUnit.DAYS.toMillis(1)), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE_PER_PIN,\n                Format.ISO_SIMPLE, ZoneId.of(\"UTC\"), 0, 0, null);\n\n        clientPair.appClient.createReport(1, report);\n        report = clientPair.appClient.parseReportFromResponse(2);\n        assertNotNull(report);\n        assertEquals(0, report.nextReportAt);\n        assertEquals(0, report.lastReportAt);\n\n        verify(holder.mailWrapper, never()).sendReportEmail(eq(\"test@gmail.com\"),\n                any(),\n                any(),\n                any());\n\n        clientPair.appClient.exportReport(1, 1);\n        report = clientPair.appClient.parseReportFromResponse(3);\n        assertNotNull(report);\n        assertEquals(0, report.nextReportAt);\n        assertEquals(System.currentTimeMillis(), report.lastReportAt, 2000);\n        assertEquals(OK, report.lastRunResult);\n\n        String date = LocalDate.now(report.tzName).toString();\n        String filename = getUserName() + \"_Blynk_\" + report.id + \"_\" + date + \".zip\";\n        verify(holder.mailWrapper, timeout(3000)).sendReportEmail(eq(\"test@gmail.com\"),\n                eq(\"Your one time OneTime Report is ready\"),\n                eq(\"http://127.0.0.1:18080/\" + filename),\n                eq(\"Report name: OneTime Report<br>Period: One time\"));\n        sleep(200);\n        assertEquals(1, holder.reportScheduler.getCompletedTaskCount());\n        assertEquals(1, holder.reportScheduler.getTaskCount());\n        assertEquals(0, holder.reportScheduler.map.size());\n        assertEquals(0, holder.reportScheduler.getActiveCount());\n\n        Path result = Paths.get(FileUtils.CSV_DIR,\n                getUserName() + \"_\" + AppNameUtil.BLYNK + \"_\" + report.id + \"_\" + date + \".zip\");\n        assertTrue(Files.exists(result));\n        String resultCsvString = readStringFromFirstZipEntry(result);\n        assertNotNull(resultCsvString);\n        String[] split = resultCsvString.split(\"[,\\n]\");\n        String nowFormatted = DateTimeFormatter\n                .ofPattern(Format.ISO_SIMPLE.pattern)\n                .withZone(ZoneId.of(\"UTC\"))\n                .format(Instant.ofEpochMilli(now));\n        assertEquals(nowFormatted, split[0]);\n        assertEquals(1.11D, Double.parseDouble(split[1]), 0.0001);\n    }\n\n    @Test\n    public void testMultipleReceiversFroOneTimeReport() throws Exception {\n        String tempDir = holder.props.getProperty(\"data.folder\");\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n        Path pinReportingDataPath10 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.VIRTUAL, (short) 1, GraphGranularityType.MINUTE));\n        long now = System.currentTimeMillis();\n        FileUtils.write(pinReportingDataPath10, 1.11D, now);\n\n        ReportDataStream reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, \"Temperature\", true);\n        ReportSource reportSource = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream},\n                1,\n                new int[] {0}\n        );\n\n        ReportingWidget reportingWidget = new ReportingWidget();\n        reportingWidget.height = 1;\n        reportingWidget.width = 1;\n        reportingWidget.reportSources = new ReportSource[] {\n                reportSource\n        };\n\n        clientPair.appClient.createWidget(1, reportingWidget);\n        clientPair.appClient.verifyResult(ok(1));\n\n        Report report = new Report(1, \"OneTime Report\",\n                new ReportSource[] {reportSource},\n                new OneTimeReport(TimeUnit.DAYS.toMillis(1)), \"test@gmail.com,test2@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE_PER_PIN,\n                Format.ISO_SIMPLE, ZoneId.of(\"UTC\"), 0, 0, null);\n\n        clientPair.appClient.createReport(1, report);\n        report = clientPair.appClient.parseReportFromResponse(2);\n        assertNotNull(report);\n        assertEquals(0, report.nextReportAt);\n        assertEquals(0, report.lastReportAt);\n\n        verify(holder.mailWrapper, never()).sendReportEmail(eq(\"test@gmail.com,test2@gmail.com\"),\n                any(),\n                any(),\n                any());\n\n        clientPair.appClient.exportReport(1, 1);\n        report = clientPair.appClient.parseReportFromResponse(3);\n        assertNotNull(report);\n        assertEquals(0, report.nextReportAt);\n        assertEquals(System.currentTimeMillis(), report.lastReportAt, 2000);\n        assertEquals(OK, report.lastRunResult);\n\n        String date = LocalDate.now(report.tzName).toString();\n        String filename = getUserName() + \"_Blynk_\" + report.id + \"_\" + date + \".zip\";\n        verify(holder.mailWrapper, timeout(3000)).sendReportEmail(eq(\"test@gmail.com,test2@gmail.com\"),\n                eq(\"Your one time OneTime Report is ready\"),\n                eq(\"http://127.0.0.1:18080/\" + filename),\n                eq(\"Report name: OneTime Report<br>Period: One time\"));\n        sleep(200);\n        assertEquals(1, holder.reportScheduler.getCompletedTaskCount());\n        assertEquals(1, holder.reportScheduler.getTaskCount());\n        assertEquals(0, holder.reportScheduler.map.size());\n        assertEquals(0, holder.reportScheduler.getActiveCount());\n\n        Path result = Paths.get(FileUtils.CSV_DIR,\n                getUserName() + \"_\" + AppNameUtil.BLYNK + \"_\" + report.id + \"_\" + date + \".zip\");\n        assertTrue(Files.exists(result));\n        String resultCsvString = readStringFromFirstZipEntry(result);\n        assertNotNull(resultCsvString);\n        String[] split = resultCsvString.split(\"[,\\n]\");\n        String nowFormatted = DateTimeFormatter\n                .ofPattern(Format.ISO_SIMPLE.pattern)\n                .withZone(ZoneId.of(\"UTC\"))\n                .format(Instant.ofEpochMilli(now));\n        assertEquals(nowFormatted, split[0]);\n        assertEquals(1.11D, Double.parseDouble(split[1]), 0.0001);\n    }\n\n    @Test\n    public void testStreamsAreCorrectlyFiltered() throws Exception {\n        String tempDir = holder.props.getProperty(\"data.folder\");\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n        Path pinReportingDataPath11 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.VIRTUAL, (short) 1, GraphGranularityType.MINUTE));\n        Path pinReportingDataPath12 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.VIRTUAL, (short) 2, GraphGranularityType.MINUTE));\n        long now = System.currentTimeMillis();\n        FileUtils.write(pinReportingDataPath11, 1.11D, now);\n        FileUtils.write(pinReportingDataPath12, 1.12D, now);\n\n        ReportDataStream reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, \"Temperature\", true);\n        ReportDataStream reportDataStream2 = new ReportDataStream((short) 2, PinType.VIRTUAL, \"Temperature2\", true);\n        ReportSource reportSource = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream, reportDataStream2},\n                1,\n                new int[] {0}\n        );\n\n        ReportingWidget reportingWidget = new ReportingWidget();\n        reportingWidget.height = 1;\n        reportingWidget.width = 1;\n        reportingWidget.reportSources = new ReportSource[] {\n                reportSource\n        };\n\n        clientPair.appClient.createWidget(1, reportingWidget);\n        clientPair.appClient.verifyResult(ok(1));\n\n        reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, \"Temperature\", false);\n        Report report = new Report(1, \"OneTime Report\",\n                new ReportSource[] {new TileTemplateReportSource(\n                        new ReportDataStream[] {reportDataStream, reportDataStream2},\n                        1,\n                        new int[] {0}\n                )},\n                new OneTimeReport(TimeUnit.DAYS.toMillis(1)), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE_PER_PIN,\n                Format.ISO_SIMPLE, ZoneId.of(\"UTC\"), 0, 0, null);\n\n        clientPair.appClient.createReport(1, report);\n        report = clientPair.appClient.parseReportFromResponse(2);\n        assertNotNull(report);\n        assertEquals(0, report.nextReportAt);\n        assertEquals(0, report.lastReportAt);\n\n        verify(holder.mailWrapper, never()).sendReportEmail(eq(\"test@gmail.com\"),\n                any(),\n                any(),\n                any());\n\n        clientPair.appClient.exportReport(1, 1);\n        report = clientPair.appClient.parseReportFromResponse(3);\n        assertNotNull(report);\n        assertEquals(0, report.nextReportAt);\n        assertEquals(System.currentTimeMillis(), report.lastReportAt, 2000);\n        assertEquals(OK, report.lastRunResult);\n\n        String date = LocalDate.now(report.tzName).toString();\n        String filename = getUserName() + \"_Blynk_\" + report.id + \"_\" + date + \".zip\";\n        verify(holder.mailWrapper, timeout(3000)).sendReportEmail(eq(\"test@gmail.com\"),\n                eq(\"Your one time OneTime Report is ready\"),\n                eq(\"http://127.0.0.1:18080/\" + filename),\n                eq(\"Report name: OneTime Report<br>Period: One time\"));\n        sleep(200);\n        assertEquals(1, holder.reportScheduler.getCompletedTaskCount());\n        assertEquals(1, holder.reportScheduler.getTaskCount());\n        assertEquals(0, holder.reportScheduler.map.size());\n        assertEquals(0, holder.reportScheduler.getActiveCount());\n\n        Path result = Paths.get(FileUtils.CSV_DIR,\n                getUserName() + \"_\" + AppNameUtil.BLYNK + \"_\" + report.id + \"_\" + date + \".zip\");\n        assertTrue(Files.exists(result));\n        String resultCsvString = readStringFromFirstZipEntry(result);\n        assertNotNull(resultCsvString);\n        String[] split = resultCsvString.split(\"[,\\n]\");\n        String nowFormatted = DateTimeFormatter\n                .ofPattern(Format.ISO_SIMPLE.pattern)\n                .withZone(ZoneId.of(\"UTC\"))\n                .format(Instant.ofEpochMilli(now));\n        assertEquals(2, split.length);\n        assertEquals(nowFormatted, split[0]);\n        assertEquals(1.12D, Double.parseDouble(split[1]), 0.0001);\n    }\n\n    @Test\n    public void testExportIsLimited() throws Exception {\n        String tempDir = holder.props.getProperty(\"data.folder\");\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n        Path pinReportingDataPath10 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.VIRTUAL, (short) 1, GraphGranularityType.MINUTE));\n        long now = System.currentTimeMillis();\n        FileUtils.write(pinReportingDataPath10, 1.11D, now);\n\n        ReportDataStream reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, \"Temperature\", true);\n        ReportSource reportSource = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream},\n                1,\n                new int[] {0}\n        );\n\n        ReportingWidget reportingWidget = new ReportingWidget();\n        reportingWidget.height = 1;\n        reportingWidget.width = 1;\n        reportingWidget.reportSources = new ReportSource[] {\n                reportSource\n        };\n\n        clientPair.appClient.createWidget(1, reportingWidget);\n        clientPair.appClient.verifyResult(ok(1));\n\n        Report report = new Report(1, \"OneTime Report\",\n                new ReportSource[] {reportSource},\n                new OneTimeReport(TimeUnit.DAYS.toMillis(1)), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE_PER_PIN,\n                Format.ISO_SIMPLE, ZoneId.of(\"UTC\"), 0, 0, null);\n\n        clientPair.appClient.createReport(1, report);\n        report = clientPair.appClient.parseReportFromResponse(2);\n        assertNotNull(report);\n        assertEquals(0, report.nextReportAt);\n        assertEquals(0, report.lastReportAt);\n\n        verify(holder.mailWrapper, never()).sendReportEmail(eq(\"test@gmail.com\"),\n                any(),\n                any(),\n                any());\n\n        clientPair.appClient.exportReport(1, 1);\n        report = clientPair.appClient.parseReportFromResponse(3);\n        assertNotNull(report);\n        assertEquals(0, report.nextReportAt);\n        assertEquals(System.currentTimeMillis(), report.lastReportAt, 2000);\n        assertEquals(OK, report.lastRunResult);\n\n        clientPair.appClient.exportReport(1, 1);\n        clientPair.appClient.verifyResult(new ResponseMessage(4, QUOTA_LIMIT));\n    }\n\n    @Test\n    public void testOneTimeReportIsTriggeredWithAnotherFormat() throws Exception {\n        String tempDir = holder.props.getProperty(\"data.folder\");\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n        Path pinReportingDataPath10 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.VIRTUAL, (short) 1, GraphGranularityType.MINUTE));\n        long now = System.currentTimeMillis();\n        FileUtils.write(pinReportingDataPath10, 1.11D, now);\n\n        ReportDataStream reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, \"Temperature\", true);\n        ReportSource reportSource = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream},\n                1,\n                new int[] {0}\n        );\n\n        ReportingWidget reportingWidget = new ReportingWidget();\n        reportingWidget.height = 1;\n        reportingWidget.width = 1;\n        reportingWidget.reportSources = new ReportSource[] {\n                reportSource\n        };\n\n        clientPair.appClient.createWidget(1, reportingWidget);\n        clientPair.appClient.verifyResult(ok(1));\n\n        Report report = new Report(1, \"OneTime Report\",\n                new ReportSource[] {reportSource},\n                new OneTimeReport(TimeUnit.DAYS.toMillis(1)), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE_PER_PIN,\n                Format.TS, ZoneId.of(\"UTC\"), 0, 0, null);\n\n        clientPair.appClient.createReport(1, report);\n        report = clientPair.appClient.parseReportFromResponse(2);\n        assertNotNull(report);\n        assertEquals(0, report.nextReportAt);\n        assertEquals(0, report.lastReportAt);\n\n        verify(holder.mailWrapper, never()).sendReportEmail(eq(\"test@gmail.com\"),\n                any(),\n                any(),\n                any());\n\n        clientPair.appClient.exportReport(1, 1);\n        report = clientPair.appClient.parseReportFromResponse(3);\n        assertNotNull(report);\n        assertEquals(0, report.nextReportAt);\n        assertEquals(System.currentTimeMillis(), report.lastReportAt, 2000);\n        assertEquals(OK, report.lastRunResult);\n\n        String date = LocalDate.now(report.tzName).toString();\n        String filename = getUserName() + \"_Blynk_\" + report.id + \"_\" + date + \".zip\";\n        verify(holder.mailWrapper, timeout(3000)).sendReportEmail(eq(\"test@gmail.com\"),\n                eq(\"Your one time OneTime Report is ready\"),\n                eq(\"http://127.0.0.1:18080/\" + filename),\n                eq(\"Report name: OneTime Report<br>Period: One time\"));\n        sleep(200);\n        assertEquals(1, holder.reportScheduler.getCompletedTaskCount());\n        assertEquals(1, holder.reportScheduler.getTaskCount());\n        assertEquals(0, holder.reportScheduler.map.size());\n        assertEquals(0, holder.reportScheduler.getActiveCount());\n\n        Path result = Paths.get(FileUtils.CSV_DIR,\n                getUserName() + \"_\" + AppNameUtil.BLYNK + \"_\" + report.id + \"_\" + date + \".zip\");\n        assertTrue(Files.exists(result));\n        String resultCsvString = readStringFromFirstZipEntry(result);\n        assertNotNull(resultCsvString);\n        String[] split = resultCsvString.split(\"[,\\n]\");\n        assertEquals(now, Long.parseLong(split[0]), 2000);\n        assertEquals(1.11D, Double.parseDouble(split[1]), 0.0001);\n    }\n\n    @Test\n    public void testOneTimeReportIsTriggeredWithCustomJson() throws Exception {\n        ReportDataStream reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, \"Temperature\", true);\n        ReportSource reportSource = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream},\n                1,\n                new int[] {0}\n        );\n\n        ReportingWidget reportingWidget = new ReportingWidget();\n        reportingWidget.height = 1;\n        reportingWidget.width = 1;\n        reportingWidget.reportSources = new ReportSource[] {\n                reportSource\n        };\n\n        clientPair.appClient.createWidget(1, reportingWidget);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.createReport(1, \"{\\\"id\\\":12838,\\\"name\\\":\\\"Report\\\",\\\"reportSources\\\":[{\\\"type\\\":\\\"TILE_TEMPLATE\\\",\\\"templateId\\\":11844,\\\"deviceIds\\\":[0],\\\"reportDataStreams\\\":[{\\\"pin\\\":1,\\\"pinType\\\":\\\"VIRTUAL\\\",\\\"label\\\":\\\"Temperature\\\",\\\"isSelected\\\":true},{\\\"pin\\\":0,\\\"pinType\\\":\\\"VIRTUAL\\\",\\\"label\\\":\\\"Humidity\\\",\\\"isSelected\\\":true},{\\\"pin\\\":2,\\\"pinType\\\":\\\"VIRTUAL\\\",\\\"label\\\":\\\"Heat\\\",\\\"isSelected\\\":true}]}],\\\"reportType\\\":{\\\"type\\\":\\\"ONE_TIME\\\",\\\"rangeMillis\\\":86400000},\\\"recipients\\\":\\\"alexkipar@gmail.com\\\",\\\"granularityType\\\":\\\"HOURLY\\\",\\\"isActive\\\":true,\\\"reportOutput\\\":\\\"CSV_FILE_PER_DEVICE_PER_PIN\\\",\\\"tzName\\\":\\\"Europe/Kiev\\\",\\\"nextReportAt\\\":0,\\\"lastReportAt\\\":1528309698795,\\\"lastRunResult\\\":\\\"ERROR\\\"}\");\n        Report report = clientPair.appClient.parseReportFromResponse(2);\n        assertNotNull(report);\n\n        clientPair.appClient.exportReport(1, 12838);\n        report = clientPair.appClient.parseReportFromResponse(3);\n        assertNotNull(report);\n        assertEquals(0, report.nextReportAt);\n        assertEquals(System.currentTimeMillis(), report.lastReportAt, 2000);\n        assertEquals(ReportResult.NO_DATA, report.lastRunResult);\n    }\n\n    @Test\n    public void testOneTimeReportIsTriggeredAndNoData() throws Exception {\n        ReportDataStream reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, \"Temperature\", true);\n        ReportSource reportSource = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream},\n                1,\n                new int[] {0}\n        );\n\n        ReportingWidget reportingWidget = new ReportingWidget();\n        reportingWidget.height = 1;\n        reportingWidget.width = 1;\n        reportingWidget.reportSources = new ReportSource[] {\n                reportSource\n        };\n\n        clientPair.appClient.createWidget(1, reportingWidget);\n        clientPair.appClient.verifyResult(ok(1));\n\n        Report report = new Report(1, \"OneTime Report\",\n                new ReportSource[] {reportSource},\n                new OneTimeReport(TimeUnit.DAYS.toMillis(1)), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE_PER_PIN, null, ZoneId.of(\"UTC\"), 0, 0, null);\n\n        clientPair.appClient.createReport(1, report);\n        report = clientPair.appClient.parseReportFromResponse(2);\n        assertNotNull(report);\n        assertEquals(0, report.nextReportAt);\n        assertEquals(0, report.lastReportAt);\n\n        clientPair.appClient.exportReport(1, 1);\n        report = clientPair.appClient.parseReportFromResponse(3);\n        assertNotNull(report);\n        assertEquals(0, report.nextReportAt);\n        assertEquals(System.currentTimeMillis(), report.lastReportAt, 2000);\n        assertEquals(ReportResult.NO_DATA, report.lastRunResult);\n\n        verify(holder.mailWrapper, never()).sendReportEmail(eq(\"test@gmail.com\"),\n                any(),\n                any(),\n                any());\n        sleep(200);\n        assertEquals(1, holder.reportScheduler.getCompletedTaskCount());\n        assertEquals(1, holder.reportScheduler.getTaskCount());\n        assertEquals(0, holder.reportScheduler.map.size());\n        assertEquals(0, holder.reportScheduler.getActiveCount());\n    }\n\n    @Test\n    public void testOneTimeReportIsTriggeredAndNoData2() throws Exception {\n        String tempDir = holder.props.getProperty(\"data.folder\");\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n        Path pinReportingDataPath10 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.VIRTUAL, (short) 1, GraphGranularityType.MINUTE));\n        FileUtils.write(pinReportingDataPath10, 1.11D, 111111);\n\n\n        ReportDataStream reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, \"Temperature\", true);\n        ReportSource reportSource = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream},\n                1,\n                new int[] {0}\n        );\n\n        ReportingWidget reportingWidget = new ReportingWidget();\n        reportingWidget.height = 1;\n        reportingWidget.width = 1;\n        reportingWidget.reportSources = new ReportSource[] {\n                reportSource\n        };\n\n        clientPair.appClient.createWidget(1, reportingWidget);\n        clientPair.appClient.verifyResult(ok(1));\n\n        Report report = new Report(1, \"OneTime Report\",\n                new ReportSource[] {reportSource},\n                new OneTimeReport(TimeUnit.DAYS.toMillis(1)), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE_PER_PIN, null, ZoneId.of(\"UTC\"), 0, 0, null);\n\n        clientPair.appClient.createReport(1, report);\n        report = clientPair.appClient.parseReportFromResponse(2);\n        assertNotNull(report);\n        assertEquals(0, report.nextReportAt);\n        assertEquals(0, report.lastReportAt);\n\n        clientPair.appClient.exportReport(1, 1);\n        report = clientPair.appClient.parseReportFromResponse(3);\n        assertNotNull(report);\n        assertEquals(0, report.nextReportAt);\n        assertEquals(System.currentTimeMillis(), report.lastReportAt, 2000);\n        assertEquals(ReportResult.NO_DATA, report.lastRunResult);\n\n        verify(holder.mailWrapper, never()).sendReportEmail(eq(\"test@gmail.com\"),\n                any(),\n                any(),\n                any());\n        sleep(200);\n        assertEquals(1, holder.reportScheduler.getCompletedTaskCount());\n        assertEquals(1, holder.reportScheduler.getTaskCount());\n        assertEquals(0, holder.reportScheduler.map.size());\n        assertEquals(0, holder.reportScheduler.getActiveCount());\n    }\n\n    @Test\n    public void testExpiredReportIsNotAddedToTheProject() throws Exception {\n        ReportDataStream reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, \"Temperature\", true);\n        ReportSource reportSource = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream},\n                1,\n                new int[] {0}\n        );\n\n        ReportingWidget reportingWidget = new ReportingWidget();\n        reportingWidget.id = 222222;\n        reportingWidget.height = 1;\n        reportingWidget.width = 1;\n        reportingWidget.reportSources = new ReportSource[] {\n                reportSource\n        };\n\n        clientPair.appClient.createWidget(1, reportingWidget);\n        clientPair.appClient.verifyResult(ok(1));\n\n        long now = System.currentTimeMillis() + 1500;\n\n        Report report = new Report(1, \"MonthlyReport\",\n                new ReportSource[] {reportSource},\n                new MonthlyReport(now, ReportDurationType.CUSTOM, now, now, DayOfMonth.FIRST), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE_PER_PIN, null, ZoneId.of(\"UTC\"), 0, 0, null);\n\n        clientPair.appClient.createReport(1, report);\n        clientPair.appClient.verifyResult(illegalCommandBody(2));\n\n        clientPair.appClient.getWidget(1, 222222);\n        ReportingWidget reportingWidget2 = (ReportingWidget) JsonParser.parseWidget(clientPair.appClient.getBody(3), 0);\n        assertNotNull(reportingWidget2);\n        assertEquals(0, reportingWidget2.reports.length);\n\n\n        verify(holder.mailWrapper, never()).sendReportEmail(eq(\"test@gmail.com\"),\n                any(),\n                any(),\n                any());\n        sleep(200);\n        assertEquals(0, holder.reportScheduler.getCompletedTaskCount());\n        assertEquals(0, holder.reportScheduler.getTaskCount());\n        assertEquals(0, holder.reportScheduler.map.size());\n        assertEquals(0, holder.reportScheduler.getActiveCount());\n    }\n\n    @Test\n    public void testExpiredReportIsNotAddedToTheProject2() throws Exception {\n        ReportDataStream reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, \"Temperature\", true);\n        ReportSource reportSource = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream},\n                1,\n                new int[] {0}\n        );\n\n        ReportingWidget reportingWidget = new ReportingWidget();\n        reportingWidget.id = 222222;\n        reportingWidget.height = 1;\n        reportingWidget.width = 1;\n        reportingWidget.reportSources = new ReportSource[] {\n                reportSource\n        };\n\n        clientPair.appClient.createWidget(1, reportingWidget);\n        clientPair.appClient.verifyResult(ok(1));\n\n        long now = System.currentTimeMillis() + 1500;\n\n        Report report = new Report(1, \"MonthlyReport\",\n                new ReportSource[] {reportSource},\n                new MonthlyReport(now, ReportDurationType.CUSTOM, now, now + 30L * 86400_000, DayOfMonth.FIRST), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE_PER_PIN, null, ZoneId.of(\"UTC\"), 0, 0, null);\n\n        clientPair.appClient.createReport(1, report);\n        report = clientPair.appClient.parseReportFromResponse(2);\n        assertNotNull(report);\n\n        report = new Report(1, \"MonthlyReport2\",\n                new ReportSource[] {reportSource},\n                new MonthlyReport(now, ReportDurationType.CUSTOM, now, now, DayOfMonth.FIRST), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE_PER_PIN, null, ZoneId.of(\"UTC\"), 0, 0, null);\n\n        clientPair.appClient.updateReport(1, report);\n        clientPair.appClient.verifyResult(illegalCommandBody(3));\n\n        clientPair.appClient.getWidget(1, 222222);\n        ReportingWidget reportingWidget2 = (ReportingWidget) JsonParser.parseWidget(clientPair.appClient.getBody(4), 0);\n        assertNotNull(reportingWidget2);\n        assertEquals(1, reportingWidget2.reports.length);\n        assertEquals(\"MonthlyReport\", reportingWidget2.reports[0].name);\n    }\n\n    @Test\n    public void testDailyReportWithSinglePointIsTriggeredAndOneRecordIsFiltered() throws Exception {\n        String tempDir = holder.props.getProperty(\"data.folder\");\n        Path userReportFolder = Paths.get(tempDir, \"data\", getUserName());\n        if (Files.notExists(userReportFolder)) {\n            Files.createDirectories(userReportFolder);\n        }\n        Path pinReportingDataPath10 = Paths.get(tempDir, \"data\", getUserName(),\n                ReportingDiskDao.generateFilename(1, 0, PinType.VIRTUAL, (short) 1, GraphGranularityType.MINUTE));\n        long pointNow = System.currentTimeMillis();\n        FileUtils.write(pinReportingDataPath10, 1.12D, pointNow - TimeUnit.HOURS.toMillis(25));\n        FileUtils.write(pinReportingDataPath10, 1.11D, pointNow);\n\n        ReportDataStream reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, null, true);\n        ReportSource reportSource = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream},\n                1,\n                new int[] {0}\n        );\n\n        ReportingWidget reportingWidget = new ReportingWidget();\n        reportingWidget.id = 222222;\n        reportingWidget.height = 1;\n        reportingWidget.width = 1;\n        reportingWidget.reportSources = new ReportSource[] {\n                reportSource\n        };\n\n        clientPair.appClient.createWidget(1, reportingWidget);\n        clientPair.appClient.verifyResult(ok(1));\n\n        //a bit upfront\n        long now = System.currentTimeMillis() + 1500;\n\n        Report report = new Report(1, \"DailyReport\",\n                new ReportSource[] {reportSource},\n                new DailyReport(now, ReportDurationType.INFINITE, 0, 0), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE_PER_PIN,\n                Format.ISO_SIMPLE, ZoneId.of(\"UTC\"), 0, 0, null);\n        clientPair.appClient.createReport(1, report);\n\n        report = clientPair.appClient.parseReportFromResponse(2);\n        assertNotNull(report);\n        assertEquals(System.currentTimeMillis(), report.nextReportAt, 3000);\n\n        String date = LocalDate.now(report.tzName).toString();\n        String filename = getUserName() + \"_Blynk_\" + report.id + \"_\" + date + \".zip\";\n        verify(holder.mailWrapper, timeout(3000)).sendReportEmail(eq(\"test@gmail.com\"),\n                eq(\"Your daily DailyReport is ready\"),\n                eq(\"http://127.0.0.1:18080/\" + filename),\n                any());\n        sleep(200);\n        assertEquals(1, holder.reportScheduler.getCompletedTaskCount());\n        assertEquals(2, holder.reportScheduler.getTaskCount());\n\n        Path result = Paths.get(FileUtils.CSV_DIR,\n                getUserName() + \"_\" + AppNameUtil.BLYNK + \"_\" + report.id + \"_\" + date + \".zip\");\n        assertTrue(Files.exists(result));\n        String resultCsvString = readStringFromFirstZipEntry(result);\n        String[] split = resultCsvString.split(\"[,\\n]\");\n        String nowFormatted = DateTimeFormatter\n                .ofPattern(Format.ISO_SIMPLE.pattern)\n                .withZone(ZoneId.of(\"UTC\"))\n                .format(Instant.ofEpochMilli(pointNow));\n        assertEquals(nowFormatted, split[0]);\n        assertEquals(1.11D, Double.parseDouble(split[1]), 0.0001);\n\n        clientPair.appClient.getWidget(1, 222222);\n        ReportingWidget reportingWidget2 = (ReportingWidget) JsonParser.parseWidget(clientPair.appClient.getBody(3), 0);\n        assertNotNull(reportingWidget2);\n        assertEquals(OK, reportingWidget2.reports[0].lastRunResult);\n    }\n\n    private String readStringFromFirstZipEntry(Path path) throws Exception {\n        ZipFile zipFile = new ZipFile(path.toString());\n\n        Enumeration<? extends ZipEntry> entries = zipFile.entries();\n\n        if (entries.hasMoreElements()) {\n            ZipEntry entry = entries.nextElement();\n            return readStringFromZipEntry(zipFile, entry);\n        }\n        throw new RuntimeException(\"Error reading result gzip file \" + path.toString());\n    }\n\n    private String readStringFromZipEntry(ZipFile zipFile, ZipEntry entry) throws Exception {\n        try (BufferedInputStream bis = new BufferedInputStream(zipFile.getInputStream(entry))) {\n            return new String(bis.readAllBytes(), UTF_16);\n        }\n    }\n}\n\n\n\n\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/tcp/SetPropertyTest.java",
    "content": "package cc.blynk.integration.tcp;\n\nimport cc.blynk.integration.SingleServerInstancePerTest;\nimport cc.blynk.server.core.model.Profile;\nimport cc.blynk.server.core.model.device.Tag;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.controls.Button;\nimport cc.blynk.server.core.model.widgets.controls.Slider;\nimport cc.blynk.server.core.model.widgets.controls.Step;\nimport cc.blynk.server.core.model.widgets.others.Player;\nimport cc.blynk.server.core.model.widgets.others.Video;\nimport cc.blynk.server.core.model.widgets.ui.Menu;\nimport cc.blynk.server.core.model.widgets.ui.image.Image;\nimport cc.blynk.server.core.protocol.model.messages.ResponseMessage;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport static cc.blynk.integration.TestUtil.createTag;\nimport static cc.blynk.integration.TestUtil.illegalCommand;\nimport static cc.blynk.integration.TestUtil.ok;\nimport static cc.blynk.integration.TestUtil.setProperty;\nimport static cc.blynk.server.core.protocol.enums.Response.ILLEGAL_COMMAND_BODY;\nimport static org.junit.Assert.assertArrayEquals;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\nimport static org.junit.Assert.assertTrue;\nimport static org.mockito.Mockito.after;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.eq;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.timeout;\nimport static org.mockito.Mockito.verify;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/2/2015.\n *\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class SetPropertyTest extends SingleServerInstancePerTest {\n\n    @Test\n    public void testSetWidgetProperty() throws Exception {\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(1);\n\n        Widget widget = profile.dashBoards[0].findWidgetByPin(0, (short) 4, PinType.VIRTUAL);\n        assertNotNull(widget);\n        assertEquals(\"Some Text\", widget.label);\n\n        clientPair.hardwareClient.setProperty(4, \"label\", \"MyNewLabel\");\n        clientPair.hardwareClient.verifyResult(ok(1));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(setProperty(1, \"1-0 4 label MyNewLabel\")));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        profile = clientPair.appClient.parseProfile(1);\n\n        widget = profile.dashBoards[0].findWidgetByPin(0, (short) 4, PinType.VIRTUAL);\n        assertNotNull(widget);\n        assertEquals(\"MyNewLabel\", widget.label);\n    }\n\n    @Test\n    //https://github.com/blynkkk/blynk-server/issues/756\n    public void testSetWidgetPropertyIsNotRestoredForTagWidgetAfterOverriding() throws Exception {\n        Tag tag0 = new Tag(100_000, \"Tag1\", new int[] {0});\n\n        clientPair.appClient.createTag(1, tag0);\n        String createdTag = clientPair.appClient.getBody();\n        Tag tag = JsonParser.parseTag(createdTag, 0);\n        assertNotNull(tag);\n        assertEquals(100_000, tag.id);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(createTag(1, tag)));\n\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(2);\n\n        Slider slider = (Slider) profile.dashBoards[0].findWidgetByPin(0, (short) 4, PinType.VIRTUAL);\n        assertNotNull(slider);\n        slider.width = 2;\n        slider.height = 2;\n        assertEquals(\"Some Text\", slider.label);\n        slider.deviceId = tag0.id;\n\n        clientPair.appClient.updateWidget(1, slider);\n        clientPair.appClient.verifyResult(ok(3));\n\n        clientPair.hardwareClient.setProperty(4, \"label\", \"MyNewLabel\");\n        clientPair.hardwareClient.verifyResult(ok(1));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(setProperty(1, \"1-0 4 label MyNewLabel\")));\n\n        clientPair.appClient.deactivate(1);\n        clientPair.appClient.verifyResult(ok(4));\n\n        slider.label = \"Some Text2\";\n        clientPair.appClient.updateWidget(1, slider);\n        clientPair.appClient.verifyResult(ok(5));\n\n        clientPair.appClient.activate(1);\n        clientPair.appClient.verifyResult(ok(6));\n        verify(clientPair.appClient.responseMock, after(500).never()).channelRead(any(), eq(setProperty(1111, \"1-0 4 label MyNewLabel\")));\n\n    }\n\n    @Test\n    public void testSetButtonProperty() throws Exception {\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":102, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":5, \\\"y\\\":0, \\\"tabId\\\":0, \\\"onLabel\\\":\\\"On\\\", \\\"offLabel\\\":\\\"Off\\\" , \\\"type\\\":\\\"BUTTON\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":17}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.setProperty(17, \"onLabel\", \"вкл\");\n        clientPair.hardwareClient.setProperty(17, \"offLabel\", \"выкл\");\n        clientPair.hardwareClient.verifyResult(ok(1));\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(ok(2)));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(setProperty(1, \"1-0 17 onLabel вкл\")));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(setProperty(2, \"1-0 17 offLabel выкл\")));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(1);\n\n        Widget widget = profile.dashBoards[0].findWidgetByPin(0, (short) 17, PinType.VIRTUAL);\n        assertNotNull(widget);\n        assertTrue(widget instanceof Button);\n        Button button = (Button) widget;\n\n        assertEquals(\"вкл\", button.onLabel);\n        assertEquals(\"выкл\", button.offLabel);\n    }\n\n\n    @Test\n    public void testSetBooleanProperty() throws Exception {\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":102, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":5, \\\"y\\\":0, \\\"tabId\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"PLAYER\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":17}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.setProperty(17, \"isOnPlay\", \"true\");\n        clientPair.hardwareClient.verifyResult(ok(1));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(setProperty(1, \"1-0 17 isOnPlay true\")));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(1);\n\n        Widget widget = profile.dashBoards[0].findWidgetByPin(0, (short) 17, PinType.VIRTUAL);\n        assertNotNull(widget);\n        assertTrue(widget instanceof Player);\n        Player playerWidget = (Player) widget;\n\n        assertTrue(playerWidget.isOnPlay);\n    }\n\n    @Test\n    public void testSetStringArrayWidgetPropertyForMenu() throws Exception {\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":102, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":5, \\\"y\\\":0, \\\"tabId\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"MENU\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":17}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.setProperty(17, \"labels\", \"label1 label2 label3\");\n        clientPair.hardwareClient.verifyResult(ok(1));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(setProperty(1, \"1-0 17 labels label1 label2 label3\")));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(1);\n\n        Widget widget = profile.dashBoards[0].findWidgetByPin(0, (short) 17, PinType.VIRTUAL);\n        assertNotNull(widget);\n        assertTrue(widget instanceof Menu);\n        Menu menuWidget = (Menu) widget;\n\n        assertArrayEquals(new String[] {\"label1\", \"label2\", \"label3\"}, menuWidget.labels);\n    }\n\n    @Test\n    public void testSetWrongWidgetProperty() throws Exception {\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(1);\n\n        Widget widget = profile.dashBoards[0].findWidgetByPin(0, (short) 4, PinType.VIRTUAL);\n        assertEquals(widget.label, \"Some Text\");\n\n        clientPair.hardwareClient.setProperty(4, \"YYY\", \"MyNewLabel\");\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(new ResponseMessage(1, ILLEGAL_COMMAND_BODY)));\n\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        profile = clientPair.appClient.parseProfile(2);\n        widget = profile.dashBoards[0].findWidgetByPin(0, (short) 4, PinType.VIRTUAL);\n\n        assertEquals(widget.label, \"Some Text\");\n    }\n\n    @Test\n    public void testSetWrongWidgetProperty2() throws Exception {\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(1);\n\n        Widget widget = profile.dashBoards[0].findWidgetByPin(0, (short) 4, PinType.VIRTUAL);\n        assertEquals(widget.x, 1);\n\n        clientPair.hardwareClient.setProperty(4, \"x\", \"0\");\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(new ResponseMessage(1, ILLEGAL_COMMAND_BODY)));\n\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        profile = clientPair.appClient.parseProfile(2);\n        widget = profile.dashBoards[0].findWidgetByPin(0, (short) 4, PinType.VIRTUAL);\n\n        assertEquals(widget.x, 1);\n    }\n\n    @Test\n    public void testSetWrongWidgetProperty3() throws Exception {\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(1);\n\n        Widget widget = profile.dashBoards[0].findWidgetByPin(0, (short) 4, PinType.VIRTUAL);\n        assertEquals(widget.x, 1);\n\n        clientPair.hardwareClient.setProperty(4, \"url\", \"0\");\n\n        //we do not fail here, as many widgets now may be assigned to the same pin\n        clientPair.hardwareClient.verifyResult(ok(1));\n        //verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(new ResponseMessage(1, ILLEGAL_COMMAND_BODY)));\n    }\n\n    @Test\n    public void testSetColorForWidget() throws Exception {\n        clientPair.hardwareClient.setProperty(4, \"color\", \"#23C48E\");\n        clientPair.hardwareClient.verifyResult(ok(1));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(setProperty(1, \"1-0 4 color #23C48E\")));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(1);\n\n        Widget widget = profile.dashBoards[0].findWidgetByPin(0, (short) 4, PinType.VIRTUAL);\n        assertEquals(600084223, widget.color);\n    }\n\n    @Test\n    public void setMinMaxProperty() throws Exception {\n        clientPair.hardwareClient.setProperty(4, \"min\", \"10\");\n        clientPair.hardwareClient.setProperty(4, \"max\", \"20\");\n        clientPair.hardwareClient.verifyResult(ok(1));\n        clientPair.hardwareClient.verifyResult(ok(2));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(setProperty(1, \"1-0 4 min 10\")));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(setProperty(2, \"1-0 4 max 20\")));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(1);\n        profile.dashBoards[0].updatedAt = 0;\n\n        Widget widget = profile.dashBoards[0].findWidgetByPin(0, (short) 4, PinType.VIRTUAL);\n        assertEquals(10, ((OnePinWidget) widget).min, 0.0001);\n        assertEquals(20, ((OnePinWidget) widget).max, 0.0001);\n    }\n\n    @Test\n    public void setMinMaxPropertyFloat() throws Exception {\n        clientPair.hardwareClient.setProperty(4, \"min\", \"10.1\");\n        clientPair.hardwareClient.setProperty(4, \"max\", \"20.2\");\n        clientPair.hardwareClient.verifyResult(ok(1));\n        clientPair.hardwareClient.verifyResult(ok(2));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(setProperty(1, \"1-0 4 min 10.1\")));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(setProperty(2, \"1-0 4 max 20.2\")));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(1);\n        profile.dashBoards[0].updatedAt = 0;\n\n        Widget widget = profile.dashBoards[0].findWidgetByPin(0, (short) 4, PinType.VIRTUAL);\n        assertEquals(10.1, ((OnePinWidget) widget).min, 0.0001);\n        assertEquals(20.2, ((OnePinWidget) widget).max, 0.0001);\n    }\n\n    @Test\n    public void setMinMaxWrongPropertyFloat() throws Exception {\n        clientPair.hardwareClient.setProperty(4, \"min\", \"10.11-1\");\n        clientPair.hardwareClient.setProperty(4, \"max\", \"20.22-2\");\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(illegalCommand(1)));\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(illegalCommand(2)));\n        verify(clientPair.appClient.responseMock, after(50).never()).channelRead(any(), any());\n        verify(clientPair.appClient.responseMock, after(50).never()).channelRead(any(), any());\n    }\n\n    @Test\n    public void testSetColorShouldNotWorkForNonActiveProject() throws Exception {\n        clientPair.appClient.deactivate(1);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.setProperty(4, \"color\", \"#23C48E\");\n        verify(clientPair.hardwareClient.responseMock, after(500).never()).channelRead(any(), eq(ok(1)));\n        verify(clientPair.appClient.responseMock, after(500).never()).channelRead(any(), eq(setProperty(1, \"1 4 color #23C48E\")));\n    }\n\n    @Test\n    public void testSetUrlForVideo() throws Exception {\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":102, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":5, \\\"y\\\":0, \\\"tabId\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"VIDEO\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":17}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.setProperty(17, \"url\", \"http://123.com\");\n        clientPair.hardwareClient.verifyResult(ok(1));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(setProperty(1, \"1-0 17 url http://123.com\")));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(1);\n\n        Widget widget = profile.dashBoards[0].findWidgetByPin(0, (short) 17, PinType.VIRTUAL);\n        assertNotNull(widget);\n        assertTrue(widget instanceof Video);\n        Video videoWidget = (Video) widget;\n\n        assertEquals(\"http://123.com\", videoWidget.url);\n    }\n\n    @Test\n    public void testSetUrlsForImageWidget() throws Exception {\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":102, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":5, \\\"y\\\":0, \\\"tabId\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"IMAGE\\\", \\\"urls\\\":[\\\"https://blynk.cc/123.jpg\\\"], \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":17}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.setProperty(17, \"urls\", \"http://123.com\");\n        clientPair.hardwareClient.verifyResult(ok(1));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(setProperty(1, \"1-0 17 urls http://123.com\")));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(1);\n\n        Widget widget = profile.dashBoards[0].findWidgetByPin(0, (short) 17, PinType.VIRTUAL);\n        assertNotNull(widget);\n        assertTrue(widget instanceof Image);\n        Image imageWidget = (Image) widget;\n\n        assertArrayEquals(new String[] {\"http://123.com\"}, imageWidget.urls);\n\n        clientPair.hardwareClient.setProperty(17, \"urls\", \"http://123.com\", \"http://124.com\");\n        clientPair.hardwareClient.verifyResult(ok(2));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(setProperty(2, \"1-0 17 urls http://123.com http://124.com\")));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        profile = clientPair.appClient.parseProfile(1);\n\n        widget = profile.dashBoards[0].findWidgetByPin(0, (short) 17, PinType.VIRTUAL);\n        assertNotNull(widget);\n        assertTrue(widget instanceof Image);\n        imageWidget = (Image) widget;\n\n        assertArrayEquals(new String[] {\"http://123.com\", \"http://124.com\"}, imageWidget.urls);\n    }\n\n    @Test\n    public void testSetUrlForImageWidget() throws Exception {\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":102, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":5, \\\"y\\\":0, \\\"tabId\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"IMAGE\\\", \\\"urls\\\":[\\\"https://blynk.cc/123.jpg\\\"], \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":17}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.setProperty(17, \"url\", \"1\", \"http://123.com\");\n        clientPair.hardwareClient.verifyResult(ok(1));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(setProperty(1, \"1-0 17 url 1 http://123.com\")));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(1);\n\n        Widget widget = profile.dashBoards[0].findWidgetByPin(0, (short) 17, PinType.VIRTUAL);\n        assertNotNull(widget);\n        assertTrue(widget instanceof Image);\n        Image imageWidget = (Image) widget;\n\n        assertArrayEquals(new String[] {\"http://123.com\"}, imageWidget.urls);\n\n        clientPair.hardwareClient.setProperty(17, \"url\", \"2\", \"http://123.com\");\n        clientPair.hardwareClient.verifyResult(ok(2));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(setProperty(2, \"1-0 17 url 2 http://123.com\")));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        profile = clientPair.appClient.parseProfile(1);\n\n        widget = profile.dashBoards[0].findWidgetByPin(0, (short) 17, PinType.VIRTUAL);\n        assertNotNull(widget);\n        assertTrue(widget instanceof Image);\n        imageWidget = (Image) widget;\n\n        assertArrayEquals(new String[] {\"http://123.com\"}, imageWidget.urls);\n    }\n\n    @Test\n    public void testPropertyIsNotRestoredAfterWidgetCreated() throws Exception {\n        clientPair.hardwareClient.setProperty(122, \"label\", \"new\");\n        clientPair.hardwareClient.verifyResult(ok(1));\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":102, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":5, \\\"y\\\":0, \\\"tabId\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"VIDEO\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":122}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.deactivate(1);\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.appClient.activate(1);\n        verify(clientPair.appClient.responseMock, timeout(1000)).channelRead(any(), eq(ok(3)));\n        verify(clientPair.appClient.responseMock, never()).channelRead(any(), eq(setProperty(1111, \"1 122 label new\")));\n    }\n\n    @Test\n    public void testPropertyIsNotRestoredAfterWidgetUpdated() throws Exception {\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":102, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":5, \\\"y\\\":0, \\\"tabId\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"VIDEO\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":17}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.setProperty(17, \"url\", \"http://123.com\");\n        clientPair.hardwareClient.verifyResult(ok(1));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(setProperty(1, \"1-0 17 url http://123.com\")));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(1);\n\n        Widget widget = profile.dashBoards[0].findWidgetByPin(0, (short) 17, PinType.VIRTUAL);\n        assertNotNull(widget);\n        assertTrue(widget instanceof Video);\n        Video videoWidget = (Video) widget;\n\n        assertEquals(\"http://123.com\", videoWidget.url);\n\n        clientPair.appClient.updateWidget(1, \"{\\\"id\\\":102, \\\"url\\\":\\\"http://updated.com\\\", \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":5, \\\"y\\\":0, \\\"tabId\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"VIDEO\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":17}\");\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.appClient.deactivate(1);\n        clientPair.appClient.verifyResult(ok(3));\n\n        clientPair.appClient.activate(1);\n        verify(clientPair.appClient.responseMock, timeout(1000)).channelRead(any(), eq(ok(4)));\n        verify(clientPair.appClient.responseMock, never()).channelRead(any(), eq(setProperty(1111, \"1 17 url http://updated.com\")));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        profile = clientPair.appClient.parseProfile(1);\n        assertEquals(0, profile.pinsStorage.size());\n    }\n\n    @Test\n    public void testStepPropertyForStepWidget() throws Exception {\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":102, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":5, \\\"y\\\":0, \\\"tabId\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"STEP\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":17}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.setProperty(17, \"step\", \"1.1\");\n        clientPair.hardwareClient.verifyResult(ok(1));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(setProperty(1, \"1-0 17 step 1.1\")));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(1);\n\n        Widget widget = profile.dashBoards[0].findWidgetByPin(0, (short) 17, PinType.VIRTUAL);\n        assertNotNull(widget);\n        assertTrue(widget instanceof Step);\n        Step stepWidget = (Step) widget;\n\n        assertEquals(1.1, stepWidget.step, 0.00001);\n    }\n\n    @Test\n    public void testSetColorForWidgetFromApp() throws Exception {\n        clientPair.appClient.send(\"setProperty 1 4 color #23C48E\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(2);\n\n        Widget widget = profile.dashBoards[0].getWidgetById(4);\n        assertEquals(600084223, widget.color);\n    }\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/tcp/ShareProfileWorkflowTest.java",
    "content": "package cc.blynk.integration.tcp;\n\nimport cc.blynk.integration.SingleServerInstancePerTest;\nimport cc.blynk.integration.model.tcp.TestAppClient;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.DashboardSettings;\nimport cc.blynk.server.core.model.DataStream;\nimport cc.blynk.server.core.model.Profile;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.enums.Theme;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.notifications.Notification;\nimport cc.blynk.server.core.model.widgets.notifications.Twitter;\nimport cc.blynk.server.core.model.widgets.others.eventor.Eventor;\nimport cc.blynk.server.core.model.widgets.others.eventor.Rule;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.action.BaseAction;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.action.SetPinAction;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.action.SetPinActionType;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.condition.number.GreaterThan;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.nio.charset.StandardCharsets;\nimport java.util.zip.DeflaterOutputStream;\n\nimport static cc.blynk.integration.TestUtil.b;\nimport static cc.blynk.integration.TestUtil.hardware;\nimport static cc.blynk.integration.TestUtil.notAllowed;\nimport static cc.blynk.integration.TestUtil.ok;\nimport static cc.blynk.integration.TestUtil.parseProfile;\nimport static cc.blynk.integration.TestUtil.readTestUserProfile;\nimport static cc.blynk.integration.TestUtil.setProperty;\nimport static cc.blynk.server.core.protocol.enums.Command.ACTIVATE_DASHBOARD;\nimport static cc.blynk.server.core.protocol.enums.Command.APP_SYNC;\nimport static cc.blynk.server.core.protocol.enums.Command.DEACTIVATE_DASHBOARD;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_ENERGY;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE;\nimport static cc.blynk.server.core.protocol.enums.Command.SHARING;\nimport static cc.blynk.server.core.protocol.model.messages.MessageFactory.produce;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertFalse;\nimport static org.junit.Assert.assertNotEquals;\nimport static org.junit.Assert.assertNotNull;\nimport static org.junit.Assert.assertNull;\nimport static org.junit.Assert.assertTrue;\nimport static org.mockito.Mockito.after;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.eq;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.timeout;\nimport static org.mockito.Mockito.verify;\n\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/2/2015.\n *\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class ShareProfileWorkflowTest extends SingleServerInstancePerTest {\n\n    private static OnePinWidget getWidgetByPin(Profile profile, int pin) {\n        for (Widget widget : profile.dashBoards[0].widgets) {\n            if (widget instanceof OnePinWidget) {\n                OnePinWidget onePinWidget = (OnePinWidget) widget;\n                if (onePinWidget.pin != DataStream.NO_PIN && onePinWidget.pin == pin) {\n                    return onePinWidget;\n                }\n            }\n        }\n        return null;\n    }\n\n    @Test\n    public void testGetShareTokenNoDashId() throws Exception {\n        clientPair.appClient.send(\"getShareToken\");\n        verify(clientPair.appClient.responseMock, timeout(1000)).channelRead(any(), eq(notAllowed(1)));\n    }\n\n    @Test\n    public void testGetShareToken() throws Exception {\n        clientPair.appClient.send(\"getShareToken 1\");\n\n        String token = clientPair.appClient.getBody();\n        assertNotNull(token);\n        assertEquals(32, token.length());\n\n        TestAppClient appClient2 = new TestAppClient(properties);\n        appClient2.start();\n\n        appClient2.send(\"shareLogin \" + getUserName() + \" \" + token + \" Android 24\");\n        verify(appClient2.responseMock, timeout(500)).channelRead(any(), eq(ok(1)));\n\n        appClient2.send(\"loadProfileGzipped\");\n        Profile serverProfile = appClient2.parseProfile(2);\n        DashBoard serverDash = serverProfile.dashBoards[0];\n\n        Profile profile = parseProfile(readTestUserProfile());\n        Twitter twitter = profile.dashBoards[0].getTwitterWidget();\n        clearPrivateData(twitter);\n        Notification notification = profile.dashBoards[0].getNotificationWidget();\n        clearPrivateData(notification);\n\n        profile.dashBoards[0].updatedAt = serverDash.updatedAt;\n        assertNull(serverDash.sharedToken);\n        serverDash.devices = null;\n        profile.dashBoards[0].devices = null;\n\n        assertEquals(profile.dashBoards[0].toString(), serverDash.toString());\n\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        profile = clientPair.appClient.parseProfile(2);\n\n        profile.dashBoards[0].updatedAt = 0;\n        Notification originalNotification = profile.dashBoards[0].getNotificationWidget();\n        assertNotNull(originalNotification);\n        assertEquals(1, originalNotification.androidTokens.size());\n        assertEquals(\"token\", originalNotification.androidTokens.get(\"uid\"));\n\n        Twitter originalTwitter = profile.dashBoards[0].getTwitterWidget();\n        assertNotNull(originalTwitter);\n        assertEquals(\"token\", originalTwitter.token);\n        assertEquals(\"secret\", originalTwitter.secret);\n    }\n\n    @Test\n    public void getShareTokenAndLoginViaIt() throws Exception {\n        clientPair.appClient.send(\"getShareToken 1\");\n\n        String token = clientPair.appClient.getBody();\n        assertNotNull(token);\n        assertEquals(32, token.length());\n\n\n        TestAppClient appClient2 = new TestAppClient(properties);\n        appClient2.start();\n        appClient2.send(\"shareLogin \" + getUserName() + \" \" + token + \" Android 24\");\n\n        verify(appClient2.responseMock, timeout(500)).channelRead(any(), eq(ok(1)));\n\n        clientPair.appClient.send(\"hardware 1 vw 1 1\");\n        verify(appClient2.responseMock, timeout(1000)).channelRead(any(), eq(produce(2, APP_SYNC, b(\"1 vw 1 1\"))));\n\n        appClient2.send(\"hardware 1 vw 2 2\");\n        verify(clientPair.appClient.responseMock, timeout(1000)).channelRead(any(), eq(produce(2, APP_SYNC, b(\"1 vw 2 2\"))));\n\n        clientPair.appClient.reset();\n        clientPair.hardwareClient.reset();\n        appClient2.reset();\n\n        appClient2.send(\"hardware 1 ar 30\");\n        verify(clientPair.appClient.responseMock, after(500).never()).channelRead(any(), any());\n        verify(clientPair.hardwareClient.responseMock, after(500).never()).channelRead(any(), eq(produce(1, HARDWARE, b(\"ar 30\"))));\n\n        clientPair.appClient.send(\"hardware 1 ar 30\");\n        verify(appClient2.responseMock, after(500).never()).channelRead(any(), any());\n        verify(clientPair.hardwareClient.responseMock, after(500).never()).channelRead(any(), eq(produce(2, HARDWARE, b(\"ar 30\"))));\n\n        clientPair.hardwareClient.reset();\n        clientPair.hardwareClient.send(\"ping\");\n\n        appClient2.send(\"hardware 1 pm 2 2\");\n        verify(clientPair.appClient.responseMock, after(500).never()).channelRead(any(), any());\n        verify(clientPair.hardwareClient.responseMock,  after(500).atMost(1)).channelRead(any(), any());\n\n        clientPair.appClient.send(\"hardware 1 ar 30\");\n        verify(appClient2.responseMock, after(500).never()).channelRead(any(), any());\n        verify(clientPair.hardwareClient.responseMock, after(500).never()).channelRead(any(), eq(produce(2, HARDWARE, b(\"ar 30\"))));\n\n        clientPair.appClient.send(\"hardware 1 pm 2 2\");\n        verify(appClient2.responseMock, after(250).never()).channelRead(any(), any());\n        verify(clientPair.hardwareClient.responseMock, after(250).never()).channelRead(any(), eq(produce(3, HARDWARE, b(\"pm 2 2\"))));\n    }\n\n    @Test\n    public void getShareMultipleTokensAndLoginViaIt() throws Exception {\n        clientPair.appClient.send(\"getShareToken 1\");\n\n        String token1 = clientPair.appClient.getBody();\n        assertNotNull(token1);\n        assertEquals(32, token1.length());\n\n        DashBoard dash = new DashBoard();\n        dash.id = 2;\n        dash.name = \"test\";\n        clientPair.appClient.createDash(dash);\n        verify(clientPair.appClient.responseMock, timeout(1000)).channelRead(any(), eq(ok(2)));\n\n        DashboardSettings settings = new DashboardSettings(dash.name,\n                true, Theme.Blynk, false, false, false, false, 0, false);\n\n        clientPair.appClient.send(\"updateSettings 2\\0\" + JsonParser.toJson(settings));\n        clientPair.appClient.verifyResult(ok(3));\n\n        clientPair.appClient.send(\"getShareToken 2\");\n\n        String token2 = clientPair.appClient.getBody(4);\n        assertNotNull(token2);\n        assertEquals(32, token2.length());\n\n        TestAppClient appClient2 = new TestAppClient(properties);\n        appClient2.start();\n        appClient2.send(\"shareLogin \" + getUserName() + \" \" + token1 + \" Android 24\");\n        verify(appClient2.responseMock, timeout(500)).channelRead(any(), eq(ok(1)));\n\n        TestAppClient appClient3 = new TestAppClient(properties);\n        appClient3.start();\n        appClient3.send(\"shareLogin \" + getUserName() + \" \" + token2 + \" Android 24\");\n        verify(appClient3.responseMock, timeout(500)).channelRead(any(), eq(ok(1)));\n\n        clientPair.appClient.send(\"hardware 1 vw 1 1\");\n        verify(appClient2.responseMock, timeout(1000)).channelRead(any(), eq(produce(5, APP_SYNC, b(\"1 vw 1 1\"))));\n\n        appClient2.send(\"hardware 1 vw 2 2\");\n        verify(clientPair.appClient.responseMock, timeout(1000)).channelRead(any(), eq(produce(2, APP_SYNC, b(\"1 vw 2 2\"))));\n\n        clientPair.appClient.reset();\n        clientPair.hardwareClient.reset();\n        appClient2.reset();\n\n        appClient2.send(\"hardware 1 ar 30\");\n        verify(clientPair.appClient.responseMock, after(500).never()).channelRead(any(), any());\n        verify(clientPair.hardwareClient.responseMock, after(500).never()).channelRead(any(), eq(produce(1, HARDWARE, b(\"ar 30\"))));\n\n        clientPair.appClient.send(\"hardware 1 ar 30\");\n        verify(appClient2.responseMock, after(500).never()).channelRead(any(), any());\n        verify(clientPair.hardwareClient.responseMock, after(500).never()).channelRead(any(), eq(produce(2, HARDWARE, b(\"ar 30\"))));\n\n        clientPair.hardwareClient.reset();\n        clientPair.hardwareClient.send(\"ping\");\n\n        appClient2.send(\"hardware 1 pm 2 2\");\n        verify(clientPair.appClient.responseMock, after(500).never()).channelRead(any(), any());\n        verify(clientPair.hardwareClient.responseMock,  after(500).atMost(1)).channelRead(any(), any());\n\n        clientPair.appClient.send(\"hardware 1 ar 30\");\n        verify(appClient2.responseMock, after(500).never()).channelRead(any(), any());\n        verify(clientPair.hardwareClient.responseMock, after(500).never()).channelRead(any(), eq(produce(2, HARDWARE, b(\"ar 30\"))));\n\n        clientPair.appClient.send(\"hardware 1 pm 2 2\");\n        verify(appClient2.responseMock, after(250).never()).channelRead(any(), any());\n        verify(clientPair.hardwareClient.responseMock, after(250).never()).channelRead(any(), eq(produce(3, HARDWARE, b(\"pm 2 2\"))));\n    }\n\n    @Test\n    public void testSharingChargingCorrect() throws Exception {\n        clientPair.appClient.send(\"getEnergy\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(1, GET_ENERGY, \"7500\")));\n        clientPair.appClient.reset();\n\n        clientPair.appClient.send(\"getShareToken 1\");\n        String token = clientPair.appClient.getBody();\n        assertNotNull(token);\n        assertEquals(32, token.length());\n\n        clientPair.appClient.send(\"getEnergy\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(2, GET_ENERGY, \"6500\")));\n\n        clientPair.appClient.reset();\n\n        clientPair.appClient.send(\"getShareToken 1\");\n        token = clientPair.appClient.getBody();\n        assertNotNull(token);\n        assertEquals(32, token.length());\n\n        clientPair.appClient.send(\"getEnergy\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(2, GET_ENERGY, \"6500\")));\n        clientPair.appClient.reset();\n\n        clientPair.appClient.send(\"getShareToken 1\");\n        token = clientPair.appClient.getBody();\n        assertNotNull(token);\n        assertEquals(32, token.length());\n\n        clientPair.appClient.send(\"getShareToken 1\");\n        clientPair.appClient.send(\"getShareToken 1\");\n        clientPair.appClient.send(\"getShareToken 1\");\n\n        clientPair.appClient.send(\"getEnergy\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(5, GET_ENERGY, \"6500\")));\n    }\n\n    @Test\n    public void checkStateWasChanged() throws Exception {\n        clientPair.appClient.send(\"getShareToken 1\");\n\n        String token = clientPair.appClient.getBody();\n        assertNotNull(token);\n        assertEquals(32, token.length());\n\n        TestAppClient appClient2 = new TestAppClient(properties);\n        appClient2.start();\n        appClient2.send(\"shareLogin \" + getUserName() + \" \" + token + \" Android 24\");\n        appClient2.verifyResult(ok(1));\n\n        clientPair.appClient.send(\"hardware 1 vw 1 1\");\n        appClient2.verifyResult(produce(2, APP_SYNC, b(\"1 vw 1 1\")));\n\n        appClient2.send(\"hardware 1 vw 2 2\");\n        clientPair.appClient.verifyResult(produce(2, APP_SYNC, b(\"1 vw 2 2\")));\n\n        clientPair.appClient.reset();\n        appClient2.reset();\n\n        //check from master side\n        clientPair.appClient.send(\"hardware 1 aw 3 1\");\n        clientPair.hardwareClient.verifyResult(produce(1, HARDWARE, b(\"aw 3 1\")));\n\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(1);\n\n        OnePinWidget tmp = getWidgetByPin(profile, 3);\n\n        assertNotNull(tmp);\n        assertEquals(\"1\", tmp.value);\n\n\n        //check from slave side\n        appClient2.send(\"hardware 1 aw 3 150\");\n        clientPair.hardwareClient.verifyResult(produce(1, HARDWARE, b(\"aw 3 150\")));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        profile = clientPair.appClient.parseProfile(1);\n\n        tmp = getWidgetByPin(profile, 3);\n\n        assertNotNull(tmp);\n        assertEquals(\"150\", tmp.value);\n\n        //check from hard side\n        clientPair.hardwareClient.send(\"hardware aw 3 151\");\n        clientPair.appClient.verifyResult(produce(1, HARDWARE, b(\"1-0 aw 3 151\")));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        profile = clientPair.appClient.parseProfile(1);\n\n        tmp = getWidgetByPin(profile, 3);\n\n        assertNotNull(tmp);\n        assertEquals(\"151\", tmp.value);\n    }\n\n    @Test\n    public void checkSetPropertyWasChanged() throws Exception {\n        clientPair.appClient.send(\"getShareToken 1\");\n\n        String token = clientPair.appClient.getBody();\n        assertNotNull(token);\n        assertEquals(32, token.length());\n\n        TestAppClient appClient2 = new TestAppClient(properties);\n        appClient2.start();\n        appClient2.send(\"shareLogin \" + getUserName() + \" \" + token + \" Android 24\");\n        appClient2.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"setProperty 1 color 123\");\n        clientPair.appClient.verifyResult(setProperty(1, \"1-0 1 color 123\"));\n        appClient2.verifyResult(setProperty(1, \"1-0 1 color 123\"));\n    }\n\n    @Test\n    public void checkSharingMessageWasReceived() throws Exception {\n        clientPair.appClient.send(\"getShareToken 1\");\n\n        String token = clientPair.appClient.getBody();\n        assertNotNull(token);\n        assertEquals(32, token.length());\n\n        TestAppClient appClient2 = new TestAppClient(properties);\n        appClient2.start();\n        appClient2.send(\"shareLogin \" + getUserName() + \" \" + token + \" Android 24\");\n\n        verify(appClient2.responseMock, timeout(500)).channelRead(any(), eq(ok(1)));\n\n        clientPair.appClient.send(\"sharing 1 off\");\n        clientPair.appClient.verifyResult(ok(2));\n\n        verify(appClient2.responseMock, timeout(500)).channelRead(any(), eq(produce(2, SHARING, b(\"1 off\"))));\n    }\n\n    @Test\n    public void checkSharingMessageWasReceivedMultipleRecievers() throws Exception {\n        clientPair.appClient.send(\"getShareToken 1\");\n\n        String token = clientPair.appClient.getBody();\n        assertNotNull(token);\n        assertEquals(32, token.length());\n\n        TestAppClient appClient2 = new TestAppClient(properties);\n        appClient2.start();\n        appClient2.send(\"shareLogin \" + getUserName() + \" \" + token + \" Android 24\");\n\n        verify(appClient2.responseMock, timeout(500)).channelRead(any(), eq(ok(1)));\n\n        TestAppClient appClient3 = new TestAppClient(properties);\n        appClient3.start();\n        appClient3.send(\"shareLogin \" + getUserName() + \" \" + token + \" Android 24\");\n\n        verify(appClient3.responseMock, timeout(500)).channelRead(any(), eq(ok(1)));\n\n        clientPair.appClient.send(\"sharing 1 off\");\n        clientPair.appClient.verifyResult(ok(2));\n\n        verify(appClient2.responseMock, timeout(500)).channelRead(any(), eq(produce(2, SHARING, b(\"1 off\"))));\n        verify(appClient3.responseMock, timeout(500)).channelRead(any(), eq(produce(2, SHARING, b(\"1 off\"))));\n    }\n\n    @Test\n    public void checkSharingMessageWasReceivedAlsoForNonSharedApp() throws Exception {\n        clientPair.appClient.send(\"getShareToken 1\");\n\n        String token = clientPair.appClient.getBody();\n        assertNotNull(token);\n        assertEquals(32, token.length());\n\n        TestAppClient appClient2 = new TestAppClient(properties);\n        appClient2.start();\n        appClient2.send(\"shareLogin \" + getUserName() + \" \" + token + \" Android 24\");\n\n        verify(appClient2.responseMock, timeout(500)).channelRead(any(), eq(ok(1)));\n\n        TestAppClient appClient3 = new TestAppClient(properties);\n        appClient3.start();\n        appClient3.login(getUserName(), \"1\", \"Android\", \"1.10.4\");\n\n        verify(appClient3.responseMock, timeout(500)).channelRead(any(), eq(ok(1)));\n\n        clientPair.appClient.send(\"sharing 1 off\");\n        clientPair.appClient.verifyResult(ok(2));\n\n        verify(appClient2.responseMock, timeout(500)).channelRead(any(), eq(produce(2, SHARING, b(\"1 off\"))));\n        verify(appClient3.responseMock, timeout(500)).channelRead(any(), eq(produce(2, SHARING, b(\"1 off\"))));\n    }\n\n    @Test\n    public void eventorWorksInSharedModeFromAppSide() throws Exception {\n        DataStream triggerDataStream = new DataStream((short) 1, PinType.VIRTUAL);\n        DataStream dataStream = new DataStream((short) 2, PinType.VIRTUAL);\n        SetPinAction setPinAction = new SetPinAction(dataStream, \"123\", SetPinActionType.CUSTOM);\n        Rule rule = new Rule(triggerDataStream, null, new GreaterThan(37), new BaseAction[] {setPinAction}, true);\n\n        Eventor eventor = new Eventor();\n        eventor.rules = new Rule[] {\n                rule\n        };\n\n        clientPair.appClient.createWidget(1, eventor);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.send(\"getShareToken 1\");\n\n        String token = clientPair.appClient.getBody(2);\n        assertNotNull(token);\n        assertEquals(32, token.length());\n\n        TestAppClient appClient2 = new TestAppClient(properties);\n        appClient2.start();\n        appClient2.send(\"shareLogin \" + getUserName() + \" \" + token + \" Android 24\");\n\n        verify(appClient2.responseMock, timeout(500)).channelRead(any(), eq(ok(1)));\n\n        appClient2.send(\"hardware 1 vw 1 38\");\n\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(produce(2, HARDWARE, b(\"vw 1 38\"))));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(2, APP_SYNC, b(\"1 vw 1 38\"))));\n\n        clientPair.hardwareClient.verifyResult(hardware(888, \"vw 2 123\"));\n        clientPair.appClient.verifyResult(hardware(888, \"1-0 vw 2 123\"));\n        verify(appClient2.responseMock, timeout(500)).channelRead(any(), eq(produce(888, HARDWARE, b(\"1-0 vw 2 123\"))));\n    }\n\n    @Test\n    public void checkBothClientsReceiveMessage() throws Exception {\n        clientPair.appClient.send(\"getShareToken 1\");\n\n        String token = clientPair.appClient.getBody();\n        assertNotNull(token);\n        assertEquals(32, token.length());\n\n        TestAppClient appClient2 = new TestAppClient(properties);\n        appClient2.start();\n        appClient2.send(\"shareLogin \" + getUserName() + \" \" + token + \" Android 24\");\n\n        verify(appClient2.responseMock, timeout(500)).channelRead(any(), eq(ok(1)));\n\n        //check from hard side\n        clientPair.hardwareClient.send(\"hardware aw 3 151\");\n        verify(clientPair.appClient.responseMock, timeout(1000)).channelRead(any(), eq(produce(1, HARDWARE, b(\"1-0 aw 3 151\"))));\n        verify(appClient2.responseMock, timeout(1000)).channelRead(any(), eq(produce(1, HARDWARE, b(\"1-0 aw 3 151\"))));\n\n        clientPair.hardwareClient.send(\"hardware aw 3 152\");\n        verify(clientPair.appClient.responseMock, timeout(1000)).channelRead(any(), eq(produce(2, HARDWARE, b(\"1-0 aw 3 152\"))));\n        verify(appClient2.responseMock, timeout(1000)).channelRead(any(), eq(produce(2, HARDWARE, b(\"1-0 aw 3 152\"))));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(1);\n\n        OnePinWidget tmp = getWidgetByPin(profile, 3);\n\n        assertNotNull(tmp);\n        assertEquals(\"152\", tmp.value);\n    }\n\n    @Test\n    public void wrongSharedToken() throws Exception {\n        clientPair.appClient.send(\"getShareToken 1\");\n\n        String token = clientPair.appClient.getBody();\n        assertNotNull(token);\n        assertEquals(32, token.length());\n\n        TestAppClient appClient2 = new TestAppClient(properties);\n        appClient2.start();\n        appClient2.send(\"shareLogin \" + getUserName() + \" \" + token+\"a\" + \" Android 24\");\n\n        verify(appClient2.responseMock, timeout(1000)).channelRead(any(), eq(notAllowed(1)));\n    }\n\n    @Test\n    public void revokeSharedToken() throws Exception {\n        clientPair.appClient.send(\"getShareToken 1\");\n\n        String token = clientPair.appClient.getBody();\n        assertNotNull(token);\n        assertEquals(32, token.length());\n\n        TestAppClient appClient2 = new TestAppClient(properties);\n        appClient2.start();\n        appClient2.send(\"shareLogin \" + getUserName() + \" \" + token + \" Android 24\");\n\n        verify(appClient2.responseMock, timeout(1000)).channelRead(any(), eq(ok(1)));\n\n        clientPair.appClient.reset();\n        appClient2.reset();\n\n        assertFalse(clientPair.appClient.isClosed());\n        assertFalse(appClient2.isClosed());\n\n        clientPair.appClient.send(\"refreshShareToken 1\");\n        token = clientPair.appClient.getBody();\n        assertNotNull(token);\n        assertEquals(32, token.length());\n\n        verify(appClient2.responseMock, timeout(1000)).channelRead(any(), eq(notAllowed(1)));\n\n        assertFalse(clientPair.appClient.isClosed());\n        assertTrue(appClient2.isClosed());\n\n        TestAppClient appClient3 = new TestAppClient(properties);\n        appClient3.start();\n        appClient3.send(\"shareLogin \" + getUserName() + \" \" + token + \" Android 24\");\n\n        verify(appClient3.responseMock, timeout(1000)).channelRead(any(), eq(ok(1)));\n    }\n\n    @Test\n    public void testDeactivateAndActivateForSubscriptions() throws Exception {\n        clientPair.appClient.send(\"getShareToken 1\");\n\n        String token = clientPair.appClient.getBody();\n        assertNotNull(token);\n        assertEquals(32, token.length());\n\n        TestAppClient appClient2 = new TestAppClient(properties);\n        appClient2.start();\n        appClient2.send(\"shareLogin \" + getUserName() + \" \" + token + \" Android 24\");\n\n        TestAppClient appClient3 = new TestAppClient(properties);\n        appClient3.start();\n        appClient3.send(\"shareLogin \" + getUserName() + \" \" + token + \" Android 24\");\n\n        verify(appClient2.responseMock, timeout(500)).channelRead(any(), eq(ok(1)));\n        verify(appClient3.responseMock, timeout(500)).channelRead(any(), eq(ok(1)));\n\n        clientPair.appClient.deactivate(1);\n        clientPair.appClient.verifyResult(ok(2));\n\n        verify(appClient2.responseMock, timeout(500)).channelRead(any(), eq(produce(2, DEACTIVATE_DASHBOARD, \"1\")));\n        verify(appClient3.responseMock, timeout(500)).channelRead(any(), eq(produce(2, DEACTIVATE_DASHBOARD, \"1\")));\n\n        clientPair.appClient.activate(1);\n        clientPair.appClient.verifyResult(ok(3));\n\n        verify(appClient2.responseMock, timeout(500)).channelRead(any(), eq(produce(3, ACTIVATE_DASHBOARD, \"1\")));\n        verify(appClient3.responseMock, timeout(500)).channelRead(any(), eq(produce(3, ACTIVATE_DASHBOARD, \"1\")));\n    }\n\n    @Test\n    public void testDeactivateOnLogout() throws Exception {\n        clientPair.appClient.send(\"getShareToken 1\");\n\n        String token = clientPair.appClient.getBody();\n        assertNotNull(token);\n        assertEquals(32, token.length());\n\n        TestAppClient appClient2 = new TestAppClient(properties);\n        appClient2.start();\n        appClient2.send(\"shareLogin \" + getUserName() + \" \" + token + \" Android 24\");\n\n        TestAppClient appClient3 = new TestAppClient(properties);\n        appClient3.start();\n        appClient3.send(\"shareLogin \" + getUserName() + \" \" + token + \" Android 24\");\n\n        verify(appClient2.responseMock, timeout(500)).channelRead(any(), eq(ok(1)));\n        verify(appClient3.responseMock, timeout(500)).channelRead(any(), eq(ok(1)));\n\n        clientPair.appClient.send(\"deactivate\");\n        clientPair.appClient.verifyResult(ok(2));\n\n        verify(appClient2.responseMock, timeout(500)).channelRead(any(), eq(produce(2, DEACTIVATE_DASHBOARD, \"\")));\n        verify(appClient3.responseMock, timeout(500)).channelRead(any(), eq(produce(2, DEACTIVATE_DASHBOARD, \"\")));\n    }\n\n    @Test\n    public void loadGzippedProfileForSharedBoard() throws Exception {\n        clientPair.appClient.send(\"getShareToken 1\");\n\n        String token = clientPair.appClient.getBody();\n        assertNotNull(token);\n        assertEquals(32, token.length());\n\n        TestAppClient appClient2 = new TestAppClient(properties);\n        appClient2.start();\n        appClient2.send(\"shareLogin \" + getUserName() + \" \" + token + \" Android 24\");\n\n        verify(appClient2.responseMock, timeout(500)).channelRead(any(), eq(ok(1)));\n\n        clientPair.appClient.reset();\n\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        String parentProfileString = clientPair.appClient.getBody();\n        Profile parentProfile = JsonParser.parseProfileFromString(parentProfileString);\n\n        appClient2.send(\"loadProfileGzipped\");\n        String body2 = appClient2.getBody(2);\n\n        Twitter twitter = parentProfile.dashBoards[0].getTwitterWidget();\n        clearPrivateData(twitter);\n        Notification notification = parentProfile.dashBoards[0].getNotificationWidget();\n        clearPrivateData(notification);\n        for (Device device : parentProfile.dashBoards[0].devices) {\n            device.token = null;\n            device.hardwareInfo = null;\n            device.deviceOtaInfo = null;\n            device.lastLoggedIP = null;\n            device.disconnectTime = 0;\n            device.firstConnectTime = 0;\n            device.dataReceivedAt = 0;\n            device.connectTime = 0;\n            device.status = null;\n        }\n        parentProfile.dashBoards[0].sharedToken = null;\n\n        assertEquals(parentProfile.toString()\n                        .replace(\"\\\"disconnectTime\\\":0,\", \"\")\n                        .replace(\"\\\"firstConnectTime\\\":0,\", \"\")\n                        .replace(\"\\\"dataReceivedAt\\\":0,\", \"\")\n                        .replace(\"\\\"connectTime\\\":0,\", \"\"),\n                body2);\n    }\n\n    @Test\n    public void loadGzippedDashForSharedBoard() throws Exception {\n        clientPair.appClient.send(\"getShareToken 1\");\n\n        String token = clientPair.appClient.getBody();\n        assertNotNull(token);\n        assertEquals(32, token.length());\n\n        TestAppClient appClient2 = new TestAppClient(properties);\n        appClient2.start();\n        appClient2.send(\"shareLogin \" + getUserName() + \" \" + token + \" Android 24\");\n\n        verify(appClient2.responseMock, timeout(500)).channelRead(any(), eq(ok(1)));\n\n        clientPair.appClient.reset();\n\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        String parentProfileString = clientPair.appClient.getBody();\n        Profile parentProfile = JsonParser.parseProfileFromString(parentProfileString);\n\n        appClient2.send(\"loadProfileGzipped 1\");\n        String body2 = appClient2.getBody(2);\n\n        Twitter twitter = parentProfile.dashBoards[0].getTwitterWidget();\n        clearPrivateData(twitter);\n        Notification notification = parentProfile.dashBoards[0].getNotificationWidget();\n        clearPrivateData(notification);\n        for (Device device : parentProfile.dashBoards[0].devices) {\n            device.token = null;\n            device.hardwareInfo = null;\n            device.deviceOtaInfo = null;\n            device.lastLoggedIP = null;\n            device.disconnectTime = 0;\n            device.firstConnectTime = 0;\n            device.dataReceivedAt = 0;\n            device.connectTime = 0;\n            device.status = null;\n        }\n        parentProfile.dashBoards[0].sharedToken = null;\n\n        assertEquals(parentProfile.dashBoards[0].toString()\n                        .replace(\"\\\"disconnectTime\\\":0,\", \"\")\n                        .replace(\"\\\"firstConnectTime\\\":0,\", \"\")\n                        .replace(\"\\\"dataReceivedAt\\\":0,\", \"\")\n                        .replace(\"\\\"connectTime\\\":0,\", \"\"),\n                body2);\n    }\n\n\n    public static byte[] compress(String value) throws IOException {\n        byte[] stringData = value.getBytes(StandardCharsets.UTF_8);\n        ByteArrayOutputStream baos = new ByteArrayOutputStream(stringData.length);\n\n        try (OutputStream out = new DeflaterOutputStream(baos)) {\n            out.write(stringData);\n        }\n\n        return baos.toByteArray();\n    }\n\n    @Test\n    public void testGetShareTokenAndRefresh() throws Exception {\n        clientPair.appClient.send(\"getShareToken 1\");\n\n        String token = clientPair.appClient.getBody();\n        assertNotNull(token);\n        assertEquals(32, token.length());\n\n        TestAppClient appClient2 = new TestAppClient(properties);\n        appClient2.start();\n\n        appClient2.send(\"shareLogin \" + getUserName() + \" \" + token + \" Android 24\");\n        verify(appClient2.responseMock, timeout(500)).channelRead(any(), eq(ok(1)));\n\n        appClient2.send(\"loadProfileGzipped\");\n        Profile serverProfile = appClient2.parseProfile(2);\n        DashBoard dashboard = serverProfile.dashBoards[0];\n\n        assertNotNull(dashboard);\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"refreshShareToken 1\");\n        String refreshedToken = clientPair.appClient.getBody();\n        assertNotNull(refreshedToken);\n        assertNotEquals(refreshedToken, token);\n\n        TestAppClient appClient3 = new TestAppClient(properties);\n        appClient3.start();\n        appClient3.send(\"shareLogin \" + getUserName() + \" \" + token + \" Android 24\");\n        verify(appClient3.responseMock, timeout(500)).channelRead(any(), eq(notAllowed(1)));\n\n        TestAppClient appClient4 = new TestAppClient(properties);\n        appClient4.start();\n        appClient4.send(\"shareLogin \" + getUserName() + \" \" + refreshedToken + \" Android 24\");\n        verify(appClient4.responseMock, timeout(500)).channelRead(any(), eq(ok(1)));\n\n        appClient4.send(\"loadProfileGzipped\");\n        serverProfile = appClient4.parseProfile(2);\n        DashBoard serverDash = serverProfile.dashBoards[0];\n\n        assertNotNull(dashboard);\n        Profile profile = parseProfile(readTestUserProfile());\n        Twitter twitter = profile.dashBoards[0].getTwitterWidget();\n        clearPrivateData(twitter);\n        Notification notification = profile.dashBoards[0].getNotificationWidget();\n        clearPrivateData(notification);\n\n        //one field update, cause it is hard to compare.\n        profile.dashBoards[0].updatedAt = serverDash.updatedAt;\n        assertNull(serverDash.sharedToken);\n\n        serverDash.devices = null;\n        profile.dashBoards[0].devices = null;\n\n        assertEquals(profile.dashBoards[0].toString(), serverDash.toString());\n        //System.out.println(dashboard);\n    }\n\n    @Test\n    public void testMasterMasterSyncWorksWithoutToken() throws Exception {\n        TestAppClient appClient2 = new TestAppClient(properties);\n        appClient2.start();\n        appClient2.login(getUserName(), \"1\", \"Android\", \"24\");\n\n        verify(appClient2.responseMock, timeout(500)).channelRead(any(), eq(ok(1)));\n\n        clientPair.appClient.send(\"hardware 1 vw 1 1\");\n        verify(appClient2.responseMock, timeout(1000)).channelRead(any(), eq(produce(1, APP_SYNC, b(\"1 vw 1 1\"))));\n\n        appClient2.send(\"hardware 1 vw 2 2\");\n        verify(clientPair.appClient.responseMock, timeout(1000)).channelRead(any(), eq(produce(2, APP_SYNC, b(\"1 vw 2 2\"))));\n    }\n\n    @Test\n    public void checkLogoutCommandForSharedApp() throws Exception {\n        clientPair.appClient.send(\"getShareToken 1\");\n\n        String token = clientPair.appClient.getBody();\n        assertNotNull(token);\n        assertEquals(32, token.length());\n\n\n        TestAppClient appClient2 = new TestAppClient(properties);\n        appClient2.start();\n        appClient2.send(\"shareLogin \" + getUserName() + \" \" + token + \" Android 24\");\n\n        verify(appClient2.responseMock, timeout(500)).channelRead(any(), eq(ok(1)));\n\n        clientPair.appClient.send(\"hardware 1 vw 1 1\");\n        verify(appClient2.responseMock, timeout(1000)).channelRead(any(), eq(produce(2, APP_SYNC, b(\"1 vw 1 1\"))));\n\n        clientPair.appClient.reset();\n        clientPair.hardwareClient.reset();\n        appClient2.reset();\n\n        appClient2.send(\"addPushToken 1\\0uid2\\0token2\");\n        verify(appClient2.responseMock, timeout(500)).channelRead(any(), eq(ok(1)));\n\n        appClient2.send(\"logout uid2\");\n        verify(appClient2.responseMock, timeout(500)).channelRead(any(), eq(ok(2)));\n    }\n\n    @Test\n    public void testSharedProjectDoesntReceiveCommandFromOtherProjects() throws Exception {\n        DashBoard dash = new DashBoard();\n        dash.id = 333;\n        dash.name = \"AAAa\";\n        dash.isShared = true;\n        Device device = new Device();\n        device.id = 0;\n        device.name = \"123\";\n        dash.devices = new Device[] {device};\n\n        clientPair.appClient.createDash(dash);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.send(\"getShareToken 333\");\n        String token = clientPair.appClient.getBody(2);\n        assertNotNull(token);\n        assertEquals(32, token.length());\n\n        TestAppClient appClient2 = new TestAppClient(properties);\n        appClient2.start();\n        appClient2.send(\"shareLogin \" + getUserName() + \" \" + token + \" Android 24\");\n\n        verify(appClient2.responseMock, timeout(500)).channelRead(any(), eq(ok(1)));\n\n        clientPair.hardwareClient.send(\"hardware vw 1 1\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(1, HARDWARE, b(\"1-0 vw 1 1\"))));\n        verify(appClient2.responseMock, never()).channelRead(any(), eq(produce(1, HARDWARE, b(\"1-0 vw 1 1\"))));\n    }\n\n    private static void clearPrivateData(Notification n) {\n        n.iOSTokens.clear();\n        n.androidTokens.clear();\n    }\n\n    private static void clearPrivateData(Twitter t) {\n        t.username = null;\n        t.token = null;\n        t.secret = null;\n    }\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/tcp/SimplePerformanceTest.java",
    "content": "package cc.blynk.integration.tcp;\n\nimport cc.blynk.integration.SingleServerInstancePerTest;\nimport cc.blynk.integration.TestUtil;\nimport cc.blynk.integration.model.SimpleClientHandler;\nimport cc.blynk.integration.model.tcp.ClientPair;\nimport cc.blynk.integration.model.tcp.TestAppClient;\nimport cc.blynk.integration.model.tcp.TestHardClient;\nimport cc.blynk.utils.properties.ServerProperties;\nimport io.netty.channel.nio.NioEventLoopGroup;\nimport org.junit.Before;\nimport org.junit.Ignore;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\n\nimport static org.junit.Assert.assertEquals;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/2/2015.\n *\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class SimplePerformanceTest extends SingleServerInstancePerTest {\n\n    private NioEventLoopGroup sharedNioEventLoopGroup;\n\n    @Before\n    public void initTP()  {\n        this.sharedNioEventLoopGroup = new NioEventLoopGroup();\n    }\n\n    @Test\n    @Ignore\n    public void testConnectAppAndHardware() throws Exception {\n        int clientNumber = 200;\n        ExecutorService executorService = Executors.newFixedThreadPool(4);\n\n        ClientPair[] clients = new ClientPair[clientNumber];\n        List<Future<ClientPair>> futures = new ArrayList<>();\n\n        long start = System.currentTimeMillis();\n        for (int i = 0; i < clientNumber; i++) {\n            String email = \"dima\" + i +  \"@mail.ua\";\n\n            Future<ClientPair> future = executorService.submit(\n                    () -> initClientsWithSharedNio(\"localhost\",\n                            properties.getHttpsPort(), properties.getHttpPort(),\n                            email, \"1\", null, properties)\n            );\n            futures.add(future);\n        }\n\n        int counter = 0;\n        for (Future<ClientPair> clientPairFuture : futures) {\n            clients[counter] = clientPairFuture.get();\n            //removing mocks, replace with real class\n            clients[counter].appClient.replace(new SimpleClientHandler());\n            clients[counter].hardwareClient.replace(new SimpleClientHandler());\n            counter++;\n        }\n\n        System.out.println(clientNumber + \" client pairs created in \" + (System.currentTimeMillis() - start));\n        assertEquals(clientNumber, counter);\n    }\n\n    private ClientPair initClientsWithSharedNio(String host, int appPort, int hardPort, String user, String pass, String jsonProfile,\n                                        ServerProperties properties) throws Exception {\n\n        TestAppClient appClient = new TestAppClient(host, appPort, properties, sharedNioEventLoopGroup);\n        TestHardClient hardClient = new TestHardClient(host, hardPort, sharedNioEventLoopGroup);\n\n        return TestUtil.initAppAndHardPair(appClient, hardClient, user, pass, jsonProfile, 10000);\n    }\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/tcp/SyncWorkflowTest.java",
    "content": "package cc.blynk.integration.tcp;\n\nimport cc.blynk.integration.SingleServerInstancePerTest;\nimport cc.blynk.integration.model.tcp.TestHardClient;\nimport cc.blynk.server.core.dao.UserKey;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.Profile;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.BoardType;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.controls.Timer;\nimport cc.blynk.server.core.model.widgets.ui.TimeInput;\nimport cc.blynk.server.core.protocol.enums.Command;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.server.core.protocol.model.messages.common.HardwareMessage;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.ArgumentCaptor;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport java.time.DateTimeException;\nimport java.time.LocalDateTime;\nimport java.time.ZoneId;\nimport java.util.List;\n\nimport static cc.blynk.integration.TestUtil.appSync;\nimport static cc.blynk.integration.TestUtil.b;\nimport static cc.blynk.integration.TestUtil.createDevice;\nimport static cc.blynk.integration.TestUtil.ok;\nimport static cc.blynk.integration.TestUtil.setProperty;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_ENERGY;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE_CONNECTED;\nimport static cc.blynk.server.core.protocol.model.messages.MessageFactory.produce;\nimport static org.junit.Assert.assertArrayEquals;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\nimport static org.mockito.Mockito.after;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.eq;\nimport static org.mockito.Mockito.timeout;\nimport static org.mockito.Mockito.verify;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/2/2015.\n *\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class SyncWorkflowTest extends SingleServerInstancePerTest {\n\n    @Test\n    public void testHardSyncReturnHardwareCommands() throws Exception {\n        clientPair.hardwareClient.sync();\n        verify(clientPair.hardwareClient.responseMock, timeout(1000).times(8)).channelRead(any(), any());\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"dw 1 1\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"dw 2 1\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"dw 5 1\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"aw 3 0\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"vw 4 244\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"aw 7 3\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"aw 30 3\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"vw 13 60 143 158\"))));\n    }\n\n    @Test\n    public void testHardSyncReturnNoSetPropertyCommands() throws Exception {\n        clientPair.hardwareClient.setProperty(44, \"label\", \"hello\");\n        clientPair.hardwareClient.verifyResult(ok(1));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(setProperty(1, \"1-0 44 label hello\")));\n        clientPair.hardwareClient.reset();\n\n        testHardSyncReturnHardwareCommands();\n    }\n\n    @Test\n    public void testHardSyncReturnNothingNoWidgetOnPin() throws Exception {\n        clientPair.hardwareClient.sync(PinType.VIRTUAL, 22);\n        verify(clientPair.hardwareClient.responseMock, after(400).never()).channelRead(any(), any());\n    }\n\n    @Test\n    public void testHardSyncReturnValueForNoWidgetOnVirtualPin() throws Exception {\n        clientPair.hardwareClient.send(\"hardware vw 67 100\");\n\n        clientPair.hardwareClient.sync(PinType.VIRTUAL, 67);\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(produce(2, HARDWARE, b(\"vw 67 100\"))));\n\n        clientPair.hardwareClient.reset();\n\n        clientPair.hardwareClient.sync();\n        verify(clientPair.hardwareClient.responseMock, timeout(1000).times(9)).channelRead(any(), any());\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"dw 1 1\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"dw 2 1\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"dw 5 1\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"aw 3 0\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"vw 4 244\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"aw 7 3\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"aw 30 3\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"vw 13 60 143 158\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"vw 67 100\"))));\n    }\n\n    @Test\n    public void testHardSyncReturnValueForNoWidgetOnAnalogPin() throws Exception {\n        clientPair.hardwareClient.send(\"hardware aw 66 100\");\n\n        clientPair.hardwareClient.sync(PinType.ANALOG, 66);\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(produce(2, HARDWARE, b(\"aw 66 100\"))));\n\n        clientPair.hardwareClient.reset();\n\n        clientPair.hardwareClient.sync();\n        verify(clientPair.hardwareClient.responseMock, timeout(1000).times(9)).channelRead(any(), any());\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"dw 1 1\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"dw 2 1\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"dw 5 1\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"aw 3 0\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"vw 4 244\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"aw 7 3\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"aw 30 3\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"vw 13 60 143 158\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"aw 66 100\"))));\n    }\n\n    @Test\n    public void testHardSyncReturn1HardwareCommand() throws Exception {\n        clientPair.hardwareClient.sync(PinType.VIRTUAL, 4);\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(produce(1, HARDWARE, b(\"vw 4 244\"))));\n    }\n\n    @Test\n    public void testLCDOnActivateSendsCorrectBodySimpleMode() throws Exception {\n        clientPair.appClient.createWidget(1, \"{\\\"type\\\":\\\"LCD\\\",\\\"id\\\":1923810267,\\\"x\\\":0,\\\"y\\\":6,\\\"color\\\":600084223,\\\"width\\\":8,\\\"height\\\":2,\\\"tabId\\\":0,\\\"\" +\n                \"pins\\\":[\" +\n                \"{\\\"pin\\\":10,\\\"pinType\\\":\\\"VIRTUAL\\\",\\\"pwmMode\\\":false,\\\"rangeMappingOn\\\":false,\\\"min\\\":0,\\\"max\\\":1023, \\\"value\\\":\\\"10\\\"},\" +\n                \"{\\\"pin\\\":11,\\\"pinType\\\":\\\"VIRTUAL\\\",\\\"pwmMode\\\":false,\\\"rangeMappingOn\\\":false,\\\"min\\\":0,\\\"max\\\":1023, \\\"value\\\":\\\"11\\\"}],\" +\n                \"\\\"advancedMode\\\":false,\\\"textLight\\\":false,\\\"textLightOn\\\":false,\\\"frequency\\\":1000}\");\n\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.activate(1);\n\n        verify(clientPair.appClient.responseMock, timeout(500).times(13)).channelRead(any(), any());\n\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 10 10\")));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(appSync(b(\"1-0 vw 11 11\"))));\n\n\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 1 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 2 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 3 0\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 5 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 4 244\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 7 3\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 30 3\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 0 89.888037459418\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 11 -58.74774244674501\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 13 60 143 158\")));\n    }\n\n    @Test\n    public void testLCDOnActivateSendsCorrectBodyAdvancedMode() throws Exception {\n        clientPair.appClient.createWidget(1, \"{\\\"type\\\":\\\"LCD\\\",\\\"id\\\":1923810267,\\\"x\\\":0,\\\"y\\\":6,\\\"color\\\":600084223,\\\"width\\\":8,\\\"height\\\":2,\\\"tabId\\\":0,\\\"\" +\n                \"pins\\\":[\" +\n                \"{\\\"pin\\\":10,\\\"pinType\\\":\\\"VIRTUAL\\\",\\\"pwmMode\\\":false,\\\"rangeMappingOn\\\":false,\\\"min\\\":0,\\\"max\\\":1023},\" +\n                \"{\\\"pin\\\":11,\\\"pinType\\\":\\\"VIRTUAL\\\",\\\"pwmMode\\\":false,\\\"rangeMappingOn\\\":false,\\\"min\\\":0,\\\"max\\\":1023}],\" +\n                \"\\\"advancedMode\\\":true,\\\"textLight\\\":false,\\\"textLightOn\\\":false,\\\"frequency\\\":1000}\");\n\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 10 p x y 10\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new HardwareMessage(1, b(\"1-0 vw 10 p x y 10\"))));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.activate(1);\n\n        verify(clientPair.appClient.responseMock, timeout(500).times(12)).channelRead(any(), any());\n\n        clientPair.appClient.verifyResult(ok(1));\n\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(appSync(b(\"1-0 vw 10 p x y 10\"))));\n\n\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 1 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 2 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 3 0\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 5 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 4 244\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 7 3\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 30 3\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 0 89.888037459418\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 11 -58.74774244674501\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 13 60 143 158\")));\n    }\n\n    @Test\n    public void testHardSyncReturnRTCWithoutTimezone() throws Exception {\n        clientPair.hardwareClient.send(\"internal rtc\");\n\n        long expectedTS = System.currentTimeMillis() / 1000;\n\n        ArgumentCaptor<StringMessage> objectArgumentCaptor = ArgumentCaptor.forClass(StringMessage.class);\n        verify(clientPair.hardwareClient.responseMock, timeout(500).times(1)).channelRead(any(), objectArgumentCaptor.capture());\n\n        List<StringMessage> arguments = objectArgumentCaptor.getAllValues();\n        StringMessage hardMessage = arguments.get(0);\n        assertEquals(1, hardMessage.id);\n        assertEquals(Command.BLYNK_INTERNAL, hardMessage.command);\n        assertEquals(14, hardMessage.body.length());\n        String tsString = hardMessage.body.split(\"\\0\")[1];\n        long ts = Long.valueOf(tsString);\n\n        assertEquals(expectedTS, ts, 7200 + 100);\n    }\n\n    @Test\n    public void testHardSyncReturnRTCWithUTCTimezone() throws Exception {\n        clientPair.appClient.createWidget(1, \"{\\\"type\\\":\\\"RTC\\\",\\\"id\\\":99, \" +\n                \"\\\"x\\\":0,\\\"y\\\":0,\\\"width\\\":2,\\\"height\\\":1}\");\n\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"internal rtc\");\n\n        long expectedTS = System.currentTimeMillis() / 1000;\n\n        ArgumentCaptor<StringMessage> objectArgumentCaptor = ArgumentCaptor.forClass(StringMessage.class);\n        verify(clientPair.hardwareClient.responseMock, timeout(500).times(1)).channelRead(any(), objectArgumentCaptor.capture());\n\n        List<StringMessage> arguments = objectArgumentCaptor.getAllValues();\n        StringMessage hardMessage = arguments.get(0);\n        assertEquals(1, hardMessage.id);\n        assertEquals(Command.BLYNK_INTERNAL, hardMessage.command);\n        assertEquals(14, hardMessage.body.length());\n        String tsString = hardMessage.body.split(\"\\0\")[1];\n        long ts = Long.valueOf(tsString);\n\n        assertEquals(expectedTS, ts, 7200 + 100);\n    }\n\n    @Test(expected = DateTimeException.class)\n    public void testWrongAsiaTimeZone() {\n        ZoneId.of(\"Asia/Hanoi\");\n    }\n\n    @Test\n    public void testCorrectAsiaTimeZone() {\n        ZoneId.of(\"Asia/Ho_Chi_Minh\");\n    }\n\n    @Test\n    public void testHardSyncReturnRTCWithUTCTimezonePlus3() throws Exception {\n        ZoneId zoneId = ZoneId.of(\"Europe/Kiev\");\n\n        clientPair.appClient.createWidget(1, \"{\\\"type\\\":\\\"RTC\\\",\\\"id\\\":99, \" +\n                \"\\\"x\\\":0,\\\"y\\\":0,\\\"width\\\":1,\\\"height\\\":1,\" +\n                \"\\\"tzName\\\":\\\"TZ\\\"}\".replace(\"TZ\", zoneId.toString()));\n\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"internal rtc\");\n\n        int offset = LocalDateTime.now().atZone(zoneId).getOffset().getTotalSeconds();\n\n        long expectedTS = System.currentTimeMillis() / 1000 + LocalDateTime.now().atZone(zoneId).getOffset().getTotalSeconds();\n\n        ArgumentCaptor<StringMessage> objectArgumentCaptor = ArgumentCaptor.forClass(StringMessage.class);\n        verify(clientPair.hardwareClient.responseMock, timeout(500).times(1)).channelRead(any(), objectArgumentCaptor.capture());\n\n        List<StringMessage> arguments = objectArgumentCaptor.getAllValues();\n        StringMessage hardMessage = arguments.get(0);\n        assertEquals(1, hardMessage.id);\n        assertEquals(Command.BLYNK_INTERNAL, hardMessage.command);\n        assertEquals(14, hardMessage.body.length());\n        String tsString = hardMessage.body.split(\"\\0\")[1];\n        long ts = Long.valueOf(tsString);\n\n        assertEquals(expectedTS, ts, offset + 100);\n    }\n\n    @Test\n    public void testHardSyncReturnRTCWithUTCTimezoneMinus3() throws Exception {\n        ZoneId zoneId = ZoneId.of(\"Brazil/East\");\n\n        clientPair.appClient.createWidget(1, \"{\\\"type\\\":\\\"RTC\\\",\\\"id\\\":99, \" +\n                \"\\\"x\\\":0,\\\"y\\\":0,\\\"width\\\":1,\\\"height\\\":1,\" +\n                \"\\\"tzName\\\":\\\"TZ\\\"}\".replace(\"TZ\", zoneId.toString()));\n\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"internal rtc\");\n\n        long expectedTS = System.currentTimeMillis() / 1000 + LocalDateTime.now().atZone(zoneId).getOffset().getTotalSeconds();\n\n        ArgumentCaptor<StringMessage> objectArgumentCaptor = ArgumentCaptor.forClass(StringMessage.class);\n        verify(clientPair.hardwareClient.responseMock, timeout(500).times(1)).channelRead(any(), objectArgumentCaptor.capture());\n\n        List<StringMessage> arguments = objectArgumentCaptor.getAllValues();\n        StringMessage hardMessage = arguments.get(0);\n        assertEquals(1, hardMessage.id);\n        assertEquals(Command.BLYNK_INTERNAL, hardMessage.command);\n        assertEquals(14, hardMessage.body.length());\n        String tsString = hardMessage.body.split(\"\\0\")[1];\n        long ts = Long.valueOf(tsString);\n\n        assertEquals(expectedTS, ts, -LocalDateTime.now().atZone(zoneId).getOffset().getTotalSeconds() + 100);\n    }\n\n\n    @Test\n    public void testHardSyncForTimeInputWidget() throws Exception {\n        clientPair.appClient.createWidget(1, \"{\\\"type\\\":\\\"TIME_INPUT\\\",\\\"id\\\":99, \\\"pin\\\":99, \\\"pinType\\\":\\\"VIRTUAL\\\", \" +\n                \"\\\"x\\\":0,\\\"y\\\":0,\\\"width\\\":2,\\\"height\\\":1}\");\n\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.send(\"hardware 1-0 vw \" + b(\"99 82800 82860 Europe/Kiev 1\"));\n        verify(clientPair.hardwareClient.responseMock, timeout(500).times(1)).channelRead(any(), eq(produce(2, HARDWARE, b(\"vw 99 82800 82860 Europe/Kiev 1\"))));\n\n        clientPair.hardwareClient.sync(PinType.VIRTUAL, 99);\n        verify(clientPair.hardwareClient.responseMock, timeout(500).times(1)).channelRead(any(), eq(produce(1, HARDWARE, b(\"vw 99 82800 82860 Europe/Kiev 1\"))));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(1);\n        TimeInput timeInput = (TimeInput) profile.dashBoards[0].findWidgetByPin(0, (short) 99, PinType.VIRTUAL);\n        assertNotNull(timeInput);\n        assertEquals(82800, timeInput.startAt);\n        assertEquals(82860, timeInput.stopAt);\n        assertEquals(ZoneId.of(\"Europe/Kiev\"), timeInput.tzName);\n        assertArrayEquals(new int[] {1}, timeInput.days);\n    }\n\n    @Test\n    public void testSyncForTimer() throws Exception {\n        User user = holder.userDao.users.get(new UserKey(getUserName(), \"Blynk\"));\n        Widget widget = user.profile.dashBoards[0].findWidgetByPin(0, (short) 5, PinType.DIGITAL);\n        Timer timer = (Timer) widget;\n        timer.value = \"100500\";\n\n        clientPair.hardwareClient.sync(PinType.DIGITAL, 5);\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(produce(1, HARDWARE, b(\"dw 5 100500\"))));\n\n        Thread thread = new Thread(() -> {\n            timer.value = \"200300\";\n        });\n\n        thread.start();\n        thread.join();\n\n        clientPair.hardwareClient.sync(PinType.DIGITAL, 5);\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(produce(2, HARDWARE, b(\"dw 5 200300\"))));\n\n        clientPair.hardwareClient.reset();\n\n        clientPair.hardwareClient.sync();\n        verify(clientPair.hardwareClient.responseMock, timeout(1000).times(8)).channelRead(any(), any());\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"dw 1 1\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"dw 2 1\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"dw 5 200300\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"aw 3 0\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"vw 4 244\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"aw 7 3\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"aw 30 3\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"vw 13 60 143 158\"))));\n    }\n\n\n\n    @Test\n    public void testTerminalSendsSyncOnActivate() throws Exception {\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(1);\n        assertEquals(16, profile.dashBoards[0].widgets.length);\n\n        clientPair.appClient.send(\"getEnergy\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(2, GET_ENERGY, \"7500\")));\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":102, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":5, \\\"y\\\":0, \\\"tabId\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"TERMINAL\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":17}\");\n        clientPair.appClient.verifyResult(ok(3));\n\n        clientPair.hardwareClient.send(\"hardware vw 17 a\");\n        clientPair.hardwareClient.send(\"hardware vw 17 b\");\n        clientPair.hardwareClient.send(\"hardware vw 17 c\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(1, HARDWARE, b(\"1-0 vw 17 a\"))));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(2, HARDWARE, b(\"1-0 vw 17 b\"))));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(3, HARDWARE, b(\"1-0 vw 17 c\"))));\n\n        clientPair.appClient.deactivate(1);\n        clientPair.appClient.verifyResult(ok(4));\n\n        clientPair.appClient.activate(1);\n        clientPair.appClient.verifyResult(ok(5));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(appSync(b(\"1-0 vm 17 a b c\"))));\n    }\n\n    @Test\n    public void testLCDSendsSyncOnActivate() throws Exception {\n        clientPair.hardwareClient.send(\"hardware vw 20 p 0 0 Hello\");\n        clientPair.hardwareClient.send(\"hardware vw 20 p 0 1 World\");\n\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(1, HARDWARE, b(\"1-0 vw 20 p 0 0 Hello\"))));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(2, HARDWARE, b(\"1-0 vw 20 p 0 1 World\"))));\n\n        clientPair.appClient.activate(1);\n        clientPair.appClient.verifyResult(ok(1));\n\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(appSync(b(\"1-0 vw 20 p 0 0 Hello\"))));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(appSync(b(\"1-0 vw 20 p 0 1 World\"))));\n    }\n\n    @Test\n    public void testLCDSendsSyncOnActivate2() throws Exception {\n        clientPair.hardwareClient.send(\"hardware vw 20 p 0 0 H1\");\n        clientPair.hardwareClient.send(\"hardware vw 20 p 0 1 H2\");\n        clientPair.hardwareClient.send(\"hardware vw 20 p 0 2 H3\");\n        clientPair.hardwareClient.send(\"hardware vw 20 p 0 3 H4\");\n        clientPair.hardwareClient.send(\"hardware vw 20 p 0 4 H5\");\n        clientPair.hardwareClient.send(\"hardware vw 20 p 0 5 H6\");\n        clientPair.hardwareClient.send(\"hardware vw 20 p 0 6 H7\");\n\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(1, HARDWARE, b(\"1-0 vw 20 p 0 0 H1\"))));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(2, HARDWARE, b(\"1-0 vw 20 p 0 1 H2\"))));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(3, HARDWARE, b(\"1-0 vw 20 p 0 2 H3\"))));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(4, HARDWARE, b(\"1-0 vw 20 p 0 3 H4\"))));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(5, HARDWARE, b(\"1-0 vw 20 p 0 4 H5\"))));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(6, HARDWARE, b(\"1-0 vw 20 p 0 5 H6\"))));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(7, HARDWARE, b(\"1-0 vw 20 p 0 6 H7\"))));\n\n        clientPair.appClient.activate(1);\n        clientPair.appClient.verifyResult(ok(1));\n\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(appSync(b(\"1-0 vw 20 p 0 1 H2\"))));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(appSync(b(\"1-0 vw 20 p 0 2 H3\"))));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(appSync(b(\"1-0 vw 20 p 0 3 H4\"))));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(appSync(b(\"1-0 vw 20 p 0 4 H5\"))));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(appSync(b(\"1-0 vw 20 p 0 5 H6\"))));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(appSync(b(\"1-0 vw 20 p 0 6 H7\"))));\n    }\n\n\n    @Test\n    public void testSyncWorksForGauge() throws Exception {\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":155, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"GAUGE\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":100}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 100 101\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new HardwareMessage(1, b(\"1-0 vw 100 101\"))));\n\n        clientPair.hardwareClient.sync(PinType.VIRTUAL, 100);\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(new HardwareMessage(2, b(\"vw 100 101\"))));\n    }\n\n    @Test\n    public void testSyncForMultiPins() throws Exception {\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":155, \\\"width\\\":1, \\\"height\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"GAUGE\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":100}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 100 100\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new HardwareMessage(1, b(\"1-0 vw 100 100\"))));\n\n        clientPair.hardwareClient.send(\"hardware vw 101 101\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new HardwareMessage(2, b(\"1-0 vw 101 101\"))));\n\n        clientPair.hardwareClient.sync(PinType.VIRTUAL, 100, 101);\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(new HardwareMessage(3, b(\"vw 100 100\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(new HardwareMessage(3, b(\"vw 101 101\"))));\n    }\n\n    @Test\n    public void testActivateAndGetSync() throws Exception {\n        clientPair.appClient.activate(1);\n\n        verify(clientPair.appClient.responseMock, timeout(500).times(11)).channelRead(any(), any());\n\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 1 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 2 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 3 0\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 5 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 4 244\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 7 3\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 30 3\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 0 89.888037459418\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 11 -58.74774244674501\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 13 60 143 158\")));\n    }\n\n    @Test\n    public void testSyncForMultiDevices() throws Exception {\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":188, \\\"width\\\":1, \\\"height\\\":1, \\\"deviceId\\\":1, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Some Text\\\", \\\"type\\\":\\\"BUTTON\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":4, \\\"value\\\":1}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice(2);\n        assertNotNull(device);\n        assertNotNull(device.token);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(createDevice(2, device)));\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", properties.getHttpPort());\n        hardClient2.start();\n\n        hardClient2.login(device.token);\n        hardClient2.verifyResult(ok(1));\n        hardClient2.reset();\n\n        clientPair.hardwareClient.sync();\n        verify(clientPair.hardwareClient.responseMock, timeout(1000).times(8)).channelRead(any(), any());\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"dw 1 1\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"dw 2 1\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"dw 5 1\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"aw 3 0\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"vw 4 244\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"aw 7 3\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"aw 30 3\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(1, HARDWARE, b(\"vw 13 60 143 158\"))));\n\n        hardClient2.sync();\n        verify(hardClient2.responseMock, timeout(500)).channelRead(any(), eq(produce(1, HARDWARE, b(\"vw 4 1\"))));\n    }\n\n    @Test\n    public void testSyncForMultiDevicesNoWidget() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", properties.getHttpPort());\n        hardClient2.start();\n\n        hardClient2.login(device.token);\n        hardClient2.verifyResult(ok(1));\n        hardClient2.reset();\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(1, HARDWARE_CONNECTED, \"1-1\")));\n\n        clientPair.hardwareClient.send(\"hardware vw 119 1\");\n        hardClient2.send(\"hardware vw 119 1\");\n\n        clientPair.hardwareClient.sync();\n        verify(clientPair.hardwareClient.responseMock, timeout(1000).times(9)).channelRead(any(), any());\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(2, HARDWARE, b(\"dw 1 1\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(2, HARDWARE, b(\"dw 2 1\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(2, HARDWARE, b(\"dw 5 1\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(2, HARDWARE, b(\"aw 3 0\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(2, HARDWARE, b(\"vw 4 244\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(2, HARDWARE, b(\"aw 7 3\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(2, HARDWARE, b(\"aw 30 3\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(2, HARDWARE, b(\"vw 13 60 143 158\"))));\n        verify(clientPair.hardwareClient.responseMock, timeout(100)).channelRead(any(), eq(produce(2, HARDWARE, b(\"vw 119 1\"))));\n\n        hardClient2.sync();\n        verify(hardClient2.responseMock, timeout(100)).channelRead(any(), eq(produce(2, HARDWARE, b(\"vw 119 1\"))));\n    }\n\n    @Test\n    public void testHardSyncSinglePinFor2DEvices() throws Exception {\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP32_Dev_Board);\n        Device device2 = new Device(2, \"My Device2\", BoardType.ESP32_Dev_Board);\n\n        DashBoard dash = new DashBoard();\n        dash.id = 2;\n        dash.name = \"123\";\n        dash.isActive = true;\n\n        clientPair.appClient.createDash(dash);\n        clientPair.appClient.verifyResult(ok(1));\n\n        Device tempDevice1;\n        clientPair.appClient.createDevice(1, device1);\n        tempDevice1 = clientPair.appClient.parseDevice(2);\n        assertNotNull(tempDevice1);\n        assertNotNull(tempDevice1.token);\n        clientPair.appClient.verifyResult(createDevice(2, tempDevice1));\n\n        Device tempDevice2;\n        clientPair.appClient.createDevice(2, device2);\n        tempDevice2 = clientPair.appClient.parseDevice(3);\n        assertNotNull(tempDevice2);\n        assertNotNull(tempDevice2.token);\n        clientPair.appClient.verifyResult(createDevice(3, tempDevice2));\n\n        //set pin state from the app\n        clientPair.appClient.send(\"hardware 1-1 vw 44 444\");\n        clientPair.appClient.send(\"hardware 2-2 vw 44 445\");\n\n        TestHardClient hardClient1 = new TestHardClient(\"localhost\", properties.getHttpPort());\n        hardClient1.start();\n        hardClient1.login(tempDevice1.token);\n        hardClient1.verifyResult(ok(1));\n        hardClient1.reset();\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", properties.getHttpPort());\n        hardClient2.start();\n        hardClient2.login(tempDevice2.token);\n        hardClient2.verifyResult(ok(1));\n        hardClient2.reset();\n\n        hardClient1.sync(PinType.VIRTUAL, 44);\n        hardClient1.verifyResult(produce(1, HARDWARE, b(\"vw 44 444\")));\n\n        hardClient2.sync(PinType.VIRTUAL, 44);\n        hardClient2.verifyResult(produce(1, HARDWARE, b(\"vw 44 445\")));\n\n        hardClient1.send(\"hardware vw 45 555\");\n        hardClient2.send(\"hardware vw 45 556\");\n\n        hardClient1.sync(PinType.VIRTUAL, 45);\n        hardClient1.verifyResult(produce(3, HARDWARE, b(\"vw 45 555\")));\n\n        hardClient2.sync(PinType.VIRTUAL, 45);\n        hardClient2.verifyResult(produce(3, HARDWARE, b(\"vw 45 556\")));\n    }\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/tcp/TableCommandsTest.java",
    "content": "package cc.blynk.integration.tcp;\n\nimport cc.blynk.integration.SingleServerInstancePerTest;\nimport cc.blynk.server.core.model.Profile;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.widgets.ui.table.Row;\nimport cc.blynk.server.core.model.widgets.ui.table.Table;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport static cc.blynk.integration.TestUtil.b;\nimport static cc.blynk.integration.TestUtil.ok;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE;\nimport static cc.blynk.server.core.protocol.model.messages.MessageFactory.produce;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertFalse;\nimport static org.junit.Assert.assertNotNull;\nimport static org.junit.Assert.assertTrue;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.eq;\nimport static org.mockito.Mockito.timeout;\nimport static org.mockito.Mockito.verify;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 7/09/2016.\n *\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class TableCommandsTest extends SingleServerInstancePerTest {\n\n    @Test\n    public void testAllTableCommands() throws Exception {\n        Table table = new Table();\n        table.pin = 123;\n        table.pinType = PinType.VIRTUAL;\n        table.isClickableRows = true;\n        table.isReoderingAllowed = true;\n        table.height = 2;\n        table.width = 2;\n\n        clientPair.appClient.createWidget(1, table);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 123 clr\");\n        verify(clientPair.hardwareClient.responseMock, timeout(500).times(0)).channelRead(any(), any());\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(1, HARDWARE, b(\"1-0 vw 123 clr\"))));\n\n        clientPair.hardwareClient.send(\"hardware vw 123 add 0 Row0 row0\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(2, HARDWARE, b(\"1-0 vw 123 add 0 Row0 row0\"))));\n\n        table = loadTable();\n        Row row;\n\n        assertNotNull(table);\n        assertNotNull(table.rows);\n        assertEquals(1, table.rows.size());\n        row = table.rows.poll();\n        assertNotNull(row);\n        assertEquals(0, row.id);\n        assertEquals(\"Row0\", row.name);\n        assertEquals(\"row0\", row.value);\n        assertTrue(row.isSelected);\n        assertEquals(0, table.currentRowIndex);\n\n        clientPair.hardwareClient.send(\"hardware vw 123 pick 2\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(3, HARDWARE, b(\"1-0 vw 123 pick 2\"))));\n\n        table = loadTable();\n        assertNotNull(table);\n        assertNotNull(table.rows);\n        assertEquals(1, table.rows.size());\n        row = table.rows.poll();\n        assertNotNull(row);\n        assertEquals(2, table.currentRowIndex);\n\n        clientPair.hardwareClient.send(\"hardware vw 123 add 1 Row1 row1\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(4, HARDWARE, b(\"1-0 vw 123 add 1 Row1 row1\"))));\n        clientPair.hardwareClient.send(\"hardware vw 123 add 2 Row2 row2\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(5, HARDWARE, b(\"1-0 vw 123 add 2 Row2 row2\"))));\n        clientPair.hardwareClient.send(\"hardware vw 123 pick 2\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(6, HARDWARE, b(\"1-0 vw 123 pick 2\"))));\n\n        table = loadTable();\n        assertNotNull(table);\n        assertNotNull(table.rows);\n        assertEquals(3, table.rows.size());\n        row = table.rows.poll();\n        assertNotNull(row);\n        assertEquals(2, table.currentRowIndex);\n\n        clientPair.hardwareClient.send(\"hardware vw 123 deselect 1\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(7, HARDWARE, b(\"1-0 vw 123 deselect 1\"))));\n\n        table = loadTable();\n        assertNotNull(table);\n        assertNotNull(table.rows);\n        assertEquals(3, table.rows.size());\n        table.rows.poll();\n        row = table.rows.poll();\n        assertNotNull(row);\n        assertFalse(row.isSelected);\n\n        clientPair.hardwareClient.send(\"hardware vw 123 select 1\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(8, HARDWARE, b(\"1-0 vw 123 select 1\"))));\n\n        table = loadTable();\n        assertNotNull(table);\n        assertNotNull(table.rows);\n        assertEquals(3, table.rows.size());\n        table.rows.poll();\n        row = table.rows.poll();\n        assertNotNull(row);\n        assertTrue(row.isSelected);\n\n        /*\n        clientPair.hardwareClient.send(\"hardware vw 123 order 0 2\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(9, HARDWARE, b(\"1-0 vw 123 order 0 2\"))));\n\n        table = loadTable();\n        assertNotNull(table);\n        assertNotNull(table.rows);\n        assertEquals(3, table.rows.size());\n\n        assertEquals(1, table.rows.poll().id);\n        assertEquals(2, table.rows.poll().id);\n        assertEquals(0, table.rows.poll().id);\n        */\n\n        clientPair.hardwareClient.send(\"hardware vw 123 clr\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(9, HARDWARE, b(\"1-0 vw 123 clr\"))));\n        table = loadTable();\n        assertNotNull(table);\n        assertNotNull(table.rows);\n        assertEquals(0, table.rows.size());\n    }\n\n    @Test\n    public void testTableUpdateExistingRow() throws Exception {\n        Table table = new Table();\n        table.pin = 123;\n        table.pinType = PinType.VIRTUAL;\n        table.isClickableRows = true;\n        table.isReoderingAllowed = true;\n        table.height = 2;\n        table.width = 2;\n\n        clientPair.appClient.createWidget(1, table);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 123 clr\");\n        verify(clientPair.hardwareClient.responseMock, timeout(500).times(0)).channelRead(any(), any());\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(1, HARDWARE, b(\"1-0 vw 123 clr\"))));\n\n        clientPair.hardwareClient.send(\"hardware vw 123 add 0 Row0 row0\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(2, HARDWARE, b(\"1-0 vw 123 add 0 Row0 row0\"))));\n\n        table = loadTable();\n        Row row;\n\n        assertNotNull(table);\n        assertNotNull(table.rows);\n        assertEquals(1, table.rows.size());\n        row = table.rows.poll();\n        assertNotNull(row);\n        assertEquals(0, row.id);\n        assertEquals(\"Row0\", row.name);\n        assertEquals(\"row0\", row.value);\n        assertTrue(row.isSelected);\n        assertEquals(0, table.currentRowIndex);\n\n        clientPair.hardwareClient.send(\"hardware vw 123 update 0 Row0Updated row0Updated\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(3, HARDWARE, b(\"1-0 vw 123 update 0 Row0Updated row0Updated\"))));\n\n        table = loadTable();\n\n        assertNotNull(table);\n        assertNotNull(table.rows);\n        assertEquals(1, table.rows.size());\n        row = table.rows.poll();\n        assertNotNull(row);\n        assertEquals(0, row.id);\n        assertEquals(\"Row0Updated\", row.name);\n        assertEquals(\"row0Updated\", row.value);\n        assertTrue(row.isSelected);\n        assertEquals(0, table.currentRowIndex);\n    }\n\n    @Test\n    public void testTableRowLimit() throws Exception {\n        Table table = new Table();\n        table.pin = 123;\n        table.pinType = PinType.VIRTUAL;\n        table.isClickableRows = true;\n        table.isReoderingAllowed = true;\n        table.width = 2;\n        table.height = 2;\n\n        clientPair.appClient.createWidget(1, table);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 123 clr\");\n        verify(clientPair.hardwareClient.responseMock, timeout(500).times(0)).channelRead(any(), any());\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(1, HARDWARE, b(\"1-0 vw 123 clr\"))));\n\n        for (int i = 1; i <= 101; i++) {\n            String cmd = \"vw 123 add \" + i + \" Row0 row0\";\n            clientPair.hardwareClient.send(\"hardware \" + cmd);\n            verify(clientPair.appClient.responseMock, timeout(700)).channelRead(any(), eq(produce(i + 1, HARDWARE, b(\"1-0 \" + cmd))));\n        }\n\n\n        table = loadTable();\n        Row row;\n\n        assertNotNull(table);\n        assertNotNull(table.rows);\n        assertEquals(100, table.rows.size());\n        for (int i = 2; i <= 101; i++) {\n            row = table.rows.poll();\n            assertNotNull(row);\n            assertEquals(i, row.id);\n        }\n    }\n\n    @Test\n    public void testTableAcceptsOnlyUniqueIds() throws Exception {\n        Table table = new Table();\n        table.pin = 123;\n        table.pinType = PinType.VIRTUAL;\n        table.isClickableRows = true;\n        table.isReoderingAllowed = true;\n        table.width = 2;\n        table.height = 2;\n\n        clientPair.appClient.createWidget(1, table);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 123 clr\");\n        verify(clientPair.hardwareClient.responseMock, timeout(500).times(0)).channelRead(any(), any());\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(1, HARDWARE, b(\"1-0 vw 123 clr\"))));\n\n        clientPair.hardwareClient.send(\"hardware vw 123 add 0 row0 val0\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(2, HARDWARE, b(\"1-0 vw 123 add 0 row0 val0\"))));\n\n        clientPair.hardwareClient.send(\"hardware vw 123 add 0 row1 val1\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(3, HARDWARE, b(\"1-0 vw 123 add 0 row1 val1\"))));\n\n        table = loadTable();\n\n        assertEquals(1, table.rows.size());\n        assertEquals(\"row1\", table.rows.peek().name);\n        assertEquals(\"val1\", table.rows.peek().value);\n    }\n\n    private Table loadTable() throws Exception {\n        clientPair.appClient.reset();\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(1);\n        return (Table) profile.dashBoards[0].findWidgetByPin(0, (short) 123, PinType.VIRTUAL);\n    }\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/tcp/TagCommandsTest.java",
    "content": "package cc.blynk.integration.tcp;\n\nimport cc.blynk.integration.SingleServerInstancePerTest;\nimport cc.blynk.integration.model.tcp.TestHardClient;\nimport cc.blynk.server.core.model.device.BoardType;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.device.Tag;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.protocol.model.messages.common.HardwareMessage;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport static cc.blynk.integration.TestUtil.appSync;\nimport static cc.blynk.integration.TestUtil.b;\nimport static cc.blynk.integration.TestUtil.createDevice;\nimport static cc.blynk.integration.TestUtil.createTag;\nimport static cc.blynk.integration.TestUtil.illegalCommand;\nimport static cc.blynk.integration.TestUtil.ok;\nimport static org.junit.Assert.assertArrayEquals;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.eq;\nimport static org.mockito.Mockito.timeout;\nimport static org.mockito.Mockito.verify;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/2/2015.\n *\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class TagCommandsTest extends SingleServerInstancePerTest {\n\n    @Test\n    public void testAddNewTag() throws Exception {\n        Tag tag0 = new Tag(100_000, \"Tag1\");\n\n        clientPair.appClient.createTag(1, tag0);\n        String createdTag = clientPair.appClient.getBody();\n        Tag tag = JsonParser.parseTag(createdTag, 0);\n        assertNotNull(tag);\n        assertEquals(100_000, tag.id);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(createTag(1, tag)));\n\n        clientPair.appClient.reset();\n\n        clientPair.appClient.send(\"getTags 1\");\n        String response = clientPair.appClient.getBody();\n\n        Tag[] tags = JsonParser.MAPPER.readValue(response, Tag[].class);\n        assertNotNull(tags);\n        assertEquals(1, tags.length);\n\n        assertEqualTag(tag0, tags[0]);\n    }\n\n    @Test\n    public void testUpdateExistingDevice() throws Exception {\n        Tag tag0 = new Tag(100_000, \"Tag1\");\n\n        clientPair.appClient.createTag(1, tag0);\n        String createdTag = clientPair.appClient.getBody();\n        Tag tag = JsonParser.parseTag(createdTag, 0);\n        assertNotNull(tag);\n        assertEquals(100_000, tag.id);\n        assertEquals(\"Tag1\", tag.name);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(createTag(1, tag)));\n\n        clientPair.appClient.reset();\n\n        tag0 = new Tag(100_000, \"TagUPDATED\");\n\n        clientPair.appClient.updateTag(1, tag0);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.reset();\n\n        clientPair.appClient.send(\"getTags 1\");\n        String response = clientPair.appClient.getBody();\n\n        Tag[] tags = JsonParser.MAPPER.readValue(response, Tag[].class);\n        assertNotNull(tags);\n        assertEquals(1, tags.length);\n\n        assertEqualTag(tag0, tags[0]);\n    }\n\n    @Test\n    public void testUpdateNonExistingTag() throws Exception {\n        Tag tag0 = new Tag(100_000, \"Tag1\");\n\n        clientPair.appClient.updateTag(1, tag0);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(illegalCommand(1)));\n    }\n\n    @Test\n    public void testUpdateTagWithSameName() throws Exception {\n        Tag tag0 = new Tag(100_000, \"Tag1\");\n\n        clientPair.appClient.createTag(1, tag0);\n        String createdTag = clientPair.appClient.getBody();\n        Tag tag = JsonParser.parseTag(createdTag, 0);\n        assertNotNull(tag);\n        assertEquals(100_000, tag.id);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(createTag(1, tag)));\n\n        clientPair.appClient.reset();\n\n        Tag tag1 = new Tag(100_001, \"Tag1\");\n\n        clientPair.appClient.createTag(1, tag1);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(illegalCommand(1)));\n    }\n\n\n    @Test\n    public void testDeletedNewlyAddedTag() throws Exception {\n        Tag tag0 = new Tag(100_000, \"My Dashboard\");\n\n        clientPair.appClient.createTag(1, tag0);\n        String createdTag = clientPair.appClient.getBody();\n        Tag tag = JsonParser.parseTag(createdTag, 0);\n        assertNotNull(tag);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(createTag(1, tag0.toString())));\n\n        clientPair.appClient.reset();\n\n        clientPair.appClient.send(\"getTags 1\");\n        String response = clientPair.appClient.getBody();\n\n        Tag[] tags = JsonParser.MAPPER.readValue(response, Tag[].class);\n        assertNotNull(tags);\n        assertEquals(1, tags.length);\n\n        assertEqualTag(tag0, tags[0]);\n\n        clientPair.appClient.deleteTag(1, tag0.id);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(ok(2)));\n\n\n        clientPair.appClient.reset();\n\n        clientPair.appClient.send(\"getTags 1\");\n        response = clientPair.appClient.getBody();\n        tags = JsonParser.MAPPER.readValue(response, Tag[].class);\n\n        assertNotNull(tags);\n        assertEquals(0, tags.length);\n    }\n\n    @Test\n    public void testAddNewTagForMultipleDevicesAndAssignWidgetAndVerifySync() throws Exception {\n        //creating new device\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", properties.getHttpPort());\n        hardClient2.start();\n\n        hardClient2.login(device.token);\n        hardClient2.verifyResult(ok(1));\n        clientPair.appClient.reset();\n\n        //creating new tag\n        Tag tag0 = new Tag(100_000, \"Tag1\");\n        //assigning 2 devices on 1 tag.\n        tag0.deviceIds = new int[] {0, 1};\n\n        clientPair.appClient.createTag(1, tag0);\n        String createdTag = clientPair.appClient.getBody();\n        Tag tag = JsonParser.parseTag(createdTag, 0);\n        assertNotNull(tag);\n        assertEquals(100_000, tag.id);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(createTag(1, tag)));\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":88, \\\"width\\\":1, \\\"height\\\":1, \\\"deviceId\\\":100000, \\\"x\\\":0, \\\"y\\\":0, \\\"label\\\":\\\"Button\\\", \\\"type\\\":\\\"BUTTON\\\", \\\"pinType\\\":\\\"VIRTUAL\\\", \\\"pin\\\":88}\");\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.appClient.send(\"hardware 1-100000 vw 88 100\");\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(new HardwareMessage(3, b(\"vw 88 100\"))));\n        verify(hardClient2.responseMock, timeout(500)).channelRead(any(), eq(new HardwareMessage(3, b(\"vw 88 100\"))));\n\n        clientPair.hardwareClient.sync(PinType.VIRTUAL, 88);\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(new HardwareMessage(1, b(\"vw 88 100\"))));\n\n        hardClient2.sync(PinType.VIRTUAL, 88);\n        verify(hardClient2.responseMock, timeout(500)).channelRead(any(), eq(new HardwareMessage(2, b(\"vw 88 100\"))));\n\n        clientPair.appClient.reset();\n\n        clientPair.appClient.sync(1);\n\n        verify(clientPair.appClient.responseMock, timeout(1000).times(14)).channelRead(any(), any());\n\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 1 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 2 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 3 0\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 dw 5 1\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 4 244\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 7 3\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 aw 30 3\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 0 89.888037459418\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 11 -58.74774244674501\")));\n        clientPair.appClient.verifyResult(appSync(b(\"1-0 vw 13 60 143 158\")));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(appSync(b(\"1-0 vw 88 100\"))));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(appSync(b(\"1-0 vw 88 100\"))));\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(appSync(b(\"1-100000 vw 88 100\"))));\n\n    }\n\n    private static void assertEqualTag(Tag expected, Tag real) {\n        assertEquals(expected.id, real.id);\n        assertEquals(expected.name, real.name);\n        assertArrayEquals(expected.deviceIds, real.deviceIds);\n    }\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/tcp/TimerTest.java",
    "content": "package cc.blynk.integration.tcp;\n\nimport cc.blynk.integration.SingleServerInstancePerTest;\nimport cc.blynk.integration.model.tcp.TestHardClient;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.DataStream;\nimport cc.blynk.server.core.model.device.BoardType;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.device.Tag;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.controls.Timer;\nimport cc.blynk.server.core.model.widgets.others.eventor.Eventor;\nimport cc.blynk.server.core.model.widgets.others.eventor.Rule;\nimport cc.blynk.server.core.model.widgets.others.eventor.TimerTime;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.action.BaseAction;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.action.SetPinAction;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.action.SetPinActionType;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.action.notification.NotifyAction;\nimport cc.blynk.server.core.model.widgets.ui.tiles.DeviceTiles;\nimport cc.blynk.server.core.model.widgets.ui.tiles.TileTemplate;\nimport cc.blynk.server.core.model.widgets.ui.tiles.templates.ButtonTileTemplate;\nimport cc.blynk.server.notifications.push.android.AndroidGCMMessage;\nimport cc.blynk.server.notifications.push.enums.Priority;\nimport cc.blynk.utils.DateTimeUtils;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.ArgumentCaptor;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport java.time.LocalDateTime;\nimport java.time.LocalTime;\nimport java.time.ZoneId;\nimport java.time.ZonedDateTime;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.ScheduledExecutorService;\nimport java.util.concurrent.TimeUnit;\n\nimport static cc.blynk.integration.TestUtil.b;\nimport static cc.blynk.integration.TestUtil.createDevice;\nimport static cc.blynk.integration.TestUtil.createTag;\nimport static cc.blynk.integration.TestUtil.hardware;\nimport static cc.blynk.integration.TestUtil.ok;\nimport static cc.blynk.server.core.model.serialization.JsonParser.MAPPER;\nimport static cc.blynk.server.workers.timer.TimerWorker.TIMER_MSG_ID;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotEquals;\nimport static org.junit.Assert.assertNotNull;\nimport static org.mockito.Mockito.after;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.eq;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.timeout;\nimport static org.mockito.Mockito.verify;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/2/2015.\n *\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class TimerTest extends SingleServerInstancePerTest {\n\n    private ScheduledExecutorService ses;\n\n    @Before\n    public void initSES() {\n        ses = Executors.newScheduledThreadPool(1);\n    }\n\n    @After\n    public void closeSES() {\n        ses.shutdownNow();\n    }\n\n\n    @Test\n    public void testTimerEvent() throws Exception {\n        ses.scheduleAtFixedRate(holder.timerWorker, 0, 1000, TimeUnit.MILLISECONDS);\n\n        TimerTime timerTime = new TimerTime(\n                0,\n                new int[] {1,2,3,4,5,6,7},\n                //adding 2 seconds just to be sure we no gonna miss timer event\n                LocalTime.now(DateTimeUtils.UTC).toSecondOfDay() + 2,\n                DateTimeUtils.UTC\n        );\n\n\n        DataStream dataStream = new DataStream((short) 1,PinType.VIRTUAL);\n        SetPinAction setPinAction = new SetPinAction(dataStream, \"1\", SetPinActionType.CUSTOM);\n\n        Eventor eventor = new Eventor(new Rule[] {\n                new Rule(dataStream, timerTime, null, new BaseAction[] {setPinAction}, true)\n        });\n\n        clientPair.appClient.createWidget(1, eventor);\n        clientPair.appClient.verifyResult(ok(1));\n\n        verify(clientPair.appClient.responseMock, timeout(3000)).channelRead(any(), eq(hardware(TIMER_MSG_ID, \"1-0 vw 1 1\")));\n        verify(clientPair.hardwareClient.responseMock, timeout(3000)).channelRead(any(), eq(hardware(TIMER_MSG_ID, \"vw 1 1\")));\n    }\n\n\n    @Test\n    public void testTimerEventNotActive() throws Exception {\n        ses.scheduleAtFixedRate(holder.timerWorker, 0, 1000, TimeUnit.MILLISECONDS);\n\n        TimerTime timerTime = new TimerTime(\n                0,\n                new int[] {1,2,3,4,5,6,7},\n                //adding 2 seconds just to be sure we no gonna miss timer event\n                LocalTime.now(DateTimeUtils.UTC).toSecondOfDay() + 2,\n                DateTimeUtils.UTC\n        );\n\n\n        DataStream dataStream = new DataStream((short)1,PinType.VIRTUAL);\n        SetPinAction setPinAction = new SetPinAction(dataStream, \"1\", SetPinActionType.CUSTOM);\n\n        Eventor eventor = new Eventor(new Rule[] {\n                new Rule(dataStream, timerTime, null, new BaseAction[] {setPinAction}, true)\n        });\n\n        clientPair.appClient.createWidget(1, eventor);\n        clientPair.appClient.verifyResult(ok(1));\n\n        verify(clientPair.appClient.responseMock, timeout(3000)).channelRead(any(), eq(hardware(TIMER_MSG_ID, \"1-0 vw 1 1\")));\n        verify(clientPair.hardwareClient.responseMock, timeout(3000)).channelRead(any(), eq(hardware(TIMER_MSG_ID, \"vw 1 1\")));\n\n        clientPair.appClient.reset();\n        clientPair.hardwareClient.reset();\n\n\n        eventor = new Eventor(new Rule[] {\n                new Rule(dataStream, new TimerTime(\n                        0,\n                        new int[] {1,2,3,4,5,6,7},\n                        //adding 2 seconds just to be sure we no gonna miss timer event\n                        LocalTime.now(DateTimeUtils.UTC).toSecondOfDay() + 1,\n                        DateTimeUtils.UTC\n                ),\n                        null, new BaseAction[] {setPinAction}, false)\n        });\n\n        clientPair.appClient.updateWidget(1, eventor);\n        clientPair.appClient.verifyResult(ok(1));\n\n        verify(clientPair.appClient.responseMock, after(1500).never()).channelRead(any(), eq(hardware(TIMER_MSG_ID, \"1-0 vw 1 1\")));\n        verify(clientPair.hardwareClient.responseMock, after(1500).never()).channelRead(any(), eq(hardware(TIMER_MSG_ID, \"vw 1 1\")));\n    }\n\n    @Test\n    public void testTimerEventWithMultiActions() throws Exception {\n        TimerTime timerTime = new TimerTime(\n                0,\n                new int[] {1,2,3,4,5,6,7},\n                //adding 2 seconds just to be sure we no gonna miss timer event\n                LocalTime.now(DateTimeUtils.UTC).toSecondOfDay() + 2,\n                DateTimeUtils.UTC\n        );\n\n\n        DataStream dataStream = new DataStream((short)1,PinType.VIRTUAL);\n        SetPinAction setPinAction = new SetPinAction(dataStream, \"1\", SetPinActionType.CUSTOM);\n\n        DataStream dataStream2 = new DataStream((short)2,PinType.VIRTUAL);\n        SetPinAction setPinAction2 = new SetPinAction(dataStream2, \"2\", SetPinActionType.CUSTOM);\n\n        Rule rule = new Rule(null, timerTime, null, new BaseAction[] {setPinAction, setPinAction2}, true);\n\n        Eventor eventor = new Eventor(new Rule[] {\n                rule\n        });\n\n        clientPair.appClient.createWidget(1, eventor);\n        clientPair.appClient.verifyResult(ok(1));\n\n        ses.scheduleAtFixedRate(holder.timerWorker, 0, 1000, TimeUnit.MILLISECONDS);\n\n        verify(clientPair.appClient.responseMock, timeout(2100)).channelRead(any(), eq(hardware(TIMER_MSG_ID, \"1-0 vw 1 1\")));\n        verify(clientPair.appClient.responseMock, timeout(2100)).channelRead(any(), eq(hardware(TIMER_MSG_ID, \"1-0 vw 2 2\")));\n        verify(clientPair.hardwareClient.responseMock, timeout(2100)).channelRead(any(), eq(hardware(TIMER_MSG_ID, \"vw 1 1\")));\n        verify(clientPair.hardwareClient.responseMock, timeout(2100)).channelRead(any(), eq(hardware(TIMER_MSG_ID, \"vw 2 2\")));\n    }\n\n    @Test\n    public void testTimerEventWithMultiActions1() throws Exception {\n        ses.scheduleAtFixedRate(holder.timerWorker, 0, 1000, TimeUnit.MILLISECONDS);\n\n        TimerTime timerTime = new TimerTime(\n                0,\n                new int[] {1,2,3,4,5,6,7},\n                //adding 2 seconds just to be sure we no gonna miss timer event\n                LocalTime.now(DateTimeUtils.UTC).toSecondOfDay() + 2,\n                DateTimeUtils.UTC\n        );\n\n        DataStream dataStream = new DataStream((short) 1,PinType.VIRTUAL);\n        SetPinAction setPinAction = new SetPinAction(dataStream, \"1\", SetPinActionType.CUSTOM);\n        NotifyAction notifyAction = new NotifyAction(\"Hello\");\n        Rule rule = new Rule(null, timerTime, null, new BaseAction[] {setPinAction, notifyAction}, true);\n\n        Eventor eventor = new Eventor(new Rule[] {\n                rule\n        });\n\n        clientPair.appClient.createWidget(1, eventor);\n        clientPair.appClient.verifyResult(ok(1));\n\n        ArgumentCaptor<AndroidGCMMessage> objectArgumentCaptor = ArgumentCaptor.forClass(AndroidGCMMessage.class);\n        verify(holder.gcmWrapper, timeout(2000).times(1)).send(objectArgumentCaptor.capture(), any(), any());\n        AndroidGCMMessage message = objectArgumentCaptor.getValue();\n\n        String expectedJson = new AndroidGCMMessage(\"token\", Priority.normal, \"Hello\", 1).toJson();\n        assertEquals(expectedJson, message.toJson());\n\n        verify(clientPair.appClient.responseMock, timeout(2000)).channelRead(any(), eq(hardware(TIMER_MSG_ID, \"1-0 vw 1 1\")));\n        verify(clientPair.hardwareClient.responseMock, timeout(2000)).channelRead(any(), eq(hardware(TIMER_MSG_ID, \"vw 1 1\")));\n    }\n\n    @Test\n    public void testIsTimeMethod() {\n        ZonedDateTime currentDateTime = ZonedDateTime.now(DateTimeUtils.UTC).withHour(23);\n        //kiev is +2, so as currentDateTime has 23 hour. kiev should be always ahead.\n        LocalDateTime userDateTime = currentDateTime.withZoneSameInstant(ZoneId.of(\"Europe/Kiev\")).toLocalDateTime();\n        assertNotEquals(currentDateTime.getDayOfWeek(), userDateTime.getDayOfWeek());\n    }\n\n    @Test\n    public void testTimerEventWithWrongDayDontWork() throws Exception {\n        ses.scheduleAtFixedRate(holder.timerWorker, 0, 1000, TimeUnit.MILLISECONDS);\n\n        ZonedDateTime now = ZonedDateTime.now(DateTimeUtils.UTC);\n        int currentDayIndex = now.getDayOfWeek().ordinal();\n\n        int[] days = new int[] {1,2,3,4,5,6,7};\n        //removing today day from expected days so timer doesnt work.\n        days[currentDayIndex] = -1;\n\n        TimerTime timerTime = new TimerTime(\n                0,\n                days,\n                //adding 2 seconds just to be sure we no gonna miss timer event\n                LocalTime.now(DateTimeUtils.UTC).toSecondOfDay() + 1,\n                DateTimeUtils.UTC\n        );\n\n        DataStream dataStream = new DataStream((short)1,PinType.VIRTUAL);\n        SetPinAction setPinAction = new SetPinAction(dataStream, \"1\", SetPinActionType.CUSTOM);\n        Rule rule = new Rule(null, timerTime, null,  new BaseAction[] {setPinAction}, true);\n\n        Eventor eventor = new Eventor(new Rule[] {\n                rule\n        });\n\n        clientPair.appClient.createWidget(1, eventor);\n        clientPair.appClient.verifyResult(ok(1));\n\n        verify(clientPair.appClient.responseMock, after(700).never()).channelRead(any(), eq(hardware(TIMER_MSG_ID, \"1-0 vw 1 1\")));\n        verify(clientPair.hardwareClient.responseMock, after(700).never()).channelRead(any(), eq(hardware(TIMER_MSG_ID, \"vw 1 1\")));\n    }\n\n    @Test\n    public void testAddTimerWidgetWithStartTimeTriggered() throws Exception {\n        ses.scheduleAtFixedRate(holder.timerWorker, 0, 1000, TimeUnit.MILLISECONDS);\n        Timer timer = new Timer();\n        timer.id = 112;\n        timer.x = 1;\n        timer.y = 1;\n        timer.width = 2;\n        timer.height = 1;\n        timer.pinType = PinType.DIGITAL;\n        timer.pin = 5;\n        timer.startValue = \"1\";\n        LocalTime localDateTime = LocalTime.now(ZoneId.of(\"UTC\"));\n        int curTime = localDateTime.toSecondOfDay();\n        timer.startTime = curTime + 1;\n\n        clientPair.appClient.createWidget(1, timer);\n        clientPair.appClient.verifyResult(ok(1));\n\n        verify(clientPair.hardwareClient.responseMock, timeout(1500).times(1)).channelRead(any(), any());\n        verify(clientPair.hardwareClient.responseMock, timeout(2000)).channelRead(any(), eq(hardware(7777, \"dw 5 1\")));\n    }\n\n    @Test\n    public void testAddTimerWidgetWithStopAndStartTimeTriggered() throws Exception {\n        ses.scheduleAtFixedRate(holder.timerWorker, 0, 1000, TimeUnit.MILLISECONDS);\n        Timer timer = new Timer();\n        timer.id = 112;\n        timer.x = 1;\n        timer.y = 1;\n        timer.width = 2;\n        timer.height = 1;\n        timer.pinType = PinType.DIGITAL;\n        timer.pin = 5;\n        timer.stopValue = \"0\";\n        LocalTime localDateTime = LocalTime.now(ZoneId.of(\"UTC\"));\n        int curTime = localDateTime.toSecondOfDay();\n        timer.stopTime = curTime + 1;\n\n        clientPair.appClient.createWidget(1, timer);\n        clientPair.appClient.verifyResult(ok(1));\n\n        verify(clientPair.hardwareClient.responseMock, timeout(1500).times(1)).channelRead(any(), any());\n        verify(clientPair.hardwareClient.responseMock, timeout(2000)).channelRead(any(), eq(hardware(7777, \"dw 5 0\")));\n    }\n\n    @Test\n    public void testAddTimerWidgetWithStopTimeTriggered() throws Exception {\n        ses.scheduleAtFixedRate(holder.timerWorker, 0, 1000, TimeUnit.MILLISECONDS);\n        Timer timer = new Timer();\n        timer.id = 112;\n        timer.x = 1;\n        timer.y = 1;\n        timer.pinType = PinType.DIGITAL;\n        timer.pin = 5;\n        timer.width = 2;\n        timer.height = 1;\n        timer.startValue = \"1\";\n        timer.stopValue = \"0\";\n        LocalTime localDateTime = LocalTime.now(ZoneId.of(\"UTC\"));\n        int curTime = localDateTime.toSecondOfDay();\n        timer.startTime = curTime + 1;\n        timer.stopTime = curTime + 2;\n\n        clientPair.appClient.createWidget(1, timer);\n        clientPair.appClient.verifyResult(ok(1));\n\n        verify(clientPair.hardwareClient.responseMock, timeout(2500).times(2)).channelRead(any(), any());\n        verify(clientPair.hardwareClient.responseMock, timeout(2000)).channelRead(any(), eq(hardware(7777, \"dw 5 0\")));\n        verify(clientPair.hardwareClient.responseMock, timeout(2000)).channelRead(any(), eq(hardware(7777, \"dw 5 1\")));\n    }\n\n\n    @Test\n    public void testAddTimerWidgetWithStopTimeAndRemove() throws Exception {\n        ses.scheduleAtFixedRate(holder.timerWorker, 0, 1000, TimeUnit.MILLISECONDS);\n        Timer timer = new Timer();\n        timer.id = 112;\n        timer.x = 1;\n        timer.y = 1;\n        timer.pinType = PinType.DIGITAL;\n        timer.pin = 5;\n        timer.width = 2;\n        timer.height = 1;\n        timer.startValue = \"1\";\n        timer.stopValue = \"0\";\n        LocalTime localDateTime = LocalTime.now(ZoneId.of(\"UTC\"));\n        int curTime = localDateTime.toSecondOfDay();\n        timer.startTime = curTime + 1;\n        timer.stopTime = curTime + 2;\n\n        clientPair.appClient.createWidget(1, timer);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.deleteWidget(1, 112);\n        clientPair.appClient.verifyResult(ok(2));\n\n        verify(clientPair.hardwareClient.responseMock, after(2500).never()).channelRead(any(), any());\n    }\n\n    @Test\n    public void testAddFewTimersWidgetWithStartTimeTriggered() throws Exception {\n        ses.scheduleAtFixedRate(holder.timerWorker, 0, 1000, TimeUnit.MILLISECONDS);\n        Timer timer = new Timer();\n        timer.id = 112;\n        timer.x = 1;\n        timer.y = 1;\n        timer.pinType = PinType.DIGITAL;\n        timer.pin = 5;\n        timer.startValue = \"1\";\n        timer.width = 2;\n        timer.height = 1;\n        LocalTime localDateTime = LocalTime.now(ZoneId.of(\"UTC\"));\n        int curTime = localDateTime.toSecondOfDay();\n        timer.startTime = curTime + 1;\n\n        clientPair.appClient.createWidget(1, timer);\n        clientPair.appClient.verifyResult(ok(1));\n\n        timer.id = 113;\n        timer.startValue = \"2\";\n\n        clientPair.appClient.createWidget(1, timer);\n        clientPair.appClient.verifyResult(ok(2));\n\n        verify(clientPair.hardwareClient.responseMock, timeout(2500).times(2)).channelRead(any(), any());\n        verify(clientPair.hardwareClient.responseMock, timeout(2000)).channelRead(any(), eq(hardware(7777, \"dw 5 1\")));\n        verify(clientPair.hardwareClient.responseMock, timeout(2000)).channelRead(any(), eq(hardware(7777, \"dw 5 2\")));\n    }\n\n    @Test\n    public void testAddTimerWithSameStartStopTriggered() throws Exception {\n        ses.scheduleAtFixedRate(holder.timerWorker, 0, 1000, TimeUnit.MILLISECONDS);\n        Timer timer = new Timer();\n        timer.id = 112;\n        timer.x = 1;\n        timer.y = 1;\n        timer.pinType = PinType.DIGITAL;\n        timer.pin = 5;\n        timer.width = 2;\n        timer.height = 1;\n        timer.startValue = \"0\";\n        timer.stopValue = \"1\";\n        LocalTime localDateTime = LocalTime.now(ZoneId.of(\"UTC\"));\n        int curTime = localDateTime.toSecondOfDay();\n        timer.startTime = curTime + 1;\n        timer.stopTime = curTime + 1;\n\n        clientPair.appClient.createWidget(1, timer);\n        clientPair.appClient.verifyResult(ok(1));\n\n        verify(clientPair.hardwareClient.responseMock, timeout(2500).times(2)).channelRead(any(), any());\n        verify(clientPair.hardwareClient.responseMock, timeout(2000)).channelRead(any(), eq(hardware(7777, \"dw 5 0\")));\n        verify(clientPair.hardwareClient.responseMock, timeout(2000)).channelRead(any(), eq(hardware(7777, \"dw 5 1\")));\n    }\n\n    @Test\n    public void testUpdateTimerWidgetWithStopTimeTriggered() throws Exception {\n        ses.scheduleAtFixedRate(holder.timerWorker, 0, 1000, TimeUnit.MILLISECONDS);\n        Timer timer = new Timer();\n        timer.id = 112;\n        timer.x = 1;\n        timer.y = 1;\n        timer.width = 2;\n        timer.height = 1;\n        timer.pinType = PinType.DIGITAL;\n        timer.pin = 5;\n        timer.startValue = \"1\";\n        timer.stopValue = \"0\";\n        LocalTime localDateTime = LocalTime.now(ZoneId.of(\"UTC\"));\n        int curTime = localDateTime.toSecondOfDay();\n        timer.startTime = curTime + 1;\n        timer.stopTime = curTime + 2;\n\n        clientPair.appClient.createWidget(1, timer);\n        clientPair.appClient.verifyResult(ok(1));\n\n        timer.startValue = \"11\";\n        timer.stopValue = \"10\";\n\n        clientPair.appClient.updateWidget(1, timer);\n        clientPair.appClient.verifyResult(ok(1));\n\n        verify(clientPair.hardwareClient.responseMock, timeout(2500).times(2)).channelRead(any(), any());\n        verify(clientPair.hardwareClient.responseMock, timeout(2000)).channelRead(any(), eq(hardware(7777, \"dw 5 11\")));\n        verify(clientPair.hardwareClient.responseMock, timeout(2000)).channelRead(any(), eq(hardware(7777, \"dw 5 10\")));\n    }\n\n    @Test\n    public void testStopTimerTrigger() throws Exception {\n        ses.scheduleAtFixedRate(holder.timerWorker, 0, 1000, TimeUnit.MILLISECONDS);\n        Timer timer = new Timer();\n        timer.id = 112;\n        timer.x = 1;\n        timer.y = 1;\n        timer.width = 2;\n        timer.height = 1;\n        timer.pinType = PinType.DIGITAL;\n        timer.pin = 5;\n        timer.startValue = \"1\";\n        timer.stopValue = \"0\";\n        LocalTime localDateTime = LocalTime.now(ZoneId.of(\"UTC\"));\n        int curTime = localDateTime.toSecondOfDay();\n        timer.startTime = curTime + 1;\n        timer.stopTime = curTime + 2;\n\n        clientPair.appClient.createWidget(1, timer);\n        clientPair.appClient.verifyResult(ok(1));\n\n        timer.startTime = -1;\n        timer.stopTime = -1;\n\n        clientPair.appClient.updateWidget(1, timer);\n        clientPair.appClient.verifyResult(ok(1));\n\n        verify(clientPair.hardwareClient.responseMock, after(2500).times(0)).channelRead(any(), any());\n        //verify(clientPair.hardwareClient.responseMock, timeout(2500).times(2)).channelRead(any(), any());\n        //verify(clientPair.hardwareClient.responseMock, timeout(2000)).channelRead(any(), eq(hardware(7777, \"dw 5 11\")));\n        //verify(clientPair.hardwareClient.responseMock, timeout(2000)).channelRead(any(), eq(hardware(7777, \"dw 5 10\")));\n    }\n\n    @Test\n    public void testDashTimerNotTriggered() throws Exception {\n        ses.scheduleAtFixedRate(holder.timerWorker, 0, 1000, TimeUnit.MILLISECONDS);\n        Timer timer = new Timer();\n        timer.id = 112;\n        timer.x = 1;\n        timer.y = 1;\n        timer.width = 2;\n        timer.height = 1;\n        timer.pinType = PinType.DIGITAL;\n        timer.pin = 5;\n        timer.startValue = \"1\";\n        timer.stopValue = \"0\";\n        LocalTime localDateTime = LocalTime.now(ZoneId.of(\"UTC\"));\n        int curTime = localDateTime.toSecondOfDay();\n        timer.startTime = curTime + 1;\n        timer.stopTime = curTime + 2;\n\n        clientPair.appClient.createWidget(1, timer);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.deleteDash(1);\n        clientPair.appClient.verifyResult(ok(2));\n\n        verify(clientPair.hardwareClient.responseMock, after(2500).times(0)).channelRead(any(), any());\n    }\n\n\n\n    @Test\n    public void testTimerWidgetTriggeredAndSendCommandToCorrectDevice() throws Exception {\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", properties.getHttpPort());\n        hardClient2.start();\n\n        ses.scheduleAtFixedRate(holder.timerWorker, 0, 1000, TimeUnit.MILLISECONDS);\n\n        clientPair.appClient.deactivate(1);\n        clientPair.appClient.verifyResult(ok(1));\n\n        Timer timer = new Timer();\n        timer.id = 1;\n        timer.x = 1;\n        timer.y = 1;\n        timer.pinType = PinType.DIGITAL;\n        timer.pin = 5;\n        timer.startValue = \"1\";\n        timer.stopValue = \"0\";\n        LocalTime localDateTime = LocalTime.now(ZoneId.of(\"UTC\"));\n        int curTime = localDateTime.toSecondOfDay();\n        timer.startTime = curTime + 1;\n        timer.stopTime = curTime + 2;\n\n        DashBoard dashBoard = new DashBoard();\n        dashBoard.id = 1;\n        dashBoard.name = \"Test\";\n        dashBoard.widgets = new Widget[] {timer};\n\n        clientPair.appClient.updateDash(dashBoard);\n        clientPair.appClient.verifyResult(ok(2));\n\n        dashBoard.id = 2;\n        clientPair.appClient.createDash(dashBoard);\n        clientPair.appClient.verifyResult(ok(3));\n\n        clientPair.appClient.activate(1);\n        clientPair.appClient.verifyResult(ok(4));\n\n        clientPair.appClient.reset();\n        clientPair.appClient.createDevice(2, new Device(1, \"Device\", BoardType.ESP8266));\n        Device device = clientPair.appClient.parseDevice();\n        hardClient2.login(device.token);\n        hardClient2.verifyResult(ok(1));\n        hardClient2.reset();\n\n        verify(clientPair.hardwareClient.responseMock, timeout(2000)).channelRead(any(), eq(hardware(7777, \"dw 5 1\")));\n        clientPair.hardwareClient.reset();\n        verify(clientPair.hardwareClient.responseMock, timeout(2000)).channelRead(any(), eq(hardware(7777, \"dw 5 0\")));\n\n        verify(hardClient2.responseMock, never()).channelRead(any(), any());\n        hardClient2.stop().awaitUninterruptibly();\n    }\n\n    @Test\n    public void testTimerWidgetTriggered() throws Exception {\n        ses.scheduleAtFixedRate(holder.timerWorker, 0, 1000, TimeUnit.MILLISECONDS);\n\n        clientPair.appClient.deactivate(1);\n        clientPair.appClient.verifyResult(ok(1));\n\n        Timer timer = new Timer();\n        timer.id = 1;\n        timer.x = 1;\n        timer.y = 1;\n        timer.pinType = PinType.DIGITAL;\n        timer.pin = 5;\n        timer.startValue = \"1\";\n        timer.stopValue = \"0\";\n        LocalTime localDateTime = LocalTime.now(ZoneId.of(\"UTC\"));\n        int curTime = localDateTime.toSecondOfDay();\n        timer.startTime = curTime + 1;\n        timer.stopTime = curTime + 2;\n\n        DashBoard dashBoard = new DashBoard();\n        dashBoard.id = 1;\n        dashBoard.name = \"Test\";\n        dashBoard.widgets = new Widget[] {timer};\n\n        clientPair.appClient.updateDash(dashBoard);\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.appClient.activate(1);\n        clientPair.appClient.verifyResult(ok(3));\n\n        verify(clientPair.hardwareClient.responseMock, timeout(2000)).channelRead(any(), eq(hardware(7777, \"dw 5 1\")));\n        clientPair.hardwareClient.reset();\n        verify(clientPair.hardwareClient.responseMock, timeout(2000)).channelRead(any(), eq(hardware(7777, \"dw 5 0\")));\n    }\n\n    @Test\n    public void testTimerWorksWithTag() throws Exception {\n        //creating new device\n        Device device1 = new Device(1, \"My Device\", BoardType.ESP8266);\n\n        clientPair.appClient.createDevice(1, device1);\n        Device device = clientPair.appClient.parseDevice();\n        assertNotNull(device);\n        assertNotNull(device.token);\n        clientPair.appClient.verifyResult(createDevice(1, device));\n\n        TestHardClient hardClient2 = new TestHardClient(\"localhost\", properties.getHttpPort());\n        hardClient2.start();\n\n        hardClient2.login(device.token);\n        hardClient2.verifyResult(ok(1));\n        clientPair.appClient.reset();\n\n        //creating new tag\n        Tag tag0 = new Tag(100_000, \"Tag1\");\n        //assigning 2 devices on 1 tag.\n        tag0.deviceIds = new int[] {0, 1};\n\n        clientPair.appClient.createTag(1, tag0);\n        String createdTag = clientPair.appClient.getBody();\n        Tag tag = JsonParser.parseTag(createdTag, 0);\n        assertNotNull(tag);\n        assertEquals(100_000, tag.id);\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(createTag(1, tag)));\n\n        ses.scheduleAtFixedRate(holder.timerWorker, 0, 1000, TimeUnit.MILLISECONDS);\n        Timer timer = new Timer();\n        timer.id = 112;\n        timer.x = 1;\n        timer.y = 1;\n        timer.width = 2;\n        timer.height = 1;\n        timer.pinType = PinType.DIGITAL;\n        timer.pin = 5;\n        timer.startValue = \"1\";\n        timer.deviceId = 100_000;\n\n        LocalTime localDateTime = LocalTime.now(ZoneId.of(\"UTC\"));\n        int curTime = localDateTime.toSecondOfDay();\n        timer.startTime = curTime + 1;\n\n        clientPair.appClient.createWidget(1, timer);\n        clientPair.appClient.verifyResult(ok(2));\n\n        verify(clientPair.hardwareClient.responseMock, timeout(2000)).channelRead(any(), eq(hardware(7777, \"dw 5 1\")));\n        verify(hardClient2.responseMock, timeout(2000)).channelRead(any(), eq(hardware(7777, \"dw 5 1\")));\n    }\n\n    @Test\n    public void testTimerWidgetTriggeredAndSyncWorks() throws Exception {\n        ses.scheduleAtFixedRate(holder.timerWorker, 0, 1000, TimeUnit.MILLISECONDS);\n\n        clientPair.appClient.deactivate(1);\n        clientPair.appClient.verifyResult(ok(1));\n\n        Timer timer = new Timer();\n        timer.id = 1;\n        timer.x = 1;\n        timer.y = 1;\n        timer.pinType = PinType.VIRTUAL;\n        timer.pin = 5;\n        timer.startValue = \"1\";\n        timer.stopValue = \"0\";\n        LocalTime localDateTime = LocalTime.now(ZoneId.of(\"UTC\"));\n        int curTime = localDateTime.toSecondOfDay();\n        timer.startTime = curTime + 1;\n        timer.stopTime = curTime + 2;\n\n        DashBoard dashBoard = new DashBoard();\n        dashBoard.id = 1;\n        dashBoard.name = \"Test\";\n        dashBoard.widgets = new Widget[] {timer};\n\n        clientPair.appClient.updateDash(dashBoard);\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.appClient.activate(1);\n        clientPair.appClient.verifyResult(ok(3));\n\n        verify(clientPair.hardwareClient.responseMock, timeout(2000)).channelRead(any(), eq(hardware(7777, \"vw 5 1\")));\n        clientPair.hardwareClient.reset();\n        clientPair.hardwareClient.sync(PinType.VIRTUAL, 5);\n        verify(clientPair.hardwareClient.responseMock, timeout(2000)).channelRead(any(), eq(hardware(1, \"vw 5 1\")));\n\n        verify(clientPair.hardwareClient.responseMock, timeout(2000)).channelRead(any(), eq(hardware(7777, \"vw 5 0\")));\n\n        clientPair.hardwareClient.sync(PinType.VIRTUAL, 5);\n        verify(clientPair.hardwareClient.responseMock, timeout(2000)).channelRead(any(), eq(hardware(2, \"vw 5 0\")));\n    }\n\n    @Test\n    public void testAddTimerWidgetToDeviceTilesWithStartTimeTriggered() throws Exception {\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = 21321;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(1));\n\n        TileTemplate tileTemplate = new ButtonTileTemplate(1,\n                null, new int[] {0}, \"name\", \"name\", \"iconName\", BoardType.ESP8266, new DataStream((short) 111, PinType.VIRTUAL),\n                false, false, false, null, null);\n\n        clientPair.appClient.send(\"createTemplate \" + b(\"1 \" + deviceTiles.id + \" \")\n                + MAPPER.writeValueAsString(tileTemplate));\n        clientPair.appClient.verifyResult(ok(2));\n\n        ses.scheduleAtFixedRate(holder.timerWorker, 0, 1000, TimeUnit.MILLISECONDS);\n        Timer timer = new Timer();\n        timer.id = 112;\n        timer.x = 1;\n        timer.y = 1;\n        timer.width = 2;\n        timer.height = 1;\n        timer.pinType = PinType.DIGITAL;\n        timer.pin = 5;\n        timer.startValue = \"1\";\n        timer.deviceId = -1;\n        LocalTime localDateTime = LocalTime.now(ZoneId.of(\"UTC\"));\n        int curTime = localDateTime.toSecondOfDay();\n        timer.startTime = curTime + 1;\n\n        clientPair.appClient.createWidget(1, b(\"21321 1 \") + JsonParser.MAPPER.writeValueAsString(timer));\n        clientPair.appClient.verifyResult(ok(3));\n\n        verify(clientPair.hardwareClient.responseMock, timeout(1500).times(1)).channelRead(any(), any());\n        verify(clientPair.hardwareClient.responseMock, timeout(2000)).channelRead(any(), eq(hardware(7777, \"dw 5 1\")));\n        verify(clientPair.appClient.responseMock, timeout(2000)).channelRead(any(), eq(hardware(7777, \"1-0 dw 5 1\")));\n    }\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/tcp/WebhookTest.java",
    "content": "package cc.blynk.integration.tcp;\n\nimport cc.blynk.integration.SingleServerInstancePerTest;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.widgets.others.webhook.Header;\nimport cc.blynk.server.core.model.widgets.others.webhook.WebHook;\nimport cc.blynk.server.core.protocol.model.messages.common.HardwareMessage;\nimport cc.blynk.server.servers.application.MobileAndHttpsServer;\nimport cc.blynk.server.servers.hardware.HardwareAndHttpAPIServer;\nimport cc.blynk.utils.StringUtils;\nimport cc.blynk.utils.properties.ServerProperties;\nimport org.asynchttpclient.AsyncHttpClient;\nimport org.asynchttpclient.DefaultAsyncHttpClient;\nimport org.asynchttpclient.DefaultAsyncHttpClientConfig;\nimport org.asynchttpclient.Response;\nimport org.junit.AfterClass;\nimport org.junit.BeforeClass;\nimport org.junit.Ignore;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.concurrent.Future;\n\nimport static cc.blynk.integration.BaseTest.getRelativeDataFolder;\nimport static cc.blynk.integration.TestUtil.b;\nimport static cc.blynk.integration.TestUtil.consumeJsonPinValues;\nimport static cc.blynk.integration.TestUtil.createHolderWithIOMock;\nimport static cc.blynk.integration.TestUtil.hardware;\nimport static cc.blynk.integration.TestUtil.ok;\nimport static cc.blynk.server.core.model.widgets.others.webhook.SupportedWebhookMethod.GET;\nimport static cc.blynk.server.core.model.widgets.others.webhook.SupportedWebhookMethod.PUT;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE;\nimport static cc.blynk.server.core.protocol.model.messages.MessageFactory.produce;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertTrue;\nimport static org.mockito.Mockito.after;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.eq;\nimport static org.mockito.Mockito.timeout;\nimport static org.mockito.Mockito.verify;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 5/09/2016.\n *\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class WebhookTest extends SingleServerInstancePerTest {\n\n    private static AsyncHttpClient httpclient;\n    private static String httpServerUrl;\n\n    @BeforeClass\n    //shadow parent method by purpose\n    public static void init() throws Exception {\n        properties = new ServerProperties(Collections.emptyMap());\n        properties.setProperty(\"data.folder\", getRelativeDataFolder(\"/profiles\"));\n\n        holder = createHolderWithIOMock(properties, \"no-db.properties\");\n        hardwareServer = new HardwareAndHttpAPIServer(holder).start();\n        appServer = new MobileAndHttpsServer(holder).start();\n\n        httpServerUrl = String.format(\"http://localhost:%s/\", properties.getHttpPort());\n        httpclient = new DefaultAsyncHttpClient(\n                new DefaultAsyncHttpClientConfig.Builder()\n                        .setUserAgent(\"\")\n                        .setKeepAlive(true)\n                        .build());\n    }\n\n    @AfterClass\n    public static void closeHttpClient() throws Exception {\n        httpclient.close();\n    }\n\n    @Test\n    @Ignore\n    public void testThingsSpeakIntegrationTest() throws Exception {\n        WebHook webHook = new WebHook();\n        webHook.url = \"https://api.thingspeak.com/update?api_key=API_KEY&field1=%s\".replace(\"API_KEY\", \"\");\n        webHook.method = GET;\n        webHook.pin = 123;\n        webHook.pinType = PinType.VIRTUAL;\n\n        clientPair.appClient.createWidget(1, webHook);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 123 10\");\n        verify(clientPair.hardwareClient.responseMock, after(1000).times(0)).channelRead(any(), any());\n    }\n\n    @Test\n    @Ignore\n    public void testSome3dPartyWeatherServiceTest() throws Exception {\n        WebHook webHook = new WebHook();\n        webHook.url = \"http://api.sunrise-sunset.org/json?lat=36.7201600&lng=-4.4203400&date=2016-08-25\";\n        webHook.method = GET;\n        webHook.pin = 123;\n        webHook.pinType = PinType.VIRTUAL;\n\n        clientPair.appClient.createWidget(1, webHook);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 123 10\");\n        String expectedResponse = \"vw\" + StringUtils.BODY_SEPARATOR_STRING + \"123\" + StringUtils.BODY_SEPARATOR_STRING +\n                \"{\\\"results\\\":{\\\"sunrise\\\":\\\"7:30:27 AM\\\",\\\"sunset\\\":\\\"5:14:34 PM\\\",\\\"solar_noon\\\":\\\"12:22:31 PM\\\",\\\"day_length\\\":\\\"09:44:07\\\",\\\"civil_twilight_begin\\\":\\\"7:01:53 AM\\\",\\\"civil_twilight_end\\\":\\\"5:43:08 PM\\\",\\\"nautical_twilight_begin\\\":\\\"6:29:39 AM\\\",\\\"nautical_twilight_end\\\":\\\"6:15:23 PM\\\",\\\"astronomical_twilight_begin\\\":\\\"5:58:15 AM\\\",\\\"astronomical_twilight_end\\\":\\\"6:46:46 PM\\\"},\\\"status\\\":\\\"OK\\\"}\";\n        clientPair.hardwareClient.verifyResult(hardware(888, expectedResponse));\n    }\n\n    @Test\n    @Ignore\n    public void testSome3dPartyWeatherServiceTriggerFromAppTest() throws Exception {\n        WebHook webHook = new WebHook();\n        webHook.url = \"http://api.sunrise-sunset.org/json?lat=36.7201600&lng=-4.4203400&date=2016-08-25\";\n        webHook.method = GET;\n        webHook.pin = 123;\n        webHook.pinType = PinType.VIRTUAL;\n\n        clientPair.appClient.createWidget(1, webHook);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.send(\"hardware 1 vw 123 10\");\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(produce(2, HARDWARE, b(\"vw 123 10\"))));\n\n        String expectedResponse = \"vw\" + StringUtils.BODY_SEPARATOR_STRING + \"123\" + StringUtils.BODY_SEPARATOR_STRING +\n                \"{\\\"results\\\":{\\\"sunrise\\\":\\\"7:30:27 AM\\\",\\\"sunset\\\":\\\"5:14:34 PM\\\",\\\"solar_noon\\\":\\\"12:22:31 PM\\\",\\\"day_length\\\":\\\"09:44:07\\\",\\\"civil_twilight_begin\\\":\\\"7:01:53 AM\\\",\\\"civil_twilight_end\\\":\\\"5:43:08 PM\\\",\\\"nautical_twilight_begin\\\":\\\"6:29:39 AM\\\",\\\"nautical_twilight_end\\\":\\\"6:15:23 PM\\\",\\\"astronomical_twilight_begin\\\":\\\"5:58:15 AM\\\",\\\"astronomical_twilight_end\\\":\\\"6:46:46 PM\\\"},\\\"status\\\":\\\"OK\\\"}\";\n        clientPair.hardwareClient.verifyResult(hardware(888, expectedResponse));\n    }\n\n    @Test\n    public void testReservedREgexCharForReplaceArgumentsInWebhook() throws Exception {\n        WebHook webHook = new WebHook();\n        webHook.url = httpServerUrl + \"4ae3851817194e2596cf1b7103603ef8/update/V124\";\n        webHook.method = PUT;\n        webHook.headers = new Header[] {new Header(\"Content-Type\", \"application/json\")};\n        webHook.body = \"[\\\"/pin/\\\"]\";\n        webHook.pin = 123;\n        webHook.pinType = PinType.VIRTUAL;\n        webHook.width = 2;\n        webHook.height = 1;\n\n        clientPair.appClient.createWidget(1, webHook);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 123 $$\");\n        verify(clientPair.hardwareClient.responseMock, after(1000).times(0)).channelRead(any(), any());\n\n        Future<Response> f = httpclient.prepareGet(httpServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/V124\").execute();\n        Response response = f.get();\n\n        assertEquals(200, response.getStatusCode());\n        List<String> values = consumeJsonPinValues(response.getResponseBody());\n        assertEquals(1, values.size());\n        assertEquals(\"$$\", values.get(0));\n    }\n\n    @Test\n    public void testWebhookWorksWithBlynkHttpApiNoPlaceHolder() throws Exception {\n        WebHook webHook = new WebHook();\n        webHook.url = httpServerUrl + \"4ae3851817194e2596cf1b7103603ef8/update/V124\";\n        webHook.method = PUT;\n        webHook.headers = new Header[] {new Header(\"Content-Type\", \"application/json\")};\n        webHook.body = \"[\\\"124\\\"]\";\n        webHook.pin = 123;\n        webHook.pinType = PinType.VIRTUAL;\n        webHook.width = 2;\n        webHook.height = 1;\n\n        clientPair.appClient.createWidget(1, webHook);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 123 10\");\n        verify(clientPair.hardwareClient.responseMock, after(1000).times(0)).channelRead(any(), any());\n\n        Future<Response> f = httpclient.prepareGet(httpServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/V124\").execute();\n        Response response = f.get();\n\n        assertEquals(200, response.getStatusCode());\n        List<String> values = consumeJsonPinValues(response.getResponseBody());\n        assertEquals(1, values.size());\n        assertEquals(\"124\", values.get(0));\n    }\n\n    @Test\n    public void testWebhookWorksWithBlynkHttpApiPlaceHolderAndTextPlain() throws Exception {\n        WebHook webHook = new WebHook();\n        webHook.url = httpServerUrl + \"4ae3851817194e2596cf1b7103603ef8/update/V125\";\n        webHook.method = PUT;\n        webHook.headers = new Header[] {new Header(\"Content-Type\", \"text/plain\")};\n        webHook.body = \"[\\\"/pin/\\\"]\";\n        webHook.pin = 123;\n        webHook.pinType = PinType.VIRTUAL;\n        webHook.width = 2;\n        webHook.height = 1;\n\n        clientPair.appClient.createWidget(1, webHook);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 123 10\");\n        verify(clientPair.hardwareClient.responseMock, after(1000).times(0)).channelRead(any(), any());\n\n        Future<Response> f = httpclient.prepareGet(httpServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/V125\").execute();\n        Response response = f.get();\n\n        assertEquals(400, response.getStatusCode());\n    }\n\n    @Test\n    public void testWebhookWorksWithBlynkHttpApiWithArrayPlaceholderInURL() throws Exception {\n        WebHook webHook = new WebHook();\n        webHook.url = httpServerUrl + \"4ae3851817194e2596cf1b7103603ef8/update/V124?value=/pin[0]/&value=/pin[1]/&value=/pin[2]/\";\n        webHook.method = GET;\n        webHook.headers = new Header[] {new Header(\"Content-Type\", \"application/json\")};\n        webHook.pin = 123;\n        webHook.pinType = PinType.VIRTUAL;\n        webHook.width = 2;\n        webHook.height = 1;\n\n        clientPair.appClient.createWidget(1, webHook);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 123 \" + b(\"10 11 12\"));\n        verify(clientPair.hardwareClient.responseMock, after(1000).times(0)).channelRead(any(), any());\n\n        Future<Response> f = httpclient.prepareGet(httpServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/V124\").execute();\n        Response response = f.get();\n\n        assertEquals(200, response.getStatusCode());\n        List<String> values = consumeJsonPinValues(response.getResponseBody());\n        assertEquals(3, values.size());\n        assertEquals(\"10\", values.get(0));\n        assertEquals(\"11\", values.get(1));\n        assertEquals(\"12\", values.get(2));\n    }\n\n    @Test\n    public void testWebhookWorksWithBlynkHttpApiWithArray10PlaceholdersInURL() throws Exception {\n        WebHook webHook = new WebHook();\n        webHook.url = httpServerUrl + \"4ae3851817194e2596cf1b7103603ef8/update/V124?\" +\n                \"value=/pin[0]/\" +\n                \"&value=/pin[1]/\" +\n                \"&value=/pin[2]/\" +\n                \"&value=/pin[3]/\" +\n                \"&value=/pin[4]/\" +\n                \"&value=/pin[5]/\" +\n                \"&value=/pin[6]/\" +\n                \"&value=/pin[7]/\" +\n                \"&value=/pin[8]/\" +\n                \"&value=/pin[9]/\";\n\n        webHook.method = GET;\n        webHook.headers = new Header[] {new Header(\"Content-Type\", \"application/json\")};\n        webHook.pin = 123;\n        webHook.pinType = PinType.VIRTUAL;\n        webHook.width = 2;\n        webHook.height = 1;\n\n        clientPair.appClient.createWidget(1, webHook);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 123 \" + b(\"0 1 2 3 4 5 6 7 8 9\"));\n        verify(clientPair.hardwareClient.responseMock, after(1000).times(0)).channelRead(any(), any());\n\n        Future<Response> f = httpclient.prepareGet(httpServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/V124\").execute();\n        Response response = f.get();\n\n        assertEquals(200, response.getStatusCode());\n        List<String> values = consumeJsonPinValues(response.getResponseBody());\n        assertEquals(10, values.size());\n        for (int i = 0; i < 10; i++) {\n            assertEquals(\"\" + i, values.get(i));\n        }\n    }\n\n    @Test\n    public void testWebhookWorksWithBlynkHttpApiWithDateTimePlaceholder() throws Exception {\n        WebHook webHook = new WebHook();\n        webHook.url = httpServerUrl + \"4ae3851817194e2596cf1b7103603ef8/update/V124\";\n        webHook.method = PUT;\n        webHook.headers = new Header[] {new Header(\"Content-Type\", \"application/json\")};\n        webHook.body = \"[\\\"/datetime_iso/\\\"]\";\n        webHook.pin = 123;\n        webHook.pinType = PinType.VIRTUAL;\n        webHook.width = 2;\n        webHook.height = 1;\n\n        clientPair.appClient.createWidget(1, webHook);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 123 10\");\n        verify(clientPair.hardwareClient.responseMock, after(1000).times(0)).channelRead(any(), any());\n\n        Future<Response> f = httpclient.prepareGet(httpServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/V124\").execute();\n        Response response = f.get();\n\n        assertEquals(200, response.getStatusCode());\n        List<String> values = consumeJsonPinValues(response.getResponseBody());\n        assertEquals(1, values.size());\n        assertTrue(values.get(0).endsWith(\"Z\"));\n        assertTrue(values.get(0).contains(\"T\"));\n    }\n\n    @Test\n    public void testWebhookWorksWithBlynkHttpApiWithDateTimePlaceholderAndPins() throws Exception {\n        WebHook webHook = new WebHook();\n        webHook.url = httpServerUrl + \"4ae3851817194e2596cf1b7103603ef8/update/V124\";\n        webHook.method = PUT;\n        webHook.headers = new Header[] {new Header(\"Content-Type\", \"application/json\")};\n        webHook.body = \"[\\\"/datetime_iso/,/pin[0]/,/pin[1]/\\\"]\";\n        webHook.pin = 123;\n        webHook.pinType = PinType.VIRTUAL;\n        webHook.width = 2;\n        webHook.height = 1;\n\n        clientPair.appClient.createWidget(1, webHook);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 123 10 11\");\n        verify(clientPair.hardwareClient.responseMock, after(1000).times(0)).channelRead(any(), any());\n\n        Future<Response> f = httpclient.prepareGet(httpServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/V124\").execute();\n        Response response = f.get();\n\n        assertEquals(200, response.getStatusCode());\n        List<String> values = consumeJsonPinValues(response.getResponseBody());\n        assertEquals(1, values.size());\n        String[] resp = values.get(0).split(\",\");\n        String dateTime = resp[0];\n        String pin0 = resp[1];\n        String pin1 = resp[2];\n        assertTrue(dateTime.endsWith(\"Z\"));\n        assertTrue(dateTime.contains(\"T\"));\n        assertEquals(\"10\", pin0);\n        assertEquals(\"11\", pin1);\n    }\n\n    @Test\n    public void testWebhookWorksWithBlynkHttpApiWithPlaceholder() throws Exception {\n        WebHook webHook = new WebHook();\n        webHook.url = httpServerUrl + \"4ae3851817194e2596cf1b7103603ef8/update/V124\";\n        webHook.method = PUT;\n        webHook.headers = new Header[] {new Header(\"Content-Type\", \"application/json\")};\n        webHook.body = \"[%s]\";\n        webHook.pin = 123;\n        webHook.pinType = PinType.VIRTUAL;\n        webHook.width = 2;\n        webHook.height = 1;\n\n        clientPair.appClient.createWidget(1, webHook);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 123 10\");\n        verify(clientPair.hardwareClient.responseMock, after(1000).times(0)).channelRead(any(), any());\n\n        Future<Response> f = httpclient.prepareGet(httpServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/V124\").execute();\n        Response response = f.get();\n\n        assertEquals(200, response.getStatusCode());\n        List<String> values = consumeJsonPinValues(response.getResponseBody());\n        assertEquals(1, values.size());\n        assertEquals(\"10\", values.get(0));\n    }\n\n    @Test\n    public void testWebhookWorksWithBlynkHttpApiWithArrayPlaceholder() throws Exception {\n        WebHook webHook = new WebHook();\n        webHook.url = httpServerUrl + \"4ae3851817194e2596cf1b7103603ef8/update/V124\";\n        webHook.method = PUT;\n        webHook.headers = new Header[] {new Header(\"Content-Type\", \"application/json\")};\n        webHook.body = \"[/pin[0]/,/pin[1]/,/pin[2]/]\";\n        webHook.pin = 123;\n        webHook.pinType = PinType.VIRTUAL;\n        webHook.width = 2;\n        webHook.height = 1;\n\n        clientPair.appClient.createWidget(1, webHook);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 123 \" + b(\"10 11 12\"));\n        verify(clientPair.hardwareClient.responseMock, after(1000).times(0)).channelRead(any(), any());\n\n        Future<Response> f = httpclient.prepareGet(httpServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/V124\").execute();\n        Response response = f.get();\n\n        assertEquals(200, response.getStatusCode());\n        List<String> values = consumeJsonPinValues(response.getResponseBody());\n        assertEquals(3, values.size());\n        assertEquals(\"10\", values.get(0));\n        assertEquals(\"11\", values.get(1));\n        assertEquals(\"12\", values.get(2));\n    }\n\n    @Test\n    public void testWebhookWorksWithBlynkHttpApiWithArrayPlaceholder2() throws Exception {\n        WebHook webHook = new WebHook();\n        webHook.url = httpServerUrl + \"4ae3851817194e2596cf1b7103603ef8/update/V124\";\n        webHook.method = PUT;\n        webHook.headers = new Header[] {new Header(\"Content-Type\", \"application/json\")};\n        webHook.body = \"[/pin[0]/]\";\n        webHook.pin = 123;\n        webHook.pinType = PinType.VIRTUAL;\n        webHook.width = 2;\n        webHook.height = 1;\n\n        clientPair.appClient.createWidget(1, webHook);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 123 10\");\n        verify(clientPair.hardwareClient.responseMock, after(1000).times(0)).channelRead(any(), any());\n\n        Future<Response> f = httpclient.prepareGet(httpServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/V124\").execute();\n        Response response = f.get();\n\n        assertEquals(200, response.getStatusCode());\n        List<String> values = consumeJsonPinValues(response.getResponseBody());\n        assertEquals(1, values.size());\n        assertEquals(\"10\", values.get(0));\n    }\n\n    @Test\n    public void testWebhookWorksWithBlynkHttpApiWithPlaceholderQuotaLimit() throws Exception {\n        WebHook webHook = new WebHook();\n        webHook.url = httpServerUrl + \"4ae3851817194e2596cf1b7103603ef8/update/V124\";\n        webHook.method = PUT;\n        webHook.headers = new Header[] {new Header(\"Content-Type\", \"application/json\")};\n        webHook.body = \"[%s]\";\n        webHook.pin = 123;\n        webHook.pinType = PinType.VIRTUAL;\n        webHook.width = 2;\n        webHook.height = 1;\n\n        clientPair.appClient.createWidget(1, webHook);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.hardwareClient.send(\"hardware vw 123 10\");\n        verify(clientPair.hardwareClient.responseMock, after(500).times(0)).channelRead(any(), any());\n\n        Future<Response> f = httpclient.prepareGet(httpServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/V124\").execute();\n        Response response = f.get();\n\n        assertEquals(200, response.getStatusCode());\n        List<String> values = consumeJsonPinValues(response.getResponseBody());\n        assertEquals(1, values.size());\n        assertEquals(\"10\", values.get(0));\n\n        clientPair.hardwareClient.send(\"hardware vw 123 11\");\n        verify(clientPair.hardwareClient.responseMock, after(600).times(0)).channelRead(any(), any());\n\n        f = httpclient.prepareGet(httpServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/V124\").execute();\n        response = f.get();\n\n        assertEquals(200, response.getStatusCode());\n        values = consumeJsonPinValues(response.getResponseBody());\n        assertEquals(1, values.size());\n        assertEquals(\"10\", values.get(0));\n\n\n        clientPair.hardwareClient.send(\"hardware vw 123 12\");\n        verify(clientPair.hardwareClient.responseMock, after(500).times(0)).channelRead(any(), any());\n\n        f = httpclient.prepareGet(httpServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/V124\").execute();\n        response = f.get();\n\n        assertEquals(200, response.getStatusCode());\n        values = consumeJsonPinValues(response.getResponseBody());\n        assertEquals(1, values.size());\n        assertEquals(\"12\", values.get(0));\n    }\n\n    @Test\n    public void testWebhookWorksWithBlynkHttpApiNoPlaceHolderAppSideTrigger() throws Exception {\n        WebHook webHook = new WebHook();\n        webHook.url = httpServerUrl + \"4ae3851817194e2596cf1b7103603ef8/update/V124\";\n        webHook.method = PUT;\n        webHook.headers = new Header[] {new Header(\"Content-Type\", \"application/json\")};\n        webHook.body = \"[\\\"124\\\"]\";\n        webHook.pin = 123;\n        webHook.pinType = PinType.VIRTUAL;\n        webHook.width = 2;\n        webHook.height = 1;\n\n        clientPair.appClient.createWidget(1, webHook);\n        clientPair.appClient.verifyResult(ok(1));\n\n        //125564119 is id of project with 4ae3851817194e2596cf1b7103603ef8 token\n        clientPair.appClient.send(\"hardware 1 vw 123 10\");\n        verify(clientPair.hardwareClient.responseMock, after(500).times(1)).channelRead(any(), eq(hardware(2, \"vw 123 10\")));\n\n        Future<Response> f = httpclient.prepareGet(httpServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/V124\").execute();\n        Response response = f.get();\n\n        assertEquals(200, response.getStatusCode());\n        List<String> values = consumeJsonPinValues(response.getResponseBody());\n        assertEquals(1, values.size());\n        assertEquals(\"124\", values.get(0));\n    }\n\n    @Test\n    public void testWebhookWorksWithBlynkHttpApiAppSideTriggerCheckLimit() throws Exception {\n        WebHook webHook = new WebHook();\n        webHook.url = httpServerUrl + \"4ae3851817194e2596cf1b7103603ef8/update/V124\";\n        webHook.method = PUT;\n        webHook.headers = new Header[] {new Header(\"Content-Type\", \"application/json\")};\n        webHook.body = \"[\\\"/pin/\\\"]\";\n        webHook.pin = 123;\n        webHook.pinType = PinType.VIRTUAL;\n        webHook.width = 2;\n        webHook.height = 1;\n\n        clientPair.appClient.createWidget(1, webHook);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.send(\"hardware 1 vw 123 10\");\n        verify(clientPair.hardwareClient.responseMock, after(500)).channelRead(any(), eq(hardware(2, \"vw 123 10\")));\n\n        Future<Response> f = httpclient.prepareGet(httpServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/V124\").execute();\n        Response response = f.get();\n\n        assertEquals(200, response.getStatusCode());\n        List<String> values = consumeJsonPinValues(response.getResponseBody());\n        assertEquals(1, values.size());\n        assertEquals(\"10\", values.get(0));\n\n        clientPair.appClient.send(\"hardware 1 vw 123 11\");\n        verify(clientPair.hardwareClient.responseMock, after(1000)).channelRead(any(), eq(hardware(3, \"vw 123 11\")));\n\n        f = httpclient.prepareGet(httpServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/V124\").execute();\n        response = f.get();\n\n        assertEquals(200, response.getStatusCode());\n        values = consumeJsonPinValues(response.getResponseBody());\n        assertEquals(1, values.size());\n        assertEquals(\"10\", values.get(0));\n\n\n        clientPair.appClient.send(\"hardware 1 vw 123 11\");\n        verify(clientPair.hardwareClient.responseMock, after(500)).channelRead(any(), eq(hardware(4, \"vw 123 11\")));\n\n        f = httpclient.prepareGet(httpServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/V124\").execute();\n        response = f.get();\n\n        assertEquals(200, response.getStatusCode());\n        values = consumeJsonPinValues(response.getResponseBody());\n        assertEquals(1, values.size());\n        assertEquals(\"11\", values.get(0));\n    }\n\n    @Test\n    public void testWebhookInvalidUrl() throws Exception {\n        WebHook webHook = new WebHook();\n        webHook.url = \"\";\n        webHook.method = PUT;\n        webHook.headers = new Header[]{new Header(\"Content-Type\", \"application/json\")};\n        webHook.body = \"[\\\"/pin/\\\"]\";\n        webHook.pin = 123;\n        webHook.pinType = PinType.VIRTUAL;\n        webHook.width = 2;\n        webHook.height = 1;\n        webHook.id = 111;\n\n\n        webHook.url = \"http://adasd.com\";\n        clientPair.appClient.createWidget(1, webHook);\n        clientPair.appClient.verifyResult(ok(1));\n\n        webHook.id = 222;\n        webHook.url = \"https://adasd.com\";\n        clientPair.appClient.createWidget(1, webHook);\n        clientPair.appClient.verifyResult(ok(2));\n\n        webHook.id = 333;\n        webHook.url = \"Http://adasd.com\";\n        clientPair.appClient.createWidget(1, webHook);\n        clientPair.appClient.verifyResult(ok(3));\n    }\n\n    @Test\n    public void testWebhookWorksWithUrlPlaceholder() throws Exception {\n        WebHook webHook = new WebHook();\n        webHook.url = \"/pin/\";\n        webHook.method = PUT;\n        webHook.headers = new Header[] {new Header(\"Content-Type\", \"application/json\")};\n        webHook.body = \"[\\\"text\\\"]\";\n        webHook.pin = 123;\n        webHook.pinType = PinType.VIRTUAL;\n        webHook.width = 2;\n        webHook.height = 1;\n\n        clientPair.appClient.createWidget(1, webHook);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.send(\"hardware 1 vw 123 \" + httpServerUrl + \"4ae3851817194e2596cf1b7103603ef8/update/V124\");\n        verify(clientPair.hardwareClient.responseMock, after(500).times(1)).channelRead(any(), eq(\n                new HardwareMessage(2, b(\"vw 123 \" + httpServerUrl + \"4ae3851817194e2596cf1b7103603ef8/update/V124\"))));\n\n        Future<Response> f = httpclient.prepareGet(httpServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/V124\").execute();\n        Response response = f.get();\n\n        assertEquals(200, response.getStatusCode());\n        List<String> values = consumeJsonPinValues(response.getResponseBody());\n        assertEquals(1, values.size());\n        assertEquals(\"text\", values.get(0));\n    }\n\n    @Test\n    public void testWebhookWorksWithUrlPlaceholder2() throws Exception {\n        WebHook webHook = new WebHook();\n        webHook.url = \"/pin/\";\n        webHook.method = PUT;\n        webHook.headers = new Header[] {new Header(\"Content-Type\", \"application/json\")};\n        webHook.pin = 123;\n        webHook.pinType = PinType.VIRTUAL;\n        webHook.width = 2;\n        webHook.height = 1;\n\n        clientPair.appClient.createWidget(1, webHook);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.send(\"hardware 1 vw 123 1\");\n        clientPair.hardwareClient.verifyResult(hardware(2, \"vw 123 1\"));\n\n        Future<Response> f = httpclient.prepareGet(httpServerUrl + \"4ae3851817194e2596cf1b7103603ef8/get/V126\").execute();\n        Response response = f.get();\n\n        assertEquals(400, response.getStatusCode());\n        assertEquals(\"Requested pin doesn't exist in the app.\", response.getResponseBody());\n    }\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/tcp/WidgetWorkflowTest.java",
    "content": "package cc.blynk.integration.tcp;\n\nimport cc.blynk.integration.SingleServerInstancePerTest;\nimport cc.blynk.server.core.model.Profile;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.outputs.ValueDisplay;\nimport cc.blynk.server.core.model.widgets.ui.DeviceSelector;\nimport cc.blynk.server.core.model.widgets.ui.tiles.DeviceTiles;\nimport cc.blynk.server.core.protocol.model.messages.ResponseMessage;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport java.nio.file.Files;\nimport java.nio.file.Paths;\n\nimport static cc.blynk.integration.TestUtil.appSync;\nimport static cc.blynk.integration.TestUtil.hardware;\nimport static cc.blynk.integration.TestUtil.illegalCommand;\nimport static cc.blynk.integration.TestUtil.illegalCommandBody;\nimport static cc.blynk.integration.TestUtil.notAllowed;\nimport static cc.blynk.integration.TestUtil.ok;\nimport static cc.blynk.server.core.protocol.enums.Response.NOT_ALLOWED;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.eq;\nimport static org.mockito.Mockito.timeout;\nimport static org.mockito.Mockito.verify;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/2/2015.\n *\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class WidgetWorkflowTest extends SingleServerInstancePerTest {\n\n    @Before\n    public void deleteFolder() throws Exception {\n        Files.deleteIfExists(Paths.get(getDataFolder(), \"blynk\", \"userProfiles\"));\n    }\n\n    @Test\n    public void testCorrectBehaviourOnWrongInput() throws Exception {\n        clientPair.appClient.send(\"createWidget \");\n        verify(clientPair.appClient.responseMock, timeout(1000)).channelRead(any(), eq(illegalCommand(1)));\n\n        clientPair.appClient.send(\"createWidget 1\");\n        verify(clientPair.appClient.responseMock, timeout(1000)).channelRead(any(), eq(illegalCommand(2)));\n\n        clientPair.appClient.send(\"createWidget 1\" + \"\\0\");\n        verify(clientPair.appClient.responseMock, timeout(1000)).channelRead(any(), eq(illegalCommand(3)));\n\n        clientPair.appClient.send(\"createWidget 1\" + \"\\0\" + \"{}\");\n        verify(clientPair.appClient.responseMock, timeout(1000)).channelRead(any(), eq(notAllowed(4)));\n\n        //very large widget\n        StringBuilder sb = new StringBuilder();\n        for (int i = 0; i < 10 * 1024 + 1; i++) {\n            sb.append(\"a\");\n        }\n\n        clientPair.appClient.createWidget(1, sb.toString());\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(new ResponseMessage(5, NOT_ALLOWED)));\n    }\n\n    @Test\n    public void testCanCreateWebHookWithScheme() throws Exception {\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":1111, \\\"width\\\":1, \\\"height\\\":1,\\\"url\\\":\\\"http://123.com\\\",\\\"type\\\":\\\"WEBHOOK\\\"}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":1113, \\\"width\\\":1, \\\"height\\\":1,\\\"url\\\":\\\"https://123.com\\\",\\\"type\\\":\\\"WEBHOOK\\\"}\");\n        clientPair.appClient.verifyResult(ok(2));\n    }\n\n    @Test\n    public void testWidgetAlreadyExists() throws Exception {\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":1, \\\"width\\\":1, \\\"height\\\":1,\\\"type\\\":\\\"BUTTON\\\"}\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(notAllowed(1)));\n    }\n\n    @Test\n    public void testWidgetWrongSize() throws Exception {\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":22222, \\\"width\\\":1, \\\"height\\\":0,\\\"type\\\":\\\"BUTTON\\\"}\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(notAllowed(1)));\n    }\n\n    @Test\n    public void testCreateWidgetBadFormat() throws Exception {\n        clientPair.appClient.createWidget(1, \":600084223,\\\"isDefaultColor\\\":true,\\\"rangeMappingOn\\\":false,\\\"pin\\\":-1,\\\"pwmMode\\\":false,\\\"deviceId\\\":0,\\\"height\\\":3,\\\"id\\\":82561,\\\"tabId\\\":1,\\\"type\\\":\\\"VERTICAL_LEVEL_DISPLAY\\\",\\\"width\\\":1,\\\"x\\\":0,\\\"y\\\":6,\\\"min\\\":0,\\\"max\\\":1023}\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(illegalCommandBody(1)));\n    }\n\n    @Test\n    public void testCreateWidgetAndRemove() throws Exception {\n        clientPair.appClient.createWidget(1, \"{\\\"frequency\\\":1000,\\\"isAxisFlipOn\\\":false,\\\"color\\\":600084223,\\\"isDefaultColor\\\":true,\\\"rangeMappingOn\\\":false,\\\"pin\\\":-1,\\\"pwmMode\\\":false,\\\"deviceId\\\":0,\\\"height\\\":3,\\\"id\\\":82561,\\\"tabId\\\":1,\\\"type\\\":\\\"VERTICAL_LEVEL_DISPLAY\\\",\\\"width\\\":1,\\\"x\\\":0,\\\"y\\\":6,\\\"min\\\":0,\\\"max\\\":1023}\");\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.deleteWidget(1, 82561);\n        clientPair.appClient.verifyResult(ok(2));\n    }\n\n    @Test\n    public void testPinStorageIsCleanedOnWidgetRemoval() throws Exception {\n        ValueDisplay valueDisplay = new ValueDisplay();\n        valueDisplay.id = 82561;\n        valueDisplay.width = 2;\n        valueDisplay.height = 2;\n        valueDisplay.pin = 111;\n        valueDisplay.pinType = PinType.VIRTUAL;\n        valueDisplay.deviceId = 200_000;\n\n        clientPair.appClient.createWidget(1, valueDisplay);\n        clientPair.appClient.verifyResult(ok(1));\n\n        DeviceSelector deviceSelector = new DeviceSelector();\n        deviceSelector.id = 200000;\n        deviceSelector.x = 0;\n        deviceSelector.y = 0;\n        deviceSelector.width = 1;\n        deviceSelector.height = 1;\n        deviceSelector.deviceIds = new int[] {0};\n\n        clientPair.appClient.createWidget(1, deviceSelector);\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.hardwareClient.send(\"hardware vw 111 test\");\n        clientPair.appClient.verifyResult(hardware(1, \"1-0 vw 111 test\"));\n\n        clientPair.appClient.sync(1, 0);\n        clientPair.appClient.verifyResult(appSync(\"1-0 vw 111 test\"));\n        clientPair.appClient.reset();\n\n        clientPair.appClient.deleteWidget(1, 82561);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.sync(1, 0);\n        clientPair.appClient.neverAfter(500, appSync(\"1-0 vw 111 test\"));\n    }\n\n    @Test\n    public void testCreateWidgetAndRemoveWithDeviceTiles() throws Exception {\n        DeviceTiles deviceTiles = new DeviceTiles();\n        deviceTiles.id = 21321;\n        deviceTiles.x = 8;\n        deviceTiles.y = 8;\n        deviceTiles.width = 50;\n        deviceTiles.height = 100;\n\n        clientPair.appClient.createWidget(1, deviceTiles);\n        clientPair.appClient.verifyResult(ok(1));\n\n        clientPair.appClient.createWidget(1, \"{\\\"frequency\\\":1000,\\\"isAxisFlipOn\\\":false,\\\"color\\\":600084223,\\\"isDefaultColor\\\":true,\\\"rangeMappingOn\\\":false,\\\"pin\\\":-1,\\\"pwmMode\\\":false,\\\"deviceId\\\":0,\\\"height\\\":3,\\\"id\\\":82561,\\\"tabId\\\":1,\\\"type\\\":\\\"VERTICAL_LEVEL_DISPLAY\\\",\\\"width\\\":1,\\\"x\\\":0,\\\"y\\\":6,\\\"min\\\":0,\\\"max\\\":1023}\");\n        clientPair.appClient.verifyResult(ok(2));\n\n        clientPair.appClient.deleteWidget(1, 82561);\n        clientPair.appClient.verifyResult(ok(3));\n    }\n\n    @Test\n    // https://github.com/blynkkk/blynk-server/issues/1266\n    public void testWidgetValueNotChangedAfterUpdate() throws Exception {\n        clientPair.appClient.createWidget(1, \"{\\\"id\\\":82561, \\\"width\\\":1, \\\"height\\\":1,\\\"type\\\":\\\"BUTTON\\\", \\\"value\\\":\\\"1\\\"}\");\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        Profile profile = clientPair.appClient.parseProfile(2);\n        assertNotNull(profile);\n        Widget widget = profile.dashBoards[0].getWidgetById(82561);\n        assertEquals(\"1\", ((OnePinWidget) widget).value);\n\n        clientPair.appClient.updateWidget(1, \"{\\\"id\\\":82561, \\\"width\\\":2, \\\"height\\\":2,\\\"type\\\":\\\"BUTTON\\\"}\");\n\n        clientPair.appClient.send(\"loadProfileGzipped\");\n        profile = clientPair.appClient.parseProfile(4);\n        widget = profile.dashBoards[0].getWidgetById(82561);\n        assertEquals(\"1\", ((OnePinWidget) widget).value);\n    }\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/tools/FlahsedTokenGenerator.java",
    "content": "package cc.blynk.integration.tools;\n\nimport cc.blynk.server.core.BlockingIOProcessor;\nimport cc.blynk.server.db.DBManager;\nimport cc.blynk.server.db.model.FlashedToken;\nimport net.glxn.qrgen.core.image.ImageType;\nimport net.glxn.qrgen.javase.QRCode;\n\nimport java.io.OutputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.UUID;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.03.17.\n */\npublic class FlahsedTokenGenerator {\n\n    public static void main(String[] args) throws Exception{\n        FlashedToken[] flashedTokens = generateTokens(\"test@blynk.cc\", 100, \"Grow\", 2);\n        DBManager dbManager = new DBManager(\"db-test.properties\", new BlockingIOProcessor(4, 100), true);\n\n        dbManager.insertFlashedTokens(flashedTokens);\n\n        for (FlashedToken token : flashedTokens) {\n            Path path = Paths.get(\"/home/doom369/Downloads/grow\",  token.token + \"_\" + token.deviceId + \".jpg\");\n            generateQR(token.token, path);\n        }\n    }\n\n    private static FlashedToken[] generateTokens(String email, int count, String appName, int deviceCount) {\n        FlashedToken[] flashedTokens = new FlashedToken[count * deviceCount];\n\n        int counter = 0;\n        for (int deviceId = 0; deviceId < deviceCount; deviceId++) {\n            for (int i = 0; i < count; i++) {\n                String token = UUID.randomUUID().toString().replace(\"-\", \"\");\n                flashedTokens[counter++] = new FlashedToken(email, token, appName, 1, deviceId);\n                System.out.println(\"Token : \" + token + \", deviceId : \" + deviceId + \", appName : \" + appName);\n            }\n        }\n        return flashedTokens;\n    }\n\n    private static void generateQR(String text, Path outputFile) throws Exception {\n        try (OutputStream out = Files.newOutputStream(outputFile)) {\n            QRCode.from(text).to(ImageType.JPG).writeTo(out);\n        }\n    }\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/tools/ProjectTokenGenerator.java",
    "content": "package cc.blynk.integration.tools;\n\nimport cc.blynk.server.core.dao.TokenManager;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.enums.Theme;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.utils.AppNameUtil;\nimport cc.blynk.utils.SHA256Util;\n\nimport java.io.BufferedWriter;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.concurrent.ConcurrentHashMap;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.11.16.\n */\npublic class ProjectTokenGenerator {\n\n    public static void main(String[] args) throws Exception {\n        TokenManager tokenManager = new TokenManager(new ConcurrentHashMap<>(), null, \"\");\n        String email = \"dmitriy@blynk.cc\";\n        String pass = \"b\";\n        String appName = AppNameUtil.BLYNK;\n        User user = new User(email, SHA256Util.makeHash(pass, email), appName, \"local\", \"127.0.0.1\", false, false);\n        user.addEnergy(98000);\n\n        int count = 300;\n\n        user.profile.dashBoards = new DashBoard[count];\n        for (int i = 1; i <= count; i++) {\n            DashBoard dash = new DashBoard();\n            dash.id = i;\n            dash.theme = Theme.Blynk;\n            dash.isActive = true;\n            user.profile.dashBoards[i - 1] = dash;\n        }\n\n        List<String> tokens = new ArrayList<>();\n        for (int i = 1; i <= count; i++) {\n            //tokens.add(tokenManager.refreshToken(user, i, 0));\n        }\n\n        write(\"/path/300_tokens.txt\", tokens);\n\n        write(Paths.get(\"/path/\" + email + \".\" + appName + \".user\"), JsonParser.toJson(user));\n\n        //scp /path/dmitriy@blynk.cc.Blynk.user root@IP:/root/data/\n\n    }\n\n    private static void write(String outputPath, Collection<String> tokens) throws IOException {\n        Path path = Paths.get(outputPath);\n        write(path, tokens);\n    }\n\n    private static void write(Path path, Collection<String> tokens) throws IOException {\n        try (BufferedWriter writer = Files.newBufferedWriter(path)) {\n            for (String token : tokens) {\n                writer.write(token);\n                writer.newLine();\n            }\n        }\n    }\n\n    private static void write(Path path, String profile) throws IOException {\n        try (BufferedWriter writer = Files.newBufferedWriter(path)) {\n            writer.write(profile);\n        }\n    }\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/tools/QRGenerator.java",
    "content": "package cc.blynk.integration.tools;\n\nimport cc.blynk.server.core.BlockingIOProcessor;\nimport cc.blynk.server.db.DBManager;\nimport cc.blynk.server.db.model.Redeem;\nimport cc.blynk.utils.TokenGeneratorUtil;\nimport net.glxn.qrgen.core.image.ImageType;\nimport net.glxn.qrgen.javase.QRCode;\n\nimport java.io.OutputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Used for Redeem QRs generation.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 22.03.16.\n */\npublic class QRGenerator {\n\n    public static void main(String[] args) throws Exception {\n        DBManager dbManager = new DBManager(\"db.properties\", new BlockingIOProcessor(4, 100), true);\n        List<Redeem> redeems = generateQR(10, \"/home/doom369/QR/test\", \"test\", 50000);\n        dbManager.insertRedeems(redeems);\n    }\n\n    private static List<Redeem> generateQR(int count, String outputFolder, String campaign, int reward) throws Exception {\n        var redeems = new ArrayList<Redeem>(count);\n        for (int i = 0; i < count; i++) {\n            String token = TokenGeneratorUtil.generateNewToken();\n\n            var redeem = new Redeem(token, campaign, reward);\n            redeems.add(redeem);\n\n            Path path = Paths.get(outputFolder, String.format(\"%d.jpg\", i));\n            generateQR(redeem.formatToken(), path);\n        }\n        return redeems;\n    }\n\n    private static void generateQR(String text, Path outputFile) throws Exception {\n        try (OutputStream out = Files.newOutputStream(outputFile)) {\n            QRCode.from(text).to(ImageType.JPG).writeTo(out);\n        }\n    }\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/tools/TokenGenerator.java",
    "content": "package cc.blynk.integration.tools;\n\nimport cc.blynk.server.core.BlockingIOProcessor;\nimport cc.blynk.server.db.DBManager;\nimport cc.blynk.server.db.model.Redeem;\n\nimport java.io.BufferedWriter;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.*;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 08.02.16.\n */\npublic class TokenGenerator {\n\n    public static void main(String[] args) throws Exception {\n        //List<String> tokens = Files.readAllLines(Paths.get(\"/home/doom369/Downloads/x.csv\"));\n        Set<String> tokens = generate(10);\n\n        List<Redeem> redeems = new ArrayList<>(tokens.size());\n        for (String token : tokens) {\n            redeems.add(new Redeem(token, \"SparkFun\", 15000));\n        }\n\n        DBManager dbManager = new DBManager(\"db.properties\", new BlockingIOProcessor(4, 100), true);\n        dbManager.insertRedeems(redeems);\n    }\n\n    private static Set<String> generate(int amount) {\n        Set<String> tokens = new HashSet<>();\n\n        for (int i = 0; i < amount; i++ ) {\n            String token = UUID.randomUUID().toString().replace(\"-\", \"\");\n            tokens.add(token);\n            System.out.println(token);\n        }\n\n        return tokens;\n    }\n\n    private static void write(String outputPath, Set<String> tokens) throws IOException {\n        Path path = Paths.get(outputPath);\n        write(path, tokens);\n    }\n\n    private static void write(Path path, Set<String> tokens) throws IOException {\n        try (BufferedWriter writer = Files.newBufferedWriter(path)) {\n            for (String token : tokens) {\n                writer.write(token);\n                writer.newLine();\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/tools/UserReader.java",
    "content": "package cc.blynk.integration.tools;\n\nimport cc.blynk.server.core.BlockingIOProcessor;\nimport cc.blynk.server.core.dao.UserKey;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.db.DBManager;\n\nimport java.util.Date;\nimport java.util.concurrent.ConcurrentMap;\n\npublic class UserReader {\n\n    public static void main(String[] args) throws Exception {\n        DBManager dbManager = new DBManager(\"db-test.properties\", new BlockingIOProcessor(1, 100), true);\n        ConcurrentMap<UserKey, User> allUsers = dbManager.userDBDao.getAllUsers(\"\");\n        System.out.println(\"Users : \" + allUsers.size());\n        for (User user : allUsers.values()) {\n            for (DashBoard dashBoard : user.profile.dashBoards) {\n                for (Device device : dashBoard.devices) {\n                    System.out.println(user.email + \",\" + device.getNameOrDefault()\n                                               + \",\" + new Date(device.firstConnectTime)\n                                               + \",\" + device.token);\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "integration-tests/src/test/java/cc/blynk/integration/websocket/WebSocketTest.java",
    "content": "package cc.blynk.integration.websocket;\n\nimport cc.blynk.integration.BaseTest;\nimport cc.blynk.integration.model.tcp.ClientPair;\nimport cc.blynk.integration.model.websocket.AppWebSocketClient;\nimport cc.blynk.integration.model.websocket.WebSocketClient;\nimport cc.blynk.server.core.protocol.model.messages.common.HardwareMessage;\nimport cc.blynk.server.servers.BaseServer;\nimport cc.blynk.server.servers.application.MobileAndHttpsServer;\nimport cc.blynk.server.servers.hardware.HardwareAndHttpAPIServer;\nimport cc.blynk.utils.StringUtils;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport static cc.blynk.integration.TestUtil.b;\nimport static cc.blynk.integration.TestUtil.ok;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE;\nimport static cc.blynk.server.core.protocol.model.messages.MessageFactory.produce;\nimport static cc.blynk.utils.StringUtils.WEBSOCKET_WEB_PATH;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.timeout;\nimport static org.mockito.Mockito.verify;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 13.01.16.\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class WebSocketTest extends BaseTest {\n\n    private BaseServer webSocketServer;\n    private BaseServer appServer;\n    private ClientPair clientPair;\n    //private static Holder localHolder;\n\n    //web socket ports\n    private static int tcpWebSocketPort;\n\n    @After\n    public void shutdown() {\n        webSocketServer.close();\n        appServer.close();\n        clientPair.stop();\n        holder.close();\n    }\n\n    @Before\n    public void init() throws Exception {\n        tcpWebSocketPort = properties.getHttpPort();\n        webSocketServer = new HardwareAndHttpAPIServer(holder).start();\n        appServer = new MobileAndHttpsServer(holder).start();\n        clientPair = initAppAndHardPair(properties);\n    }\n\n    @Override\n    public String getDataFolder() {\n        return getRelativeDataFolder(\"/profiles\");\n    }\n\n    @Test\n    public void testAppWebDashSocketLogin() throws Exception{\n        AppWebSocketClient appWebSocketClient = new AppWebSocketClient(\"localhost\", properties.getHttpsPort(), WEBSOCKET_WEB_PATH);\n        appWebSocketClient.start();\n        appWebSocketClient.login(getUserName(), \"1\");\n\n        appWebSocketClient.verifyResult(ok(1));\n        appWebSocketClient.send(\"ping\");\n        appWebSocketClient.verifyResult(ok(2));\n    }\n\n    @Test\n    public void testBasicWebSocketCommandsOk2() throws Exception{\n        WebSocketClient webSocketClient = new WebSocketClient(\"localhost\", tcpWebSocketPort, \"/websockets\", false);\n        webSocketClient.start();\n        webSocketClient.send(\"login 4ae3851817194e2596cf1b7103603ef8\");\n        verify(webSocketClient.responseMock, timeout(500)).channelRead(any(), eq(ok(1)));\n        webSocketClient.send(\"ping\");\n        verify(webSocketClient.responseMock, timeout(500)).channelRead(any(), eq(ok(2)));\n    }\n\n    @Test\n    public void testBasicWebSocketCommandsOk() throws Exception{\n        WebSocketClient webSocketClient = new WebSocketClient(\"localhost\", tcpWebSocketPort, StringUtils.WEBSOCKETS_PATH, false);\n        webSocketClient.start();\n        webSocketClient.send(\"login 4ae3851817194e2596cf1b7103603ef8\");\n        verify(webSocketClient.responseMock, timeout(500)).channelRead(any(), eq(ok(1)));\n        webSocketClient.send(\"ping\");\n        verify(webSocketClient.responseMock, timeout(500)).channelRead(any(), eq(ok(2)));\n    }\n\n    @Test\n    public void testSslBasicWebSocketCommandsOk() throws Exception{\n        WebSocketClient webSocketClient = new WebSocketClient(\"localhost\", properties.getHttpsPort(), StringUtils.WEBSOCKETS_PATH, true);\n        webSocketClient.start();\n        webSocketClient.send(\"login 4ae3851817194e2596cf1b7103603ef8\");\n        verify(webSocketClient.responseMock, timeout(500)).channelRead(any(), eq(ok(1)));\n        webSocketClient.send(\"ping\");\n        verify(webSocketClient.responseMock, timeout(500)).channelRead(any(), eq(ok(2)));\n    }\n\n    @Test\n    public void testSyncBetweenWebSocketsAndAppWorks() throws Exception {\n        clientPair.appClient.reset();\n        clientPair.hardwareClient.reset();\n\n        WebSocketClient webSocketClient = new WebSocketClient(\"localhost\", tcpWebSocketPort, StringUtils.WEBSOCKETS_PATH, false);\n        webSocketClient.start();\n        webSocketClient.send(\"login \" + clientPair.token);\n        verify(webSocketClient.responseMock, timeout(500)).channelRead(any(), eq(ok(1)));\n\n        clientPair.appClient.send(\"hardware 1-0 vw 4 1\");\n        verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(produce(1, HARDWARE, b(\"vw 4 1\"))));\n        verify(webSocketClient.responseMock, timeout(500)).channelRead(any(), eq(produce(1, HARDWARE, b(\"vw 4 1\"))));\n\n        webSocketClient.send(\"hardware vw 4 2\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(2, HARDWARE, b(\"1-0 vw 4 2\"))));\n\n        clientPair.hardwareClient.send(\"hardware vw 4 3\");\n        verify(clientPair.appClient.responseMock, timeout(500)).channelRead(any(), eq(produce(1, HARDWARE, b(\"1-0 vw 4 3\"))));\n\n        clientPair.appClient.reset();\n        WebSocketClient webSocketClient2 = new WebSocketClient(\"localhost\", tcpWebSocketPort, StringUtils.WEBSOCKETS_PATH, false);\n        webSocketClient2.start();\n        webSocketClient2.send(\"login \" + clientPair.token);\n        verify(webSocketClient2.responseMock, timeout(500)).channelRead(any(), eq(ok(1)));\n        verify(webSocketClient2.responseMock, timeout(500)).channelRead(any(), eq(new HardwareMessage(1, b(\"pm 1 out 2 out 3 out 5 out 6 in 7 in 30 in 8 in\"))));\n        webSocketClient2.msgId = 1000;\n\n        for (int i = 1; i <= 10; i++) {\n            clientPair.appClient.send(\"hardware 1-0 vw 4 \" + i);\n            verify(clientPair.hardwareClient.responseMock, timeout(500)).channelRead(any(), eq(produce(i, HARDWARE, b(\"vw 4 \" + i))));\n            verify(webSocketClient.responseMock, timeout(500)).channelRead(any(), eq(produce(i, HARDWARE, b(\"vw 4 \" + i))));\n            verify(webSocketClient2.responseMock, timeout(500)).channelRead(any(), eq(produce(i, HARDWARE, b(\"vw 4 \" + i))));\n            webSocketClient2.send(\"hardsync \" + b(\"vr 4\"));\n            verify(webSocketClient2.responseMock, timeout(500)).channelRead(any(), eq(produce(1000 + i, HARDWARE, b(\"vw 4 \" + i))));\n        }\n    }\n\n    @Test\n    public void testSyncBetweenWebSocketsAndAppWorksLoop() throws Exception {\n        for (int i = 0; i < 10; i++) {\n            testSyncBetweenWebSocketsAndAppWorks();\n        }\n    }\n\n}\n"
  },
  {
    "path": "integration-tests/src/test/resources/db-test.properties",
    "content": "jdbc.url=jdbc:postgresql://localhost:5432/blynk?tcpKeepAlive=true&socketTimeout=150\nuser=test\npassword=test\nconnection.timeout.millis=30000\n\nreporting.jdbc.url=jdbc:postgresql://localhost:5432/blynk_reporting?tcpKeepAlive=true&socketTimeout=150\nreporting.user=test\nreporting.password=test\nreporting.connection.timeout.millis=30000"
  },
  {
    "path": "integration-tests/src/test/resources/json_test/user_profile_json.txt",
    "content": "{\n    \"dashBoards\" :\n        [\n            {\n             \"id\":1,\n             \"name\":\"My Dashboard\",\n             \"isActive\" : true,\n             \"isShared\" : true,\n             \"createdAt\" : 1,\n             \"widgets\"  : [\n                {\"id\":1, \"x\":1, \"y\":1, \"label\":\"Some Text\", \"type\":\"BUTTON\",         \"pinType\":\"DIGITAL\", \"pin\":1, \"value\":\"1\"},\n                {\"id\":2, \"x\":1, \"y\":1, \"label\":\"Some Text\", \"type\":\"SLIDER\",  \"pinType\":\"DIGITAL\", \"pin\":2, \"value\":\"1\", \"state\":\"ON\"},\n                {\"id\":3, \"x\":1, \"y\":1, \"label\":\"Some Text\", \"type\":\"SLIDER\",  \"pinType\":\"ANALOG\", \"pin\":3, \"value\":\"0\", \"state\":\"OFF\"},\n                {\"id\":4, \"x\":1, \"y\":1, \"label\":\"Some Text\", \"type\":\"SLIDER\",         \"pinType\":\"VIRTUAL\", \"pin\":4, \"value\":\"244\" },\n                {\"id\":5, \"x\":1, \"y\":1, \"label\":\"Some Text\", \"type\":\"TIMER\",          \"pinType\":\"DIGITAL\", \"pin\":5, \"value\":\"1\", \"startTime\":0},\n                {\"id\":6, \"x\":1, \"y\":1, \"label\":\"Some Text\", \"type\":\"LED\",            \"pinType\":\"ANALOG\", \"pin\":6},\n                {\"id\":7, \"x\":1, \"y\":1, \"label\":\"Some Text\", \"type\":\"DIGIT4_DISPLAY\", \"pinType\":\"ANALOG\", \"pin\":7, \"frequency\" : 5000, \"value\":\"3\"},\n                {\"id\":30, \"x\":1, \"y\":1, \"label\":\"Some Text\", \"type\":\"DIGIT4_DISPLAY\", \"pinType\":\"ANALOG\", \"pin\":30, \"frequency\" : 100, \"value\":\"3\"},\n                {\"id\":8, \"x\":1, \"y\":1, \"label\":\"Some Text\", \"type\":\"LABELED_VALUE_DISPLAY\",          \"pinType\":\"ANALOG\", \"pin\":8},\n                {\"id\":9, \"x\":1, \"y\":1, \"type\":\"NOTIFICATION\", \"notifyWhenOffline\":true, \"androidTokens\":{\"uid\":\"token\"}},\n                {\"id\":10, \"x\":1, \"y\":1, \"token\":\"token\", \"secret\":\"secret\", \"type\":\"TWITTER\"},\n                {\"id\":11, \"x\":1, \"y\":1, \"type\":\"RTC\", \"pinType\":\"VIRTUAL\", \"pin\":9},\n                {\"id\":12, \"x\":0, \"y\":0, \"color\":-1, \"width\":8, \"height\":2,\n                    \"type\":\"LCD\",\n                    \"pins\": [\n                                {\n                                    \"pin\":0,\n                                    \"pwmMode\":false,\n                                    \"rangeMappingOn\":false,\n                                    \"pinType\":\"VIRTUAL\",\n                                    \"value\":\"89.888037459418\",\n                                    \"min\":-100,\n                                    \"max\":100\n                                },\n                                {   \"pin\":11,\n                                    \"pwmMode\":false,\n                                    \"rangeMappingOn\":false,\n                                    \"pinType\":\"VIRTUAL\",\n                                    \"value\":\"-58.74774244674501\",\n                                    \"min\":-100,\n                                    \"max\":100\n                                }\n                            ],\n                    \"advancedMode\":false,\n                    \"textFormatLine1\":\"pin1 : /pin0/\",\n                    \"textFormatLine2\":\"pin2 : /pin1/\",\n                    \"textLight\":false,\n                    \"frequency\":1000\n                },\n                {\n                    \"type\":\"RGB\",\n                    \"id\":13,\"x\":2,\"y\":3,\"color\":616861439,\"width\":4,\"height\":3,\"tabId\":0,\n                    \"splitMode\":false,\n                    \"sendOnReleaseOn\":true,\n                    \"pins\":[\n                            {\n                                \"pin\":13,\n                                \"pwmMode\":false,\n                                \"rangeMappingOn\":false,\n                                \"pinType\":\"VIRTUAL\",\n                                \"value\":\"60\\u0000143\\u0000158\",\n                                \"min\":0,\n                                \"max\":255\n                            },\n                            {\n                                \"pin\":13,\n                                \"pwmMode\":false,\n                                \"rangeMappingOn\":false,\n                                \"pinType\":\"VIRTUAL\",\n                                \"value\":\"60\\u0000143\\u0000158\",\n                                \"min\":0,\n                                \"max\":255\n                            },\n                            {\n                                \"pin\":13,\n                                \"pwmMode\":false,\n                                \"rangeMappingOn\":false,\n                                \"pinType\":\"VIRTUAL\",\n                                \"value\":\"60\\u0000143\\u0000158\",\n                                \"min\":0,\n                                \"max\":255\n                            }\n                           ]\n                },\n                {\n                \"type\":\"ENHANCED_GRAPH\",\n                \"id\":191600,\n                \"x\":0,\n                \"y\":2,\n                \"color\":600084223,\n                \"width\":8,\n                \"height\":3,\n                \"tabId\":0,\n                \"isDefaultColor\":false,\n                \"dataStreams\":[\n                {\"graphType\":\"LINE\",\"color\":600084223,\"targetId\":0,\n                \"pin\":{\"pin\":7, \"pinType\":\"ANALOG\", \"pwmMode\":false,\"rangeMappingOn\":false,\"min\":0.0,\"max\":1023.0},\"flip\":511,\"yAxisMin\":0.0,\"yAxisMax\":100.0,\"showYAxis\":true,\"cubicSmoothingEnabled\":false,\"connectMissingPointsEnabled\":true,\"isPercentMaxMin\":true,\"yAxisScale\":\"AUTO\",\"delta\":0.0,\"userDeltaModifyAllowed\":false,\"maximumFractionDigits\":1},\n                {\"graphType\":\"LINE\",\"color\":600084223,\"targetId\":0,\n                \"pin\":{\"pin\":-1,\"pwmMode\":false,\"rangeMappingOn\":false,\"min\":0.0,\"max\":1023.0},\"flip\":511,\"yAxisMin\":0.0,\"yAxisMax\":100.0,\"showYAxis\":true,\"cubicSmoothingEnabled\":false,\"connectMissingPointsEnabled\":true,\"isPercentMaxMin\":true,\"yAxisScale\":\"AUTO\",\"delta\":0.0,\"userDeltaModifyAllowed\":false,\"maximumFractionDigits\":1}],\n                \"period\":\"LIVE\",\"textAlignment\":\"LEFT\",\"fontSize\":\"MEDIUM\",\n                \"stacking\":\"NO_STACKING\",\n                \"showTitle\":true,\n                \"showLegend\":true,\n                \"yAxisValues\":false,\n                \"xAxisValues\":false,\n                \"showXAxis\":false,\n                \"allowFullScreen\":true,\n                \"overrideYAxis\":false,\n                \"hideGradient\":false,\n                \"yAxisMin\":0.0,\n                \"yAxisMax\":0.0,\n                \"isPercentMaxMin\":false,\n                \"goalLine\":\"GOAL\",\n                \"selectedPeriods\":[\"LIVE\",\"ONE_HOUR\",\"SIX_HOURS\",\"N_DAY\",\"N_WEEK\",\"N_MONTH\",\"N_THREE_MONTHS\"]\n                },\n                {\"type\":\"LCD\",\"id\":15,\"x\":0,\"y\":6,\"color\":600084223,\"width\":8,\"height\":2,\"tabId\":0,\n                \"pins\":\n                [\n                {\"pin\":20,\n                \"pwmMode\":false,\n                \"rangeMappingOn\":false,\n                \"pinType\":\"VIRTUAL\",\n                \"min\":0,\n                \"max\":1023\n                },\n                {\n                \"pin\":20,\n                \"pwmMode\":false,\n                \"rangeMappingOn\":false,\n                \"pinType\":\"VIRTUAL\",\n                \"min\":0,\n                \"max\":1023\n                }\n                ],\n                \"advancedMode\":true,\"textLight\":false,\"textLightOn\":false,\"frequency\":0\n                }\n             ],\n             \"devices\" :\n             [\n               {\n                  \"id\":0,\n                  \"boardType\":\"Arduino UNO\",\n                  \"token\":\"12345678901234567890123456789012\",\n                  \"connectionType\":\"ETHERNET\",\n                  \"name\":\"My Device\"\n               }\n             ]\n            }\n        ]\n}"
  },
  {
    "path": "integration-tests/src/test/resources/json_test/user_profile_json_2.txt",
    "content": "{\n    \"dashBoards\" :\n        [\n            {\n             \"id\":555,\n             \"name\":\"MSecond\",\n             \"isActive\" : true,\n             \"createdAt\" : 1,\n             \"widgets\"  : [\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"BUTTON\",         \"pinType\":\"DIGITAL\", \"pin\":1, \"value\":\"1\"},\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"type\":\"NOTIFICATION\", \"notifyWhenOffline\":true, \"androidTokens\":{\"uid\":\"token\"}, \"iOSTokens\":{\"uid\":\"token\"}},\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"token\":\"token\", \"secret\":\"secret\", \"type\":\"TWITTER\"          }\n             ],\n             \"boardType\":\"MEGA\"\n            }\n        ]\n}"
  },
  {
    "path": "integration-tests/src/test/resources/json_test/user_profile_json_3_dashes.txt",
    "content": "{\n    \"dashBoards\" :\n        [\n            {\n             \"id\":1,\n             \"name\":\"My Dashboard\",\n             \"isActive\" : true,\n             \"createdAt\" : 1,\n             \"widgets\"  : [\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"BUTTON\",         \"pinType\":\"DIGITAL\", \"pin\":1, \"value\":\"1\"},\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"SLIDER\",  \"pinType\":\"DIGITAL\", \"pin\":2, \"value\":\"1\", \"state\":\"ON\"},\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"SLIDER\",  \"pinType\":\"ANALOG\", \"pin\":3, \"value\":\"0\", \"state\":\"OFF\"},\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"SLIDER\",         \"pinType\":\"VIRTUAL\", \"pin\":4, \"value\":\"244\" },\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"TIMER\",          \"pinType\":\"DIGITAL\", \"pin\":5, \"value\":\"1\", \"startTime\":0},\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"LED\",            \"pinType\":\"DIGITAL\", \"pin\":6},\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"DIGIT4_DISPLAY\", \"pinType\":\"DIGITAL\", \"pin\":7},\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"LABELED_VALUE_DISPLAY\",          \"pinType\":\"DIGITAL\", \"pin\":8}\n             ],\n             \"devices\" :\n                          [\n                            {\n                               \"id\":0,\n                               \"boardType\":\"Arduino UNO\",\n                               \"token\":\"12345678901234567890123456789012\",\n                               \"connectionType\":\"ETHERNET\",\n                               \"name\":\"My Device\"\n                            }\n                          ],\n             \"settings\" : {\"boardType\":\"Arduino UNO\", \"someParam\":\"someValue\"}\n            },\n            {\n                \"id\":2,\n                \"name\":\"My Dashboard2\",\n                \"devices\" :\n                             [\n                               {\n                                  \"id\":1,\n                                  \"boardType\":\"Arduino UNO\",\n                                  \"token\":\"12345678901234567890123456789013\",\n                                  \"connectionType\":\"ETHERNET\",\n                                  \"name\":\"My Device\"\n                               }\n                             ]\n            },\n            {\n                \"id\":3,\n                \"name\":\"My Dashboard3\",\n                \"devices\" :\n                             [\n                               {\n                                  \"id\":2,\n                                  \"boardType\":\"Arduino UNO\",\n                                  \"token\":\"12345678901234567890123456789014\",\n                                  \"connectionType\":\"ETHERNET\",\n                                  \"name\":\"My Device\"\n                               }\n                             ]\n            }\n        ],\n    \"twitter\" : {\n        \"token\" : \"123\",\n        \"tokenSecret\" : \"123\"\n    }\n}"
  },
  {
    "path": "integration-tests/src/test/resources/json_test/user_profile_json_empty_dash.txt",
    "content": "{\n    \"dashBoards\" :\n        [\n            {\n             \"id\":1,\n             \"name\":\"My Dashboard\",\n             \"isActive\" : true,\n             \"isShared\" : true,\n             \"createdAt\" : 1,\n             \"widgets\"  : [\n\n             ],\n             \"devices\" :\n             [\n               {\n                  \"id\":0,\n                  \"boardType\":\"Arduino UNO\",\n                  \"token\":\"12345678901234567890123456789012\",\n                  \"connectionType\":\"ETHERNET\",\n                  \"name\":\"My Device\"\n               }\n             ]\n            }\n        ]\n}"
  },
  {
    "path": "integration-tests/src/test/resources/json_test/user_profile_json_many_dashes.txt",
    "content": "{\n    \"dashBoards\" :\n        [\n            {\n             \"id\":1,\n             \"name\":\"My Dashboard\",\n             \"createdAt\" : 1,\n             \"widgets\"  : [\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"BUTTON\",         \"pinType\":\"DIGITAL\", \"pin\":1, \"value\":\"1\"},\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"SLIDER\",  \"pinType\":\"DIGITAL\", \"pin\":2, \"value\":\"1\", \"state\":\"ON\"},\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"SLIDER\",  \"pinType\":\"ANALOG\", \"pin\":3, \"value\":\"0\", \"state\":\"OFF\"},\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"SLIDER\",         \"pinType\":\"VIRTUAL\", \"pin\":4, \"value\":\"244\" },\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"TIMER\",          \"pinType\":\"DIGITAL\", \"pin\":5, \"value\":\"1\", \"startTime\":0},\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"LED\",            \"pinType\":\"DIGITAL\", \"pin\":6},\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"DIGIT4_DISPLAY\", \"pinType\":\"DIGITAL\", \"pin\":7},\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"LABELED_VALUE_DISPLAY\",          \"pinType\":\"DIGITAL\", \"pin\":8}\n             ],\n             \"settings\" : {\"boardType\":\"Arduino UNO\", \"someParam\":\"someValue\"}\n            },\n            {\n               \"id\":2,\n               \"name\":\"My Dashboard\"\n            },\n            {\n               \"id\":3,\n               \"name\":\"My Dashboard\"\n            },\n            {\n               \"id\":4,\n               \"name\":\"My Dashboard\"\n            },\n            {\n               \"id\":5,\n               \"name\":\"My Dashboard\"\n            },\n            {\n               \"id\":6,\n               \"name\":\"My Dashboard\"\n            },\n            {\n               \"id\":7,\n               \"name\":\"My Dashboard\"\n            },\n            {\n               \"id\":8,\n               \"name\":\"My Dashboard\"\n            },\n            {\n               \"id\":9,\n               \"name\":\"My Dashboard\"\n            },\n            {\n               \"id\":10,\n               \"name\":\"My Dashboard\"\n            },\n            {\n               \"id\":11,\n               \"name\":\"My Dashboard\"\n            }\n        ],\n    \"twitter\" : {\n        \"token\" : \"123\",\n        \"tokenSecret\" : \"123\"\n    }\n}"
  },
  {
    "path": "integration-tests/src/test/resources/log4j2-test.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration status=\"INFO\">\n    <appenders>\n        <Console name=\"Console\" target=\"SYSTEM_OUT\">\n            <PatternLayout pattern=\"%d{HH:mm:ss.SSS} %-5level - %msg%n\"/>\n        </Console>\n    </appenders>\n    <loggers>\n        <Logger name=\"io.netty\" level=\"INFO\" additivity=\"false\" />\n        <Logger name=\"com.zaxxer.hikari\" level=\"INFO\" additivity=\"false\" />\n        <root level=\"info\">\n            <appender-ref ref=\"Console\"/>\n        </root>\n    </loggers>\n</configuration>"
  },
  {
    "path": "integration-tests/src/test/resources/no_certs.properties",
    "content": "#hardware mqtt port\nhardware.mqtt.port=19440\n\nserver.ssl.cert=\nserver.ssl.key=\nserver.ssl.key.pass=\nclient.ssl.cert=\nclient.ssl.key=\n\n#application ssl and https/websockets certs may be different\n#https.cert=\n#https.key=\n#https.key.pass=\n\n#hardware ssl port\nhardware.ssl.port=10441\n\n#https port\nhttps.port=11443\n\n#http port\nhttp.port=18080\n\n#by default System.getProperty(\"java.io.tmpdir\")/blynk used\ndata.folder=\n\n#folder for logs.\n#logs.folder=./logs\n\n#log debug level. trace|debug|info|error. Defines how precise logging will be.\nlog.level=info\n\n#defines maximum allowed number of user dashboards. Needed to limit possible number of tokens.\nuser.dashboard.max.limit=10\n\n#user is limited with 100 messages per second.\nuser.message.quota.limit=100\n\n#this setting defines how often we can send mail/tweet/push or any other notification. Specified in seconds\nnotifications.frequency.user.quota.limit=60\n#maximum size of user profile in kb's\nuser.profile.max.size=16\n\n#period in millis for saving all user DB to disk.\nprofile.save.worker.period=100\n\nserver.workers.threads=2\n\n#this setting defines how big could be response for webhook GET request. Specified in kbs\nwebhooks.response.size.limit=64\n\n#specifies maximum period of time when application socket could be idle. After which\n#socket will be closed due to non activity. In seconds. Default value 600 if not provided.\napp.socket.idle.timeout=600\n#specifies maximum period of time when hardware socket could be idle. After which\n#socket will be closed due to non activity. In seconds. Default value 10 if not provided.\nhard.socket.idle.timeout=10\n\nregion=test-region\n\nserver.host=localhost\n\n#comma separated list of administrator IPs. allow access to admin UI only for those IPs.\n#you may set it for 0.0.0.0/0 to allow access for all.\n#you may use CIDR notation. For instance, 192.168.0.53/24\nallowed.administrator.ips=127.0.0.1"
  },
  {
    "path": "integration-tests/src/test/resources/profiles/u_dmitriy@blynk.cc.user",
    "content": "{\n   \"dashShareTokens\":{\n      \"125564119\":\"d81282986cb94920a82216d29d67a2b8\",\n      \"79780619\":\"f1554962745e40d3871f2e39c4dd24ff\",\n      \"698702477\":\"08b503b5224a44dbb97dd8606c648af3\"\n   },\n   \"email\":\"dmitriy@blynk.cc\",\n   \"name\" : \"Dmitriy D\",\n   \"pass\":\"DXaPEHb7PFXXxOm7BUj55+q6l5Eh9KFWCPu/Wg8OxXY=\",\n   \"lastModifiedTs\":1451305621599,\n   \"profile\":{\n      \"dashBoards\":[\n         {\n            \"id\":79780619,\n            \"name\":\"Blynk\",\n            \"timestamp\":1450970049,\n            \"devices\" : [{\"id\":0, \"token\":\"1bd322be8fb24f3691b4724332e708e9\", \"boardType\":\"ESP8266\"}],\n            \"widgets\":[\n               {\n                  \"type\":\"LCD\",\n                  \"id\":1796287982,\n                  \"x\":0,\n                  \"y\":0,\n                  \"color\":616861439,\n                  \"width\":8,\n                  \"height\":2,\n                  \"pinType\":\"ANALOG\",\n                  \"pin\":15,\n                  \"pwmMode\":false,\n                  \"rangeMappingOn\":false,\n                  \"min\":0,\n                  \"max\":0,\n                  \"frequency\":1000,\n                  \"advancedMode\":false,\n                  \"textFormatLine1\":\"/pin0/fh\",\n                  \"textFormatLine2\":\"/pin1/gh\",\n                  \"textLight\":false,\n                  \"pins\":[\n                     {\n                        \"pin\":15,\n                        \"pwmMode\":false,\n                        \"rangeMappingOn\":false,\n                        \"pinType\":\"ANALOG\",\n                        \"value\":\"ty\",\n                        \"min\":0,\n                        \"max\":255\n                     },\n                     {\n                        \"pin\":15,\n                        \"pwmMode\":false,\n                        \"rangeMappingOn\":false,\n                        \"pinType\":\"ANALOG\",\n                        \"value\":\"ty\",\n                        \"min\":0,\n                        \"max\":255\n                     }\n                  ]\n               },\n               {\n                  \"type\":\"TIMER\",\n                  \"id\":1923810248,\n                  \"x\":0,\n                  \"y\":2,\n                  \"color\":-308477697,\n                  \"width\":3,\n                  \"height\":1,\n                  \"pwmMode\":false,\n                  \"rangeMappingOn\":false,\n                  \"min\":0,\n                  \"max\":0,\n                  \"startTime\":60202,\n                  \"startValue\":\"1\",\n                  \"stopTime\":60262,\n                  \"stopValue\":\"0\"\n               },\n               {\n                  \"type\":\"TWO_AXIS_JOYSTICK\",\n                  \"id\":1923810249,\n                  \"x\":3,\n                  \"y\":2,\n                  \"color\":-308477697,\n                  \"width\":5,\n                  \"height\":4,\n                  \"pwmMode\":false,\n                  \"rangeMappingOn\":false,\n                  \"min\":0,\n                  \"max\":0,\n                  \"pins\":[\n                     {\n                        \"pin\":-1,\n                        \"pwmMode\":false,\n                        \"rangeMappingOn\":false,\n                        \"min\":0,\n                        \"max\":0\n                     },\n                     {\n                        \"pin\":-1,\n                        \"pwmMode\":false,\n                        \"rangeMappingOn\":false,\n                        \"min\":0,\n                        \"max\":0\n                     }\n                  ],\n                  \"split\":true,\n                  \"autoReturnOn\":true,\n                  \"portraitLocked\":false\n               }\n            ],\n            \"boardType\":\"Arduino UNO\",\n            \"keepScreenOn\":false,\n            \"isShared\":false,\n            \"isActive\":false\n         },\n         {\n            \"id\":125564119,\n            \"name\":\"New Project\",\n            \"devices\" : [{\"id\":0, \"token\":\"4ae3851817194e2596cf1b7103603ef8\", \"boardType\":\"ESP8266\"}],\n            \"widgets\":[\n               {\n                  \"type\":\"TWO_AXIS_JOYSTICK\",\n                  \"id\":1036300912,\n                  \"x\":0,\n                  \"y\":4,\n                  \"color\":-308477697,\n                  \"width\":5,\n                  \"height\":4,\n                  \"pin\":-1,\n                  \"pwmMode\":false,\n                  \"rangeMappingOn\":false,\n                  \"min\":0,\n                  \"max\":0,\n                  \"pins\":[\n                     {\n                        \"pin\":15,\n                        \"pinType\":\"ANALOG\",\n                        \"pwmMode\":false,\n                        \"rangeMappingOn\":false,\n                        \"min\":0,\n                        \"max\":255,\n                        \"value\" : \"1\"\n                     },\n                     {\n                        \"pin\":15,\n                        \"pinType\":\"ANALOG\",\n                        \"pwmMode\":false,\n                        \"rangeMappingOn\":false,\n                        \"min\":0,\n                        \"max\":255,\n                        \"value\" : \"2\"\n                     }\n                  ],\n                  \"split\":true,\n                  \"autoReturnOn\":true,\n                  \"portraitLocked\":false\n               },\n               {\n                  \"type\":\"BUTTON\",\n                  \"id\":1036300899,\n                  \"x\":5,\n                  \"y\":0,\n                  \"color\":616861439,\n                  \"width\":2,\n                  \"height\":2,\n                  \"pinType\":\"DIGITAL\",\n                  \"pin\":8,\n                  \"pwmMode\":false,\n                  \"rangeMappingOn\":false,\n                  \"min\":0,\n                  \"max\":0,\n                  \"value\":\"0\",\n                  \"pushMode\":true\n               },\n               {\n                  \"type\":\"LED\",\n                  \"id\":1036300908,\n                  \"x\":4,\n                  \"y\":0,\n                  \"color\":1602017535,\n                  \"width\":1,\n                  \"height\":1,\n                  \"label\":\"gggg\",\n                  \"pin\":-1,\n                  \"pwmMode\":false,\n                  \"rangeMappingOn\":false,\n                  \"min\":0,\n                  \"max\":0,\n                  \"frequency\":0\n               },\n               {\n                  \"type\":\"LED\",\n                  \"id\":1036300904,\n                  \"x\":7,\n                  \"y\":0,\n                  \"color\":1602017535,\n                  \"width\":1,\n                  \"height\":1,\n                  \"pinType\":\"VIRTUAL\",\n                  \"pin\":2,\n                  \"pwmMode\":false,\n                  \"rangeMappingOn\":false,\n                  \"min\":0,\n                  \"max\":0,\n                  \"frequency\":0\n               },\n               {\n                  \"type\":\"TIMER\",\n                  \"id\":276297114,\n                  \"x\":0,\n                  \"y\":1,\n                  \"color\":-308477697,\n                  \"width\":3,\n                  \"height\":1,\n                  \"pinType\":\"DIGITAL\",\n                  \"pin\":0,\n                  \"pwmMode\":false,\n                  \"rangeMappingOn\":false,\n                  \"min\":0,\n                  \"max\":0,\n                  \"startTime\":75600,\n                  \"startValue\":\"1\",\n                  \"stopTime\":75599,\n                  \"stopValue\":\"0\",\n                  \"value\":\"1\"\n\n               },\n               {\n                  \"type\":\"LED\",\n                  \"id\":1036300903,\n                  \"x\":4,\n                  \"y\":1,\n                  \"color\":1602017535,\n                  \"width\":1,\n                  \"height\":1,\n                  \"pinType\":\"VIRTUAL\",\n                  \"pin\":1,\n                  \"pwmMode\":false,\n                  \"rangeMappingOn\":false,\n                  \"min\":0,\n                  \"max\":0,\n                  \"frequency\":0\n               },\n               {\n                  \"type\":\"SLIDER\",\n                  \"id\":1036300911,\n                  \"x\":0,\n                  \"y\":0,\n                  \"color\":-308477697,\n                  \"width\":4,\n                  \"height\":1,\n                  \"pinType\":\"DIGITAL\",\n                  \"pin\":3,\n                  \"pwmMode\":true,\n                  \"rangeMappingOn\":false,\n                  \"min\":0,\n                  \"max\":255,\n                  \"value\":\"87\"\n               },\n               {\n                  \"type\":\"LOGGER\",\n                  \"id\":1036300905,\n                  \"x\":0,\n                  \"y\":2,\n                  \"color\":-308477697,\n                  \"width\":8,\n                  \"height\":2,\n                  \"pinType\":\"VIRTUAL\",\n                  \"pin\":5,\n                  \"pwmMode\":false,\n                  \"rangeMappingOn\":false,\n                  \"min\":0,\n                  \"max\":1023,\n                  \"frequency\":0,\n                  \"isBar\":true\n               },\n               {\n                  \"type\":\"LED\",\n                  \"id\":1036300902,\n                  \"x\":3,\n                  \"y\":1,\n                  \"color\":1602017535,\n                  \"width\":1,\n                  \"height\":1,\n                  \"pinType\":\"VIRTUAL\",\n                  \"pin\":0,\n                  \"pwmMode\":false,\n                  \"rangeMappingOn\":false,\n                  \"min\":0,\n                  \"max\":0,\n                  \"frequency\":0\n               },\n               {\n                  \"type\":\"DIGIT4_DISPLAY\",\n                  \"id\":1036300913,\n                  \"x\":5,\n                  \"y\":4,\n                  \"color\":-308477697,\n                  \"width\":2,\n                  \"height\":1,\n                  \"pinType\":\"ANALOG\",\n                  \"pin\":14,\n                  \"pwmMode\":false,\n                  \"rangeMappingOn\":false,\n                  \"min\":0,\n                  \"max\":1023,\n                  \"frequency\":1000\n               },\n               {\n                  \"type\":\"BUTTON\",\n                  \"id\":1036300914,\n                  \"x\":5,\n                  \"y\":5,\n                  \"color\":616861439,\n                  \"width\":2,\n                  \"height\":2,\n                  \"pinType\":\"DIGITAL\",\n                  \"pin\":1,\n                  \"pwmMode\":false,\n                  \"rangeMappingOn\":false,\n                  \"min\":0,\n                  \"max\":0,\n                  \"value\":\"1\",\n                  \"pushMode\":false\n               },\n               {\n                  \"type\":\"NOTIFICATION\",\n                  \"id\":1036300915,\n                  \"x\":5,\n                  \"y\":7,\n                  \"width\":2,\n                  \"height\":1,\n                  \"pin\":0,\n                  \"pwmMode\":false,\n                  \"rangeMappingOn\":false,\n                  \"min\":0,\n                  \"max\":0,\n                  \"androidTokens\":\n                     {\n                        \"98b68199-b394-4d46-81c0-6fd077000f80\":\"cu21UsfOTz0:APA91bFUWdcSSXg3w3Q4rSYxA_zeJQoBJGKs_2X0yHgRHEZ9wF32wMyPzutSwRbJk6OeoeATalVUmJbyNiXi4jjOFH8aDc4DX66pexjbQU2MkWqWdtWCCNgk4xooSyKN4ALbSVL-5ABm\"\n                     },\n                  \"notifyWhenOffline\":false,\n                  \"priority\":\"normal\"\n               },\n               {\n                   \"type\":\"EMAIL\",\n                   \"id\":1036300916,\n                   \"x\":5,\n                   \"y\":9,\n                   \"width\":2,\n                   \"height\":1\n               },\n                               {\n                                   \"type\":\"RGB\",\n                                   \"id\":13,\"x\":2,\"y\":3,\"color\":616861439,\"width\":4,\"height\":3,\"tabId\":0,\n                                   \"splitMode\":false,\n                                   \"sendOnReleaseOn\":true,\n                                   \"pins\":[\n                                           {\n                                               \"pin\":13,\n                                               \"pwmMode\":false,\n                                               \"rangeMappingOn\":false,\n                                               \"pinType\":\"VIRTUAL\",\n                                               \"value\":\"60\\u0000143\\u0000158\",\n                                               \"min\":0,\n                                               \"max\":255\n                                           },\n                                           {\n                                               \"pin\":13,\n                                               \"pwmMode\":false,\n                                               \"rangeMappingOn\":false,\n                                               \"pinType\":\"VIRTUAL\",\n                                               \"value\":\"60\\u0000143\\u0000158\",\n                                               \"min\":0,\n                                               \"max\":255\n                                           },\n                                           {\n                                               \"pin\":13,\n                                               \"pwmMode\":false,\n                                               \"rangeMappingOn\":false,\n                                               \"pinType\":\"VIRTUAL\",\n                                               \"value\":\"60\\u0000143\\u0000158\",\n                                               \"min\":0,\n                                               \"max\":255\n                                           }\n                                          ]\n                               },\n                {\n                    \"type\":\"TWO_AXIS_JOYSTICK\",\n                    \"id\":2,\"x\":2,\"y\":5,\"color\":600084223,\"width\":5,\"height\":4,\n                    \"tabId\":0,\n                    \"pins\":\n                        [\n                            {\n                                \"pin\":14,\n                                \"pwmMode\":false,\n                                \"rangeMappingOn\":false,\n                                \"pinType\":\"VIRTUAL\",\n                                \"value\":\"128\\u0000129\",\n                                \"min\":0,\n                                \"max\":255\n                            },\n                            {\n                                \"pin\":14,\n                                \"pwmMode\":false,\n                                \"rangeMappingOn\":false,\n                                \"pinType\":\"VIRTUAL\",\n                                \"value\":\"128\\u0000129\",\n                                \"min\":0,\n                                \"max\":255\n                            }\n                        ],\n                        \"split\":false,\n                        \"autoReturnOn\":true,\n                        \"portraitLocked\":false\n                }\n            ],\n            \"boardType\":\"Arduino UNO\",\n            \"keepScreenOn\":false,\n            \"isShared\":false,\n            \"isActive\":true\n         },\n         {\n            \"id\":698702475,\n            \"name\":\"A\",\n            \"timestamp\":1449678105,\n            \"devices\" : [{\"id\":0, \"token\":\"cd2f5a62151e4fa2a6ae8ea506b6e15c\", \"boardType\":\"ESP8266\"}],\n            \"widgets\":[\n               {\n                  \"type\":\"LOGGER\",\n                  \"id\":509087093,\n                  \"x\":0,\n                  \"y\":5,\n                  \"color\":-308477697,\n                  \"width\":8,\n                  \"height\":3,\n                  \"pwmMode\":false,\n                  \"rangeMappingOn\":false,\n                  \"min\":0,\n                  \"max\":0,\n                  \"pins\":[\n                     {\n                        \"pin\":-1,\n                        \"pwmMode\":false,\n                        \"rangeMappingOn\":false,\n                        \"min\":0,\n                        \"max\":0\n                     },\n                     {\n                        \"pin\":-1,\n                        \"pwmMode\":false,\n                        \"rangeMappingOn\":false,\n                        \"min\":0,\n                        \"max\":0\n                     },\n                     {\n                        \"pin\":-1,\n                        \"pwmMode\":false,\n                        \"rangeMappingOn\":false,\n                        \"min\":0,\n                        \"max\":0\n                     },\n                     {\n                        \"pin\":-1,\n                        \"pwmMode\":false,\n                        \"rangeMappingOn\":false,\n                        \"min\":0,\n                        \"max\":0\n                     }\n                  ],\n                  \"showLegends\":false\n               },\n               {\n                  \"type\":\"TIMER\",\n                  \"id\":206797172,\n                  \"x\":2,\n                  \"y\":0,\n                  \"color\":-308477697,\n                  \"width\":3,\n                  \"height\":1,\n                  \"pwmMode\":false,\n                  \"rangeMappingOn\":false,\n                  \"min\":0,\n                  \"max\":0,\n                  \"startTime\":68710,\n                  \"startValue\":\"1\",\n                  \"stopTime\":68770,\n                  \"stopValue\":\"0\"\n               },\n               {\n                  \"type\":\"BRIDGE\",\n                  \"id\":251138143,\n                  \"x\":0,\n                  \"y\":2,\n                  \"color\":-308477697,\n                  \"width\":2,\n                  \"height\":1,\n                  \"pwmMode\":false,\n                  \"rangeMappingOn\":false,\n                  \"min\":0,\n                  \"max\":0\n               },\n               {\n                  \"type\":\"LED\",\n                  \"id\":1534122532,\n                  \"x\":2,\n                  \"y\":1,\n                  \"color\":-308477697,\n                  \"width\":1,\n                  \"height\":1,\n                  \"pwmMode\":false,\n                  \"rangeMappingOn\":false,\n                  \"min\":0,\n                  \"max\":0,\n                  \"frequency\":0\n               },\n               {\n                  \"type\":\"BUTTON\",\n                  \"id\":592019947,\n                  \"x\":0,\n                  \"y\":3,\n                  \"color\":-750560001,\n                  \"width\":2,\n                  \"height\":2,\n                  \"pwmMode\":false,\n                  \"rangeMappingOn\":false,\n                  \"min\":0,\n                  \"max\":0,\n                  \"pushMode\":true\n               },\n               {\n                  \"type\":\"LED\",\n                  \"id\":651785432,\n                  \"x\":7,\n                  \"y\":0,\n                  \"color\":-308477697,\n                  \"width\":1,\n                  \"height\":1,\n                  \"pwmMode\":false,\n                  \"rangeMappingOn\":false,\n                  \"min\":0,\n                  \"max\":0,\n                  \"frequency\":0\n               },\n               {\n                  \"type\":\"TWITTER\",\n                  \"id\":1872409949,\n                  \"x\":3,\n                  \"y\":1,\n                  \"color\":-308477697,\n                  \"width\":2,\n                  \"height\":1,\n                  \"pwmMode\":false,\n                  \"rangeMappingOn\":false,\n                  \"min\":0,\n                  \"max\":0\n               },\n               {\n                  \"type\":\"GAUGE\",\n                  \"id\":1553581316,\n                  \"x\":2,\n                  \"y\":2,\n                  \"color\":1602017535,\n                  \"width\":4,\n                  \"height\":3,\n                  \"pwmMode\":false,\n                  \"rangeMappingOn\":false,\n                  \"min\":0,\n                  \"max\":1023,\n                  \"frequency\":1000\n               }\n            ],\n            \"boardType\":\"Arduino UNO\",\n            \"keepScreenOn\":false,\n            \"isShared\":false,\n            \"isActive\":false\n         },\n         {\n            \"id\":698702476,\n            \"name\":\"infoPoint\",\n            \"timestamp\":1449678105,\n            \"devices\" : [{\"id\":0, \"token\":\"f77d5ece132f4ac880d7c028d1200792\", \"boardType\":\"ESP8266\"}],\n            \"widgets\":[\n               {\n                  \"type\":\"BUTTON\",\n                  \"id\":3,\n                  \"x\":3,\n                  \"y\":0,\n                  \"color\":1602017535,\n                  \"width\":2,\n                  \"height\":2,\n                  \"pinType\":\"DIGITAL\",\n                  \"pin\":9,\n                  \"pwmMode\":false,\n                  \"rangeMappingOn\":false,\n                  \"min\":0,\n                  \"max\":0,\n                  \"pushMode\":false\n               },\n               {\n                  \"type\":\"LED\",\n                  \"id\":2,\n                  \"x\":2,\n                  \"y\":0,\n                  \"color\":1602017535,\n                  \"width\":1,\n                  \"height\":1,\n                  \"pwmMode\":false,\n                  \"rangeMappingOn\":false,\n                  \"min\":0,\n                  \"max\":0,\n                  \"frequency\":0\n               },\n               {\n                  \"type\":\"DIGIT4_DISPLAY\",\n                  \"id\":4,\n                  \"x\":0,\n                  \"y\":0,\n                  \"color\":-308477697,\n                  \"width\":2,\n                  \"height\":1,\n                  \"pinType\":\"DIGITAL\",\n                  \"pin\":10,\n                  \"pwmMode\":false,\n                  \"rangeMappingOn\":false,\n                  \"min\":0,\n                  \"max\":1023,\n                  \"frequency\":250\n               }\n            ],\n            \"boardType\":\"Arduino Yun\",\n            \"keepScreenOn\":false,\n            \"isShared\":false,\n            \"isActive\":false\n         },\n         {\n            \"id\":698702477,\n            \"timestamp\":1450882233,\n            \"devices\" : [{\"id\":0, \"token\":\"7b0a3a61322e41a5b50589cf52d775d1\", \"boardType\":\"ESP8266\"}],\n            \"widgets\":[\n               {\n                  \"type\":\"SLIDER\",\n                  \"id\":1,\n                  \"x\":0,\n                  \"y\":0,\n                  \"color\":-308477697,\n                  \"width\":8,\n                  \"height\":1,\n                  \"pinType\":\"DIGITAL\",\n                  \"pin\":3,\n                  \"pwmMode\":false,\n                  \"rangeMappingOn\":false,\n                  \"min\":0,\n                  \"max\":255\n               },\n               {\n                  \"type\":\"TERMINAL\",\n                  \"id\":6,\n                  \"x\":0,\n                  \"y\":5,\n                  \"color\":-1,\n                  \"pinType\":\"VIRTUAL\",\n                  \"pin\":17,\n                  \"width\":8,\n                  \"height\":3,\n                  \"pwmMode\":false,\n                  \"rangeMappingOn\":false,\n                  \"min\":0,\n                  \"max\":0,\n                  \"autoScrollOn\":true,\n                  \"terminalInputOn\":true\n               },\n               {\n                  \"type\":\"BUTTON\",\n                  \"id\":3,\n                  \"x\":4,\n                  \"y\":1,\n                  \"color\":616861439,\n                  \"width\":2,\n                  \"height\":2,\n                  \"pinType\":\"DIGITAL\",\n                  \"pin\":0,\n                  \"pwmMode\":false,\n                  \"rangeMappingOn\":false,\n                  \"min\":0,\n                  \"max\":0,\n                  \"pushMode\":false\n               },\n               {\n                  \"type\":\"SLIDER\",\n                  \"id\":7,\n                  \"x\":0,\n                  \"y\":2,\n                  \"color\":-308477697,\n                  \"width\":4,\n                  \"height\":1,\n                  \"pwmMode\":false,\n                  \"rangeMappingOn\":false,\n                  \"min\":0,\n                  \"max\":255\n               },\n               {\n                  \"type\":\"SLIDER\",\n                  \"id\":2,\n                  \"x\":0,\n                  \"y\":1,\n                  \"color\":-308477697,\n                  \"width\":4,\n                  \"height\":1,\n                  \"pwmMode\":false,\n                  \"rangeMappingOn\":false,\n                  \"min\":0,\n                  \"max\":255\n               },\n               {\n                  \"type\":\"LED\",\n                  \"id\":4,\n                  \"x\":6,\n                  \"y\":1,\n                  \"color\":1602017535,\n                  \"width\":1,\n                  \"height\":1,\n                  \"pwmMode\":false,\n                  \"rangeMappingOn\":false,\n                  \"min\":0,\n                  \"max\":0,\n                  \"frequency\":0\n               },\n               {\n                  \"type\":\"LABELED_VALUE_DISPLAY\",\n                  \"id\":5,\n                  \"x\":0,\n                  \"y\":3,\n                  \"color\":-308477697,\n                  \"width\":8,\n                  \"height\":2,\n                  \"pwmMode\":false,\n                  \"rangeMappingOn\":false,\n                  \"min\":0,\n                  \"max\":1023,\n                  \"frequency\":1000,\n                  \"isBar\":true\n               }\n            ],\n            \"boardType\":\"Arduino UNO\",\n            \"keepScreenOn\":false,\n            \"isShared\":false,\n            \"isActive\":false\n         }\n      ]\n   }\n}"
  },
  {
    "path": "integration-tests/src/test/resources/server.properties",
    "content": "#hardware mqtt port\nhardware.mqtt.port=18440\n\nserver.ssl.cert=/test-certs/mutual/server.crt\nserver.ssl.key=/test-certs/mutual/server.pem\nserver.ssl.key.pass=blynkawesome\n\n#application ssl and https/websockets certs may be different\n#https.cert=\n#https.key=\n#https.key.pass=\n\n#hardware ssl port\nhardware.ssl.port=9441\n\n#https port\nhttps.port=10443\n\n#http port\nhttp.port=18080\n\n#by default System.getProperty(\"java.io.tmpdir\")/blynk used\ndata.folder=\n\n#folder for logs.\n#logs.folder=./logs\n\n#log debug level. trace|debug|info|error. Defines how precise logging will be.\nlog.level=info\n\n#defines maximum allowed number of user dashboards. Needed to limit possible number of tokens.\nuser.dashboard.max.limit=10\n\n#user is limited with 100 messages per second.\nuser.message.quota.limit=100\n\n#this setting defines how often we can send mail/tweet/push or any other notification. Specified in seconds\nnotifications.frequency.user.quota.limit=60\n#maximum size of user profile in kb's\nuser.profile.max.size=16\n\n#period in millis for saving all user DB to disk.\nprofile.save.worker.period=100\n\nserver.workers.threads=2\n\nhourly.registrations.limit=100\n\n#this setting defines how big could be response for webhook GET request. Specified in kbs\nwebhooks.response.size.limit=64\n\n#specifies maximum period of time when application socket could be idle. After which\n#socket will be closed due to non activity. In seconds. Default value 600 if not provided.\napp.socket.idle.timeout=300\n#specifies maximum period of time when hardware socket could be idle. After which\n#socket will be closed due to non activity. In seconds. Default value 10 if not provided.\nhard.socket.idle.timeout=10\n\n#when enabled server will also store hardware and app IP\nallow.store.ip=false\n\nenable.db=true\n\n#mostly required for local servers setup in case user want to log raw data in CSV format\n#from his hardware\nenable.raw.db.data.store=true\n\nregion=test-region\n\nserver.host=127.0.0.1\nrestore.host=blynk-cloud.com\n\nproduct.name=Blynk\nvendor.email=vendor@blynk.cc\n\n#comma separated list of administrator IPs. allow access to admin UI only for those IPs.\n#you may set it for 0.0.0.0/0 to allow access for all.\n#you may use CIDR notation. For instance, 192.168.0.53/24\nallowed.administrator.ips=127.0.0.1"
  },
  {
    "path": "integration-tests/src/test/resources/server2.properties",
    "content": "#hardware mqtt port\nhardware.mqtt.port=19440\n\nserver.ssl.cert=/test-certs/mutual/server.crt\nserver.ssl.key=/test-certs/mutual/server.pem\nserver.ssl.key.pass=blynkawesome\n\n#hardware ssl port\nhardware.ssl.port=10441\n\n#https port\nhttps.port=18443\n\n#http port\nhttp.port=19080\n\n#by default System.getProperty(\"java.io.tmpdir\")/blynk used\ndata.folder=\n\n#folder for logs.\n#logs.folder=./logs\n\n#log debug level. trace|debug|info|error. Defines how precise logging will be.\nlog.level=info\n\n#defines maximum allowed number of user dashboards. Needed to limit possible number of tokens.\nuser.dashboard.max.limit=10\n\n#user is limited with 100 messages per second.\nuser.message.quota.limit=100\n\n#this setting defines how often we can send mail/tweet/push or any other notification. Specified in seconds\nnotifications.frequency.user.quota.limit=60\n#maximum size of user profile in kb's\nuser.profile.max.size=16\n\n#period in millis for saving all user DB to disk.\nprofile.save.worker.period=100\n\nserver.workers.threads=2\n\n#this setting defines how big could be response for webhook GET request. Specified in kbs\nwebhooks.response.size.limit=64\n\n#specifies maximum period of time when application socket could be idle. After which\n#socket will be closed due to non activity. In seconds. Default value 600 if not provided.\napp.socket.idle.timeout=600\n#specifies maximum period of time when hardware socket could be idle. After which\n#socket will be closed due to non activity. In seconds. Default value 10 if not provided.\nhard.socket.idle.timeout=10\n\n#if this property is true redirect_command will use 80 port and will ignore http.port\nforce.port.80.for.redirect=true\n\nregion=test-region\n\nserver.host=localhost2\nproduct.name=Blynk\n\nenable.db=true\n\n#comma separated list of administrator IPs. allow access to admin UI only for those IPs.\n#you may set it for 0.0.0.0/0 to allow access for all.\n#you may use CIDR notation. For instance, 192.168.0.53/24\nallowed.administrator.ips=127.0.0.1"
  },
  {
    "path": "integration-tests/src/test/resources/test-certs/mutual/app.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDBjCCAe4CCQCaLWxpmJwnpjANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJB\nVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0\ncyBQdHkgTHRkMB4XDTE1MDUwNDA5NTcwNVoXDTIwMDUwMjA5NTcwNVowRTELMAkG\nA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNVBAoTGEludGVybmV0\nIFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nAKcyZ4gObgqtmLVjMtynyxjhmnhb4j9WiKd1ej3DyDslc7NTHitoIW7L90tXtyTc\nM/c4jecY1Rt01WTuGMcqSHEkORlrFZHJq9c3bZArd+WKRq/n0tu6aX2XVL5HJxUd\ni7bSyPzChdZ0npGk/B6d9uSLdm/g192ui5ur81klut2MoHm6C19Qt0u9SjNQBatV\nD64k/oWRyweJef/ZzemwJOVnIVMXjbatPSywAwGdzLZvJhGKnz7/NzhBapPMyZJy\nOJLKfB7Tv0el5AIU8ScTIij+OEP4Aht5J98i/9jcsj5Z3BRvOj3q5fqrOBKVKeJC\nVGi/UOKYSNGCrvhioyTnXo0CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAZUKfnble\nu0dik7I6JLfqHMnVR4dgFRl14kDQzleaYroY/i0uIJ1ckvOJlwbBYVrEQkJKoKiS\nCOqDc7J5I9sAtQQQ61jPt7aQSScTctzbm4c+YB0UONXsEYPy2g/dLOQcW0NLpuz7\ncssnImBQAdZMTSrRjhbLiwYox4AhPJPolMVpOiOKmzb6mUsQKCRNzkggEPfazee8\nlKI36j9t6pZRNh8eSXDl9L2DWdJYTOyndQ+pJ3bpajNbIopX3iuamn8ex3fzUiEX\nlKsD+5e2Zjc4hvK30Tjj/URQ0/kLDsF/vsNckX5CNC2XPwfnxZvdwor4T7B0mmB9\ncm1c+CJsQqQwhw==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "integration-tests/src/test/resources/test-certs/mutual/app.pem",
    "content": "-----BEGIN ENCRYPTED PRIVATE KEY-----\nMIIE6TAbBgkqhkiG9w0BBQMwDgQIkBreBoHBYE4CAggABIIEyNYrlojzCRgWAWtN\nV04Ue/rdkF3AWCpljbVWBT2uniFwC+EHES9S6Ad3H6/vz5FfMaR+HseqIHcenKLz\nkS6kEIxyY38tg0new+v8RhCnzdIIesxcO3Ii9zpjX+Hm7yptQstd1BSK+dhRUr9y\nB8y2NXJkIlohwn1DNxl9xRA0MX07wopAaKIw+ajpFtm1dJcMKmhnc382TwKmqjRn\nNEFr2/K2k3A27xba+YN16fkRdsW0jtgsofdZXyM4FMpi1SkJO3ffyVyBJ3o3sFP3\nsZi0LpTMRM2IeBT/O1MzukWYgTfxEs5LqLkAS/EI23whAMnoqm/svFAs+T2m+Mig\nQRV2DiNENNkhlNxvU5nNCQk2qWuQSnbpk+L+XX1UP3iJysVFurX/OlR+QJ80NVa7\nZIfrslV4HAA1onw9JDIpm27/jevLHfiEPpBO2kf6BgJIoqnKsIDzz9aFahOIDQN0\nVs2iScmbFnCOq20hlG7icpwqvK6mku5kDWwhIbHKZnz+kx3vBbLik3bsSp3eB5By\nemiWS5oVYP9x/az1JAccdYuZEv6RWAwmJflqaTJrOAs6RlDRTJISeo3Mb4lwdryw\n3lEJfOrYFRDAcj5zQ4MYgQcVCzMN+JVqsU823Bf4vveVXX0lNjTqN2H5bjSnru1q\nslxq2NplbfwndAVJbiXlaCu5KkLDPAUi4JRl+ll9BqzGcUxwQFCwudkajTcc6HAb\nGYJxtB9Cnx6Ij039PWYNAZD8U8SqOIS/HRZPiThhru7VVl6Zi37Iq9yowM816Grz\ncrzkykfCV6zVu9hjqYGsYtr7D0j30Jsr5bhYsV73wRAbrb2qJn7NtcjH4rULgvle\nLIHhw1moIHuhp+zLT4LLhpKJFf4K1Cd9Vle7B/ccMXlTWCA6OcmuDIATyPUbiRZ+\nUh2SyYVcZZg2uDGKY6nOniNKQhI/oYsVxLcLKp/Mm+bchu6WaEHSfRJXKLEQmuq4\n3vgvGpGKMuF9snirzjcf5gbS6bMVlwqHVXDE0nt14YtOCuDUGZnMx1W+sQBW99z9\n1HY3S0GNi81EiwfKIf1kyx9YnjOxQhaQYznRX0EhHIU0foeoe5Gf8lqRGR66Y2y8\nrWm91jANMSe7zos2tUV46FYYO1NIoWxiXVgZ11Q1rjqhbqYa2izRLv2/yFPJT0BN\nHGSJ2GUSkL/6ngdvq3sZsWkO/7gXw6NwU31q9Wjb8trH6hG4gVWkpNfA1gisSnV+\n/oBQAFf85o+IL7K2W4AIPAMqhM1/ZnZSpYUnzsRB5c9ckBRrJahrvfWUCmwX4ZQH\nOBXTej3dfzDGeChgy95/qLAppwWbMUY8qEmLV6wJX2TGj4qhjU9ReUaaZGiRR76Z\nv/3HrK4wNkcYaDInHR/6qBduPqiUiQZPQVUfcZ8+Gu/8JSg60MTTTjl2dpHozE9u\nOVp2XGjH/W/Ry+1E8YpFDMOZIluSG7vNp/R1hlOtdpp39HFvUO+gj9ZCw8QR9Y3V\nbqzblMcXILzT2qYThdwnIBiVLKLUf9Hwlq+aHfUPfi6uC1rlT4re7gNOaBVZx/N2\n4RRza24En9r1ochxuO/wrytGaS+s4jB0h2m7C6HQSO++vdZvz8Sjc0cZ+N7vbyWB\nFinY0EgSC9xZ8ZXVSQ==\n-----END ENCRYPTED PRIVATE KEY-----\n"
  },
  {
    "path": "integration-tests/src/test/resources/test-certs/mutual/server.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDBjCCAe4CCQDeI3qPsbJ7lDANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJB\nVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0\ncyBQdHkgTHRkMB4XDTE1MDUwNDA5NTYwM1oXDTIwMDUwMjA5NTYwM1owRTELMAkG\nA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNVBAoTGEludGVybmV0\nIFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nAM3hfusdl1jcUwNEoKcYctWQu/dkcmreWS3PW9903X7hs2PC+jCFtoRQdQPdqYUt\n5T3IGBIJp72zFv+GfKt/S/SzsznqC508Ug3GlBPTRKX0TsNexzTk9vT9ghFeL8I+\neN5nRWCym/9ejSF0qmBdCBv0KQ70ilB24HYohFDrlMrTGCYbchsOmLrc8PW83rCD\nG+tOrEkKI6b2JBRypc2wPd5smlG5Z5mNeZoeW0KRrjlhi1jju3J7YqtpW4vPbDxb\nUg14uwQ42wfGsbUWKcqA939ilPBWuAQW8QK3CAifg0ALIhjUlzlaubcfIZZClhKX\nz3UzBw8+JT6+ImFCFChXm3kCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAFbHDX17l\nRypHdZVfJb7R5s05J+KWSo2Oh/7Ov2GOuJC88u8/mkBjKQvQH+p2vN9A7X58w2LS\nlwpAklpYMxJzyogjPsgfkwN9XwmwKvZEadug8YFSNpLjfy09Cmd8ybDbOHBJBsds\n4KZakUVhAOQZgdVi0n8RbvgoxWQ7f7/nsBSbUJXoPsoyjFgAhZYjJWSCbHc1lK9W\no2CRbqZ6exLcT8HO3HAFDVLcsZah5xMtiACrnKcdwvx3mvGn4Sk2+HQenJ28XQID\nI06KWjAj9HZ9A7bpb8OY8zgWU0H+BfZYQF9MTBZREkuMCiJs2lxHhzU8D0GBusE5\nOxPWw4k0lcFaCw==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "integration-tests/src/test/resources/test-certs/mutual/server.pem",
    "content": "-----BEGIN ENCRYPTED PRIVATE KEY-----\nMIIE6TAbBgkqhkiG9w0BBQMwDgQIqE0NvK+pXK8CAggABIIEyMR3e2tVE8QaSvTy\nX8x0HHYrIsxR2CCZ4cCuIoK96CffMIseGZFf3QGyYvwYCC6NkYs7vJND+2438Ypu\n6MrYLReLj/l2F2iZ5iWFstEr8StWzyfyiNZGN+VfqsGazXMXk++0RX/GjZNpbNh9\nbq1DqJBtGO/MhcJdgABFh11n06QT27UKJINYnUgl7oCgjmbwNlsne3FEl4s4BUdc\nkKaWxd8MR1CDdV/b36YA9dltEQjPS8MvY3qaQbFdhDyp/RlkORbpS4QUli34k0y6\njDY0CgI6XQ9ZrTXZhgyzEl3XB7AwTB2kCChL5IHu9Z3L4ACb5vnxqbDxtTFOIp1j\n3a7VVsi5oPaLbOh0tlcyle5wUTeubS3XjGvW/BYGczTX41IFkemNl5CZJsbovr0D\n7Yb2lYfhaEHiLUuRY/DwtEhcX+MwUcygFbY0jXKvl44hy0IEvAPoRTtu4nFyY7IK\ni/elDtINzAsfWfNYe0RIC9jgxvQshIi4dsxE0/M+vpBMcnN0TqJZWkkZCkxcODao\ndUNfSlMRvZNtMEco1LrVh4ibwhAPgrThQweRac4RJxFLl15fwpXFUBHaa9/byl+e\nJfSOki/xwLktc7gC/q8J/VIjKyRHMJIgMkUtAp17sjBZip1s/OuKnjCHZegQ+1WD\n/uqCLpsLEOi/ijzv4DXW1APwYRPDWc/9mYfJPFT2pj1JSXDMsCX83etaEA3vHS4Y\npvpszKYOD8+32zx+9k/4X/DBZHtsOHdiqvQXmHXaABLlXgUQuhzn5cvBB4fEqo9r\n4vxr32C/+z25rs90MUzqChpY+G2v8b5+jMOMTh67AlJ8NJP3NZjO5Ok2iCzwM81C\n6YteQBDoUlLEtvUVZtCqq0X2nLlTLvaAEnhiWURcRmIS8J3zOEAUmUS7SriYHuZp\nHTdaHVFIqFCHDYLufyWN4s11s4wVqefzr3hQAmVC95Cful26ZbZzA8njishqNv7L\nX+1n+ltzT7U9X1BTM2UtEdR4jQal13c3MGglMoaqBWyJaGaBfsqrSu51/Io3kSr6\nKkfq6YBP8VgaeSUwQI93YSRVmrGSyUn8Iy2/7nrsuEwYQ1el9cliHUx5hAcyGzKA\n50hfi2SLQnfikGA8zEsEJS6WgZOm9U8xaUWOI+adle+4ocHas8bxptJH+fw4VtWo\nVXAVEEt3Wze27c6q9GKBEVdWNk+5VxGBnQ/NaraXHwbijCNp3EHseFLIpcG470Qy\nwS9u6nAwktxQWGv7dz3S3Czs6PIcqd8dXuz9SpSkcCobW25LlN4e0PvKBj6kxWxn\n0hHVpg/SqiiJ9FkD1rwuIyYp+tco4UMZRcY9uqLyerxUryQ7xpewiQqJr+F9y/7z\nTQQ7gzALkJuNXS0OY3x7VPbiSjgFlarmgGoaKIgb1UDOvFkdDa3QGQsdAsFTZTdL\nLAhlKFSF/fG/DOe/MCYm/ihSrTUbjp+Q6/6lLT7/dHy1lObvtgA1zBjaFzaOY87j\nwr0gCwwhpQI1xAIr2xzndTqNwMyDL6pjK7OMypPoU6/FOZS55zdip82Rc3xM+1Uv\nmlohf/0k1adj3vvLCcG1Zab+hj/Ne53C1aZDap2HLut+xH3qEaNnfjJdKJIRVzYt\nBzRRbvOveJGNZjNJeQ==\n-----END ENCRYPTED PRIVATE KEY-----\n"
  },
  {
    "path": "integration-tests/src/test/resources/twitter4j.properties",
    "content": "## For local servers you should fill this fields by yourself\n## Go here https://apps.twitter.com/\n## Create twitter app.\n## paste here consumerKey and consumerSecret\n## Access token will be populated from mobile app.\n## Also you could read instruction here http://twitter4j.org/en/code-examples.html\ndebug=true\noauth.consumerKey=\noauth.consumerSecret="
  },
  {
    "path": "license.txt",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    {one line to give the program's name and a brief idea of what it does.}\n    Copyright (C) {year}  {name of author}\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <http://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    {project}  Copyright (C) {year}  {fullname}\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<http://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<http://www.gnu.org/philosophy/why-not-lgpl.html>.\n\n"
  },
  {
    "path": "new_server_install.md",
    "content": "        sudo apt-get update\n        sudo apt-get install fail2ban\n        sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local\n\n        sudo timedatectl set-ntp no\n        sudo apt-get install ntp\n\n        sudo service ntp stop\n        sudo ntpd -gq\n        sudo service ntp start\n\n        sudo apt-get upgrade\n        sudo apt-get dist-upgrade\n        sudo apt-get autoremove --purge\n\n        sudo add-apt-repository ppa:linuxuprising/java\n        sudo apt-get update\n        sudo apt-get install oracle-java10-installer\n\n        sudo add-apt-repository ppa:openjdk-r/ppa \\\n        && sudo apt-get update -q \\\n        && sudo apt install -y openjdk-11-jdk\n        \n        wget \"https://github.com/blynkkk/blynk-server/releases/download/v0.41.15/server-0.41.15.jar\"\n        \n\nserver.properties\n\ndata.folder=./data\nlogs.folder=./logs\nlog.level=info\nenable.db=true\nforce.port.80.for.csv=true\ncontact.email=xxx@blynk.cc\nuser.devices.limit=100000\nuser.message.quota.limit=10000\nweb.request.max.size=5242880\n#maximum number of days minute records for reporting will be stored\nstore.minute.record.days=30\nregion=test\nadmin.rootPath=/test\nproduct.name=test\nserver.host=test.blynk.cc\nrestore.host=test.blynk.cc\nadmin.email=test@blynk.cc\nadmin.pass=\nvendor.email=\n\n        \ndb.properties\n\njdbc.url=jdbc:postgresql://xxx:5432/blynk?tcpKeepAlive=true&socketTimeout=150\nuser=test\npassword=test\nconnection.timeout.millis=30000\nclean.reporting=false\n\ngcm.properties\n\nmail.properties\n\nIP Tables\n\n        sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080\n        sudo iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 9443\n        sudo iptables -t nat -A PREROUTING -p tcp --dport 8441 -j REDIRECT --to-port 9443\n        \n        sudo apt-get install iptables-persistent\n        \n        sudo iptables -t nat -A PREROUTING -p tcp --dport 8442 -j REDIRECT --to-port 8080\n        iptables-save > /etc/iptables/rules.v4"
  },
  {
    "path": "pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <name>Blynk Server</name>\n    <url>https://www.blynk.cc/</url>\n    <description>\n        Blynk - platform with iOs and Android apps to control Arduino,\n        Raspberry Pi and similar micro-controllers boards over Internet.\n    </description>\n    <organization>\n        <name>Blynk Inc.</name>\n        <url>https://www.blynk.cc/</url>\n    </organization>\n    <issueManagement>\n        <system>github</system>\n        <url>https://github.com/blynkkk/blynk-server/issues</url>\n    </issueManagement>\n\n    <groupId>cc.blynk</groupId>\n    <artifactId>blynk</artifactId>\n    <version>0.41.17-SNAPSHOT</version>\n    <packaging>pom</packaging>\n\n    <scm>\n        <url>https://github.com/blynkkk/blynk-server.git</url>\n        <connection>scm:git:ssh://git@github.com/blynkkk/blynk-server.git</connection>\n        <developerConnection>scm:git:ssh://git@github.com/blynkkk/blynk-server.git</developerConnection>\n        <tag>v0.41.2-SNAPSHOT</tag>\n    </scm>\n\n    <developers>\n        <developer>\n            <id>Dmitriy Dumanskiy</id>\n            <email>dmitriy@blynk.cc</email>\n        </developer>\n    </developers>\n\n    <modules>\n        <module>server</module>\n        <module>client</module>\n        <module>integration-tests</module>\n    </modules>\n\n    <!-- for QR gen tool -->\n    <distributionManagement>\n        <repository>\n            <id>jitpack.io</id>\n            <url>https://jitpack.io</url>\n            <layout>default</layout>\n        </repository>\n    </distributionManagement>\n\n    <repositories>\n        <repository>\n            <id>jitpack.io</id>\n            <url>https://jitpack.io</url>\n            <layout>default</layout>\n        </repository>\n    </repositories>\n\n    <profiles>\n        <profile>\n            <id>win</id>\n            <activation>\n                <os>\n                    <family>Windows</family>\n                </os>\n            </activation>\n            <properties>\n                <script.extension>.bat</script.extension>\n                <!--Just to enable build under windows. EPOLL CODE SHOULD NOT BE USED UNDER WINDOWS-->\n                <epoll.os>linux-x86_64</epoll.os>\n            </properties>\n        </profile>\n\n        <profile>\n            <id>unix</id>\n            <activation>\n                <os>\n                    <family>unix</family>\n                </os>\n            </activation>\n            <properties>\n                <script.extension>.sh</script.extension>\n                <epoll.os>linux-x86_64</epoll.os>\n            </properties>\n        </profile>\n\n    </profiles>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-compiler-plugin</artifactId>\n                <version>${maven-compiler-plugin.version}</version>\n                <configuration>\n                    <source>11</source>\n                    <target>11</target>\n                    <release>11</release>\n                </configuration>\n            </plugin>\n\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-release-plugin</artifactId>\n                <version>${maven-release-plugin.version}</version>\n                <configuration>\n                    <autoVersionSubmodules>true</autoVersionSubmodules>\n                    <tagNameFormat>v@{project.version}</tagNameFormat>\n                </configuration>\n            </plugin>\n\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-checkstyle-plugin</artifactId>\n                <version>${maven-checkstyle-plugin.version}</version>\n                <configuration>\n                    <configLocation>checkstyle.xml</configLocation>\n                    <encoding>UTF-8</encoding>\n                    <consoleOutput>true</consoleOutput>\n                    <failsOnError>true</failsOnError>\n                    <linkXRef>false</linkXRef>\n                    <excludes>**/module-info.java</excludes>\n                </configuration>\n                <executions>\n                    <execution>\n                        <id>validate</id>\n                        <phase>validate</phase>\n                        <goals>\n                            <goal>check</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n\n        </plugins>\n\n        <resources>\n            <resource>\n                <directory>src/main/resources</directory>\n                <excludes>\n                    <exclude>*.sql</exclude>\n                    <exclude>*.temp</exclude>\n                </excludes>\n            </resource>\n        </resources>\n    </build>\n\n    <properties>\n        <!-- maven plugins -->\n        <maven-release-plugin.version>2.5.3</maven-release-plugin.version>\n        <maven-compiler-plugin.version>3.8.0</maven-compiler-plugin.version>\n        <maven-shade-plugin.version>3.2.1</maven-shade-plugin.version>\n        <maven-surefire-plugin.version>2.22.1</maven-surefire-plugin.version>\n        <maven-checkstyle-plugin.version>3.0.0</maven-checkstyle-plugin.version>\n\n        <!-- dependencies -->\n        <netty.version>4.1.63.Final</netty.version>\n        <netty.boring.ssl.version>2.0.38.Final</netty.boring.ssl.version>\n        <log4j2.version>2.14.1</log4j2.version>\n        <jackson-databind.version>2.12.2</jackson-databind.version>\n        <disruptor.version>3.4.2</disruptor.version>\n        <async-http-client.version>2.12.3</async-http-client.version>\n        <postgresql.version>42.2.16</postgresql.version>\n        <HikariCP.version>3.4.1</HikariCP.version>\n        <qrgen.version>2.2.0</qrgen.version>\n        <bcpg-jdk15on.version>1.66</bcpg-jdk15on.version>\n        <acme4j-client.version>2.11</acme4j-client.version>\n        <javax.mail.version>1.6.2</javax.mail.version>\n        <javax.activation.version>1.2.0</javax.activation.version>\n\n        <!-- test dependencies -->\n        <httpclient.version>4.5.2</httpclient.version>\n        <commons-lang3.version>3.7</commons-lang3.version>\n        <junit.version>4.12</junit.version>\n        <mockito-core.version>2.18.3</mockito-core.version>\n        <commons-io.version>2.5</commons-io.version>\n        <jmh-core.version>1.19</jmh-core.version>\n\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n    </properties>\n\n    <dependencies>\n\n        <!-- For Let's Encrypt certificates generation -->\n        <dependency>\n            <groupId>org.bouncycastle</groupId>\n            <artifactId>bcpg-jdk15on</artifactId>\n            <version>${bcpg-jdk15on.version}</version>\n        </dependency>\n\n        <!-- Log4j2 dependencies -->\n        <dependency>\n            <groupId>org.apache.logging.log4j</groupId>\n            <artifactId>log4j-api</artifactId>\n            <version>${log4j2.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.logging.log4j</groupId>\n            <artifactId>log4j-core</artifactId>\n            <version>${log4j2.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.logging.log4j</groupId>\n            <artifactId>log4j-slf4j-impl</artifactId>\n            <version>${log4j2.version}</version>\n        </dependency>\n\n        <dependency>\n            <groupId>junit</groupId>\n            <artifactId>junit</artifactId>\n            <version>${junit.version}</version>\n            <scope>test</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.mockito</groupId>\n            <artifactId>mockito-core</artifactId>\n            <version>${mockito-core.version}</version>\n            <scope>test</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>commons-io</groupId>\n            <artifactId>commons-io</artifactId>\n            <version>${commons-io.version}</version>\n            <scope>test</scope>\n        </dependency>\n\n    </dependencies>\n\n\n</project>"
  },
  {
    "path": "scripts/copy_to_qa.sh",
    "content": "#!/bin/sh\n\nscp ../server/launcher/target/server-0.41.12-SNAPSHOT.jar root@warmboard.blynk.cc:/root"
  },
  {
    "path": "scripts/db",
    "content": "sudo -u postgres /usr/lib/postgresql/9.5/bin/pg_ctl -D /etc/postgresql/9.5/main start"
  },
  {
    "path": "scripts/help_tools.sh",
    "content": "#!/usr/bin/env bash\n\nsudo apt-get update\nsudo apt-get install fail2ban\nsudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local\n\nsudo apt-get install sendmail\n\napt-get install ipset\nipset create blacklist hash:ip hashsize 4096\niptables -I INPUT -m set --match-set blacklist src -j DROP\niptables -I FORWARD -m set --match-set blacklist src -j DROP"
  },
  {
    "path": "scripts/native_openssl.sh",
    "content": "#!/usr/bin/env bash\nwget http://apache.ip-connect.vn.ua/maven/maven-3/3.3.9/binaries/apache-maven-3.3.9-bin.tar.gz\nsudo tar -xzvf apache-maven-3.3.9-bin.tar.gz\nexport M2_HOME=/root/apache-maven-3.3.9\nexport M2=$M2_HOME/bin\nexport PATH=$M2:$PATH\n\nsudo apt-get install git\nsudo apt-get install autoconf automake libtool make tar libapr1-dev libssl-dev\ngit clone https://github.com/netty/netty-tcnative.git\ncd netty-tcnative\ngit checkout netty-tcnative-parent-1.1.33.Fork26\n~/apache-maven-3.3.9/bin/mvn clean install"
  },
  {
    "path": "scripts/ulimit",
    "content": "sudo nano /etc/security/limits.conf\nroot soft     nofile         100000\nroot hard     nofile         100000\n\nsudo nano /etc/pam.d/common-session\nsession required        pam_limits.so"
  },
  {
    "path": "scripts/userfull_commands",
    "content": "View number of open connections : netstat -an | awk '/^tcp/ {print $NF}' | sort | uniq -c | sort -rn\nNumber of files : find . -maxdepth 1 -type f | wc -l\n\nDelete files older than 90 days : find ./data/data/ -type f -mtime +90 -delete\nDelete hourly files older than 7 days : find ./data/data/ -type f -name '*_hourly.bin' -mtime +7 -delete\nDelete minute files older than 1 day : find ./data/data/ -type f -name '*_minute.bin' -mtime +1 -delete\nDelete empty (size < 800 bytes) profiles : find ./data/ -maxdepth 1 -type f -name '*.user' -size -850c -delete\nDelete very old user profiles : find . -maxdepth 1 -type f -name '*.user' -mtime +270 -delete\n\nView top traffic : iftop -n\nView tcp dump : tcpdump -n host x.x.x.x\nBan IP manually  : iptables -A INPUT -s IP -j DROP\n                 : iptables -A OUTPUT -d IP -j DROP\nBan IP manually, otpion 2 : ipset add blacklist IP\nDump java heap : jmap -dump:format=b,file=heap.bin <pid>\n\n\nTop consuming tables:\nSELECT nspname || '.' || relname AS \"relation\",\n    pg_size_pretty(pg_relation_size(C.oid)) AS \"size\"\n  FROM pg_class C\n  LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)\n  WHERE nspname NOT IN ('pg_catalog', 'information_schema')\n  ORDER BY pg_relation_size(C.oid) DESC\n  LIMIT 20;\n"
  },
  {
    "path": "scripts/win/README.md",
    "content": "# StartBlynkServer.cmd\n\nThis batch file allows you to run local server in simpler way. Placing it alongside the server file allows you to:\n\n- Display Server IP address\n- Display application and hardware ports\n- Running the server without the burden of writing the full path of it\n- The script is now able to detect the latest version on your local server folder and run it\n\n![Scipt in action](https://s16.postimg.org/awmfis8it/2016_09_08_22_47_01.png)\n\n![placement of script in folder with multiple server versions](https://s9.postimg.org/tjo9ysnzj/2016_09_08_22_52_31.png)\n\n"
  },
  {
    "path": "scripts/win/StartBlynkServer.cmd",
    "content": "@echo off\necho **************************\necho Starting Blynk Server\necho Script created by\necho Waleed El-Badry\necho waleed.elbadry@must.edu.eg\necho **************************\necho Your working directory is %~dp0\necho **************************\nset ip_address_string=\"IP Address\"\nrem Uncomment the following line when using Windows 7 or Windows 8 / 8.1 (with removing \"rem\")!\nset ip_address_string=\"IPv4 Address\"\necho **************************\nfor /f \"usebackq tokens=2 delims=:\" %%f in (`ipconfig ^| findstr /c:%ip_address_string%`) do (\n    echo Local Server IP Address is: %%f\n    echo Hardware Port : 8080\n\techo Application Port : 9443\n)\necho **************************\n\ncd /D %~dp0\necho Available server files\ndir *.jar\n@echo off\nfor /f \"delims=\" %%x in ('dir /od /b server*.jar') do set latestjar=%%x\n@echo on\necho Server latest version on folder is %latestjar%\njava -jar %latestjar% -dataFolder /path\nIF /I \"%ERRORLEVEL%\" NEQ \"0\" (\n    ECHO Server failed to started\n)"
  },
  {
    "path": "server/Docker/Dockerfile",
    "content": "FROM ubuntu\nMAINTAINER Florian Mauduit <f@lf.je>\n\n#############################################################\n#\n# ENV VARS\n#\n# HARDWARE_PORT \t Hardware without SSL/TLS support\n# HARDWARE_PORT_SSL\t Hardware port with SSL/TLS support\n# HTTP_PORT\t\t Blynk Dashboard\n#\n# BLYNK_SERVER_VERSION\t Blynk Server JAR version\n#\n###\n\n## Server Port\nENV BLYNK_SERVER_VERSION 0.41.16\nENV HARDWARE_MQTT_PORT 8440\nENV HTTP_PORT 8080\nENV HTTPS_PORT 9443\n\n## SSL\n#ENV SERVER_SSL_CERT\n#ENV SERVER_SSL_KEY\n#ENV SERVER_SSL_KEY_PASS\n\n## LOGS\nENV LOG_LEVEL info\n\n## OTHERS\n\nENV FORCE_PORT_80_FOR_CSV false\nENV FORCE_PORT_80_FOR_REDIRECT true\nENV USER_DEVICES_LIMIT 50\nENV USER_TAGS_LIMIT 100\nENV USER_DASHBOARD_MAX_LIMIT 100\nENV USER_WIDGET_MAX_SIZE_LIMIT 20\nENV USER_MESSAGE_QUOTA_LIMIT 100\nENV NOTIFICATIONS_QUEUE_LIMIT 2000\nENV BLOCKING_PROCESSOR_THREAD_POOL_LIMIT 6\nENV NOTIFICATIONS_FREQUENCY_USER_QUOTA_LIMIT 5\nENV WEBHOOKS_FREQUENCY_USER_QUOTA_LIMIT 1000\nENV WEBHOOKS_RESPONSE_SIZE_LIMIT 96\nENV USER_PROFILE_MAX_SIZE 128\nENV TERMINAL_STRINGS_POOL_SIZE 25\nENV MAP_STRINGS_POOL_SIZE 25\nENV LCD_STRINGS_POOL_SIZE 6\nENV TABLE_ROWS_POOL_SIZE 100\nENV PROFILE_SAVE_WORKER_PERIOD 60000\nENV STATS_PRINT_WORKER_PERIOD 60000\nENV WEB_REQUEST_MAX_SIZE 524288\nENV CSV_EXPORT_DATA_POINTS_MAX 43200\nENV HARD_SOCKET_IDLE_TIMEOUT 10\nENV ADMIN_ROOT_PATH /admin\nENV PRODUCT_NAME Blynk\nENV RESTORE_HOST blynk-cloud.com\nENV ALLOW_STORE_IP true\nENV ALLOW_READING_WIDGET_WITHOUT_ACTIVE_APP false\nENV ASYNC_LOGGER_RING_BUGGER_SIZE 2048\n\n## DB\nENV ENABLE_DB false\nENV ENABLE_RAW_DB_DATA_STORE false\n\n## Users\nENV INITIAL_ENERGY 100000\nENV ADMIN_EMAIL admin@blynk.cc\nENV ADMIN_PASS admin\n\n\n############################################################\n# Install OpenJDK\nRUN apt update && apt install -y openjdk-11-jdk libxrender1 maven\nRUN apt install -y curl\n\n\n############################################################\n\nRUN mkdir /blynk\nRUN curl -L https://github.com/blynkkk/blynk-server/releases/download/v${BLYNK_SERVER_VERSION}/server-${BLYNK_SERVER_VERSION}.jar > /blynk/server.jar\n\nRUN mkdir /data\nRUN mkdir /config && touch /config/server.properties\nVOLUME [\"/config\", \"/data/backup\"]\n\nRUN mkdir -p /usr/local/bin\nADD ./bin /usr/local/bin\nRUN chmod +x /usr/local/bin/*.sh\n\nEXPOSE ${HARDWARE_MQTT_PORT} ${HARDWARE_MQTT_PORT_SSL} ${HTTP_PORT} ${HTTPS_PORT}\n\nWORKDIR /data\nENTRYPOINT [\"/usr/local/bin/run.sh\"]\n"
  },
  {
    "path": "server/Docker/README.md",
    "content": "## HOW TO \n\n### Configure\n\nJust edit the Dockerfile and change ENV vars\n\n\n### Build\n\n```bash\ndocker build -t blynk .\n```\n\n### RUN\n\n```bash\ndocker run --name blynk-server -v ~/blynk-server/server/Docker:/data -p 8440:8440 -p 8080:8080 -p 9443:9443 -d blynk \n```\n\nDon't forget to change the port attribution if you change on the ENV vars in Dockerfile\n\n\n## UPDATE Blynk version\n\n## How to\n\nStop and remove your actual container\n\n```bash\ndocker stop blynk-server && docker rm blynk-server\n```\n\n- Edit your Blynk server version on the ENV var in Dockerfile\n- Build & Launch\n\n\nHave Fun ! :v: :whale:\n\n"
  },
  {
    "path": "server/Docker/bin/run.sh",
    "content": "#!/bin/bash\n\nset -e\n\necho \"hardware.mqtt.port=${HARDWARE_MQTT_PORT}\nhardware.ssl.port=${HARDWARE_MQTT_PORT_SSL}\nhttp.port=${HTTP_PORT}\nforce.port.80.for.csv=${FORCE_PORT_80_FOR_CSV}\nforce.port.80.for.redirect=${FORCE_PORT_80_FOR_REDIRECT}\nhttps.port=${HTTPS_PORT}\nserver.ssl.cert=${SERVER_SSL_CERT}\nserver.ssl.key=${SERVER_SSL_KEY}\nserver.ssl.key.pass=${SERVER_SSL_KEY_PASS}\ndata.folder=${DATA_FOLDER}\nlogs.folder=./logs\nlog.level=${LOG_LEVEL}\nuser.devices.limit=${USER_DEVICES_LIMIT}\nuser.tags.limit=${USER_TAGS_LIMIT}\nuser.dashboard.max.limit=${USER_DASHBOARD_MAX_LIMIT}\nuser.widget.max.size.limit=${USER_WIDGET_MAX_SIZE_LIMIT}\nuser.message.quota.limit=${USER_MESSAGE_QUOTA_LIMIT}\nnotifications.queue.limit=${NOTIFICATIONS_QUEUE_LIMIT}\nblocking.processor.thread.pool.limit=${BLOCKING_PROCESSOR_THREAD_POOL_LIMIT}\nnotifications.frequency.user.quota.limit=${NOTIFICATIONS_FREQUENCY_USER_QUOTA_LIMIT}\nwebhooks.frequency.user.quota.limit=${WEBHOOKS_FREQUENCY_USER_QUOTA_LIMIT}\nwebhooks.response.size.limit=${WEBHOOKS_RESPONSE_SIZE_LIMIT}\nuser.profile.max.size=${USER_PROFILE_MAX_SIZE}\nterminal.strings.pool.size=${TERMINAL_STRINGS_POOL_SIZE}\nmap.strings.pool.size=${MAP_STRINGS_POOL_SIZE}\nlcd.strings.pool.size=${LCD_STRINGS_POOL_SIZE}\ntable.rows.pool.size=${TABLE_ROWS_POOL_SIZE}\nprofile.save.worker.period=${PROFILE_SAVE_WORKER_PERIOD}\nstats.print.worker.period=${STATS_PRINT_WORKER_PERIOD}\nweb.request.max.size=${WEB_REQUEST_MAX_SIZE}\ncsv.export.data.points.max=${CSV_EXPORT_DATA_POINTS_MAX}\nhard.socket.idle.timeout=${HARD_SOCKET_IDLE_TIMEOUT}\nenable.db=${ENABLE_DB}\nenable.raw.db.data.store=${ENABLE_RAW_DB_DATA_STORE}\nasync.logger.ring.buffer.size=${ASYNC_LOGGER_RING_BUFFER_SIZE}\nallow.reading.widget.without.active.app=${ALLOW_READING_WIDGET_WITHOUT_ACTIVE_APP}\nallow.store.ip=${ALLOW_STORE_IP}\ninitial.energy=${INITIAL_ENERGY}\nadmin.rootPath=${ADMIN_ROOT_PATH}\nrestore.host=${RESTORE_HOST}\nproduct.name=${PRODUCT_NAME}\nadmin.email=${ADMIN_EMAIL}\nadmin.pass=${ADMIN_PASS}\n\" > /config/server.properties\n\njava -jar /blynk/server.jar -dataFolder /data -serverConfig /config/server.properties\n"
  },
  {
    "path": "server/acme/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <artifactId>server</artifactId>\n        <groupId>cc.blynk.server</groupId>\n        <version>0.41.17-SNAPSHOT</version>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n\n    <groupId>cc.blynk.server.acme</groupId>\n    <artifactId>acme</artifactId>\n\n    <dependencies>\n\n        <dependency>\n            <groupId>org.shredzone.acme4j</groupId>\n            <artifactId>acme4j-client</artifactId>\n            <version>${acme4j-client.version}</version>\n        </dependency>\n\n        <dependency>\n            <groupId>org.shredzone.acme4j</groupId>\n            <artifactId>acme4j-utils</artifactId>\n            <version>${acme4j-client.version}</version>\n        </dependency>\n\n    </dependencies>\n\n</project>"
  },
  {
    "path": "server/acme/src/main/java/cc/blynk/server/acme/AcmeClient.java",
    "content": "package cc.blynk.server.acme;\n\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\nimport org.shredzone.acme4j.Account;\nimport org.shredzone.acme4j.AccountBuilder;\nimport org.shredzone.acme4j.Authorization;\nimport org.shredzone.acme4j.Certificate;\nimport org.shredzone.acme4j.Order;\nimport org.shredzone.acme4j.Session;\nimport org.shredzone.acme4j.Status;\nimport org.shredzone.acme4j.challenge.Http01Challenge;\nimport org.shredzone.acme4j.exception.AcmeException;\nimport org.shredzone.acme4j.util.CSRBuilder;\nimport org.shredzone.acme4j.util.KeyPairUtils;\n\nimport java.io.File;\nimport java.io.FileReader;\nimport java.io.FileWriter;\nimport java.io.IOException;\nimport java.security.KeyPair;\n\n/**\n * A simple client test tool.\n * <p>\n * Pass the names of the domains as parameters.\n */\npublic class AcmeClient {\n\n    private static final Logger log = LogManager.getLogger(AcmeClient.class);\n\n    // File name of the User Key Pair\n    private static final File USER_KEY_FILE = new File(\"user.pem\");\n\n    // File name of the Domain Key Pair\n    public static final File DOMAIN_KEY_FILE = new File(\"privkey.pem\");\n\n    // File name of the signed certificate\n    public static final File DOMAIN_CHAIN_FILE = new File(\"fullchain.crt\");\n\n    private static final String PRODUCTION = \"acme://letsencrypt.org\";\n\n    // RSA key size of generated key pairs\n    private static final int KEY_SIZE = 2048;\n    private static final int ATTEMPTS = 10;\n    private static final long WAIT_MILLIS = 3000L;\n\n    private final String letsEncryptUrl;\n    private final String email;\n    private final String host;\n    private final ContentHolder contentHolder;\n\n    public AcmeClient(String email, String host, ContentHolder contentHolder) {\n        this(PRODUCTION, email, host, contentHolder);\n    }\n\n    public AcmeClient(String letsEncryptUrl, String email, String host, ContentHolder contentHolder) {\n        this.letsEncryptUrl = letsEncryptUrl;\n        this.email = email;\n        this.host = host;\n        this.contentHolder = contentHolder;\n    }\n\n    public void requestCertificate() throws Exception {\n        log.info(\"Starting up certificate retrieval process for host {} and email {}.\", host, email);\n        fetchCertificate(email, host);\n    }\n\n    /**\n     * Generates a certificate for the given domains. Also takes care for the registration\n     * process.\n     *\n     * @param domain\n     *            Domains to get a common certificate for\n     */\n    private void fetchCertificate(String contact, String domain) throws IOException, AcmeException {\n        // Load the user key file. If there is no key file, create a new one.\n        // Keep this key pair in a safe place! In a production environment, you will not be\n        // able to access your account again if you should lose the key pair.\n        KeyPair userKeyPair = loadOrCreateKeyPair(USER_KEY_FILE);\n\n        Session session = new Session(letsEncryptUrl);\n\n        // Get the Account.\n        // If there is no account yet, create a new one.\n        Account account = new AccountBuilder()\n                .agreeToTermsOfService()\n                .useKeyPair(userKeyPair)\n                .addEmail(contact)\n                .create(session);\n        log.info(\"Registered a new user, URL: {}\", account.getLocation());\n\n        // Load or create a key pair for the domains. This should not be the userKeyPair!\n        KeyPair domainKeyPair = loadOrCreateKeyPair(DOMAIN_KEY_FILE);\n\n        // Order the certificate\n        Order order = account.newOrder().domain(domain).create();\n\n        // Perform all required authorizations\n        for (Authorization auth : order.getAuthorizations()) {\n            authorize(auth);\n        }\n\n        // Generate a CSR for all of the domains, and sign it with the domain key pair.\n        CSRBuilder csrb = new CSRBuilder();\n        csrb.addDomain(domain);\n        csrb.setOrganization(\"Blynk Inc.\");\n        csrb.sign(domainKeyPair);\n\n        // Order the certificate\n        order.execute(csrb.getEncoded());\n\n        // Wait for the order to complete\n        try {\n            int attempts = ATTEMPTS;\n            while (order.getStatus() != Status.VALID && attempts-- > 0) {\n                if (order.getStatus() == Status.INVALID) {\n                    throw new AcmeException(\"Order failed... Giving up.\");\n                }\n                Thread.sleep(WAIT_MILLIS);\n                order.update();\n            }\n        } catch (InterruptedException ex) {\n            log.error(\"interrupted\", ex);\n        }\n\n        Certificate certificate = order.getCertificate();\n\n        if (certificate != null) {\n            try (FileWriter fw = new FileWriter(DOMAIN_CHAIN_FILE)) {\n                certificate.writeCertificate(fw);\n            }\n            log.info(\"Overriding certificate. Expiration date is : {}\", certificate.getCertificate().getNotAfter());\n        }\n    }\n\n    /**\n     * Loads a key pair from specified file. If the file does not exist,\n     * a new key pair is generated and saved.\n     *\n     * @return {@link KeyPair}.\n     */\n    private KeyPair loadOrCreateKeyPair(File file) throws IOException {\n        if (file.exists()) {\n            try (FileReader fr = new FileReader(file)) {\n                return KeyPairUtils.readKeyPair(fr);\n            }\n        } else {\n            KeyPair domainKeyPair = KeyPairUtils.createKeyPair(KEY_SIZE);\n            try (FileWriter fw = new FileWriter(file)) {\n                KeyPairUtils.writeKeyPair(domainKeyPair, fw);\n            }\n            return domainKeyPair;\n        }\n    }\n\n    /**\n     * Authorize a domain. It will be associated with your account, so you will be able to\n     * retrieve a signed certificate for the domain later.\n     *\n     * @param auth\n     *            {@link Authorization} to perform\n     */\n    private void authorize(Authorization auth) throws AcmeException {\n        log.info(\"Starting authorization for domain {}\", auth.getIdentifier().getDomain());\n\n        // Find the desired challenge and prepare it.\n        Http01Challenge challenge = httpChallenge(auth);\n\n        if (challenge == null) {\n            throw new AcmeException(\"No challenge found\");\n        }\n\n        contentHolder.content = challenge.getAuthorization();\n\n        // If the challenge is already verified, there's no need to execute it again.\n        if (challenge.getStatus() == Status.VALID) {\n            return;\n        }\n\n        // Now trigger the challenge.\n        challenge.trigger();\n\n        // Poll for the challenge to complete.\n        try {\n            int attempts = ATTEMPTS;\n            while (challenge.getStatus() != Status.VALID && attempts-- > 0) {\n                if (challenge.getStatus() == Status.INVALID) {\n                    throw new AcmeException(\"Challenge failed... Giving up.\");\n                }\n                Thread.sleep(WAIT_MILLIS);\n                challenge.update();\n            }\n        } catch (InterruptedException ex) {\n            log.error(\"interrupted\", ex);\n            return;\n        }\n\n        // All reattempts are used up and there is still no valid authorization?\n        if (challenge.getStatus() != Status.VALID) {\n            throw new AcmeException(\"Failed to pass the challenge for domain \"\n                    + auth.getIdentifier().getDomain() + \", ... Giving up.\");\n        }\n    }\n\n    private Http01Challenge httpChallenge(Authorization auth) throws AcmeException {\n        // Find a single http-01 challenge\n        Http01Challenge challenge = auth.findChallenge(Http01Challenge.TYPE);\n        if (challenge == null) {\n            throw new AcmeException(\"Found no \" + Http01Challenge.TYPE + \" challenge, don't know what to do...\");\n        }\n\n        // Output the challenge, wait for acknowledge...\n        log.debug(\"http://{}/.well-known/acme-challenge/{}\", auth.getIdentifier().getDomain(), challenge.getToken());\n        log.debug(\"Content: {}\", challenge.getAuthorization());\n\n        return challenge;\n    }\n\n}\n"
  },
  {
    "path": "server/acme/src/main/java/cc/blynk/server/acme/ContentHolder.java",
    "content": "package cc.blynk.server.acme;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 30.04.17.\n */\npublic class ContentHolder {\n\n    public volatile String content;\n\n}\n"
  },
  {
    "path": "server/acme/src/main/java/module-info.java",
    "content": "/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 05.10.17.\n */\nmodule cc.blynk.server.acme {\n    requires org.apache.logging.log4j;\n    requires org.shredzone.acme4j.utils;\n    requires org.shredzone.acme4j;\n\n    exports cc.blynk.server.acme;\n}"
  },
  {
    "path": "server/core/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <artifactId>server</artifactId>\n        <groupId>cc.blynk.server</groupId>\n        <version>0.41.17-SNAPSHOT</version>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n\n    <artifactId>core</artifactId>\n\n    <dependencies>\n\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n            <version>${jackson-databind.version}</version>\n        </dependency>\n\n        <!-- Needed for ASync log4j2 -->\n        <dependency>\n            <groupId>com.lmax</groupId>\n            <artifactId>disruptor</artifactId>\n            <version>${disruptor.version}</version>\n        </dependency>\n\n        <!-- SHOULD BE ADDED ONLY FOR SPECIFIC BUILDS -->\n        <dependency>\n            <groupId>io.netty</groupId>\n            <artifactId>netty-transport-native-epoll</artifactId>\n            <version>${netty.version}</version>\n            <classifier>${epoll.os}</classifier>\n        </dependency>\n        <dependency>\n            <groupId>io.netty</groupId>\n            <artifactId>netty-tcnative-boringssl-static</artifactId>\n            <version>${netty.boring.ssl.version}</version>\n            <classifier>${epoll.os}</classifier>\n        </dependency>\n        <dependency>\n            <groupId>io.netty</groupId>\n            <artifactId>netty-codec-http</artifactId>\n            <version>${netty.version}</version>\n        </dependency>\n        <!-- -->\n\n        <!-- notification modules -->\n        <dependency>\n            <groupId>cc.blynk.utils</groupId>\n            <artifactId>utils</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>cc.blynk.server.notifications</groupId>\n            <artifactId>email</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>cc.blynk.server.notifications</groupId>\n            <artifactId>push</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>cc.blynk.server.notifications</groupId>\n            <artifactId>twitter</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>cc.blynk.server.notifications</groupId>\n            <artifactId>sms</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>cc.blynk.server.acme</groupId>\n            <artifactId>acme</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n\n        <!-- DB dependencies -->\n        <dependency>\n            <groupId>org.postgresql</groupId>\n            <artifactId>postgresql</artifactId>\n            <version>${postgresql.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>com.zaxxer</groupId>\n            <artifactId>HikariCP</artifactId>\n            <version>${HikariCP.version}</version>\n        </dependency>\n\n        <dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-lang3</artifactId>\n            <version>${commons-lang3.version}</version>\n            <scope>test</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.openjdk.jmh</groupId>\n            <artifactId>jmh-core</artifactId>\n            <version>${jmh-core.version}</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.openjdk.jmh</groupId>\n            <artifactId>jmh-generator-annprocess</artifactId>\n            <version>${jmh-core.version}</version>\n            <scope>test</scope>\n        </dependency>\n\n\n    </dependencies>\n\n</project>"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/Holder.java",
    "content": "package cc.blynk.server;\n\nimport cc.blynk.server.core.BlockingIOProcessor;\nimport cc.blynk.server.core.dao.FileManager;\nimport cc.blynk.server.core.dao.ReportingDiskDao;\nimport cc.blynk.server.core.dao.SessionDao;\nimport cc.blynk.server.core.dao.TokenManager;\nimport cc.blynk.server.core.dao.UserDao;\nimport cc.blynk.server.core.dao.UserKey;\nimport cc.blynk.server.core.dao.ota.OTAManager;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.widgets.ui.reporting.ReportScheduler;\nimport cc.blynk.server.core.processors.EventorProcessor;\nimport cc.blynk.server.core.stats.GlobalStats;\nimport cc.blynk.server.db.DBManager;\nimport cc.blynk.server.db.ReportingDBManager;\nimport cc.blynk.server.internal.token.TokensPool;\nimport cc.blynk.server.notifications.mail.MailWrapper;\nimport cc.blynk.server.notifications.push.GCMWrapper;\nimport cc.blynk.server.notifications.sms.SMSWrapper;\nimport cc.blynk.server.notifications.twitter.TwitterWrapper;\nimport cc.blynk.server.transport.TransportTypeHolder;\nimport cc.blynk.server.workers.ReadingWidgetsWorker;\nimport cc.blynk.server.workers.timer.TimerWorker;\nimport cc.blynk.utils.FileUtils;\nimport cc.blynk.utils.properties.GCMProperties;\nimport cc.blynk.utils.properties.MailProperties;\nimport cc.blynk.utils.properties.ServerProperties;\nimport cc.blynk.utils.properties.SmsProperties;\nimport cc.blynk.utils.properties.TwitterProperties;\nimport io.netty.channel.epoll.Epoll;\nimport io.netty.util.internal.SystemPropertyUtil;\nimport org.asynchttpclient.DefaultAsyncHttpClient;\nimport org.asynchttpclient.DefaultAsyncHttpClientConfig;\n\nimport java.util.Collections;\nimport java.util.concurrent.ConcurrentMap;\n\n/**\n * Just a holder for all necessary objects for server instance creation.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 28.09.15.\n */\npublic class Holder {\n\n    public final FileManager fileManager;\n\n    public final SessionDao sessionDao;\n\n    public final UserDao userDao;\n\n    public final TokenManager tokenManager;\n\n    public final ReportingDiskDao reportingDiskDao;\n\n    public final DBManager dbManager;\n    public final ReportingDBManager reportingDBManager;\n\n    public final GlobalStats stats;\n\n    public final ServerProperties props;\n\n    public final BlockingIOProcessor blockingIOProcessor;\n    public final TransportTypeHolder transportTypeHolder;\n    public final TwitterWrapper twitterWrapper;\n    public final MailWrapper mailWrapper;\n    public final GCMWrapper gcmWrapper;\n    public final SMSWrapper smsWrapper;\n    public final TimerWorker timerWorker;\n    public final ReadingWidgetsWorker readingWidgetsWorker;\n    public final ReportScheduler reportScheduler;\n\n    public final EventorProcessor eventorProcessor;\n    public final DefaultAsyncHttpClient asyncHttpClient;\n\n    public final OTAManager otaManager;\n\n    public final Limits limits;\n    public final TextHolder textHolder;\n\n    public final String downloadUrl;\n\n    public final SslContextHolder sslContextHolder;\n\n    public final TokensPool tokensPool;\n\n    public Holder(ServerProperties serverProperties, MailProperties mailProperties,\n                  SmsProperties smsProperties, GCMProperties gcmProperties,\n                  TwitterProperties twitterProperties,\n                  boolean restore) {\n        disableNettyLeakDetector();\n        this.props = serverProperties;\n\n        this.fileManager = new FileManager(serverProperties.getDataFolder(), serverProperties.host);\n        this.sessionDao = new SessionDao();\n        this.blockingIOProcessor = new BlockingIOProcessor(\n                serverProperties.getIntProperty(\"blocking.processor.thread.pool.limit\", 6),\n                serverProperties.getIntProperty(\"notifications.queue.limit\", 2000)\n        );\n\n        boolean enableDB = serverProperties.isDBEnabled();\n        this.dbManager = new DBManager(blockingIOProcessor, enableDB);\n        this.reportingDBManager = new ReportingDBManager(blockingIOProcessor, enableDB);\n\n        if (restore) {\n            try {\n                ConcurrentMap<UserKey, User> allUsers = dbManager.userDBDao.getAllUsers(serverProperties.region);\n                this.userDao = new UserDao(allUsers, serverProperties.region, serverProperties.host);\n            } catch (Exception e) {\n                System.out.println(\"Error restoring data from DB!\");\n                e.printStackTrace();\n                throw new RuntimeException(e);\n            }\n        } else {\n            this.userDao = new UserDao(fileManager.deserializeUsers(), serverProperties.region, serverProperties.host);\n        }\n\n        this.tokenManager = new TokenManager(this.userDao.users, dbManager, serverProperties.host);\n        this.stats = new GlobalStats();\n        this.reportingDiskDao = new ReportingDiskDao(serverProperties.getReportingFolder(),\n                serverProperties.isRawDBEnabled() && reportingDBManager.isDBEnabled());\n\n        this.transportTypeHolder = new TransportTypeHolder(serverProperties);\n\n        this.asyncHttpClient = new DefaultAsyncHttpClient(new DefaultAsyncHttpClientConfig.Builder()\n                .setUserAgent(null)\n                .setKeepAlive(true)\n                .setUseNativeTransport(Epoll.isAvailable())\n                .setUseOpenSsl(SslContextHolder.isOpenSslAvailable())\n                .build()\n        );\n\n        this.twitterWrapper = new TwitterWrapper(twitterProperties, asyncHttpClient);\n        this.mailWrapper = new MailWrapper(mailProperties, serverProperties.productName);\n        this.gcmWrapper = new GCMWrapper(gcmProperties, asyncHttpClient, serverProperties.productName);\n        this.smsWrapper = new SMSWrapper(smsProperties, asyncHttpClient);\n\n        this.otaManager = new OTAManager(props);\n\n        this.eventorProcessor = new EventorProcessor(\n                gcmWrapper, mailWrapper, twitterWrapper, blockingIOProcessor, stats);\n        this.timerWorker = new TimerWorker(userDao, sessionDao, gcmWrapper);\n        this.readingWidgetsWorker = new ReadingWidgetsWorker(sessionDao, userDao, props.getAllowWithoutActiveApp());\n        this.limits = new Limits(props);\n        this.textHolder = new TextHolder(gcmProperties);\n\n        this.downloadUrl = FileUtils.downloadUrl(serverProperties.host,\n                props.getProperty(\"http.port\"),\n                props.getBoolProperty(\"force.port.80.for.csv\")\n        );\n        this.reportScheduler = new ReportScheduler(1, downloadUrl, mailWrapper, reportingDiskDao, userDao.users);\n\n        String contactEmail = serverProperties.getProperty(\"contact.email\", mailProperties.getSMTPUsername());\n        this.sslContextHolder = new SslContextHolder(props, contactEmail);\n        this.tokensPool = new TokensPool(serverProperties.getReportingFolder());\n    }\n\n    //for tests only\n    public Holder(ServerProperties serverProperties, TwitterWrapper twitterWrapper,\n                  MailWrapper mailWrapper,\n                  GCMWrapper gcmWrapper, SMSWrapper smsWrapper,\n                  BlockingIOProcessor blockingIOProcessor,\n                  String dbFileName) {\n        disableNettyLeakDetector();\n        this.props = serverProperties;\n\n        this.fileManager = new FileManager(serverProperties.getDataFolder(), serverProperties.host);\n        this.sessionDao = new SessionDao();\n        this.userDao = new UserDao(fileManager.deserializeUsers(), serverProperties.region, serverProperties.host);\n        this.blockingIOProcessor = blockingIOProcessor;\n\n        boolean enableDB = serverProperties.isDBEnabled();\n        this.dbManager = new DBManager(dbFileName, blockingIOProcessor, enableDB);\n        this.reportingDBManager = new ReportingDBManager(dbFileName, blockingIOProcessor, enableDB);\n\n        this.tokenManager = new TokenManager(this.userDao.users, dbManager, serverProperties.host);\n        this.stats = new GlobalStats();\n        this.reportingDiskDao = new ReportingDiskDao(serverProperties.getReportingFolder(),\n                serverProperties.isRawDBEnabled() && reportingDBManager.isDBEnabled());\n\n        this.transportTypeHolder = new TransportTypeHolder(serverProperties);\n\n        this.twitterWrapper = twitterWrapper;\n        this.mailWrapper = mailWrapper;\n        this.gcmWrapper = gcmWrapper;\n        this.smsWrapper = smsWrapper;\n\n        this.otaManager = new OTAManager(props);\n\n        this.eventorProcessor = new EventorProcessor(\n                gcmWrapper, mailWrapper, twitterWrapper, blockingIOProcessor, stats);\n        this.asyncHttpClient = new DefaultAsyncHttpClient(new DefaultAsyncHttpClientConfig.Builder()\n                .setUserAgent(null)\n                .setKeepAlive(true)\n                .setUseNativeTransport(Epoll.isAvailable())\n                .setUseOpenSsl(SslContextHolder.isOpenSslAvailable())\n                .build()\n        );\n\n        this.timerWorker = new TimerWorker(userDao, sessionDao, gcmWrapper);\n        this.readingWidgetsWorker = new ReadingWidgetsWorker(sessionDao, userDao, props.getAllowWithoutActiveApp());\n        this.limits = new Limits(props);\n        this.textHolder = new TextHolder(new GCMProperties(Collections.emptyMap()));\n\n        this.downloadUrl = FileUtils.downloadUrl(serverProperties.host,\n                props.getProperty(\"http.port\"),\n                props.getBoolProperty(\"force.port.80.for.csv\")\n        );\n        this.reportScheduler = new ReportScheduler(1, downloadUrl, mailWrapper, reportingDiskDao, userDao.users);\n\n        this.sslContextHolder = new SslContextHolder(props, \"test@blynk.cc\");\n        this.tokensPool = new TokensPool(serverProperties.getReportingFolder());\n\n    }\n\n    private static void disableNettyLeakDetector() {\n        String leakProperty = SystemPropertyUtil.get(\"io.netty.leakDetection.level\");\n        //we do not pass any with JVM option\n        if (leakProperty == null) {\n            System.setProperty(\"io.netty.leakDetection.level\", \"disabled\");\n        }\n    }\n\n    public void close() {\n        sessionDao.close();\n\n        transportTypeHolder.close();\n        asyncHttpClient.close();\n\n        reportingDiskDao.close();\n\n        System.out.println(\"Stopping BlockingIOProcessor...\");\n        blockingIOProcessor.close();\n        reportScheduler.shutdown();\n        System.out.println(\"Stopping DBManager...\");\n        dbManager.close();\n        reportingDBManager.close();\n        tokensPool.close();\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/Limits.java",
    "content": "package cc.blynk.server;\n\nimport cc.blynk.utils.properties.ServerProperties;\n\n/**\n * This is helper class for holding all user limits.\n * It is created for dependency injection mostly.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 07.01.17.\n */\npublic class Limits {\n\n    public final int webRequestMaxSize;\n\n    //user limits\n    public final int deviceLimit;\n    public final int tagsLimit;\n    public final int dashboardsLimit;\n    public final int widgetSizeLimitBytes;\n    public final int profileSizeLimitBytes;\n    public final int hourlyRegistrationsLimit;\n    public final int reportsLimit;\n\n    //hardware side limits\n    public final long notificationPeriodLimitSec;\n    public final int userQuotaLimit;\n    public final long webhookPeriodLimitation;\n    public final int webhookResponseSizeLimitBytes;\n    public final int webhookFailureLimit;\n    public final int hardwareIdleTimeout;\n    public final int appIdleTimeout;\n    public final int storeMinuteRecordDays;\n    public final int storeReportCSVDays;\n\n    public Limits(ServerProperties props) {\n        this.webRequestMaxSize = props.getIntProperty(\"web.request.max.size\", 512 * 1024);\n\n        this.deviceLimit = props.getIntProperty(\"user.devices.limit\", 50);\n        this.tagsLimit = props.getIntProperty(\"user.tags.limit\", 100);\n        this.dashboardsLimit = props.getIntProperty(\"user.dashboard.max.limit\", 100);\n        this.widgetSizeLimitBytes = props.getIntProperty(\"user.widget.max.size.limit\", 10) * 1024;\n        this.profileSizeLimitBytes = props.getIntProperty(\"user.profile.max.size\", 64) * 1024;\n\n        this.notificationPeriodLimitSec =\n                props.getLongProperty(\"notifications.frequency.user.quota.limit\", 15L) * 1000L;\n        this.userQuotaLimit = props.getIntProperty(\"user.message.quota.limit\", 100);\n        this.webhookPeriodLimitation =\n                isUnlimited(props.getLongProperty(\"webhooks.frequency.user.quota.limit\", 1000), -1L);\n        this.webhookResponseSizeLimitBytes = props.getIntProperty(\"webhooks.response.size.limit\", 64) * 1024;\n        this.webhookFailureLimit =\n                isUnlimited(props.getIntProperty(\"webhooks.failure.count.limit\", 10), Integer.MAX_VALUE);\n        this.hardwareIdleTimeout = props.getIntProperty(\"hard.socket.idle.timeout\", 0);\n        this.appIdleTimeout = props.getIntProperty(\"app.socket.idle.timeout\", 300);\n\n        this.hourlyRegistrationsLimit = props.getIntProperty(\"hourly.registrations.limit\", 1000);\n        this.storeMinuteRecordDays = props.getIntProperty(\"store.minute.record.days\", 10);\n        this.reportsLimit = 25;\n        this.storeReportCSVDays = props.getIntProperty(\"store.export.csv.report.days\", 45);\n    }\n\n    private static int isUnlimited(int val, int max) {\n        if (val == 0) {\n            return max;\n        }\n        return val;\n    }\n\n    private static long isUnlimited(long val, long max) {\n        if (val == 0) {\n            return max;\n        }\n        return val;\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/SslContextHolder.java",
    "content": "package cc.blynk.server;\n\nimport cc.blynk.server.acme.AcmeClient;\nimport cc.blynk.server.acme.ContentHolder;\nimport cc.blynk.utils.properties.ServerProperties;\nimport io.netty.handler.ssl.OpenSsl;\nimport io.netty.handler.ssl.SslContext;\nimport io.netty.handler.ssl.SslContextBuilder;\nimport io.netty.handler.ssl.SslProvider;\nimport io.netty.handler.ssl.util.SelfSignedCertificate;\nimport io.netty.util.internal.PlatformDependent;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport javax.net.ssl.SSLException;\nimport java.io.File;\nimport java.security.cert.CertificateException;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 30.04.17.\n */\npublic class SslContextHolder {\n\n    private static final Logger log = LogManager.getLogger(SslContextHolder.class);\n\n    public volatile SslContext sslCtx;\n\n    public final AcmeClient acmeClient;\n\n    private final boolean isAutoGenerationEnabled;\n\n    public final boolean isNeedInitializeOnStart;\n\n    public final ContentHolder contentHolder;\n\n    private final boolean onlyLatestTLS;\n\n    SslContextHolder(ServerProperties props, String email) {\n        this.contentHolder = new ContentHolder();\n        this.onlyLatestTLS = props.getBoolProperty(\"latest.tls\");\n\n        String certPath = props.getProperty(\"server.ssl.cert\");\n        String keyPath = props.getProperty(\"server.ssl.key\");\n        String keyPass = props.getProperty(\"server.ssl.key.pass\");\n\n        if (certPath == null || certPath.isEmpty()) {\n            log.info(\"Didn't find custom user certificates.\");\n            isAutoGenerationEnabled = true;\n        } else {\n            isAutoGenerationEnabled = false;\n        }\n\n        String host = props.getProperty(\"server.host\");\n        if (AcmeClient.DOMAIN_CHAIN_FILE.exists() && AcmeClient.DOMAIN_KEY_FILE.exists()) {\n            log.info(\"Found generated with Let's Encrypt certificates.\");\n\n            certPath = AcmeClient.DOMAIN_CHAIN_FILE.getAbsolutePath();\n            keyPath = AcmeClient.DOMAIN_KEY_FILE.getAbsolutePath();\n            keyPass = null;\n\n            this.isNeedInitializeOnStart = false;\n            this.acmeClient = new AcmeClient(email, host, contentHolder);\n        } else {\n            log.info(\"Didn't find Let's Encrypt certificates.\");\n            if (host == null || host.isEmpty() || email == null || email.isEmpty()\n                    || email.equals(\"example@gmail.com\") || email.startsWith(\"SMTP\")) {\n                log.warn(\"You didn't specified 'server.host' or 'contact.email' \"\n                        + \"properties in server.properties file. \"\n                        + \"Automatic certificate generation is turned off. \"\n                        + \"Please specify above properties for automatic certificates retrieval.\");\n                this.acmeClient = null;\n                this.isNeedInitializeOnStart = false;\n            } else {\n                log.info(\"Automatic certificate generation is turned ON.\");\n                this.acmeClient = new AcmeClient(email, host, contentHolder);\n                this.isNeedInitializeOnStart = true;\n            }\n        }\n\n        if (isOpenSslAvailable()) {\n            log.info(\"Using native openSSL provider.\");\n        }\n        SslProvider sslProvider = fetchSslProvider();\n        this.sslCtx = initSslContext(certPath, keyPath, keyPass, sslProvider);\n    }\n\n    static boolean isOpenSslAvailable() {\n        return PlatformDependent.bitMode() != 32 && OpenSsl.isAvailable();\n    }\n\n    public void regenerate() throws Exception {\n        this.acmeClient.requestCertificate();\n\n        String certPath = AcmeClient.DOMAIN_CHAIN_FILE.getAbsolutePath();\n        String keyPath = AcmeClient.DOMAIN_KEY_FILE.getAbsolutePath();\n\n        SslProvider sslProvider = fetchSslProvider();\n        this.sslCtx = initSslContext(certPath, keyPath, null, sslProvider);\n    }\n\n    public boolean runRenewalWorker() {\n        return isAutoGenerationEnabled && acmeClient != null;\n    }\n\n    public void generateInitialCertificates(ServerProperties props) {\n        if (isAutoGenerationEnabled && isNeedInitializeOnStart) {\n            System.out.println(\"Generating own initial certificates...\");\n            try {\n                regenerate();\n                System.out.println(\"Success! The certificate for your domain \"\n                        + props.getProperty(\"server.host\") + \" has been generated!\");\n            } catch (Exception e) {\n                System.out.println(\"Error during certificate generation.\");\n                System.out.println(e.getMessage());\n            }\n        }\n    }\n\n    private SslContext initSslContext(String serverCertPath, String serverKeyPath, String serverPass,\n                                      SslProvider sslProvider) {\n        try {\n            File serverCert = new File(serverCertPath);\n            File serverKey = new File(serverKeyPath);\n\n            if (!serverCert.exists() || !serverKey.exists()) {\n                log.warn(\"ATTENTION. Server certificate paths (cert : '{}', key : '{}') not valid.\"\n                                + \" Using embedded server certs and one way ssl. This is not secure.\"\n                                + \" Please replace it with your own certs.\",\n                        serverCert.getAbsolutePath(), serverKey.getAbsolutePath());\n\n                return build(sslProvider);\n            }\n\n            return build(serverCert, serverKey, serverPass, sslProvider);\n        } catch (CertificateException | SSLException | IllegalArgumentException e) {\n            log.error(\"Error initializing ssl context. Reason : {}\", e.getMessage());\n            throw new RuntimeException(e.getMessage());\n        }\n    }\n\n    private static SslProvider fetchSslProvider() {\n        return isOpenSslAvailable() ? SslProvider.OPENSSL : SslProvider.JDK;\n    }\n\n    public static SslContext build(SslProvider sslProvider) throws CertificateException, SSLException {\n        SelfSignedCertificate ssc = new SelfSignedCertificate();\n        return SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())\n                .sslProvider(sslProvider)\n                .build();\n    }\n\n    public SslContext build(File serverCert, File serverKey,\n                            String serverPass, SslProvider sslProvider) throws SSLException {\n        SslContextBuilder sslContextBuilder;\n        if (serverPass == null || serverPass.isEmpty()) {\n            sslContextBuilder = SslContextBuilder.forServer(serverCert, serverKey)\n                    .sslProvider(sslProvider);\n        } else {\n            sslContextBuilder = SslContextBuilder.forServer(serverCert, serverKey, serverPass)\n                    .sslProvider(sslProvider);\n        }\n        if (this.onlyLatestTLS) {\n            sslContextBuilder.protocols(\"TLSv1.3\", \"TLSv1.2\");\n        }\n        return sslContextBuilder.build();\n    }\n\n    public static SslContext build(File serverCert, File serverKey, String serverPass,\n                                   SslProvider sslProvider, File clientCert) throws SSLException {\n        log.info(\"Creating SSL context for cert '{}', key '{}', key pass '{}'\",\n                serverCert.getAbsolutePath(), serverKey.getAbsoluteFile(), serverPass);\n        if (serverPass == null || serverPass.isEmpty()) {\n            return SslContextBuilder.forServer(serverCert, serverKey)\n                    .sslProvider(sslProvider)\n                    .trustManager(clientCert)\n                    .build();\n        } else {\n            return SslContextBuilder.forServer(serverCert, serverKey, serverPass)\n                    .sslProvider(sslProvider)\n                    .trustManager(clientCert)\n                    .build();\n        }\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/TextHolder.java",
    "content": "package cc.blynk.server;\n\nimport cc.blynk.utils.properties.GCMProperties;\n\nimport static cc.blynk.utils.FileLoaderUtil.readAppResetEmailConfirmationTemplateAsString;\nimport static cc.blynk.utils.FileLoaderUtil.readAppResetEmailTemplateAsString;\nimport static cc.blynk.utils.FileLoaderUtil.readDynamicMailBody;\nimport static cc.blynk.utils.FileLoaderUtil.readRegisterEmailTemplate;\nimport static cc.blynk.utils.FileLoaderUtil.readResetPassLandingTemplateAsString;\nimport static cc.blynk.utils.FileLoaderUtil.readStaticMailBody;\nimport static cc.blynk.utils.FileLoaderUtil.readTemplateIdMailBody;\nimport static cc.blynk.utils.FileLoaderUtil.readTokenMailBody;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 27.03.18.\n */\npublic class TextHolder {\n\n    public volatile String tokenBody;\n    public final String dynamicMailBody;\n    public final String staticMailBody;\n    public final String templateIdMailBody;\n    public final String pushNotificationBody;\n    public final String resetPassLandingTemplate;\n    public final String appResetEmailTemplate;\n    public final String appResetEmailConfirmationTemplate;\n    public final String registerEmailTemplate;\n\n    TextHolder(GCMProperties gcmProperties) {\n        this.tokenBody = readTokenMailBody();\n        this.dynamicMailBody = readDynamicMailBody();\n        this.staticMailBody = readStaticMailBody();\n        this.templateIdMailBody = readTemplateIdMailBody();\n        this.pushNotificationBody = gcmProperties.getNotificationBody();\n        this.resetPassLandingTemplate = readResetPassLandingTemplateAsString();\n        this.appResetEmailTemplate = readAppResetEmailTemplateAsString();\n        this.appResetEmailConfirmationTemplate = readAppResetEmailConfirmationTemplateAsString();\n        this.registerEmailTemplate = readRegisterEmailTemplate();\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/common/BaseSimpleChannelInboundHandler.java",
    "content": "package cc.blynk.server.common;\n\nimport cc.blynk.server.core.protocol.exceptions.BaseServerException;\nimport cc.blynk.server.core.protocol.model.messages.MessageBase;\nimport cc.blynk.server.core.session.StateHolderBase;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.ChannelInboundHandlerAdapter;\nimport io.netty.util.ReferenceCountUtil;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.core.protocol.handlers.DefaultExceptionHandler.handleBaseServerException;\nimport static cc.blynk.server.core.protocol.handlers.DefaultExceptionHandler.handleGeneralException;\nimport static cc.blynk.server.internal.CommonByteBufUtil.illegalCommand;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/3/2015.\n */\npublic abstract class BaseSimpleChannelInboundHandler<I> extends ChannelInboundHandlerAdapter {\n\n    protected static final Logger log = LogManager.getLogger(BaseSimpleChannelInboundHandler.class);\n\n    private final Class<I> type;\n\n    protected BaseSimpleChannelInboundHandler(Class<I> type) {\n        this.type = type;\n    }\n\n    private static int getMsgId(Object o) {\n        if (o instanceof MessageBase) {\n            return ((MessageBase) o).id;\n        }\n        return 0;\n    }\n\n    @Override\n    @SuppressWarnings(\"unchecked\")\n    public void channelRead(ChannelHandlerContext ctx, Object msg) {\n        if (type.isInstance(msg)) {\n            try {\n                messageReceived(ctx, (I) msg);\n            } catch (NumberFormatException nfe) {\n                log.debug(\"Error parsing number. {}\", nfe.getMessage());\n                ctx.writeAndFlush(illegalCommand(getMsgId(msg)), ctx.voidPromise());\n            } catch (BaseServerException bse) {\n                handleBaseServerException(ctx, bse, getMsgId(msg));\n            } catch (Exception e) {\n                handleGeneralException(ctx, e);\n            } finally {\n                ReferenceCountUtil.release(msg);\n            }\n        }\n    }\n\n    /**\n     * <strong>Please keep in mind that this method will be renamed to\n     * {@code messageReceived(ChannelHandlerContext, I)} in 5.0.</strong>\n     *\n     * Is called for each message of type {@link I}.\n     *\n     * @param ctx           the {@link ChannelHandlerContext} which this SimpleChannelInboundHandler\n     *                      belongs to\n     * @param msg           the message to handle\n     */\n    public abstract void messageReceived(ChannelHandlerContext ctx, I msg);\n\n    public abstract StateHolderBase getState();\n\n    @Override\n    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {\n        handleGeneralException(ctx, cause);\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/common/handlers/AlreadyLoggedHandler.java",
    "content": "package cc.blynk.server.common.handlers;\n\nimport cc.blynk.server.core.protocol.model.messages.MessageBase;\nimport cc.blynk.server.core.protocol.model.messages.appllication.LoginMessage;\nimport io.netty.channel.ChannelHandler;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.SimpleChannelInboundHandler;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.core.protocol.handlers.DefaultExceptionHandler.handleGeneralException;\nimport static cc.blynk.server.internal.CommonByteBufUtil.alreadyRegistered;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n */\n@ChannelHandler.Sharable\npublic class AlreadyLoggedHandler extends SimpleChannelInboundHandler<MessageBase> {\n\n    private static final Logger log = LogManager.getLogger(AlreadyLoggedHandler.class);\n\n    @Override\n    protected void channelRead0(ChannelHandlerContext ctx, MessageBase msg) {\n        if (msg instanceof LoginMessage) {\n            if (ctx.channel().isWritable()) {\n                ctx.writeAndFlush(alreadyRegistered(msg.id), ctx.voidPromise());\n            }\n        } else {\n            if (log.isDebugEnabled()) {\n                log.debug(\"Hardware not logged. {}. Closing.\", ctx.channel().remoteAddress());\n            }\n            ctx.close();\n        }\n    }\n\n    @Override\n    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {\n        handleGeneralException(ctx, cause);\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/common/handlers/UserNotLoggedHandler.java",
    "content": "package cc.blynk.server.common.handlers;\n\nimport cc.blynk.server.core.protocol.model.messages.MessageBase;\nimport cc.blynk.server.core.protocol.model.messages.appllication.RegisterMessage;\nimport io.netty.channel.ChannelHandler;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.SimpleChannelInboundHandler;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.core.protocol.handlers.DefaultExceptionHandler.handleGeneralException;\nimport static cc.blynk.server.internal.CommonByteBufUtil.notAllowed;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n */\n@ChannelHandler.Sharable\npublic class UserNotLoggedHandler extends SimpleChannelInboundHandler<MessageBase> {\n\n    private static final Logger log = LogManager.getLogger(Logger.class);\n\n    @Override\n    protected void channelRead0(ChannelHandlerContext ctx, MessageBase msg) {\n        log.debug(\"User not logged. {}. Closing.\", ctx.channel().remoteAddress());\n        if (msg instanceof RegisterMessage) {\n            if (ctx.channel().isWritable()) {\n                ctx.writeAndFlush(notAllowed(msg.id), ctx.voidPromise());\n            }\n        }\n        ctx.close();\n    }\n\n    @Override\n    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {\n        handleGeneralException(ctx, cause);\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/common/handlers/logic/PingLogic.java",
    "content": "package cc.blynk.server.common.handlers.logic;\n\nimport io.netty.channel.ChannelHandlerContext;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic final class PingLogic {\n\n    private PingLogic() {\n    }\n\n    public static void messageReceived(ChannelHandlerContext ctx, int messageId) {\n        if (ctx.channel().isWritable()) {\n            ctx.writeAndFlush(ok(messageId), ctx.voidPromise());\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/BlockingIOProcessor.java",
    "content": "package cc.blynk.server.core;\n\nimport cc.blynk.utils.BlynkTPFactory;\n\nimport java.io.Closeable;\nimport java.util.concurrent.ArrayBlockingQueue;\nimport java.util.concurrent.ThreadPoolExecutor;\nimport java.util.concurrent.TimeUnit;\n\n/**\n * Wrapper around ThreadPoolExecutor that should perform blocking IO operations.\n * Due to async nature of netty performing Blocking operations withing netty pipeline\n * will cause performance issues. So Blocking operations should always\n * executed via this wrapper.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 07.04.15.\n */\npublic class BlockingIOProcessor implements Closeable {\n\n    private static final int MINIMUM_ALLOWED_POOL_SIZE = 3;\n\n    //pool for messaging\n    public final ThreadPoolExecutor messagingExecutor;\n\n    //DB pool is needed as in case DB goes down messaging still should work\n    public final ThreadPoolExecutor dbExecutor;\n\n    //DB pool is needed as in case DB goes down messaging still should work\n    public final ThreadPoolExecutor dbReportingExecutor;\n\n    public final ThreadPoolExecutor dbGetServerExecutor;\n\n    //separate pool for history graph data\n    public final ThreadPoolExecutor historyExecutor;\n\n    public BlockingIOProcessor(int poolSize, int maxQueueSize) {\n        //pool size can't be less than 3.\n        poolSize = Math.max(MINIMUM_ALLOWED_POOL_SIZE, poolSize);\n        this.messagingExecutor = new ThreadPoolExecutor(\n                poolSize / 4, poolSize / 3,\n                2L, TimeUnit.MINUTES,\n                new ArrayBlockingQueue<>(maxQueueSize),\n                BlynkTPFactory.build(\"Messaging\")\n        );\n\n        this.dbExecutor = new ThreadPoolExecutor(\n                poolSize / 3,\n                poolSize / 2, 2L,\n                TimeUnit.MINUTES,\n                new ArrayBlockingQueue<>(250),\n                BlynkTPFactory.build(\"db\"));\n        //local server doesn't use DB usually, so this thread may be not necessary\n        this.dbExecutor.allowCoreThreadTimeOut(true);\n\n        this.dbReportingExecutor = new ThreadPoolExecutor(\n                1,\n                1, 2L,\n                TimeUnit.MINUTES,\n                new ArrayBlockingQueue<>(100),\n                BlynkTPFactory.build(\"reporting-db\"));\n\n        this.dbGetServerExecutor = new ThreadPoolExecutor(poolSize, poolSize, 2L,\n                TimeUnit.MINUTES, new ArrayBlockingQueue<>(250),\n                BlynkTPFactory.build(\"getServer\"));\n\n        this.historyExecutor = new ThreadPoolExecutor(poolSize / 4, poolSize / 2, 2L,\n                TimeUnit.MINUTES, new ArrayBlockingQueue<>(250),\n                BlynkTPFactory.build(\"history\"));\n    }\n\n    public void execute(Runnable task) {\n        messagingExecutor.execute(task);\n    }\n\n    public void executeDB(Runnable task) {\n        dbExecutor.execute(task);\n    }\n\n    public void executeReportingDB(Runnable task) {\n        dbExecutor.execute(task);\n    }\n\n    public void executeHistory(Runnable task) {\n        historyExecutor.execute(task);\n    }\n\n    public void executeDBGetServer(Runnable task) {\n        dbGetServerExecutor.execute(task);\n    }\n\n    @Override\n    public void close() {\n        dbExecutor.shutdown();\n        messagingExecutor.shutdown();\n        historyExecutor.shutdown();\n        dbGetServerExecutor.shutdown();\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/dao/CSVGenerator.java",
    "content": "package cc.blynk.server.core.dao;\n\nimport cc.blynk.server.core.model.DataStream;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.widgets.outputs.graph.GraphGranularityType;\nimport cc.blynk.server.core.protocol.exceptions.NoDataException;\nimport io.netty.util.CharsetUtil;\n\nimport java.io.BufferedWriter;\nimport java.io.OutputStream;\nimport java.io.OutputStreamWriter;\nimport java.nio.ByteBuffer;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.zip.GZIPOutputStream;\n\nimport static cc.blynk.utils.FileUtils.CSV_DIR;\nimport static cc.blynk.utils.FileUtils.writeBufToCsv;\n\n/**\n * Simply generates CSV file from reporting data.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 08.03.17.\n */\npublic class CSVGenerator {\n\n    //43200 == 30 * 24 * 60 is minutes points for 1 month\n    //todo move to limits\n    private final static int FETCH_COUNT = Integer.parseInt(System.getProperty(\"csv.export.data.points.max\", \"43200\"));\n    private final ReportingDiskDao reportingDao;\n\n    CSVGenerator(ReportingDiskDao reportingDao) {\n        this.reportingDao = reportingDao;\n    }\n\n    public Path createCSV(User user, int dashId, int inDeviceId, PinType pinType, short pin, int... deviceIds)\n            throws Exception {\n        if (!DataStream.isValid(pin, pinType)) {\n            throw new IllegalStateException(\"Wrong pin format.\");\n        }\n\n        Path path = generateExportCSVPath(user.email, dashId, inDeviceId, pinType, pin);\n\n        try (OutputStream output = Files.newOutputStream(path);\n             BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(\n                     new GZIPOutputStream(output), CharsetUtil.US_ASCII))) {\n\n            int emptyDataCounter = 0;\n            for (int deviceId : deviceIds) {\n                ByteBuffer onePinData = reportingDao.getByteBufferFromDisk(user, dashId, deviceId,\n                        pinType, pin, FETCH_COUNT, GraphGranularityType.MINUTE, 0);\n                if (onePinData != null) {\n                    writeBufToCsv(writer, onePinData, deviceId);\n                } else {\n                    emptyDataCounter++;\n                }\n            }\n            if (emptyDataCounter == deviceIds.length) {\n                throw new NoDataException();\n            }\n        }\n\n        return path;\n    }\n\n    private static Path generateExportCSVPath(String email, int dashId, int deviceId, PinType pinType, short pin) {\n        return Paths.get(CSV_DIR, format(email, dashId, deviceId, pinType, pin));\n    }\n\n    public static final String EXPORT_CSV_EXTENSION = \".csv.gz\";\n\n    //\"%s_%s_%c%d.csv.gz\"\n    private static String format(String email, int dashId, int deviceId, PinType pinType, short pin) {\n        long now = System.currentTimeMillis();\n        return email + \"_\" + dashId + \"_\" + deviceId + \"_\"\n                + pinType.pintTypeChar + pin + \"_\" + now + EXPORT_CSV_EXTENSION;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/dao/FileManager.java",
    "content": "package cc.blynk.server.core.dao;\n\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.model.storage.key.DashPinPropertyStorageKey;\nimport cc.blynk.server.core.model.storage.key.DashPinStorageKey;\nimport cc.blynk.server.core.model.storage.key.PinPropertyStorageKey;\nimport cc.blynk.server.core.model.storage.key.PinStorageKey;\nimport cc.blynk.server.core.model.storage.value.PinStorageValue;\nimport cc.blynk.utils.FileUtils;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.FileSystems;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.PathMatcher;\nimport java.nio.file.Paths;\nimport java.nio.file.StandardOpenOption;\nimport java.text.SimpleDateFormat;\nimport java.util.Collections;\nimport java.util.Date;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentMap;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\nimport static java.nio.file.Files.createDirectories;\nimport static java.util.function.Function.identity;\n\n\n/**\n * Class responsible for saving/reading user data to/from disk.\n *\n * User: ddumanskiy\n * Date: 8/11/13\n * Time: 6:53 PM\n */\npublic class FileManager {\n\n    private static final Logger log = LogManager.getLogger(FileManager.class);\n    private static final String USER_FILE_EXTENSION = \".user\";\n\n    /**\n     * Folder where all user profiles are stored locally.\n     */\n    private Path dataDir;\n\n    private static final String DELETED_DATA_DIR_NAME = \"deleted\";\n    private static final String BACKUP_DATA_DIR_NAME = \"backup\";\n    private static final String CLONE_DATA_DIR_NAME = \"clone\";\n    private Path deletedDataDir;\n    private Path backupDataDir;\n    private String cloneDataDir;\n    private final String host;\n\n    public FileManager(String dataFolder, String host) {\n        if (dataFolder == null || dataFolder.isEmpty() || dataFolder.equals(\"/path\")) {\n            System.out.println(\"WARNING : '\" + dataFolder + \"' does not exists. \"\n                    + \"Please specify correct -dataFolder parameter.\");\n            dataFolder = Paths.get(System.getProperty(\"java.io.tmpdir\"), \"blynk\").toString();\n            System.out.println(\"Your data may be lost during server restart. Using temp folder : \" + dataFolder);\n        }\n        try {\n            Path dataFolderPath = Paths.get(dataFolder);\n            this.dataDir = createDirectories(dataFolderPath);\n            this.deletedDataDir = createDirectories(Paths.get(dataFolder, DELETED_DATA_DIR_NAME));\n            this.backupDataDir = createDirectories(Paths.get(dataFolder, BACKUP_DATA_DIR_NAME));\n            this.cloneDataDir = createDirectories(Paths.get(dataFolder, CLONE_DATA_DIR_NAME)).toString();\n        } catch (Exception e) {\n            Path tempDir = Paths.get(System.getProperty(\"java.io.tmpdir\"), \"blynk\");\n\n            System.out.println(\"WARNING : could not find folder '\" + dataFolder + \"'. \"\n                    + \"Please specify correct -dataFolder parameter.\");\n            System.out.println(\"Your data may be lost during server restart. Using temp folder : \"\n                    + tempDir.toString());\n\n            try {\n                this.dataDir = createDirectories(tempDir);\n                this.deletedDataDir = createDirectories(\n                        Paths.get(this.dataDir.toString(), DELETED_DATA_DIR_NAME));\n                this.backupDataDir = createDirectories(\n                        Paths.get(this.dataDir.toString(), BACKUP_DATA_DIR_NAME));\n                this.cloneDataDir = createDirectories(\n                        Paths.get(this.dataDir.toString(), CLONE_DATA_DIR_NAME)).toString();\n            } catch (Exception ioe) {\n                throw new RuntimeException(ioe);\n            }\n        }\n\n        this.host = host;\n        log.info(\"Using data dir '{}'\", dataDir);\n    }\n\n    public Path getDataDir() {\n        return dataDir;\n    }\n\n    public Path generateFileName(String email, String appName) {\n        return Paths.get(dataDir.toString(), email + \".\" + appName + USER_FILE_EXTENSION);\n    }\n\n    public Path generateBackupFileName(String email, String appName) {\n        return Paths.get(backupDataDir.toString(), email + \".\" + appName + \".user.\"\n                + new SimpleDateFormat(\"yyyy-MM-dd\").format(new Date()));\n    }\n\n    private Path generateOldFileName(String userName) {\n        return Paths.get(dataDir.toString(), \"u_\" + userName + USER_FILE_EXTENSION);\n    }\n\n    public boolean delete(String email, String appName) {\n        Path file = generateFileName(email, appName);\n        try {\n            FileUtils.move(file, this.deletedDataDir);\n        } catch (IOException e) {\n            log.debug(\"Failed to move file. {}\", e.getMessage());\n            return false;\n        }\n        return true;\n    }\n\n    public void overrideUserFile(User user) throws IOException {\n        Path path = generateFileName(user.email, user.appName);\n\n        JsonParser.writeUser(path.toFile(), user);\n\n        removeOldFile(user.email);\n    }\n\n    private void removeOldFile(String email) {\n        //this oldFileName is migration code. should be removed in future versions\n        Path oldFileName = generateOldFileName(email);\n        try {\n            Files.deleteIfExists(oldFileName);\n        } catch (Exception e) {\n            log.error(\"Error removing old file. {}\", oldFileName, e);\n        }\n    }\n\n    /**\n     * Loads all user profiles one by one from disk using dataDir as starting point.\n     *\n     * @return mapping between username and it's profile.\n     */\n    public ConcurrentMap<UserKey, User> deserializeUsers() {\n        log.debug(\"Starting reading user DB.\");\n\n        PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher(\"glob:**\" + USER_FILE_EXTENSION);\n        ConcurrentMap<UserKey, User> temp;\n        try {\n            temp = Files.walk(dataDir, 1).parallel()\n                    .filter(path -> Files.isRegularFile(path) && pathMatcher.matches(path))\n                    .flatMap(path -> {\n                        try {\n                            User user = JsonParser.parseUserFromFile(path);\n                            makeProfileChanges(user);\n\n                            return Stream.of(user);\n                        } catch (IOException ioe) {\n                            String errorMessage = ioe.getMessage();\n                            log.error(\"Error parsing file '{}'. Error : {}\", path, errorMessage);\n                            if (errorMessage != null\n                                    && (errorMessage.contains(\"end-of-input\")\n                                    || errorMessage.contains(\"Illegal character\"))) {\n                                return restoreFromBackup(path.getFileName());\n                            }\n                        }\n                        return Stream.empty();\n                    })\n                    .collect(Collectors.toConcurrentMap(UserKey::new, identity()));\n        } catch (Exception e) {\n            log.error(\"Error reading user profiles from disk. {}\", e.getMessage());\n            throw new RuntimeException(e);\n        }\n\n        log.debug(\"Reading user DB finished.\");\n        return temp;\n    }\n\n    private Stream<User> restoreFromBackup(Path restoreFileNamePath) {\n        log.info(\"Trying to recover from backup...\");\n        String filename = restoreFileNamePath.toString();\n        try {\n            File[] files = backupDataDir.toFile().listFiles(\n                    (dir, name) -> name.startsWith(filename)\n            );\n\n            File backupFile = FileUtils.getLatestFile(files);\n            if (backupFile == null) {\n                log.info(\"Didn't find any files for recovery :(.\");\n                return Stream.empty();\n            }\n            log.info(\"Found {}. You are lucky today :).\", backupFile.getAbsoluteFile());\n\n            User user = JsonParser.parseUserFromFile(backupFile);\n            makeProfileChanges(user);\n            //profile saver thread is launched after file manager is initialized.\n            //so making sure user profile will be saved\n            //this is not very important as profile will be updated by user anyway.\n            user.lastModifiedTs = System.currentTimeMillis() + 10 * 1000;\n            log.info(\"Restored.\", backupFile.getAbsoluteFile());\n            return Stream.of(user);\n        } catch (Exception e) {\n            //ignore\n            log.error(\"Restoring from backup failed. {}\", e.getMessage());\n        }\n        return Stream.empty();\n    }\n\n    //public is for tests only\n    public void makeProfileChanges(User user) {\n        if (user.email == null) {\n            user.email = user.name;\n        }\n        user.ip = host;\n        for (DashBoard dash : user.profile.dashBoards) {\n            user.profile.setOfflineDevice(dash);\n            if (dash.pinsStorage != null && dash.pinsStorage.size() > 0) {\n                int dashId = dash.id;\n                for (Map.Entry<PinStorageKey, PinStorageValue> pinsStorageEntry : dash.pinsStorage.entrySet()) {\n                    PinStorageKey key = pinsStorageEntry.getKey();\n                    DashPinStorageKey dashPinStorageKey;\n                    if (key instanceof PinPropertyStorageKey) {\n                        dashPinStorageKey = new DashPinPropertyStorageKey(dashId, (PinPropertyStorageKey) key);\n                    } else {\n                        dashPinStorageKey = new DashPinStorageKey(dashId, key);\n                    }\n                    PinStorageValue value = pinsStorageEntry.getValue();\n                    user.profile.pinsStorage.put(dashPinStorageKey, value);\n                }\n                dash.pinsStorage = Collections.emptyMap();\n            }\n        }\n    }\n\n    public Map<String, Integer> getUserProfilesSize() {\n        Map<String, Integer> userProfileSize = new HashMap<>();\n        File[] files = dataDir.toFile().listFiles();\n        if (files != null) {\n            for (File file : files) {\n                if (file.isFile() && file.getName().endsWith(USER_FILE_EXTENSION)) {\n                    userProfileSize.put(file.getName(), (int) file.length());\n                }\n            }\n        }\n        return userProfileSize;\n    }\n\n    public boolean writeCloneProjectToDisk(String token, String json) {\n        try {\n            Path path = Paths.get(cloneDataDir, token);\n            Files.write(path, json.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE);\n            return true;\n        } catch (Exception e) {\n            log.error(\"Error saving cloned project to disk. {}\", e.getMessage());\n        }\n        return false;\n    }\n\n    public String readClonedProjectFromDisk(String token) {\n        Path path = Paths.get(cloneDataDir, token);\n        try {\n            return new String(Files.readAllBytes(path), StandardCharsets.UTF_8);\n        } catch (Exception e) {\n            log.warn(\"Didn't find cloned project on disk. Path {}. Reason {}\", path.toString(), e.getMessage());\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/dao/RegularTokenManager.java",
    "content": "package cc.blynk.server.core.dao;\n\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Device;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.concurrent.ConcurrentHashMap;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 22.09.15.\n */\nclass RegularTokenManager {\n\n    private static final Logger log = LogManager.getLogger(RegularTokenManager.class);\n\n    final ConcurrentHashMap<String, TokenValue> cache;\n\n    RegularTokenManager(Collection<User> users) {\n        ///in average user has 2 devices\n        this.cache = new ConcurrentHashMap<>(users.size() == 0 ? 16 : users.size() * 2);\n        for (User user : users) {\n            if (user.profile != null) {\n                for (DashBoard dashBoard : user.profile.dashBoards) {\n                    for (Device device : dashBoard.devices) {\n                        if (device.token != null) {\n                            cache.put(device.token, new TokenValue(user, dashBoard, device));\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    String assignToken(User user, DashBoard dash, Device device, String newToken, boolean isTemporary) {\n        // Clean old token from cache if exists.\n        String oldToken = deleteDeviceToken(device.token);\n\n        //assign new token\n        device.token = newToken;\n        TokenValue tokenValue = isTemporary\n                ? new TemporaryTokenValue(user, dash, device)\n                : new TokenValue(user, dash, device);\n        cache.put(newToken, tokenValue);\n\n        user.lastModifiedTs = System.currentTimeMillis();\n\n        log.debug(\"Generated token for user {}, dashId {}, deviceId {} is {}.\",\n                user.email, dash.id, device.id, newToken);\n\n        return oldToken;\n    }\n\n    String deleteDeviceToken(String deviceToken) {\n        if (deviceToken != null) {\n            cache.remove(deviceToken);\n            return deviceToken;\n        }\n        return null;\n    }\n\n    TokenValue getUserByToken(String token) {\n        return cache.get(token);\n    }\n\n    String[] deleteProject(DashBoard dash) {\n        ArrayList<String> removedTokens = new ArrayList<>(dash.devices.length);\n        for (Device device : dash.devices) {\n            if (device != null && device.token != null) {\n                cache.remove(device.token);\n                removedTokens.add(device.token);\n            }\n        }\n        return removedTokens.toArray(new String[0]);\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/dao/ReportingDiskDao.java",
    "content": "package cc.blynk.server.core.dao;\n\nimport cc.blynk.server.core.dao.functions.GraphFunction;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.outputs.graph.AggregationFunctionType;\nimport cc.blynk.server.core.model.widgets.outputs.graph.GraphGranularityType;\nimport cc.blynk.server.core.model.widgets.outputs.graph.Superchart;\nimport cc.blynk.server.core.protocol.exceptions.NoDataException;\nimport cc.blynk.server.core.reporting.GraphPinRequest;\nimport cc.blynk.server.core.reporting.average.AverageAggregatorProcessor;\nimport cc.blynk.server.core.reporting.raw.BaseReportingKey;\nimport cc.blynk.server.core.reporting.raw.GraphValue;\nimport cc.blynk.server.core.reporting.raw.RawDataCacheForGraphProcessor;\nimport cc.blynk.server.core.reporting.raw.RawDataProcessor;\nimport cc.blynk.utils.FileUtils;\nimport cc.blynk.utils.NumberUtil;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.io.Closeable;\nimport java.io.IOException;\nimport java.nio.ByteBuffer;\nimport java.nio.file.DirectoryStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.TreeMap;\nimport java.util.function.Function;\n\nimport static cc.blynk.server.internal.EmptyArraysUtil.EMPTY_BYTES;\nimport static cc.blynk.utils.FileUtils.CSV_DIR;\nimport static cc.blynk.utils.FileUtils.SIZE_OF_REPORT_ENTRY;\nimport static cc.blynk.utils.StringUtils.DEVICE_SEPARATOR;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/18/2015.\n */\npublic class ReportingDiskDao implements Closeable {\n\n    private static final Logger log = LogManager.getLogger(ReportingDiskDao.class);\n\n    public final AverageAggregatorProcessor averageAggregator;\n    public final RawDataCacheForGraphProcessor rawDataCacheForGraphProcessor;\n    public final RawDataProcessor rawDataProcessor;\n    public final CSVGenerator csvGenerator;\n\n    public final String dataFolder;\n\n    private final boolean enableRawDbDataStore;\n\n    private static final Function<Path, Boolean> NO_FILTER = s -> true;\n\n    //for test only\n    public ReportingDiskDao(String reportingFolder, AverageAggregatorProcessor averageAggregator,\n                            boolean isEnabled) {\n        this.averageAggregator = averageAggregator;\n        this.rawDataCacheForGraphProcessor = new RawDataCacheForGraphProcessor();\n        this.dataFolder = reportingFolder;\n        this.enableRawDbDataStore = isEnabled;\n        this.rawDataProcessor = new RawDataProcessor(enableRawDbDataStore);\n        this.csvGenerator = new CSVGenerator(this);\n    }\n\n    public ReportingDiskDao(String reportingFolder, boolean isEnabled) {\n        this.averageAggregator = new AverageAggregatorProcessor(reportingFolder);\n        this.rawDataCacheForGraphProcessor = new RawDataCacheForGraphProcessor();\n        this.dataFolder = reportingFolder;\n        this.enableRawDbDataStore = isEnabled;\n        this.rawDataProcessor = new RawDataProcessor(enableRawDbDataStore);\n        this.csvGenerator = new CSVGenerator(this);\n        createCSVFolder();\n    }\n\n    private static void createCSVFolder() {\n        try {\n            Files.createDirectories(Paths.get(CSV_DIR));\n        } catch (IOException ioe) {\n            log.error(\"Error creating temp '{}' folder for csv export data.\", CSV_DIR);\n        }\n    }\n\n    public ByteBuffer getByteBufferFromDisk(User user, int dashId, int deviceId,\n                                            PinType pinType, short pin, int count,\n                                            GraphGranularityType type, int skipCount) {\n        Path userDataFile = Paths.get(\n                dataFolder,\n                FileUtils.getUserStorageDir(user.email, user.appName),\n                generateFilename(dashId, deviceId, pinType, pin, type)\n        );\n        if (Files.exists(userDataFile)) {\n            try {\n                return FileUtils.read(userDataFile, count, skipCount);\n            } catch (Exception ioe) {\n                log.error(ioe);\n            }\n        }\n\n        return null;\n    }\n\n    private static boolean hasData(byte[][] data) {\n        for (byte[] pinData : data) {\n            if (pinData.length > 0) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    private ByteBuffer getDataForTag(User user, GraphPinRequest graphPinRequest) {\n        TreeMap<Long, GraphFunction> data = new TreeMap<>();\n        for (int deviceId : graphPinRequest.deviceIds) {\n            ByteBuffer localByteBuf = getByteBufferFromDisk(user,\n                    graphPinRequest.dashId, deviceId,\n                    graphPinRequest.pinType, graphPinRequest.pin,\n                    graphPinRequest.count, graphPinRequest.type,\n                    graphPinRequest.skipCount\n            );\n            addBufferToResult(data, graphPinRequest.functionType, localByteBuf);\n        }\n\n        return toByteBuf(data);\n    }\n\n    private static void addBufferToResult(TreeMap<Long, GraphFunction> data,\n                                          AggregationFunctionType functionType,\n                                          ByteBuffer localByteBuf) {\n        if (localByteBuf != null) {\n            while (localByteBuf.hasRemaining()) {\n                double newVal = localByteBuf.getDouble();\n                Long ts = localByteBuf.getLong();\n                GraphFunction graphFunctionObj = data.get(ts);\n                if (graphFunctionObj == null) {\n                    graphFunctionObj = functionType.produce();\n                    data.put(ts, graphFunctionObj);\n                }\n                graphFunctionObj.apply(newVal);\n            }\n        }\n    }\n\n    private static ByteBuffer toByteBuf(TreeMap<Long, GraphFunction> data) {\n        ByteBuffer result = ByteBuffer.allocate(data.size() * SIZE_OF_REPORT_ENTRY);\n        for (Map.Entry<Long, GraphFunction> entry : data.entrySet()) {\n            result.putDouble(entry.getValue().getResult())\n                    .putLong(entry.getKey());\n        }\n        return result;\n    }\n\n    private ByteBuffer getByteBufferFromDisk(User user, GraphPinRequest graphPinRequest) {\n        try {\n            if (graphPinRequest.isTag) {\n                return getDataForTag(user, graphPinRequest);\n            } else {\n                return getByteBufferFromDisk(user,\n                        graphPinRequest.dashId, graphPinRequest.deviceId,\n                        graphPinRequest.pinType, graphPinRequest.pin,\n                        graphPinRequest.count, graphPinRequest.type,\n                        graphPinRequest.skipCount\n                );\n            }\n        } catch (Exception e) {\n            log.error(\"Error getting data from disk.\", e);\n            return null;\n        }\n    }\n\n    private Path getUserReportingFolderPath(User user) {\n        return Paths.get(dataFolder, FileUtils.getUserStorageDir(user.email, user.appName));\n    }\n\n    public int delete(User user) {\n        return delete(user, NO_FILTER);\n    }\n\n    public int delete(User user, Function<Path, Boolean> filter) {\n        log.debug(\"Removing all reporting data for {}\", user.email);\n        Path reportingFolderPath = getUserReportingFolderPath(user);\n\n        int removedFilesCounter = 0;\n        try {\n            if (Files.exists(reportingFolderPath)) {\n                try (DirectoryStream<Path> reportingFolder = Files.newDirectoryStream(reportingFolderPath, \"*\")) {\n                    for (Path reportingFile : reportingFolder) {\n                        if (filter.apply(reportingFile)) {\n                            log.trace(\"Removing {}\", reportingFile);\n                            FileUtils.deleteQuietly(reportingFile);\n                            removedFilesCounter++;\n                        }\n                    }\n                }\n            }\n        } catch (Exception e) {\n            log.error(\"Error removing file : {}.\", reportingFolderPath);\n        }\n        return removedFilesCounter;\n    }\n\n    private static boolean containsPrefix(List<String> prefixes, String filename) {\n        for (String prefix : prefixes) {\n            if (filename.startsWith(prefix)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    private static String generateFilename(int dashId, int deviceId, char pinType, short pin, String type) {\n        return generateFilenamePrefix(dashId, deviceId) + pinType + pin + \"_\" + type + \".bin\";\n    }\n\n    private static String generateFilenamePrefix(int dashId, int deviceId, String pin) {\n        return generateFilenamePrefix(dashId, deviceId) + pin + \"_\";\n    }\n\n    private static String generateFilenamePrefix(int dashId, int deviceId) {\n        return \"history_\" + dashId + DEVICE_SEPARATOR + deviceId + \"_\";\n    }\n\n    private static void delete(String userReportingDir, int dashId, int deviceId, PinType pinType, short pin,\n                               GraphGranularityType reportGranularity) {\n        Path userDataFile = Paths.get(userReportingDir,\n                generateFilename(dashId, deviceId, pinType, pin, reportGranularity));\n        FileUtils.deleteQuietly(userDataFile);\n    }\n\n    public static String generateFilename(int dashId, int deviceId,\n                                          PinType pinType, short pin, GraphGranularityType type) {\n        return generateFilename(dashId, deviceId, pinType.pintTypeChar, pin, type.label);\n    }\n\n    public int delete(User user, int dashId, int deviceId, String[] pins) throws IOException {\n        log.debug(\"Removing selected pin data for dashId {}, deviceId {}.\", dashId, deviceId);\n        Path userReportingPath = getUserReportingFolderPath(user);\n\n        int count = 0;\n        List<String> prefixes = new ArrayList<>();\n        for (String pin : pins) {\n            prefixes.add(generateFilenamePrefix(dashId, deviceId, pin));\n        }\n        try (DirectoryStream<Path> userReportingFolder = Files.newDirectoryStream(userReportingPath, \"*\")) {\n            for (Path reportingFile : userReportingFolder) {\n                String userFileName = reportingFile.getFileName().toString();\n                if (containsPrefix(prefixes, userFileName)) {\n                    FileUtils.deleteQuietly(reportingFile);\n                    count++;\n                }\n            }\n        }\n        return count;\n    }\n\n    public int delete(User user, int dashId, int deviceId) throws IOException {\n        log.debug(\"Removing all pin data for dashId {}, deviceId {}.\", dashId, deviceId);\n        Path userReportingPath = getUserReportingFolderPath(user);\n\n        int count = 0;\n        if (Files.exists(userReportingPath)) {\n            String fileNamePrefix = generateFilenamePrefix(dashId, deviceId);\n            try (DirectoryStream<Path> userReportingFolder = Files.newDirectoryStream(userReportingPath, \"*\")) {\n                for (Path reportingFile : userReportingFolder) {\n                    if (reportingFile.getFileName().toString().startsWith(fileNamePrefix)) {\n                        FileUtils.deleteQuietly(reportingFile);\n                        count++;\n                    }\n                }\n            }\n        }\n        return count;\n    }\n\n    public void delete(User user, int dashId, int deviceId, PinType pinType, short pin) {\n        log.debug(\"Removing {}{} pin data for dashId {}, deviceId {}.\", pinType.pintTypeChar, pin, dashId, deviceId);\n        String userReportingDir = getUserReportingFolderPath(user).toString();\n\n        for (GraphGranularityType reportGranularity : GraphGranularityType.getValues()) {\n            delete(userReportingDir, dashId, deviceId, pinType, pin, reportGranularity);\n        }\n    }\n\n    public void process(User user, DashBoard dash, int deviceId, short pin, PinType pinType, String value, long ts) {\n        try {\n            double doubleVal = NumberUtil.parseDouble(value);\n            process(user, dash, deviceId, pin, pinType, value, ts, doubleVal);\n        } catch (Exception e) {\n            //just in case\n            log.trace(\"Error collecting reporting entry.\");\n        }\n    }\n\n    private void process(User user, DashBoard dash, int deviceId, short pin, PinType pinType,\n                         String value, long ts, double doubleVal) {\n        if (enableRawDbDataStore) {\n            rawDataProcessor.collect(\n                    new BaseReportingKey(user.email, user.appName, dash.id, deviceId, pinType, pin),\n                    ts, value, doubleVal);\n        }\n\n        //not a number, nothing to aggregate\n        if (doubleVal == NumberUtil.NO_RESULT) {\n            return;\n        }\n\n        //store history data only for the pins assigned to the superchart\n        Widget widgetWithLogPins = user.profile.getWidgetWithLoggedPin(dash, deviceId, pin, pinType);\n        if (widgetWithLogPins != null) {\n            BaseReportingKey key = new BaseReportingKey(user.email, user.appName, dash.id, deviceId, pinType, pin);\n            averageAggregator.collect(key, ts, doubleVal);\n            if (widgetWithLogPins instanceof Superchart) {\n                if (((Superchart) widgetWithLogPins).hasLivePeriodsSelected()) {\n                    rawDataCacheForGraphProcessor.collect(key, new GraphValue(doubleVal, ts));\n                }\n            }\n        }\n    }\n\n    public byte[][] getReportingData(User user, GraphPinRequest[] requestedPins) throws NoDataException {\n        byte[][] values = new byte[requestedPins.length][];\n\n        for (int i = 0; i < requestedPins.length; i++) {\n            GraphPinRequest graphPinRequest = requestedPins[i];\n            log.debug(\"Getting data for graph pin : {}.\", graphPinRequest);\n            if (graphPinRequest.isValid()) {\n                ByteBuffer byteBuffer = graphPinRequest.isLiveData()\n                        //live graph data is not on disk but in memory\n                        ? rawDataCacheForGraphProcessor.getLiveGraphData(user, graphPinRequest)\n                        : getByteBufferFromDisk(user, graphPinRequest);\n                values[i] = byteBuffer == null ? EMPTY_BYTES : byteBuffer.array();\n            } else {\n                values[i] = EMPTY_BYTES;\n            }\n        }\n\n        if (!hasData(values)) {\n            throw new NoDataException();\n        }\n\n        return values;\n    }\n\n    @Override\n    public void close() {\n        System.out.println(\"Stopping aggregator...\");\n        this.averageAggregator.close();\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/dao/SessionDao.java",
    "content": "package cc.blynk.server.core.dao;\n\nimport cc.blynk.server.core.model.auth.Session;\nimport cc.blynk.server.core.model.auth.User;\nimport io.netty.channel.Channel;\nimport io.netty.channel.EventLoop;\nimport io.netty.channel.group.DefaultChannelGroup;\nimport io.netty.handler.codec.http.FullHttpRequest;\nimport io.netty.handler.codec.http.HttpHeaderNames;\nimport io.netty.handler.codec.http.cookie.Cookie;\nimport io.netty.handler.codec.http.cookie.ServerCookieDecoder;\nimport io.netty.util.AttributeKey;\nimport io.netty.util.concurrent.GlobalEventExecutor;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.util.Set;\nimport java.util.UUID;\nimport java.util.concurrent.ConcurrentHashMap;\n\n/**\n * Holds session info related to specific user.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/18/2015.\n */\npublic class SessionDao {\n\n    public final static AttributeKey<User> userAttributeKey = AttributeKey.valueOf(\"user\");\n    private static final Logger log = LogManager.getLogger(SessionDao.class);\n\n    public final ConcurrentHashMap<UserKey, Session> userSession = new ConcurrentHashMap<>();\n\n    public Session get(UserKey userKey) {\n        return userSession.get(userKey);\n    }\n\n    //threadsafe\n    public Session getOrCreateSessionByUser(UserKey key, EventLoop initialEventLoop) {\n        Session group = userSession.get(key);\n        //only one side came\n        if (group == null) {\n            Session value = new Session(initialEventLoop);\n            group = userSession.putIfAbsent(key, value);\n            if (group == null) {\n                log.trace(\"Creating unique session for user: {}\", key);\n                return value;\n            }\n        }\n\n        return group;\n    }\n\n\n\n\n    public static final String SESSION_COOKIE = \"session\";\n    private final ConcurrentHashMap<String, User> httpSession = new ConcurrentHashMap<>();\n\n    public String generateNewSession(User user) {\n        String sessionId = UUID.randomUUID().toString();\n        httpSession.put(sessionId, user);\n        return sessionId;\n    }\n\n    public boolean isValid(Cookie cookie) {\n        return cookie.name().equals(SESSION_COOKIE);\n    }\n\n    public User getUserFromCookie(FullHttpRequest request) {\n        String cookieString = request.headers().get(HttpHeaderNames.COOKIE);\n\n        if (cookieString != null) {\n            Set<Cookie> cookies = ServerCookieDecoder.STRICT.decode(cookieString);\n            if (!cookies.isEmpty()) {\n                for (Cookie cookie : cookies) {\n                    if (isValid(cookie)) {\n                        String token = cookie.value();\n                        return httpSession.get(token);\n                    }\n                }\n            }\n        }\n\n        return null;\n    }\n\n    public void closeHardwareChannelByDashId(UserKey userKey, int dashId) {\n        Session session = userSession.get(userKey);\n        session.closeHardwareChannelByDashId(dashId);\n    }\n\n    public void close() {\n        System.out.println(\"Closing all sockets...\");\n        DefaultChannelGroup allChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);\n        userSession.forEach((userKey, session) -> {\n            allChannels.addAll(session.appChannels);\n            allChannels.addAll(session.hardwareChannels);\n        });\n        allChannels.close().awaitUninterruptibly();\n    }\n\n    public void closeAppChannelsByUser(UserKey userKey) {\n        Session session = userSession.get(userKey);\n        if (session != null) {\n            for (Channel appChannel : session.appChannels) {\n                appChannel.close();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/dao/SharedTokenManager.java",
    "content": "package cc.blynk.server.core.dao;\n\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.util.Collection;\nimport java.util.concurrent.ConcurrentHashMap;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 22.09.15.\n */\npublic class SharedTokenManager {\n\n    private static final Logger log = LogManager.getLogger(SharedTokenManager.class);\n\n    public static final String ALL = \"*\";\n\n    final ConcurrentHashMap<String, SharedTokenValue> cache;\n\n    SharedTokenManager(Collection<User> users) {\n        this.cache = new ConcurrentHashMap<>();\n        for (User user : users) {\n            for (DashBoard dashBoard : user.profile.dashBoards) {\n                if (dashBoard.sharedToken != null) {\n                    cache.put(dashBoard.sharedToken, new SharedTokenValue(user, dashBoard.id));\n                }\n            }\n        }\n    }\n\n    public void assignToken(User user, DashBoard dash, String newToken) {\n        // Clean old token from cache if exists.\n        String oldToken = dash.sharedToken;\n        if (oldToken != null) {\n            cache.remove(oldToken);\n        }\n\n        //assign new token\n        dash.sharedToken = newToken;\n        dash.updatedAt = System.currentTimeMillis();\n        user.lastModifiedTs = dash.updatedAt;\n\n        cache.put(newToken, new SharedTokenValue(user, dash.id));\n\n        log.info(\"Generated shared token for user {} and dashId {} is {}.\", user.email, dash.id, newToken);\n    }\n\n    SharedTokenValue getUserByToken(String token) {\n        return cache.get(token);\n    }\n\n    void deleteProject(DashBoard dash) {\n        if (dash.sharedToken != null) {\n            cache.remove(dash.sharedToken);\n            log.info(\"Deleted {} shared token.\", dash.sharedToken);\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/dao/SharedTokenValue.java",
    "content": "package cc.blynk.server.core.dao;\n\nimport cc.blynk.server.core.model.auth.User;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 17.11.16.\n */\npublic class SharedTokenValue {\n\n    public final User user;\n\n    public final int dashId;\n\n    SharedTokenValue(User user, int dashId) {\n        this.user = user;\n        this.dashId = dashId;\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/dao/TemporaryTokenValue.java",
    "content": "package cc.blynk.server.core.dao;\n\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Device;\n\nimport java.util.concurrent.TimeUnit;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 16.11.16.\n */\npublic final class TemporaryTokenValue extends TokenValue {\n\n    private static final long EXPIRATION_PERIOD = TimeUnit.DAYS.toMillis(7);\n    private final long created;\n\n    TemporaryTokenValue(User user, DashBoard dash, Device device) {\n        super(user, dash, device);\n        this.created = System.currentTimeMillis();\n    }\n\n    @Override\n    public boolean isExpired(long now) {\n        return created + EXPIRATION_PERIOD < now;\n    }\n\n    @Override\n    public boolean isTemporary() {\n        return true;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/dao/TokenManager.java",
    "content": "package cc.blynk.server.core.dao;\n\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.db.DBManager;\nimport cc.blynk.utils.TokenGeneratorUtil;\n\nimport java.util.Collection;\nimport java.util.concurrent.ConcurrentMap;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 14.10.16.\n */\npublic class TokenManager {\n\n    private final RegularTokenManager regularTokenManager;\n    private final SharedTokenManager sharedTokenManager;\n    private final DBManager dbManager;\n    private final String host;\n\n    public TokenManager(ConcurrentMap<UserKey, User> users, DBManager dbManager, String host) {\n        Collection<User> allUsers = users.values();\n        this.regularTokenManager = new RegularTokenManager(allUsers);\n        this.sharedTokenManager = new SharedTokenManager(allUsers);\n        this.dbManager = dbManager;\n        this.host = host;\n    }\n\n    public void deleteDevice(Device device) {\n        String token = device.token;\n        if (token != null) {\n            regularTokenManager.deleteDeviceToken(token);\n            dbManager.removeToken(token);\n        }\n    }\n\n    public void deleteDash(DashBoard dash) {\n        //todo clear shared token from DB?\n        sharedTokenManager.deleteProject(dash);\n        String[] removedTokens = regularTokenManager.deleteProject(dash);\n        dbManager.removeToken(removedTokens);\n    }\n\n    public TokenValue getTokenValueByToken(String token) {\n        return regularTokenManager.getUserByToken(token);\n    }\n\n    public SharedTokenValue getUserBySharedToken(String token) {\n        return sharedTokenManager.getUserByToken(token);\n    }\n\n    public void assignToken(User user, DashBoard dash, Device device, String newToken, boolean isTemporary) {\n        String oldToken = regularTokenManager.assignToken(user, dash, device, newToken, isTemporary);\n\n        dbManager.assignServerToToken(newToken, host, user.email, dash.id, device.id);\n        if (oldToken != null) {\n            dbManager.removeToken(oldToken);\n        }\n    }\n\n    public void assignToken(User user, DashBoard dash, Device device, String newToken) {\n        assignToken(user, dash, device, newToken, false);\n    }\n\n    public String refreshToken(User user, DashBoard dash, Device device) {\n        String newToken = TokenGeneratorUtil.generateNewToken();\n        assignToken(user, dash, device, newToken);\n        return newToken;\n    }\n\n    public String refreshSharedToken(User user, DashBoard dash) {\n        String newToken = TokenGeneratorUtil.generateNewToken();\n        sharedTokenManager.assignToken(user, dash, newToken);\n        return newToken;\n    }\n\n    public void updateRegularCache(String token, TokenValue tokenValue) {\n        regularTokenManager.cache.put(token, new TokenValue(tokenValue.user, tokenValue.dash, tokenValue.device));\n    }\n\n    public void updateRegularCache(String token, User user, DashBoard dash, Device device) {\n        regularTokenManager.cache.put(token, new TokenValue(user, dash, device));\n    }\n\n    public void updateSharedCache(String token, User user, int dashId) {\n        sharedTokenManager.cache.put(token, new SharedTokenValue(user, dashId));\n    }\n\n    public boolean clearTemporaryTokens() {\n        long now = System.currentTimeMillis();\n        return regularTokenManager.cache.entrySet().removeIf(entry -> entry.getValue().isExpired(now));\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/dao/TokenValue.java",
    "content": "package cc.blynk.server.core.dao;\n\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Device;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 16.11.16.\n */\npublic class TokenValue {\n\n    public final User user;\n\n    public final DashBoard dash;\n\n    public final Device device;\n\n    public TokenValue(User user, DashBoard dash, Device device) {\n        this.user = user;\n        this.dash = dash;\n        this.device = device;\n    }\n\n    public boolean isTemporary() {\n        return false;\n    }\n\n    public boolean isExpired(long now) {\n        return false;\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/dao/UserDao.java",
    "content": "package cc.blynk.server.core.dao;\n\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.App;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.enums.ProvisionType;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.others.webhook.WebHook;\nimport cc.blynk.server.workers.timer.TimerWorker;\nimport cc.blynk.utils.AppNameUtil;\nimport cc.blynk.utils.TokenGeneratorUtil;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentMap;\nimport java.util.stream.Collectors;\n\n/**\n * Helper class for holding info regarding registered users and profiles.\n *\n * User: ddumanskiy\n * Date: 8/11/13\n * Time: 4:02 PM\n */\npublic class UserDao {\n\n    private static final Logger log = LogManager.getLogger(UserDao.class);\n\n    public final ConcurrentMap<UserKey, User> users;\n    private final String region;\n    private final String host;\n\n    public UserDao(ConcurrentMap<UserKey, User> users, String region, String host) {\n        //reading DB to RAM.\n        this.users = users;\n        this.region = region;\n        this.host = host;\n        log.info(\"Region : {}. Host : {}.\", region, host);\n    }\n\n    public boolean isUserExists(String name, String appName) {\n        return users.get(new UserKey(name, appName)) != null;\n    }\n\n    public boolean isSuperAdminExists() {\n        User user = getSuperAdmin();\n        return user != null;\n    }\n\n    public User getSuperAdmin() {\n        for (User user : users.values()) {\n            if (user.isSuperAdmin) {\n                return user;\n            }\n        }\n        return null;\n    }\n\n    public User getByName(String name, String appName) {\n        return users.get(new UserKey(name, appName));\n    }\n\n    public boolean contains(String name, String appName) {\n        return users.containsKey(new UserKey(name, appName));\n    }\n\n    //for tests only\n    public Map<UserKey, User> getUsers() {\n        return users;\n    }\n\n    public List<User> searchByUsername(String name, String appName) {\n        if (name == null) {\n            return new ArrayList<>(users.values());\n        }\n\n        return users.values().stream().filter(user -> user.email.contains(name)\n                && (appName == null || user.appName.equals(appName))).collect(Collectors.toList());\n    }\n\n    public User delete(UserKey userKey) {\n        return users.remove(userKey);\n    }\n\n    public User delete(String name, String appName) {\n        return delete(new UserKey(name, appName));\n    }\n\n    public void add(User user) {\n        users.put(new UserKey(user), user);\n    }\n\n    public Map<String, Integer> getBoardsUsage() {\n        Map<String, Integer> boards = new HashMap<>();\n        for (User user : users.values()) {\n            for (DashBoard dashBoard : user.profile.dashBoards) {\n                for (Device device : dashBoard.devices) {\n                    if (device.boardType != null) {\n                        String label = device.boardType.label;\n                        Integer i = boards.getOrDefault(label, 0);\n                        boards.put(label, ++i);\n                    }\n                }\n            }\n        }\n        return boards;\n    }\n\n    public Map<String, Integer> getFacebookLogin() {\n        Map<String, Integer> facebookLogin = new HashMap<>();\n        for (User user : users.values()) {\n            facebookLogin.compute(\n                    user.isFacebookUser\n                            ? AppNameUtil.FACEBOOK\n                            : AppNameUtil.BLYNK, (k, v) -> v == null ? 1 : v++\n            );\n        }\n        return facebookLogin;\n    }\n\n    public Map<String, Integer> getWidgetsUsage() {\n        Map<String, Integer> widgets = new HashMap<>();\n        for (User user : users.values()) {\n            for (DashBoard dashBoard : user.profile.dashBoards) {\n                if (dashBoard.widgets != null) {\n                    for (Widget widget : dashBoard.widgets) {\n                        Integer i = widgets.getOrDefault(widget.getClass().getSimpleName(), 0);\n                        widgets.put(widget.getClass().getSimpleName(), ++i);\n                    }\n                }\n            }\n        }\n        return widgets;\n    }\n\n    public Map<String, Integer> getProjectsPerUser() {\n        Map<String, Integer> projectsPerUser = new HashMap<>();\n        for (User user : users.values()) {\n            String key = String.valueOf(user.profile.dashBoards.length);\n            Integer i = projectsPerUser.getOrDefault(key, 0);\n            projectsPerUser.put(key, ++i);\n        }\n        return projectsPerUser;\n    }\n\n    public Map<String, Integer> getLibraryVersion() {\n        Map<String, Integer> data = new HashMap<>();\n        for (User user : users.values()) {\n            for (DashBoard dashBoard : user.profile.dashBoards) {\n                for (Device device : dashBoard.devices) {\n                    if (device.hardwareInfo != null && device.hardwareInfo.blynkVersion != null) {\n                        String key = device.hardwareInfo.blynkVersion;\n                        Integer i = data.getOrDefault(key, 0);\n                        data.put(key, ++i);\n                    }\n                }\n            }\n        }\n        return data;\n    }\n\n    public Map<String, Integer> getCpuType() {\n        Map<String, Integer> data = new HashMap<>();\n        for (User user : users.values()) {\n            for (DashBoard dashBoard : user.profile.dashBoards) {\n                for (Device device : dashBoard.devices) {\n                    if (device.hardwareInfo != null && device.hardwareInfo.cpuType != null) {\n                        String key = device.hardwareInfo.cpuType;\n                        Integer i = data.getOrDefault(key, 0);\n                        data.put(key, ++i);\n                    }\n                }\n            }\n        }\n        return data;\n    }\n\n    public Map<String, Integer> getConnectionType() {\n        Map<String, Integer> data = new HashMap<>();\n        for (User user : users.values()) {\n            for (DashBoard dashBoard : user.profile.dashBoards) {\n                for (Device device : dashBoard.devices) {\n                    if (device.hardwareInfo != null && device.hardwareInfo.connectionType != null) {\n                        String key = device.hardwareInfo.connectionType;\n                        Integer i = data.getOrDefault(key, 0);\n                        data.put(key, ++i);\n                    }\n                }\n            }\n        }\n        return data;\n    }\n\n    public Map<String, Integer> getHardwareBoards() {\n        Map<String, Integer> data = new HashMap<>();\n        for (User user : users.values()) {\n            for (DashBoard dashBoard : user.profile.dashBoards) {\n                for (Device device : dashBoard.devices) {\n                    if (device.hardwareInfo != null && device.hardwareInfo.boardType != null) {\n                        String key = device.hardwareInfo.boardType;\n                        Integer i = data.getOrDefault(key, 0);\n                        data.put(key, ++i);\n                    }\n                }\n            }\n        }\n        return data;\n    }\n\n    public Map<String, Integer> getFilledSpace() {\n        Map<String, Integer> filledSpace = new HashMap<>();\n        for (User user : users.values()) {\n            for (DashBoard dashBoard : user.profile.dashBoards) {\n                int sum = 0;\n                for (Widget widget : dashBoard.widgets) {\n                    if (widget.height < 0 || widget.width < 0) {\n                        //log.error(\"Widget without length fields. User : {}\", user.name);\n                        continue;\n                    }\n                    sum += widget.height * widget.width;\n                }\n\n                String key = String.valueOf(sum);\n                Integer i = filledSpace.getOrDefault(key, 0);\n                filledSpace.put(key, ++i);\n            }\n        }\n        return filledSpace;\n    }\n\n    public Map<String, Integer> getWebHookHosts() {\n        Map<String, Integer> data = new HashMap<>();\n        for (User user : users.values()) {\n            for (DashBoard dashBoard : user.profile.dashBoards) {\n                for (Widget widget : dashBoard.widgets) {\n                    if (widget instanceof WebHook) {\n                        WebHook webHook = (WebHook) widget;\n                        if (webHook.url != null) {\n                            try {\n                                String key = getHost(webHook.url);\n                                Integer i = data.getOrDefault(key, 0);\n                                data.put(key, ++i);\n                            } catch (Exception e) {\n                                //don't care if we couldn't parse.\n                            }\n                        }\n                    }\n                }\n            }\n        }\n        return data;\n    }\n\n    public void createProjectForExportedApp(TimerWorker timerWorker,\n                                            TokenManager tokenManager,\n                                            User newUser, String appName, int msgId) {\n        if (appName.equals(AppNameUtil.BLYNK)) {\n            return;\n        }\n\n        User parentUser = null;\n        App app = null;\n\n        for (User user : users.values()) {\n            app = user.profile.getAppById(appName);\n            if (app != null) {\n                parentUser = user;\n                break;\n            }\n        }\n\n        if (app == null) {\n            log.error(\"Unable to find app with id {}\", appName);\n            return;\n        }\n\n        if (app.isMultiFace) {\n            log.info(\"App supports multi faces. Skipping profile creation.\");\n            return;\n        }\n\n        int dashId = app.projectIds[0];\n        DashBoard dash = parentUser.profile.getDashByIdOrThrow(dashId);\n\n        //todo ugly, but quick. refactor\n        DashBoard clonedDash = JsonParser.parseDashboard(JsonParser.toJsonRestrictiveDashboard(dash), msgId);\n\n        clonedDash.id = 1;\n        clonedDash.parentId = dash.parentId;\n        clonedDash.createdAt = System.currentTimeMillis();\n        clonedDash.updatedAt = clonedDash.createdAt;\n        clonedDash.isActive = true;\n        clonedDash.eraseWidgetValues();\n        removeDevicesProvisionedFromDeviceTiles(clonedDash);\n\n        clonedDash.addTimers(timerWorker, new UserKey(newUser));\n\n        newUser.profile.dashBoards = new DashBoard[] {clonedDash};\n\n        if (app.provisionType == ProvisionType.STATIC) {\n            for (Device device : clonedDash.devices) {\n                device.erase();\n            }\n        } else {\n            for (Device device : clonedDash.devices) {\n                device.erase();\n                String token = TokenGeneratorUtil.generateNewToken();\n                tokenManager.assignToken(newUser, clonedDash, device, token);\n            }\n        }\n    }\n\n    //removes devices that has no widgets assigned to\n    //probably those devices were added via device tiles widget\n    private static void removeDevicesProvisionedFromDeviceTiles(DashBoard dash) {\n        List<Device> list = new ArrayList<>(Arrays.asList(dash.devices));\n        list.removeIf(device -> !dash.hasWidgetsByDeviceId(device.id));\n        dash.devices = list.toArray(new Device[0]);\n    }\n\n\n    /**\n     * Will take a url such as http://www.stackoverflow.com and return www.stackoverflow.com\n     */\n    private static String getHost(String url) {\n        if (url == null || url.length() == 0) {\n            return \"\";\n        }\n\n        int doubleslash = url.indexOf(\"//\");\n        if (doubleslash == -1) {\n            doubleslash = 0;\n        } else {\n            doubleslash += 2;\n        }\n\n        int end = url.indexOf('/', doubleslash);\n        end = end >= 0 ? end : url.length();\n\n        int port = url.indexOf(':', doubleslash);\n        end = (port > 0 && port < end) ? port : end;\n\n        return url.substring(doubleslash, end);\n    }\n\n    public User addFacebookUser(String email, String appName) {\n        log.debug(\"Adding new facebook user {}. App : {}\", email, appName);\n        User newUser = new User(email, null, appName, region, host, true, false);\n        users.put(new UserKey(email, appName), newUser);\n        return newUser;\n    }\n\n    public User add(String email, String passHash, String appName) {\n        log.debug(\"Adding new user {}. App : {}\", email, appName);\n        User newUser = new User(email, passHash, appName, region, host, false, false);\n        users.put(new UserKey(email, appName), newUser);\n        return newUser;\n    }\n\n    public void add(String email, String passHash, String appName, boolean isSuperAdmin) {\n        log.debug(\"Adding new user {}. App : {}\", email, appName);\n        User newUser = new User(email, passHash, appName, region, host, false, isSuperAdmin);\n        users.put(new UserKey(email, appName), newUser);\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/dao/UserKey.java",
    "content": "package cc.blynk.server.core.dao;\n\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.utils.AppNameUtil;\n\nimport java.util.Objects;\n\n/**\n * User key for session. Holds user email and app user belongs to.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 17.05.16.\n */\npublic final class UserKey {\n\n    public final String email;\n\n    public final String appName;\n\n    public UserKey(User user) {\n        this(user.email, user.appName);\n    }\n\n    public UserKey(String email, String appName) {\n        this.email = email;\n        this.appName = Objects.requireNonNullElse(appName, AppNameUtil.BLYNK);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (!(o instanceof UserKey)) {\n            return false;\n        }\n\n        UserKey userKey = (UserKey) o;\n\n        if (email != null ? !email.equals(userKey.email) : userKey.email != null) {\n            return false;\n        }\n        return appName != null ? appName.equals(userKey.appName) : userKey.appName == null;\n    }\n\n    @Override\n    public int hashCode() {\n        int result = email != null ? email.hashCode() : 0;\n        result = 31 * result + (appName != null ? appName.hashCode() : 0);\n        return result;\n    }\n\n    @Override\n    public String toString() {\n        return \"UserKey{\"\n                + \"email='\" + email + '\\''\n                + \", appName='\" + appName + '\\''\n                + '}';\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/dao/functions/AverageGraphFunction.java",
    "content": "package cc.blynk.server.core.dao.functions;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.07.17.\n */\npublic class AverageGraphFunction implements GraphFunction {\n\n    private int count;\n    private double sum;\n\n    public AverageGraphFunction() {\n        this.count = 0;\n        this.sum = 0;\n    }\n\n    @Override\n    public void apply(double newValue) {\n        this.count++;\n        this.sum += newValue;\n    }\n\n    @Override\n    public double getResult() {\n        return sum / count;\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/dao/functions/GraphFunction.java",
    "content": "package cc.blynk.server.core.dao.functions;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.07.17.\n */\npublic interface GraphFunction {\n\n    void apply(double value);\n\n    double getResult();\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/dao/functions/MaxGraphFunction.java",
    "content": "package cc.blynk.server.core.dao.functions;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.07.17.\n */\npublic class MaxGraphFunction implements GraphFunction {\n\n    private double value = Double.MIN_VALUE;\n\n    @Override\n    public void apply(double newValue) {\n        this.value = Math.max(value, newValue);\n    }\n\n    @Override\n    public double getResult() {\n        return value;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/dao/functions/MedianGraphFunction.java",
    "content": "package cc.blynk.server.core.dao.functions;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.07.17.\n */\npublic class MedianGraphFunction implements GraphFunction {\n\n    private final ArrayList<Double> array;\n\n    public MedianGraphFunction() {\n        this.array = new ArrayList<>();\n    }\n\n    @Override\n    public void apply(double newValue) {\n        array.add(newValue);\n    }\n\n    @Override\n    public double getResult() {\n        Collections.sort(array);\n        int middle = array.size() / 2;\n        if (array.size() % 2 == 0) {\n            return (array.get(middle) + array.get(middle - 1)) / 2;\n        }\n        return array.get(middle);\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/dao/functions/MinGraphFunction.java",
    "content": "package cc.blynk.server.core.dao.functions;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.07.17.\n */\npublic class MinGraphFunction implements GraphFunction {\n\n    private double value = Double.MAX_VALUE;\n\n    @Override\n    public void apply(double newValue) {\n        this.value = Math.min(value, newValue);\n    }\n\n    @Override\n    public double getResult() {\n        return value;\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/dao/functions/SumGraphFunction.java",
    "content": "package cc.blynk.server.core.dao.functions;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.07.17.\n */\npublic class SumGraphFunction implements GraphFunction {\n\n    private double sum;\n\n    public SumGraphFunction() {\n        this.sum = 0;\n    }\n\n    @Override\n    public void apply(double newValue) {\n        this.sum += newValue;\n    }\n\n    @Override\n    public double getResult() {\n        return sum;\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/dao/ota/OTAInfo.java",
    "content": "package cc.blynk.server.core.dao.ota;\n\nimport static cc.blynk.utils.StringUtils.BODY_SEPARATOR;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 22.08.17.\n */\npublic class OTAInfo {\n\n    final long initiatedAt;\n    final String initiatedBy;\n    final String pathToFirmware;\n    final String build;\n    final String projectName;\n\n    OTAInfo(String initiatedBy, String pathToFirmware, String build, String projectName) {\n        this.initiatedAt = System.currentTimeMillis();\n        this.initiatedBy = initiatedBy;\n        this.pathToFirmware = pathToFirmware;\n        this.build = build;\n        this.projectName = projectName;\n    }\n\n    public String makeHardwareBody(String serverHostUrl) {\n        return makeHardwareBody(serverHostUrl, pathToFirmware);\n    }\n\n    public static String makeHardwareBody(String serverHostUrl, String pathToFirmware) {\n        return \"ota\" + BODY_SEPARATOR + serverHostUrl + pathToFirmware;\n    }\n\n    public boolean matches(String dashName) {\n        return projectName == null || projectName.equalsIgnoreCase(dashName);\n    }\n\n    @Override\n    public String toString() {\n        return \"OTAInfo{\"\n                + \"initiatedAt=\" + initiatedAt\n                + \", initiatedBy='\" + initiatedBy + '\\''\n                + \", pathToFirmware='\" + pathToFirmware + '\\''\n                + \", build='\" + build + '\\''\n                + \", projectName='\" + projectName + '\\''\n                + '}';\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/dao/ota/OTAManager.java",
    "content": "package cc.blynk.server.core.dao.ota;\n\nimport cc.blynk.server.core.dao.UserKey;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.device.DeviceOtaInfo;\nimport cc.blynk.server.core.model.device.HardwareInfo;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.utils.properties.ServerProperties;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.io.IOException;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.concurrent.ConcurrentHashMap;\n\nimport static cc.blynk.server.core.protocol.enums.Command.BLYNK_INTERNAL;\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeASCIIStringMessage;\nimport static cc.blynk.utils.FileUtils.getPatternFromString;\n\n/**\n * Very basic OTA manager implementation.\n * For now it could be used only by super admin and updates firmware for all devices.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.08.17.\n */\npublic class OTAManager {\n\n    private static final Logger log = LogManager.getLogger(OTAManager.class);\n\n    public final String serverHostUrl;\n    private volatile OTAInfo allInfo;\n    private final ConcurrentHashMap<UserKey, OTAInfo> otaInfos;\n    private final String staticFilesFolder;\n\n    public OTAManager(ServerProperties props) {\n        String port = props.getProperty(\"http.port\", \"8080\");\n        this.serverHostUrl = \"http://\" + props.host + (port.equals(\"80\") ? \"\" : (\":\" + port));\n        this.staticFilesFolder = props.jarPath;\n        this.otaInfos = new ConcurrentHashMap<>();\n    }\n\n    public void initiateHardwareUpdate(ChannelHandlerContext ctx, UserKey userKey,\n                                       HardwareInfo newHardwareInfo, DashBoard dash, Device device) {\n        OTAInfo otaInfo = getOtaInfoForHardware(userKey, newHardwareInfo, dash.name);\n        if (otaInfo != null) {\n            sendOtaCommand(ctx, device, otaInfo);\n            log.info(\"Ota command is sent for user {} and device {}:{}.\",\n                    userKey.email, device.name, device.id);\n        }\n    }\n\n    private OTAInfo getOtaInfoForHardware(UserKey userKey, HardwareInfo newHardwareInfo, String dashName) {\n        if (isFirmwareVersionChanged(allInfo, newHardwareInfo)) {\n            log.info(\"Device build : {}, firmware build : {}. Firmware update is required.\",\n                    newHardwareInfo.build, allInfo.build);\n            return allInfo;\n        }\n\n        OTAInfo otaInfo = otaInfos.get(userKey);\n        if (isValidOtaInfo(otaInfo, newHardwareInfo, dashName)) {\n            return otaInfo;\n        }\n\n        return null;\n    }\n\n    private static boolean isValidOtaInfo(OTAInfo otaInfo, HardwareInfo hardwareInfo, String dashName) {\n        return isFirmwareVersionChanged(otaInfo, hardwareInfo) && otaInfo.matches(dashName);\n    }\n\n    private static boolean isFirmwareVersionChanged(OTAInfo otaInfo, HardwareInfo newHardwareInfo) {\n        return otaInfo != null && newHardwareInfo.build != null\n                && !newHardwareInfo.build.equals(otaInfo.build);\n    }\n\n    private void sendOtaCommand(ChannelHandlerContext ctx, Device device, OTAInfo otaInfo) {\n        StringMessage msg = makeASCIIStringMessage(BLYNK_INTERNAL, 7777, otaInfo.makeHardwareBody(serverHostUrl));\n        if (ctx.channel().isWritable()) {\n            device.deviceOtaInfo = new DeviceOtaInfo(otaInfo.initiatedBy,\n                    otaInfo.initiatedAt, System.currentTimeMillis());\n            ctx.write(msg, ctx.voidPromise());\n        }\n    }\n\n    public void initiate(User initiator, UserKey userKey, String projectName, String pathToFirmware) {\n        String build = fetchBuildNumber(pathToFirmware);\n        this.otaInfos.put(userKey, new OTAInfo(initiator.email, pathToFirmware, build, projectName));\n    }\n\n    public void initiateForAll(User initiator, String pathToFirmware) {\n        String build = fetchBuildNumber(pathToFirmware);\n        this.allInfo = new OTAInfo(initiator.email, pathToFirmware, build, null);\n        log.info(\"Ota initiated. {}\", allInfo);\n    }\n\n    public static String getBuildPatternFromString(Path path) {\n        try {\n            return getPatternFromString(path, \"\\0\" + \"build\" + \"\\0\");\n        } catch (IOException ioe) {\n            log.error(\"Error getting pattern from file. Reason : {}\", ioe.getMessage());\n            throw new RuntimeException(ioe);\n        }\n    }\n\n    private String fetchBuildNumber(String pathToFirmware) {\n        Path path = Paths.get(staticFilesFolder, pathToFirmware);\n        return getBuildPatternFromString(path);\n    }\n\n    public void stop(User user) {\n        this.allInfo = null;\n        otaInfos.clear();\n        log.info(\"Ota stopped by {}.\", user.email);\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/DashBoard.java",
    "content": "package cc.blynk.server.core.model;\n\nimport cc.blynk.server.core.dao.UserKey;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.device.Tag;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.enums.Theme;\nimport cc.blynk.server.core.model.enums.WidgetProperty;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.model.serialization.View;\nimport cc.blynk.server.core.model.storage.PinStorageKeyDeserializer;\nimport cc.blynk.server.core.model.storage.PinStorageValueDeserializer;\nimport cc.blynk.server.core.model.storage.key.DashPinPropertyStorageKey;\nimport cc.blynk.server.core.model.storage.key.DashPinStorageKey;\nimport cc.blynk.server.core.model.storage.key.PinStorageKey;\nimport cc.blynk.server.core.model.storage.value.PinStorageValue;\nimport cc.blynk.server.core.model.storage.value.SinglePinStorageValue;\nimport cc.blynk.server.core.model.widgets.DeviceCleaner;\nimport cc.blynk.server.core.model.widgets.MultiPinWidget;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.controls.Timer;\nimport cc.blynk.server.core.model.widgets.notifications.Mail;\nimport cc.blynk.server.core.model.widgets.notifications.Notification;\nimport cc.blynk.server.core.model.widgets.notifications.Twitter;\nimport cc.blynk.server.core.model.widgets.others.eventor.Eventor;\nimport cc.blynk.server.core.model.widgets.others.webhook.WebHook;\nimport cc.blynk.server.core.model.widgets.ui.DeviceSelector;\nimport cc.blynk.server.core.model.widgets.ui.reporting.ReportingWidget;\nimport cc.blynk.server.core.model.widgets.ui.tiles.DeviceTiles;\nimport cc.blynk.server.core.model.widgets.ui.tiles.TileTemplate;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandException;\nimport cc.blynk.server.workers.timer.TimerWorker;\nimport cc.blynk.utils.ArrayUtil;\nimport com.fasterxml.jackson.annotation.JsonView;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.Map;\n\nimport static cc.blynk.server.internal.EmptyArraysUtil.EMPTY_DEVICES;\nimport static cc.blynk.server.internal.EmptyArraysUtil.EMPTY_TAGS;\nimport static cc.blynk.server.internal.EmptyArraysUtil.EMPTY_WIDGETS;\n\n/**\n * User: ddumanskiy\n * Date: 21.11.13\n * Time: 13:04\n */\npublic class DashBoard {\n\n    //-1 means this is not child project\n    private static final int IS_PARENT_DASH = -1;\n    private static final String DEFAULT_NAME = \"New Project\";\n\n    public int id;\n\n    public int parentId = IS_PARENT_DASH;\n\n    public boolean isPreview;\n\n    public volatile String name;\n\n    public long createdAt;\n\n    public volatile long updatedAt;\n\n    public volatile Widget[] widgets = EMPTY_WIDGETS;\n\n    public volatile Device[] devices = EMPTY_DEVICES;\n\n    public volatile Tag[] tags = EMPTY_TAGS;\n\n    public volatile Theme theme = Theme.Blynk;\n\n    public volatile boolean keepScreenOn;\n\n    public volatile boolean isAppConnectedOn;\n\n    public volatile boolean isNotificationsOff;\n\n    public volatile boolean isShared;\n\n    public volatile boolean isActive;\n\n    public volatile boolean widgetBackgroundOn;\n\n    public int color = -1;\n\n    public boolean isDefaultColor = true;\n\n    @JsonView(View.Private.class)\n    public volatile String sharedToken;\n\n    @JsonView(View.Private.class)\n    @JsonDeserialize(keyUsing = PinStorageKeyDeserializer.class,\n                     contentUsing = PinStorageValueDeserializer.class)\n    @Deprecated\n    public Map<PinStorageKey, PinStorageValue> pinsStorage = Collections.emptyMap();\n\n    public boolean updateWidgets(int deviceId, short pin, PinType type, String value) {\n        boolean hasWidget = false;\n        for (Widget widget : widgets) {\n            if (widget.updateIfSame(deviceId, pin, type, value)) {\n                hasWidget = true;\n            }\n        }\n        return hasWidget;\n    }\n\n    public String getNameOrEmpty() {\n        return name == null ? \"\" : name;\n    }\n\n    public String getNameOrDefault() {\n        return name == null ? DEFAULT_NAME : name;\n    }\n\n    //multi value widgets has always priority over single value widgets.\n    //for example, we have 2 widgets on the same pin, one it terminal, another is value display.\n    //so for that pin we have to return multivalue storage\n    public PinStorageValue initStorageValueForStorageKey(DashPinStorageKey key) {\n        if (!(key instanceof DashPinPropertyStorageKey)) {\n            for (Widget widget : widgets) {\n                if (widget instanceof OnePinWidget) {\n                    OnePinWidget onePinWidget = (OnePinWidget) widget;\n                    //pim matches and widget assigned to device selector\n                    if (onePinWidget.isAssignedToDeviceSelector() && key.isSame(id, onePinWidget)) {\n                        DeviceSelector deviceSelector = getDeviceSelector(onePinWidget.deviceId);\n                        if (deviceSelector != null && ArrayUtil.contains(deviceSelector.deviceIds, key.deviceId)) {\n                            if (widget.isMultiValueWidget()) {\n                                return widget.getPinStorageValue();\n                            }\n                        }\n                    }\n                } else if (widget instanceof MultiPinWidget) {\n                    MultiPinWidget multiPinWidget = (MultiPinWidget) widget;\n                    if (multiPinWidget.isAssignedToDeviceSelector() && key.isSame(id, multiPinWidget)) {\n                        DeviceSelector deviceSelector = getDeviceSelector(multiPinWidget.deviceId);\n                        if (deviceSelector != null && ArrayUtil.contains(deviceSelector.deviceIds, key.deviceId)) {\n                            if (widget.isMultiValueWidget()) {\n                                return widget.getPinStorageValue();\n                            }\n                        }\n                    }\n                } else if (widget instanceof DeviceTiles) {\n                    DeviceTiles deviceTiles = (DeviceTiles) widget;\n                    for (TileTemplate template : deviceTiles.templates) {\n                        if (ArrayUtil.contains(template.deviceIds, key.deviceId)) {\n                            for (Widget tileWidget : template.widgets) {\n                                if (tileWidget instanceof OnePinWidget) {\n                                    if (key.isSame(id, (OnePinWidget) tileWidget)) {\n                                        if (tileWidget.isMultiValueWidget()) {\n                                            return tileWidget.getPinStorageValue();\n                                        }\n                                    }\n                                } else if (tileWidget instanceof MultiPinWidget) {\n                                    if (key.isSame(id, (MultiPinWidget) tileWidget)) {\n                                        if (tileWidget.isMultiValueWidget()) {\n                                            return tileWidget.getPinStorageValue();\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n\n        return new SinglePinStorageValue();\n    }\n\n    public void activate() {\n        isActive = true;\n        updatedAt = System.currentTimeMillis();\n    }\n\n    public void deactivate() {\n        isActive = false;\n        updatedAt = System.currentTimeMillis();\n    }\n\n    public Widget findWidgetByPin(int deviceId, short pin, PinType pinType) {\n        for (Widget widget : widgets) {\n            if (widget.isSame(deviceId, pin, pinType)) {\n                return widget;\n            }\n        }\n        return null;\n    }\n\n    public WebHook findWebhookByPin(int deviceId, short pin, PinType pinType) {\n        for (Widget widget : widgets) {\n            if (widget instanceof WebHook) {\n                WebHook webHook = (WebHook) widget;\n                if (webHook.isSameWebHook(deviceId, pin, pinType)) {\n                    return webHook;\n                }\n            }\n        }\n        return null;\n    }\n\n    public static int getWidgetIndexByIdOrThrow(Widget[] widgets, long id) {\n        for (int i = 0; i < widgets.length; i++) {\n            if (widgets[i].id == id) {\n                return i;\n            }\n        }\n        throw new IllegalCommandException(\"Widget with passed id not found.\");\n    }\n\n    public int getWidgetIndexByIdOrThrow(long id) {\n        return getWidgetIndexByIdOrThrow(widgets, id);\n    }\n\n    public boolean hasWidgetsByDeviceId(int deviceId) {\n        for (Widget widget : widgets) {\n            if (widget.isAssignedToDevice(deviceId)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    public DeviceSelector getDeviceSelector(long targetId) {\n        Widget widget = getWidgetById(targetId);\n        if (widget instanceof DeviceSelector) {\n            return (DeviceSelector) widget;\n        }\n        return null;\n    }\n\n    public Widget getWidgetByIdOrThrow(long id) {\n        return widgets[getWidgetIndexByIdOrThrow(id)];\n    }\n\n    public Widget getWidgetById(long id) {\n        return getWidgetById(widgets, id);\n    }\n\n    public Widget getWidgetByIdInDeviceTilesOrThrow(long id) {\n        for (Widget widget : widgets) {\n            if (widget instanceof DeviceTiles) {\n                DeviceTiles deviceTiles = (DeviceTiles) widget;\n                Widget widgetInDeviceTiles = deviceTiles.getWidgetById(id);\n                if (widgetInDeviceTiles != null) {\n                    return widgetInDeviceTiles;\n                }\n            }\n        }\n        throw new IllegalCommandException(\"Widget with passed id not found.\");\n    }\n\n    public static Widget getWidgetById(Widget[] widgets, long id) {\n        for (Widget widget : widgets) {\n            if (widget.id == id) {\n                return widget;\n            }\n        }\n        return null;\n    }\n\n    public Notification getNotificationWidget() {\n        return getWidgetByType(Notification.class);\n    }\n\n    public Eventor getEventorWidget() {\n        return getWidgetByType(Eventor.class);\n    }\n\n    public Twitter getTwitterWidget() {\n        return getWidgetByType(Twitter.class);\n    }\n\n    public Mail getMailWidget() {\n        return getWidgetByType(Mail.class);\n    }\n\n    public ReportingWidget getReportingWidget() {\n        return getWidgetByType(ReportingWidget.class);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public <T> T getWidgetByType(Class<T> clazz) {\n        for (Widget widget : widgets) {\n            if (clazz.isInstance(widget)) {\n                return (T) widget;\n            }\n        }\n        return null;\n    }\n\n    public String buildPMMessage(int deviceId) {\n        StringBuilder sb = new StringBuilder(\"pm\");\n        for (Widget widget : widgets) {\n            widget.append(sb, deviceId);\n        }\n        if (sb.length() == 2) {\n            return null;\n        }\n        return sb.toString();\n    }\n\n\n    public int energySum() {\n        //means this is app preview project so do no manipulation with energy\n        if (parentId != IS_PARENT_DASH) {\n            return 0;\n        }\n        int sum = 0;\n        for (Widget widget : widgets) {\n            sum += widget.getPrice();\n        }\n        return sum;\n    }\n\n    public void eraseWidgetValues() {\n        for (Widget widget : widgets) {\n            widget.erase();\n        }\n    }\n\n    public void eraseWidgetValuesForDevice(int deviceId) {\n        for (Widget widget : widgets) {\n            if (widget.isAssignedToDevice(deviceId)) {\n                widget.erase();\n            }\n            if (widget instanceof DeviceCleaner) {\n                ((DeviceCleaner) widget).deleteDevice(deviceId);\n            }\n        }\n    }\n\n    public void addTimers(TimerWorker timerWorker, UserKey userKey) {\n        for (Widget widget : widgets) {\n            if (widget instanceof DeviceTiles) {\n                timerWorker.add(userKey, (DeviceTiles) widget, id);\n            } else if (widget instanceof Timer) {\n                timerWorker.add(userKey, (Timer) widget, id, -1L, -1L);\n            } else if (widget instanceof Eventor) {\n                timerWorker.add(userKey, (Eventor) widget, id);\n            }\n        }\n    }\n\n    //todo add DashboardSettings as Dashboard field\n    public void updateSettings(DashboardSettings settings) {\n        this.name = settings.name;\n        this.isShared = settings.isShared;\n        this.theme = settings.theme;\n        this.keepScreenOn = settings.keepScreenOn;\n        this.isAppConnectedOn = settings.isAppConnectedOn;\n        this.isNotificationsOff = settings.isNotificationsOff;\n        this.widgetBackgroundOn = settings.widgetBackgroundOn;\n        this.color = settings.color;\n        this.isDefaultColor = settings.isDefaultColor;\n        this.updatedAt = System.currentTimeMillis();\n    }\n\n    public void updateFields(DashBoard updatedDashboard) {\n        this.name = updatedDashboard.name;\n        this.isShared = updatedDashboard.isShared;\n        this.theme = updatedDashboard.theme;\n        this.keepScreenOn = updatedDashboard.keepScreenOn;\n        this.isAppConnectedOn = updatedDashboard.isAppConnectedOn;\n        this.isNotificationsOff = updatedDashboard.isNotificationsOff;\n        this.widgetBackgroundOn = updatedDashboard.widgetBackgroundOn;\n        this.color = updatedDashboard.color;\n        this.isDefaultColor = updatedDashboard.isDefaultColor;\n\n        Notification newNotification = updatedDashboard.getNotificationWidget();\n        if (newNotification != null) {\n            Notification oldNotification = this.getNotificationWidget();\n            if (oldNotification != null) {\n                newNotification.iOSTokens = oldNotification.iOSTokens;\n                newNotification.androidTokens = oldNotification.androidTokens;\n            }\n        }\n\n        this.widgets = updatedDashboard.widgets;\n    }\n\n    public void updateFaceFields(DashBoard parent) {\n        this.name = parent.name;\n        this.isShared = parent.isShared;\n        this.theme = parent.theme;\n        this.keepScreenOn = parent.keepScreenOn;\n        this.isAppConnectedOn = parent.isAppConnectedOn;\n        this.isNotificationsOff = parent.isNotificationsOff;\n        this.widgetBackgroundOn = parent.widgetBackgroundOn;\n        this.color = parent.color;\n        this.isDefaultColor = parent.isDefaultColor;\n        //do not update devices by purpose\n        //this.devices = parent.devices;\n        this.widgets = copyWidgetsAndPreservePrevValues(this.widgets, parent.widgets);\n        //export app specific requirement\n        for (Widget widget : widgets) {\n            widget.isDefaultColor = false;\n        }\n        this.updatedAt = System.currentTimeMillis();\n    }\n\n    private static Widget[] copyWidgetsAndPreservePrevValues(Widget[] oldWidgets, Widget[] newWidgets) {\n        ArrayList<Widget> copy = new ArrayList<>(newWidgets.length);\n        for (Widget newWidget : newWidgets) {\n            Widget oldWidget = getWidgetById(oldWidgets, newWidget.id);\n\n            Widget copyWidget = newWidget.copy();\n\n            //for now erasing only for this types, not sure about DeviceTiles\n            if (copyWidget instanceof OnePinWidget\n                    || copyWidget instanceof MultiPinWidget\n                    || copyWidget instanceof ReportingWidget) {\n                copyWidget.erase();\n            }\n\n            if (oldWidget != null) {\n                copyWidget.updateValue(oldWidget);\n            }\n            copy.add(copyWidget);\n        }\n\n        return copy.toArray(new Widget[newWidgets.length]);\n    }\n\n    public Widget updateProperty(int deviceId, short pin, WidgetProperty widgetProperty, String propertyValue) {\n        Widget widget = null;\n        for (Widget dashWidget : widgets) {\n            if (dashWidget.isSame(deviceId, pin, PinType.VIRTUAL)) {\n                if (dashWidget.setProperty(widgetProperty, propertyValue)) {\n                    widget = dashWidget;\n                }\n            }\n        }\n        return widget;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n\n        DashBoard dashBoard = (DashBoard) o;\n\n        if (id != dashBoard.id) {\n            return false;\n        }\n        if (name != null ? !name.equals(dashBoard.name) : dashBoard.name != null) {\n            return false;\n        }\n        // Probably incorrect - comparing Object[] arrays with Arrays.equals\n        return Arrays.equals(widgets, dashBoard.widgets);\n    }\n\n    @Override\n    public int hashCode() {\n        int result = id;\n        result = 31 * result + (name != null ? name.hashCode() : 0);\n        result = 31 * result + Arrays.hashCode(widgets);\n        return result;\n    }\n\n    @Override\n    public String toString() {\n        return JsonParser.toJson(this);\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/DashboardSettings.java",
    "content": "package cc.blynk.server.core.model;\n\nimport cc.blynk.server.core.model.enums.Theme;\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 22.04.17.\n */\npublic final class DashboardSettings {\n\n    public final String name;\n\n    public final boolean isShared;\n\n    public final Theme theme;\n\n    public final boolean keepScreenOn;\n\n    public final boolean isAppConnectedOn;\n\n    public final boolean isNotificationsOff;\n\n    public final boolean widgetBackgroundOn;\n\n    public final int color;\n\n    public final boolean isDefaultColor;\n\n    @JsonCreator\n    public DashboardSettings(@JsonProperty(\"name\") String name,\n                             @JsonProperty(\"isShared\") boolean isShared,\n                             @JsonProperty(\"theme\") Theme theme,\n                             @JsonProperty(\"keepScreenOn\") boolean keepScreenOn,\n                             @JsonProperty(\"isAppConnectedOn\") boolean isAppConnectedOn,\n                             @JsonProperty(\"isNotificationsOff\") boolean isNotificationsOff,\n                             @JsonProperty(\"widgetBackgroundOn\") boolean widgetBackgroundOn,\n                             @JsonProperty(\"color\") int color,\n                             @JsonProperty(\"isDefaultColor\") boolean isDefaultColor) {\n        this.name = name;\n        this.isShared = isShared;\n        this.theme = theme == null ? Theme.Blynk : theme;\n        this.keepScreenOn = keepScreenOn;\n        this.isAppConnectedOn = isAppConnectedOn;\n        this.isNotificationsOff = isNotificationsOff;\n        this.widgetBackgroundOn = widgetBackgroundOn;\n        this.color = color;\n        this.isDefaultColor = isDefaultColor;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/DataStream.java",
    "content": "package cc.blynk.server.core.model;\n\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.enums.WidgetProperty;\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport static cc.blynk.utils.StringUtils.BODY_SEPARATOR;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 03.07.15.\n */\npublic class DataStream {\n\n    public static final int NO_PIN = -1;\n\n    public final short pin;\n\n    public final boolean pwmMode;\n\n    public final boolean rangeMappingOn;\n\n    public final PinType pinType;\n\n    public volatile String value;\n\n    public final float min;\n\n    public final float max;\n\n    public final String label;\n\n    @JsonCreator\n    public DataStream(@JsonProperty(\"pin\") short pin,\n                      @JsonProperty(\"pwmMode\") boolean pwmMode,\n                      @JsonProperty(\"rangeMappingOn\") boolean rangeMappingOn,\n                      @JsonProperty(\"pinType\") PinType pinType,\n                      @JsonProperty(\"value\") String value,\n                      @JsonProperty(\"min\") float min,\n                      @JsonProperty(\"max\") float max,\n                      @JsonProperty(\"label\") String label) {\n        this.pin = pin;\n        this.pwmMode = pwmMode;\n        this.rangeMappingOn = rangeMappingOn;\n        this.pinType = pinType;\n        this.value = value;\n        this.min = min;\n        this.max = max;\n        this.label = label;\n    }\n\n    public DataStream(DataStream dataStream) {\n        this(dataStream.pin, dataStream.pwmMode, dataStream.rangeMappingOn,\n                dataStream.pinType, dataStream.value,\n                dataStream.min, dataStream.max, dataStream.label);\n    }\n\n    public DataStream(short pin, PinType pinType) {\n        this(pin, false, false, pinType, null, 0, 255, null);\n    }\n\n    public static String makeReadingHardwareBody(char pinType, short pin) {\n        return \"\" + pinType + 'r' + BODY_SEPARATOR + pin;\n    }\n\n    public static String makeHardwareBody(char pinType, String pin, String value) {\n        return \"\" + pinType + 'w' + BODY_SEPARATOR + pin + BODY_SEPARATOR + value;\n    }\n\n    public static String makeHardwareBody(PinType pinType, short pin, String value) {\n        return makeHardwareBody(pinType.pintTypeChar, pin, value);\n    }\n\n    public static String makeHardwareBody(char pinTypeChar, short pin, String value) {\n        return \"\" + pinTypeChar + 'w' + BODY_SEPARATOR + pin + BODY_SEPARATOR + value;\n    }\n\n    public static String makeHardwareBody(boolean pwmMode, PinType pinType, short pin, String value) {\n        return pwmMode ? makeHardwareBody(PinType.ANALOG, pin, value) : makeHardwareBody(pinType, pin, value);\n    }\n\n    public static String makePropertyHardwareBody(short pin, WidgetProperty property, String value) {\n        return \"\" + pin + BODY_SEPARATOR + property.label + BODY_SEPARATOR + value;\n    }\n\n    public boolean isSame(short pin, PinType type) {\n        return this.pin == pin && (type == this.pinType || (this.pwmMode && type == PinType.ANALOG));\n    }\n\n    public String makeHardwareBody() {\n        return pwmMode ? makeHardwareBody(PinType.ANALOG, pin, value) : makeHardwareBody(pinType, pin, value);\n    }\n\n    public static boolean isValid(short pin, PinType pinType) {\n        return pin != NO_PIN && pinType != null;\n    }\n\n    public boolean isValid() {\n        return isValid(pin, pinType);\n    }\n\n    public boolean isNotEmpty() {\n        return value != null;\n    }\n\n    public boolean notEmptyAndIsValid() {\n        return isNotEmpty() && isValid();\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (!(o instanceof DataStream)) {\n            return false;\n        }\n\n        DataStream that = (DataStream) o;\n\n        if (pin != that.pin) {\n            return false;\n        }\n        if (pwmMode != that.pwmMode) {\n            return false;\n        }\n        if (rangeMappingOn != that.rangeMappingOn) {\n            return false;\n        }\n        if (Float.compare(that.min, min) != 0) {\n            return false;\n        }\n        if (Float.compare(that.max, max) != 0) {\n            return false;\n        }\n        if (pinType != that.pinType) {\n            return false;\n        }\n        if (value != null ? !value.equals(that.value) : that.value != null) {\n            return false;\n        }\n        return label != null ? label.equals(that.label) : that.label == null;\n    }\n\n    @Override\n    public int hashCode() {\n        int result = (int) pin;\n        result = 31 * result + (pwmMode ? 1 : 0);\n        result = 31 * result + (rangeMappingOn ? 1 : 0);\n        result = 31 * result + (pinType != null ? pinType.hashCode() : 0);\n        result = 31 * result + (value != null ? value.hashCode() : 0);\n        result = 31 * result + (min != +0.0f ? Float.floatToIntBits(min) : 0);\n        result = 31 * result + (max != +0.0f ? Float.floatToIntBits(max) : 0);\n        result = 31 * result + (label != null ? label.hashCode() : 0);\n        return result;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/Profile.java",
    "content": "package cc.blynk.server.core.model;\n\nimport cc.blynk.server.core.model.auth.App;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.device.Status;\nimport cc.blynk.server.core.model.device.Tag;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.enums.WidgetProperty;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.model.serialization.View;\nimport cc.blynk.server.core.model.storage.DashPinStorageKeyDeserializer;\nimport cc.blynk.server.core.model.storage.PinStorageValueDeserializer;\nimport cc.blynk.server.core.model.storage.key.DashPinPropertyStorageKey;\nimport cc.blynk.server.core.model.storage.key.DashPinStorageKey;\nimport cc.blynk.server.core.model.storage.value.PinStorageValue;\nimport cc.blynk.server.core.model.widgets.MobileSyncWidget;\nimport cc.blynk.server.core.model.widgets.MultiPinWidget;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\nimport cc.blynk.server.core.model.widgets.Target;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.outputs.graph.GraphDataStream;\nimport cc.blynk.server.core.model.widgets.outputs.graph.Superchart;\nimport cc.blynk.server.core.model.widgets.ui.DeviceSelector;\nimport cc.blynk.server.core.model.widgets.ui.reporting.ReportingWidget;\nimport cc.blynk.server.core.model.widgets.ui.tiles.DeviceTiles;\nimport cc.blynk.server.core.model.widgets.ui.tiles.Tile;\nimport cc.blynk.server.core.model.widgets.ui.tiles.TileTemplate;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandException;\nimport cc.blynk.utils.ArrayUtil;\nimport cc.blynk.utils.StringUtils;\nimport com.fasterxml.jackson.annotation.JsonView;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport io.netty.channel.Channel;\n\nimport java.util.Arrays;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static cc.blynk.server.core.model.widgets.MobileSyncWidget.ANY_TARGET;\nimport static cc.blynk.server.internal.EmptyArraysUtil.EMPTY_APPS;\nimport static cc.blynk.server.internal.EmptyArraysUtil.EMPTY_DASHBOARDS;\nimport static cc.blynk.utils.StringUtils.truncateFileName;\n\n/**\n * User: ddumanskiy\n * Date: 21.11.13\n * Time: 13:04\n */\npublic class Profile {\n\n    public volatile DashBoard[] dashBoards = EMPTY_DASHBOARDS;\n\n    public volatile App[] apps = EMPTY_APPS;\n\n    @JsonView(View.Private.class)\n    @JsonDeserialize(keyUsing = DashPinStorageKeyDeserializer.class,\n                     contentUsing = PinStorageValueDeserializer.class)\n    public final Map<DashPinStorageKey, PinStorageValue> pinsStorage = new HashMap<>();\n\n    //todo this method is very wrong, need to something with it.\n    private static final DashBoard EMPTY_DASH = new DashBoard();\n    public DashBoard getFirstDashOrEmpty() {\n        if (dashBoards.length == 0) {\n            return EMPTY_DASH;\n        }\n        return dashBoards[0];\n    }\n\n    public String getDeviceName(DashBoard dash, int deviceId) {\n        Device device = getDeviceById(dash, deviceId);\n        if (device != null) {\n            return truncateFileName(device.name);\n        }\n        return \"\";\n    }\n\n    public String getCSVDeviceName(DashBoard dash, int deviceId) {\n        Device device = getDeviceById(dash, deviceId);\n        if (device == null) {\n            return String.valueOf(deviceId);\n        }\n\n        String deviceName = device.name;\n        if (deviceName == null || deviceName.isEmpty()) {\n            return String.valueOf(deviceId);\n        }\n\n        return StringUtils.escapeCSV(deviceName);\n    }\n\n    public Device getDeviceById(DashBoard dash, int id) {\n        for (Device device : dash.devices) {\n            if (device.id == id) {\n                return device;\n            }\n        }\n        return null;\n    }\n\n    public void addDevice(DashBoard dash, Device device) {\n        dash.devices = ArrayUtil.add(dash.devices, device, Device.class);\n    }\n\n    public Device deleteDevice(DashBoard dash, int deviceId) {\n        int existingDeviceIndex = getDeviceIndexByIdOrThrow(dash, deviceId);\n        Device deviceToRemove = dash.devices[existingDeviceIndex];\n        dash.devices = ArrayUtil.remove(dash.devices, existingDeviceIndex, Device.class);\n        dash.eraseWidgetValuesForDevice(deviceId);\n        return deviceToRemove;\n    }\n\n    private int getDeviceIndexByIdOrThrow(DashBoard dash, int id) {\n        Device[] devices = dash.devices;\n        for (int i = 0; i < devices.length; i++) {\n            if (devices[i].id == id) {\n                return i;\n            }\n        }\n        throw new IllegalCommandException(\"Device with passed id not found.\");\n    }\n\n    public void setOfflineDevice(DashBoard dash) {\n        Device[] devices = dash.devices;\n        if (devices != null) {\n            for (Device device : devices) {\n                device.status = Status.OFFLINE;\n            }\n        }\n    }\n\n    public void deleteTag(DashBoard dash, int tagId) {\n        int existingTagIndex = getTagIndexByIdOrThrow(dash, tagId);\n        dash.tags = ArrayUtil.remove(dash.tags, existingTagIndex, Tag.class);\n    }\n\n    public void addTag(DashBoard dash, Tag newTag) {\n        dash.tags = ArrayUtil.add(dash.tags, newTag, Tag.class);\n    }\n\n    private int getTagIndexByIdOrThrow(DashBoard dash, int id) {\n        for (int i = 0; i < dash.tags.length; i++) {\n            if (dash.tags[i].id == id) {\n                return i;\n            }\n        }\n        throw new IllegalCommandException(\"Tag with passed id not found.\");\n    }\n\n    public Tag getTagById(DashBoard dash, int id) {\n        for (Tag tag : dash.tags) {\n            if (tag.id == id) {\n                return tag;\n            }\n        }\n        return null;\n    }\n\n    public void deleteDeviceFromTags(DashBoard dash, int deviceId) {\n        for (Tag tag : dash.tags) {\n            tag.deleteDevice(deviceId);\n        }\n    }\n\n    public void cleanPinStorage(DashBoard dash, Widget widget, boolean removeTemplates) {\n        cleanPinStorageInternalWithoutUpdatedAt(dash, widget, true, removeTemplates);\n        dash.updatedAt = System.currentTimeMillis();\n    }\n\n    public void cleanPinStorageForTileTemplate(DashBoard dash, TileTemplate tileTemplate,\n                                               boolean removeProperties) {\n        for (int deviceId : tileTemplate.deviceIds) {\n            for (Widget widget : tileTemplate.widgets) {\n                if (widget instanceof OnePinWidget) {\n                    OnePinWidget onePinWidget = (OnePinWidget) widget;\n                    cleanPinStorage(dash, onePinWidget, deviceId, removeProperties);\n                } else if (widget instanceof MultiPinWidget) {\n                    MultiPinWidget multiPinWidget = (MultiPinWidget) widget;\n                    cleanPinStorage(dash, multiPinWidget, deviceId, removeProperties);\n                }\n            }\n        }\n    }\n\n    private void cleanPinStorage(DashBoard dash,\n                                        MultiPinWidget multiPinWidget, int targetId, boolean removeProperties) {\n        if (multiPinWidget.dataStreams != null) {\n            for (DataStream dataStream : multiPinWidget.dataStreams) {\n                if (dataStream != null && dataStream.isValid()) {\n                    removePinStorageValue(dash, targetId == -1 ? multiPinWidget.deviceId : targetId,\n                            dataStream.pinType, dataStream.pin, removeProperties);\n                }\n            }\n        }\n    }\n\n    private void cleanPinStorage(DashBoard dash, OnePinWidget onePinWidget,\n                                 int targetId, boolean removeProperties) {\n        if (onePinWidget.isValid()) {\n            removePinStorageValue(dash, targetId == -1 ? onePinWidget.deviceId : targetId,\n                    onePinWidget.pinType, onePinWidget.pin, removeProperties);\n        }\n    }\n\n    private void removePinStorageValue(DashBoard dash, int targetId,\n                                       PinType pinType, short pin, boolean removeProperties) {\n        Target target;\n        if (targetId < Tag.START_TAG_ID) {\n            target = getDeviceById(dash, targetId);\n        } else if (targetId < DeviceSelector.DEVICE_SELECTOR_STARTING_ID) {\n            target = getTagById(dash, targetId);\n        } else {\n            //means widget assigned to device selector widget.\n            target = dash.getDeviceSelector(targetId);\n        }\n        if (target != null) {\n            for (int deviceId : target.getAssignedDeviceIds()) {\n                pinsStorage.remove(new DashPinStorageKey(dash.id, deviceId, pinType, pin));\n                if (removeProperties) {\n                    for (WidgetProperty widgetProperty : WidgetProperty.getValues()) {\n                        pinsStorage.remove(\n                                new DashPinPropertyStorageKey(dash.id, deviceId, pinType, pin, widgetProperty));\n                    }\n                }\n            }\n        }\n    }\n\n    public void sendAppSyncs(DashBoard dash, Channel appChannel, int targetId) {\n        sendPinStorageSyncs(dash, appChannel, targetId);\n        for (Widget widget : dash.widgets) {\n            if (widget instanceof MobileSyncWidget && appChannel.isWritable()) {\n                ((MobileSyncWidget) widget).sendAppSync(appChannel, dash.id, targetId);\n            }\n        }\n    }\n\n    private void sendPinStorageSyncs(DashBoard dash, Channel appChannel, int targetId) {\n        for (Map.Entry<DashPinStorageKey, PinStorageValue> entry : pinsStorage.entrySet()) {\n            DashPinStorageKey key = entry.getKey();\n            if ((targetId == ANY_TARGET || targetId == key.deviceId)\n                    && dash.id == key.dashId\n                    && appChannel.isWritable()) {\n                PinStorageValue pinStorageValue = entry.getValue();\n                pinStorageValue.sendAppSync(appChannel, dash.id, key);\n            }\n        }\n    }\n\n    public void cleanPinStorage(DashBoard dash, boolean removeProperties,\n                                boolean eraseTemplates) {\n        for (Widget widget : dash.widgets) {\n            cleanPinStorageInternalWithoutUpdatedAt(dash, widget, removeProperties, eraseTemplates);\n        }\n        dash.updatedAt = System.currentTimeMillis();\n    }\n\n    private void cleanPinStorageInternalWithoutUpdatedAt(DashBoard dash, Widget widget,\n                                                         boolean removeProperties, boolean eraseTemplates) {\n        if (widget instanceof OnePinWidget) {\n            OnePinWidget onePinWidget = (OnePinWidget) widget;\n            cleanPinStorage(dash, onePinWidget, -1, removeProperties);\n        } else if (widget instanceof MultiPinWidget) {\n            MultiPinWidget multiPinWidget = (MultiPinWidget) widget;\n            cleanPinStorage(dash, multiPinWidget, -1, removeProperties);\n        } else if (widget instanceof DeviceTiles) {\n            DeviceTiles deviceTiles = (DeviceTiles) widget;\n            cleanPinStorage(dash.id, deviceTiles, removeProperties);\n            if (eraseTemplates) {\n                cleanPinStorageForTemplate(dash, deviceTiles, removeProperties);\n            }\n        }\n    }\n\n    private void cleanPinStorage(int dashId, DeviceTiles deviceTiles, boolean removeProperties) {\n        for (Tile tile : deviceTiles.tiles) {\n            if (tile != null && tile.isValidDataStream()) {\n                DataStream dataStream = tile.dataStream;\n                pinsStorage.remove(new DashPinStorageKey(dashId, tile.deviceId, dataStream.pinType, dataStream.pin));\n                if (removeProperties) {\n                    for (WidgetProperty widgetProperty : WidgetProperty.getValues()) {\n                        pinsStorage.remove(new DashPinPropertyStorageKey(dashId, tile.deviceId,\n                                dataStream.pinType, dataStream.pin, widgetProperty));\n                    }\n                }\n            }\n        }\n    }\n\n    private void cleanPinStorageForTemplate(DashBoard dash,\n                                                   DeviceTiles deviceTiles, boolean removeProperties) {\n        for (TileTemplate tileTemplate : deviceTiles.templates) {\n            cleanPinStorageForTileTemplate(dash, tileTemplate, removeProperties);\n        }\n    }\n\n    public void cleanPinStorageForDevice(int deviceId) {\n        pinsStorage.entrySet().removeIf(entry -> entry.getKey().deviceId == deviceId);\n    }\n\n    public void update(DashBoard dash, int deviceId, short pin, PinType pinType, String value, long now) {\n        if (!dash.updateWidgets(deviceId, pin, pinType, value)) {\n            //special case. #237 if no widget - storing without widget.\n            putPinStorageValue(dash, deviceId, pinType, pin, value);\n        }\n\n        dash.updatedAt = now;\n    }\n\n    public void putPinPropertyStorageValue(DashBoard dash, int deviceId, PinType type, short pin,\n                                           WidgetProperty property, String value) {\n        putPinStorageValue(dash, new DashPinPropertyStorageKey(dash.id, deviceId, type, pin, property), value);\n    }\n\n    private void putPinStorageValue(DashBoard dash, int deviceId, PinType type, short pin, String value) {\n        putPinStorageValue(dash, new DashPinStorageKey(dash.id, deviceId, type, pin), value);\n    }\n\n    private void putPinStorageValue(DashBoard dash, DashPinStorageKey key, String value) {\n        PinStorageValue pinStorageValue = pinsStorage.get(key);\n        if (pinStorageValue == null) {\n            pinStorageValue = dash.initStorageValueForStorageKey(key);\n            pinsStorage.put(key, pinStorageValue);\n        }\n        pinStorageValue.update(value);\n    }\n\n    public Widget getWidgetWithLoggedPin(DashBoard dash, int deviceId, short pin, PinType pinType) {\n        for (Widget widget : dash.widgets) {\n            if (widget instanceof Superchart) {\n                Superchart graph = (Superchart) widget;\n                if (isWithinGraph(dash, graph, pin, pinType, deviceId)) {\n                    return graph;\n                }\n            }\n            if (widget instanceof DeviceTiles) {\n                DeviceTiles deviceTiles = (DeviceTiles) widget;\n                for (TileTemplate tileTemplate : deviceTiles.templates) {\n                    for (Widget tilesWidget : tileTemplate.widgets) {\n                        if (tilesWidget instanceof Superchart) {\n                            Superchart graph = (Superchart) tilesWidget;\n                            if (isWithinGraph(dash, graph, pin, pinType, deviceId, tileTemplate.deviceIds)) {\n                                return graph;\n                            }\n                        }\n                    }\n                }\n            }\n            if (widget instanceof ReportingWidget) {\n                ReportingWidget reportingWidget = (ReportingWidget) widget;\n                if (reportingWidget.hasPin(pin, pinType)) {\n                    return reportingWidget;\n                }\n            }\n        }\n        return null;\n    }\n\n    private boolean isWithinGraph(DashBoard dash, Superchart graph,\n                                         short pin, PinType pinType, int deviceId, int... deviceIds) {\n        for (GraphDataStream graphDataStream : graph.dataStreams) {\n            if (graphDataStream != null && graphDataStream.dataStream != null\n                    && graphDataStream.dataStream.isSame(pin, pinType)) {\n\n                int graphTargetId = graphDataStream.targetId;\n\n                //this is the case when datastream assigned directly to the device\n                if (deviceId == graphTargetId) {\n                    return true;\n                }\n\n                //this is the case when graph is within deviceTiles\n                if (deviceIds != null && ArrayUtil.contains(deviceIds, deviceId)) {\n                    return true;\n                }\n\n                //this is the case when graph is within device selector or tags\n                Target target;\n                if (graphTargetId < Tag.START_TAG_ID) {\n                    target = getDeviceById(dash, graphTargetId);\n                } else if (graphTargetId < DeviceSelector.DEVICE_SELECTOR_STARTING_ID) {\n                    target = getTagById(dash, graphTargetId);\n                } else {\n                    //means widget assigned to device selector widget.\n                    target = dash.getDeviceSelector(graphTargetId);\n                }\n                if (target != null && target.contains(deviceId)) {\n                    return true;\n                }\n            }\n        }\n        return false;\n    }\n\n    public int getDashIndexOrThrow(int dashId) {\n        for (int i = 0; i < dashBoards.length; i++) {\n            if (dashBoards[i].id == dashId) {\n                return i;\n            }\n        }\n        throw new IllegalCommandException(\"Dashboard with passed id not found.\");\n    }\n\n    public DashBoard getDashByIdOrThrow(int id) {\n        for (DashBoard dashBoard : dashBoards) {\n            if (dashBoard.id == id) {\n                return dashBoard;\n            }\n        }\n        throw new IllegalCommandException(\"Dashboard with passed id not found.\");\n    }\n\n    public DashBoard getDashById(int id) {\n        for (DashBoard dashBoard : dashBoards) {\n            if (dashBoard.id == id) {\n                return dashBoard;\n            }\n        }\n        return null;\n    }\n\n    public int getAppIndexById(String id) {\n        for (int i = 0; i < apps.length; i++) {\n            if (apps[i].id.equals(id)) {\n                return i;\n            }\n        }\n        return -1;\n    }\n\n    public App getAppById(String id) {\n        for (App app : apps) {\n            if (app.id.equals(id)) {\n                return app;\n            }\n        }\n        return null;\n    }\n\n    @Override\n    public String toString() {\n        return JsonParser.toJson(this);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n\n        Profile that = (Profile) o;\n\n        return Arrays.equals(dashBoards, that.dashBoards);\n    }\n\n    @Override\n    public int hashCode() {\n        return Arrays.hashCode(dashBoards);\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/auth/App.java",
    "content": "package cc.blynk.server.core.model.auth;\n\nimport cc.blynk.server.core.model.enums.ProvisionType;\nimport cc.blynk.server.core.model.enums.Theme;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 5/10/17.\n */\npublic class App {\n\n    public String id;\n\n    public volatile Theme theme;\n\n    public volatile ProvisionType provisionType;\n\n    public volatile int color;\n\n    public volatile boolean isMultiFace;\n\n    public volatile String name;\n\n    public volatile String icon;\n\n    public volatile int[] projectIds;\n\n    @JsonCreator\n    public App(@JsonProperty(\"id\") String id,\n               @JsonProperty(\"theme\") Theme theme,\n               @JsonProperty(\"provisionType\") ProvisionType provisionType,\n               @JsonProperty(\"color\") int color,\n               @JsonProperty(\"isMultiFace\") boolean isMultiFace,\n               @JsonProperty(\"name\") String name,\n               @JsonProperty(\"icon\") String icon,\n               @JsonProperty(\"projectIds\") int[] projectIds) {\n        this.id = id;\n        this.theme = theme;\n        this.provisionType = provisionType;\n        this.color = color;\n        this.isMultiFace = isMultiFace;\n        this.name = name;\n        this.icon = icon;\n        this.projectIds = projectIds;\n    }\n\n    public void update(App newApp) {\n        this.theme = newApp.theme;\n        this.provisionType = newApp.provisionType;\n        this.color = newApp.color;\n        this.isMultiFace = newApp.isMultiFace;\n        this.name = newApp.name;\n        this.icon = newApp.icon;\n        this.projectIds = newApp.projectIds;\n    }\n\n    public boolean isNotValid() {\n        return theme == null || provisionType == null || name == null\n                || name.isEmpty() || projectIds == null;\n    }\n\n    @Override\n    public String toString() {\n        return JsonParser.toJson(this);\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/auth/FacebookTokenResponse.java",
    "content": "package cc.blynk.server.core.model.auth;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 25.09.16.\n */\npublic class FacebookTokenResponse {\n\n    public final String email;\n\n    @JsonCreator\n    public FacebookTokenResponse(@JsonProperty(\"email\") String email) {\n        this.email = email;\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/auth/Session.java",
    "content": "package cc.blynk.server.core.model.auth;\n\nimport cc.blynk.server.common.BaseSimpleChannelInboundHandler;\nimport cc.blynk.server.core.protocol.handlers.decoders.MessageDecoder;\nimport cc.blynk.server.core.protocol.handlers.decoders.MobileMessageDecoder;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.server.core.session.HardwareStateHolder;\nimport cc.blynk.utils.ArrayUtil;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelFutureListener;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.EventLoop;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.util.HashSet;\nimport java.util.Set;\nimport java.util.concurrent.ConcurrentHashMap;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.deviceOffline;\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeUTF8StringMessage;\nimport static cc.blynk.server.internal.StateHolderUtil.getHardState;\nimport static cc.blynk.server.internal.StateHolderUtil.isSameDash;\nimport static cc.blynk.server.internal.StateHolderUtil.isSameDashAndDeviceId;\nimport static cc.blynk.utils.StringUtils.prependDashIdAndDeviceId;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n * <p>\n * DefaultChannelGroup.java too complicated. so doing in simple way for now.\n */\npublic class Session {\n\n    private static final Logger log = LogManager.getLogger(Session.class);\n\n    public final EventLoop initialEventLoop;\n    public final Set<Channel> appChannels = ConcurrentHashMap.newKeySet();\n    public final Set<Channel> hardwareChannels = ConcurrentHashMap.newKeySet();\n\n    private final ChannelFutureListener appRemover = future -> appChannels.remove(future.channel());\n    private final ChannelFutureListener hardRemover = future -> hardwareChannels.remove(future.channel());\n\n    public Session(EventLoop initialEventLoop) {\n        this.initialEventLoop = initialEventLoop;\n    }\n\n    public boolean isSameEventLoop(ChannelHandlerContext ctx) {\n        return isSameEventLoop(ctx.channel());\n    }\n\n    public boolean isSameEventLoop(Channel channel) {\n        return initialEventLoop == channel.eventLoop();\n    }\n\n    private static int getRequestRate(Set<Channel> channels) {\n        double sum = 0;\n        for (Channel ch : channels) {\n            MessageDecoder messageDecoder = ch.pipeline().get(MessageDecoder.class);\n            if (messageDecoder != null) {\n                sum += messageDecoder.getQuotaMeter().getOneMinuteRateNoTick();\n            } else {\n                MobileMessageDecoder mobileMessageDecoder = ch.pipeline().get(MobileMessageDecoder.class);\n                if (mobileMessageDecoder != null) {\n                    sum += mobileMessageDecoder.getQuotaMeter().getOneMinuteRateNoTick();\n                }\n            }\n        }\n        return (int) sum;\n    }\n\n    public static boolean needSync(Channel channel, String sharedToken) {\n        BaseSimpleChannelInboundHandler appHandler = channel.pipeline().get(BaseSimpleChannelInboundHandler.class);\n        return appHandler != null && appHandler.getState().contains(sharedToken);\n    }\n\n    public void addAppChannel(Channel appChannel) {\n        if (appChannels.add(appChannel)) {\n            appChannel.closeFuture().addListener(appRemover);\n        }\n    }\n\n    public void addHardChannel(Channel hardChannel) {\n        if (hardwareChannels.add(hardChannel)) {\n            hardChannel.closeFuture().addListener(hardRemover);\n        }\n    }\n\n    private Set<Channel> filter(int bodySize, int activeDashId, int[] deviceIds) {\n        Set<Channel> targetChannels = new HashSet<>();\n        for (Channel channel : hardwareChannels) {\n            HardwareStateHolder hardwareState = getHardState(channel);\n            if (hardwareState != null && hardwareState.dash.id == activeDashId\n                    && (deviceIds.length == 0 || ArrayUtil.contains(deviceIds, hardwareState.device.id))) {\n                if (hardwareState.device.fitsBufferSize(bodySize)) {\n                    targetChannels.add(channel);\n                } else {\n                    log.trace(\"Message is to large. Size {}.\", bodySize);\n                }\n            }\n        }\n        return targetChannels;\n    }\n\n    private Set<Channel> filter(int bodySize, int activeDashId, int deviceId) {\n        Set<Channel> targetChannels = new HashSet<>();\n        for (Channel channel : hardwareChannels) {\n            HardwareStateHolder hardwareState = getHardState(channel);\n            if (hardwareState != null && hardwareState.isSameDashAndDeviceId(activeDashId, deviceId)) {\n                if (hardwareState.device.fitsBufferSize(bodySize)) {\n                    targetChannels.add(channel);\n                } else {\n                    log.trace(\"Message is to large. Size {}.\", bodySize);\n                }\n            }\n        }\n        return targetChannels;\n    }\n\n    public boolean sendMessageToHardware(int activeDashId, short cmd, int msgId, String body, int deviceId) {\n        return hardwareChannels.size() == 0\n                || sendMessageToHardware(filter(body.length(), activeDashId, deviceId), cmd, msgId, body);\n    }\n\n    public boolean sendMessageToHardware(int activeDashId, short cmd, int msgId, String body, int... deviceIds) {\n        return hardwareChannels.size() == 0\n                || sendMessageToHardware(filter(body.length(), activeDashId, deviceIds), cmd, msgId, body);\n    }\n\n    public boolean sendMessageToHardware(short cmd, int msgId, String body) {\n        return sendMessageToHardware(hardwareChannels, cmd, msgId, body);\n    }\n\n    private boolean sendMessageToHardware(Set<Channel> targetChannels, short cmd, int msgId, String body) {\n        int channelsNum = targetChannels.size();\n        if (channelsNum == 0) {\n            return true; // -> no active hardware\n        }\n\n        send(targetChannels, cmd, msgId, body);\n\n        return false; // -> there is active hardware\n    }\n\n    public boolean isHardwareConnected() {\n        return hardwareChannels.size() > 0;\n    }\n\n    public boolean isHardwareConnected(int dashId, int deviceId) {\n        for (Channel channel : hardwareChannels) {\n            if (isSameDashAndDeviceId(channel, dashId, deviceId)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    public boolean isHardwareConnected(int dashId) {\n        for (Channel channel : hardwareChannels) {\n            if (isSameDash(channel, dashId)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    public void sendOfflineMessageToApps(int dashId, int deviceId) {\n        int targetsNum = appChannels.size();\n        if (targetsNum > 0) {\n            log.trace(\"Sending device offline message.\");\n\n            StringMessage deviceOfflineMessage = deviceOffline(dashId, deviceId);\n            sendMessageToMultipleReceivers(appChannels, deviceOfflineMessage);\n        }\n    }\n\n    public void sendToApps(short cmd, int msgId, int dashId, int deviceId, String body) {\n        if (isAppConnected()) {\n            String finalBody = prependDashIdAndDeviceId(dashId, deviceId, body);\n            sendToApps(cmd, msgId, dashId, finalBody);\n        }\n    }\n\n    public void sendToApps(short cmd, int msgId, int dashId, String finalBody) {\n        Set<Channel> targetChannels = filterByDash(dashId);\n\n        int targetsNum = targetChannels.size();\n        if (targetsNum > 0) {\n            send(targetChannels, cmd, msgId, finalBody);\n        }\n    }\n\n    private Set<Channel> filterByDash(int dashId) {\n        Set<Channel> targetChannels = new HashSet<>();\n        for (Channel channel : appChannels) {\n            if (isSameDash(channel, dashId)) {\n                targetChannels.add(channel);\n            }\n        }\n        return targetChannels;\n    }\n\n    private static void sendMessageToMultipleReceivers(Set<Channel> targets, StringMessage msg) {\n        for (Channel channel : targets) {\n            if (channel.isWritable()) {\n                channel.writeAndFlush(msg, channel.voidPromise());\n            }\n        }\n    }\n\n    private static void send(Set<Channel> targets, short cmd, int msgId, String body) {\n        StringMessage msg = makeUTF8StringMessage(cmd, msgId, body);\n        sendMessageToMultipleReceivers(targets, msg);\n    }\n\n    public void sendToSharedApps(Channel sendingChannel, String sharedToken, short cmd, int msgId, String body) {\n        Set<Channel> targetChannels = new HashSet<>();\n        for (Channel channel : appChannels) {\n            if (channel != sendingChannel && needSync(channel, sharedToken)) {\n                targetChannels.add(channel);\n            }\n        }\n\n        int channelsNum = targetChannels.size();\n        if (channelsNum > 0) {\n            send(targetChannels, cmd, msgId, body);\n        }\n    }\n\n    public boolean isAppConnected() {\n        return appChannels.size() > 0;\n    }\n\n    public int getAppRequestRate() {\n        return getRequestRate(appChannels);\n    }\n\n    public int getHardRequestRate() {\n        return getRequestRate(hardwareChannels);\n    }\n\n    public void closeHardwareChannelByDeviceId(int dashId, int deviceId) {\n        for (Channel channel : hardwareChannels) {\n            if (isSameDashAndDeviceId(channel, dashId, deviceId)) {\n                channel.close();\n            }\n        }\n    }\n\n    public void closeHardwareChannelByDashId(int dashId) {\n        for (Channel channel : hardwareChannels) {\n            if (isSameDash(channel, dashId)) {\n                channel.close();\n            }\n        }\n    }\n\n    public void closeAll() {\n        hardwareChannels.forEach(io.netty.channel.Channel::close);\n        appChannels.forEach(io.netty.channel.Channel::close);\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/auth/User.java",
    "content": "package cc.blynk.server.core.model.auth;\n\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.Profile;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.processors.NotificationBase;\nimport cc.blynk.utils.AppNameUtil;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * User: ddumanskiy\n * Date: 8/11/13\n * Time: 4:03 PM\n */\npublic class User {\n\n    private static final int INITIAL_ENERGY_AMOUNT = Integer.parseInt(System.getProperty(\"initial.energy\", \"2000\"));\n\n    public String name;\n\n    //key fields\n    public String email;\n    public String appName;\n    public String region;\n    public String ip;\n\n    public volatile String pass;\n\n    //used mostly to understand if user profile was changed,\n    // all other fields update ignored as it is not so important\n    public volatile long lastModifiedTs;\n\n    public String lastLoggedIP;\n    public long lastLoggedAt;\n\n    public Profile profile;\n\n    public boolean isFacebookUser;\n    public boolean isSuperAdmin;\n\n    public volatile int energy;\n\n    public transient int emailMessages;\n    private transient long emailSentTs;\n\n    //used just for tests and serialization\n    public User() {\n        this.lastModifiedTs = System.currentTimeMillis();\n        this.profile = new Profile();\n        this.energy = INITIAL_ENERGY_AMOUNT;\n        this.isFacebookUser = false;\n        this.appName = AppNameUtil.BLYNK;\n    }\n\n    public User(String email, String passHash, String appName, String region, String ip,\n                boolean isFacebookUser, boolean isSuperAdmin) {\n        this();\n        this.email = email;\n        this.name = email;\n        this.pass = passHash;\n        this.appName = appName;\n        this.region = region;\n        this.ip = ip;\n        this.isFacebookUser = isFacebookUser;\n        this.isSuperAdmin = isSuperAdmin;\n    }\n\n    //used when user is fully read from DB\n    public User(String email, String passHash, String appName, String region, String ip,\n                boolean isFacebookUser, boolean isSuperAdmin, String name,\n                long lastModifiedTs, long lastLoggedAt, String lastLoggedIP,\n                Profile profile, int energy) {\n        this.email = email;\n        this.name = email;\n        this.pass = passHash;\n        this.appName = appName;\n        this.region = region;\n        this.ip = ip;\n        this.isFacebookUser = isFacebookUser;\n        this.isSuperAdmin = isSuperAdmin;\n        this.name = name;\n        this.lastModifiedTs = lastModifiedTs;\n        this.lastLoggedAt = lastLoggedAt;\n        this.lastLoggedIP = lastLoggedIP;\n        this.profile = profile;\n        this.energy = energy;\n    }\n\n    @JsonProperty(\"id\")\n    private String id() {\n        return email + \"-\" + appName;\n    }\n\n    public boolean notEnoughEnergy(int price) {\n        return price > energy && AppNameUtil.BLYNK.equals(appName);\n    }\n\n    @SuppressWarnings(\"NonAtomicOperationOnVolatileField\")\n    public void subtractEnergy(int price) {\n        //non-atomic. we are fine with that, always updated from 1 thread\n        this.energy -= price;\n    }\n\n    @SuppressWarnings(\"NonAtomicOperationOnVolatileField\")\n    public void addEnergy(int price) {\n        //non-atomic. we are fine with that, always updated from 1 thread\n        this.energy += price;\n        this.lastModifiedTs = System.currentTimeMillis();\n    }\n\n    private static final int EMAIL_DAY_LIMIT = 100;\n    private static final long MILLIS_IN_DAY = 24 * 60 * 60 * 1000;\n\n    public void checkDailyEmailLimit() {\n        long now = System.currentTimeMillis();\n        if (now - emailSentTs < MILLIS_IN_DAY) {\n            if (emailMessages > EMAIL_DAY_LIMIT) {\n                throw NotificationBase.EXCEPTION_CACHE;\n            }\n        } else {\n            this.emailMessages = 0;\n            this.emailSentTs = now;\n        }\n    }\n\n    public boolean isUpdated(long lastStart) {\n        return (lastStart <= lastModifiedTs) || isDashUpdated(lastStart);\n    }\n\n    public void resetPass(String hash) {\n        this.pass = hash;\n        this.lastModifiedTs = System.currentTimeMillis();\n    }\n\n    private boolean isDashUpdated(long lastStart) {\n        for (DashBoard dashBoard : profile.dashBoards) {\n            if (lastStart <= dashBoard.updatedAt) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (!(o instanceof User)) {\n            return false;\n        }\n\n        User user = (User) o;\n\n        if (email != null ? !email.equals(user.email) : user.email != null) {\n            return false;\n        }\n        return !(appName != null ? !appName.equals(user.appName) : user.appName != null);\n\n    }\n\n    @Override\n    public int hashCode() {\n        int result = email != null ? email.hashCode() : 0;\n        result = 31 * result + (appName != null ? appName.hashCode() : 0);\n        return result;\n    }\n\n    @Override\n    public String toString() {\n        return JsonParser.toJson(this);\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/device/BoardType.java",
    "content": "package cc.blynk.server.core.model.device;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonValue;\n\n/**\n * Used mostly to minimize memory footprint used by boardType strings.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 08.06.18.\n */\npublic enum BoardType {\n\n    ESP8266(\"ESP8266\"),\n    Arduino_UNO(\"Arduino UNO\"),\n    NodeMCU(\"NodeMCU\"),\n    Raspberry_Pi_3_B(\"Raspberry Pi 3 B\"),\n    WeMos_D1_mini(\"WeMos D1 mini\"),\n    Arduino_Nano(\"Arduino Nano\"),\n    Arduino_Mega(\"Arduino Mega\"),\n    ESP32_Dev_Board(\"ESP32 Dev Board\"),\n    WeMos_D1(\"WeMos D1\"),\n    Generic_Board(\"Generic Board\"),\n    Raspberry_Pi_2_A_B(\"Raspberry Pi 2/A+/B+\"),\n    Particle_Photon(\"Particle Photon\"),\n    Arduino_MKR1000(\"Arduino MKR1000\"),\n    Arduino_101(\"Arduino 101\"),\n    Arduino_Yun(\"Arduino Yun\"),\n    Raspberry_Pi_A_B_v2(\"Raspberry Pi A/B (Rev2)\"),\n    Arduino_Pro_Mini(\"Arduino Pro Mini\"),\n    Arduino_Leonardo(\"Arduino Leonardo\"),\n    Raspberry_Pi_B_v1(\"Raspberry Pi B (Rev1)\"),\n    Arduino_Due(\"Arduino Due\"),\n    SparkFun_Blynk_Board(\"SparkFun Blynk Board\"),\n    Orange_Pi(\"Orange Pi\"),\n    BBC_Microbit(\"BBC Micro:bit\"),\n    Arduino_Mini(\"Arduino Mini\"),\n    Arduino_Micro(\"Arduino Micro\"),\n    Onion_Omega(\"Onion Omega\"),\n    Arduino_Pro_Micro(\"Arduino Pro Micro\"),\n    Particle_Core(\"Particle Core\"),\n    SparkFun_ESP8266_Thing(\"SparkFun ESP8266 Thing\"),\n    STM32F103C_Blue_Pill(\"STM32F103C Blue Pill\"),\n    WiPy(\"WiPy\"),\n    Particle_Electron(\"Particle Electron\"),\n    Arduino_Zero(\"Arduino Zero\"),\n    Intel_Edison(\"Intel Edison\"),\n    Teensy_3(\"Teensy 3\"),\n    LinkIt_ONE(\"LinkIt ONE\"),\n    NanoPi(\"NanoPi\"),\n    LightBlue_Bean(\"LightBlue Bean\"),\n    Intel_Galileo(\"Intel Galileo\"),\n    RedBearLab_BLE_Nano(\"RedBearLab BLE Nano\"),\n    RedBear_Duo(\"RedBear Duo\"),\n    TI_CC3200_LaunchXL(\"TI CC3200-LaunchXL\"),\n    Digistump_Oak(\"Digistump Oak\"),\n    Seeed_Wio_Link(\"Seeed Wio Link\"),\n    TI_Tiva_C_Connected(\"TI Tiva C Connected\"),\n    Samsung_ARTIK_5(\"Samsung ARTIK 5\"),\n    Microduino_CoreUSB(\"Microduino CoreUSB\"),\n    Espruino_Pico(\"Espruino Pico\"),\n    TinyDuino(\"TinyDuino\"),\n    Microduino_Core_plus(\"Microduino Core+\"),\n    chipKIT_Uno32(\"chipKIT Uno32\"),\n    The_AirBoard(\"The AirBoard\"),\n    Microduino_Core(\"Microduino Core\"),\n    Simblee(\"Simblee\"),\n    LeMaker_Banana_Pro(\"LeMaker Banana Pro\"),\n    Wildfire_v2(\"Wildfire v2\"),\n    LightBlue_Bean_plus(\"LightBlue Bean+\"),\n    SparkFun_Photon_RedBoard(\"SparkFun Photon RedBoard\"),\n    Microduino_CoreRF(\"Microduino CoreRF\"),\n    RedBearLab_CC3200_Mini(\"RedBearLab CC3200/Mini\"),\n    Bluz(\"Bluz\"),\n    LeMaker_Guitar(\"LeMaker Guitar\"),\n    panStamp_esp_output(\"panStamp esp-output\"),\n    Digistump_Digispark(\"Digistump Digispark\"),\n    RedBearLab_Blend_Micro(\"RedBearLab Blend Micro\"),\n    TI_LM4F120_LaunchPad(\"TI LM4F120 LaunchPad\"),\n    Wildfire_v3(\"Wildfire v3\"),\n    Wildfire_v4(\"Wildfire v4\"),\n    Konekt_Dash_Pro(\"Konekt Dash Pro\");\n\n    public final String label;\n\n    private static final BoardType[] values = values();\n\n    BoardType(String label) {\n        this.label = label;\n    }\n\n    @JsonCreator\n    public static BoardType fromLabel(String label) {\n        for (BoardType type : values) {\n            if (type.label.equals(label)) {\n                return type;\n            }\n        }\n        return Generic_Board;\n    }\n\n    @JsonValue\n    String label() {\n        return label;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/device/ConnectionType.java",
    "content": "package cc.blynk.server.core.model.device;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 16.11.16.\n */\npublic enum ConnectionType {\n\n    ETHERNET,\n    WI_FI,\n    USB,\n    BLUETOOTH,\n    BLE,\n    GSM\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/device/Device.java",
    "content": "package cc.blynk.server.core.model.device;\n\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.model.serialization.View;\nimport cc.blynk.server.core.model.widgets.Target;\nimport com.fasterxml.jackson.annotation.JsonView;\n\nimport static cc.blynk.server.core.model.device.HardwareInfo.DEFAULT_HARDWARE_BUFFER_SIZE;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 16.11.16.\n */\npublic class Device implements Target {\n\n    public int id;\n\n    public volatile String name;\n\n    public volatile BoardType boardType;\n\n    //used for bluetooth\n    public volatile String address;\n\n    @JsonView(View.Private.class)\n    public volatile String token;\n\n    public volatile String vendor;\n\n    public volatile ConnectionType connectionType;\n\n    @JsonView(View.Private.class)\n    public volatile Status status = Status.OFFLINE;\n\n    @JsonView(View.Private.class)\n    public volatile long disconnectTime;\n\n    @JsonView(View.Private.class)\n    public volatile long connectTime;\n\n    @JsonView(View.Private.class)\n    public volatile long firstConnectTime;\n\n    @JsonView(View.Private.class)\n    public volatile long dataReceivedAt;\n\n    @JsonView(View.Private.class)\n    public volatile String lastLoggedIP;\n\n    @JsonView(View.Private.class)\n    public volatile HardwareInfo hardwareInfo;\n\n    @JsonView(View.Private.class)\n    public volatile DeviceOtaInfo deviceOtaInfo;\n\n    public volatile String iconName;\n\n    public volatile boolean isUserIcon;\n\n    public Device(int id, String name, BoardType boardType) {\n        this.id = id;\n        this.name = name;\n        this.boardType = boardType;\n    }\n\n    public Device() {\n    }\n\n    public boolean isNotValid() {\n        return boardType == null || (name != null && name.length() > 50);\n    }\n\n    @Override\n    public int[] getDeviceIds() {\n        return new int[] {id};\n    }\n\n    @Override\n    public boolean isSelected(int deviceId) {\n        return id == deviceId;\n    }\n\n    @Override\n    public int[] getAssignedDeviceIds() {\n        return new int[] {id};\n    }\n\n    @Override\n    public boolean contains(int deviceId) {\n        return this.id == deviceId;\n    }\n\n    @Override\n    public int getDeviceId() {\n        return id;\n    }\n\n    public void update(Device newDevice) {\n        this.name = newDevice.name;\n        this.vendor = newDevice.vendor;\n        this.boardType = newDevice.boardType;\n        this.address = newDevice.address;\n        this.connectionType = newDevice.connectionType;\n        this.iconName = newDevice.iconName;\n        this.isUserIcon = newDevice.isUserIcon;\n        //that's fine. leave this fields as it is. It cannot be update from app client.\n        //this.hardwareInfo = newDevice.hardwareInfo;\n        //this.deviceOtaInfo = newDevice.deviceOtaInfo;\n    }\n\n    public void disconnected() {\n        this.status = Status.OFFLINE;\n        this.disconnectTime = System.currentTimeMillis();\n    }\n\n    public void connected() {\n        this.status = Status.ONLINE;\n        this.connectTime = System.currentTimeMillis();\n    }\n\n    public void erase() {\n        this.token = null;\n        this.disconnectTime = 0;\n        this.connectTime = 0;\n        this.firstConnectTime = 0;\n        this.dataReceivedAt = 0;\n        this.lastLoggedIP = null;\n        this.status = Status.OFFLINE;\n        this.hardwareInfo = null;\n        this.deviceOtaInfo = null;\n    }\n\n    public String getNameOrDefault() {\n        return name == null ? \"New Device\" : name;\n    }\n\n    //for single device update device always updated when ota is initiated.\n    public void updateOTAInfo(String initiatedBy) {\n        long now = System.currentTimeMillis();\n        this.deviceOtaInfo = new DeviceOtaInfo(initiatedBy, now, now);\n    }\n\n    public boolean fitsBufferSize(int bodySize) {\n        if (hardwareInfo == null) {\n            return bodySize <= DEFAULT_HARDWARE_BUFFER_SIZE;\n        }\n        return bodySize + 5 <= hardwareInfo.buffIn;\n    }\n\n    @Override\n    public String toString() {\n        return JsonParser.toJson(this);\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/device/DeviceOtaInfo.java",
    "content": "package cc.blynk.server.core.model.device;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 17.08.17.\n */\npublic class DeviceOtaInfo {\n\n    public final String otaInitiatedBy;\n\n    public final long otaInitiatedAt;\n\n    public final long otaUpdateAt;\n\n    @JsonCreator\n    public DeviceOtaInfo(@JsonProperty(\"otaInitiatedBy\") String otaInitiatedBy,\n                         @JsonProperty(\"otaInitiatedAt\") long otaInitiatedAt,\n                         @JsonProperty(\"otaUpdateAt\") long otaUpdateAt) {\n        this.otaInitiatedBy = otaInitiatedBy;\n        this.otaInitiatedAt = otaInitiatedAt;\n        this.otaUpdateAt = otaUpdateAt;\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/device/DeviceStatusDTO.java",
    "content": "package cc.blynk.server.core.model.device;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 18.09.18.\n */\npublic class DeviceStatusDTO {\n\n    public final int id;\n\n    public final String name;\n\n    public final BoardType boardType;\n\n    public final String token;\n\n    public final String vendor;\n\n    public final ConnectionType connectionType;\n\n    public final Status status;\n\n    public final long disconnectTime;\n\n    public final long connectTime;\n\n    public final long dataReceivedAt;\n\n    public final HardwareInfo hardwareInfo;\n\n    public final String iconName;\n\n    public final boolean isUserIcon;\n\n    public DeviceStatusDTO(Device device) {\n        this.id = device.id;\n        this.name = device.name;\n        this.boardType = device.boardType;\n        this.token = device.token;\n        this.vendor = device.vendor;\n        this.connectionType = device.connectionType;\n        this.status = device.status;\n        this.disconnectTime = device.disconnectTime;\n        this.connectTime = device.connectTime;\n        this.dataReceivedAt = device.dataReceivedAt;\n        this.hardwareInfo = device.hardwareInfo;\n        this.iconName = device.iconName;\n        this.isUserIcon = device.isUserIcon;\n    }\n\n    public static DeviceStatusDTO[] transform(Device[] devices) {\n        DeviceStatusDTO[] deviceStatusDTO = new DeviceStatusDTO[devices.length];\n        for (int i = 0; i < devices.length; i++) {\n            deviceStatusDTO[i] = new DeviceStatusDTO(devices[i]);\n        }\n        return deviceStatusDTO;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/device/HardwareInfo.java",
    "content": "package cc.blynk.server.core.model.device;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * This is piece of the information that hardware sends to the server\n * via \"internal\" command right after it is connected to the Blynk Cloud.\n *\n * May be absent in some cases (old firmware, java,  js, python clients)\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 18.05.16.\n */\npublic class HardwareInfo {\n\n    public static final int DEFAULT_HARDWARE_BUFFER_SIZE = 255 - 5; //5 is for blynk header\n\n    public final String version;\n\n    public final String blynkVersion;\n\n    public final String boardType;\n\n    public final String cpuType;\n\n    public final String connectionType;\n\n    public final String build;\n\n    public final String templateId;\n\n    public final int heartbeatInterval;\n\n    public final int buffIn;\n\n    @JsonCreator\n    public HardwareInfo(@JsonProperty(\"version\") String version,\n                        @JsonProperty(\"blynkVersion\") String blynkVersion,\n                        @JsonProperty(\"boardType\") String boardType,\n                        @JsonProperty(\"cpuType\") String cpuType,\n                        @JsonProperty(\"connectionType\") String connectionType,\n                        @JsonProperty(\"build\") String build,\n                        @JsonProperty(\"templateId\") String templateId,\n                        @JsonProperty(\"heartbeatInterval\") int heartbeatInterval,\n                        @JsonProperty(\"buffIn\") int buffIn) {\n        this.version = version;\n        this.blynkVersion = blynkVersion;\n        this.boardType = boardType;\n        this.cpuType = cpuType;\n        this.connectionType = connectionType;\n        this.build = build;\n        this.templateId = templateId;\n        this.heartbeatInterval = heartbeatInterval;\n        this.buffIn = buffIn <= 0 ? DEFAULT_HARDWARE_BUFFER_SIZE : buffIn;\n    }\n\n    public HardwareInfo(String[] info) {\n        HardwareInfoPrivate hardwareInfoPrivate = new HardwareInfoPrivate(info);\n        this.version = hardwareInfoPrivate.version;\n        this.blynkVersion = hardwareInfoPrivate.blynkVersion;\n        this.boardType = hardwareInfoPrivate.boardType;\n        this.cpuType = hardwareInfoPrivate.cpuType;\n        this.connectionType = hardwareInfoPrivate.connectionType;\n        this.build = hardwareInfoPrivate.build;\n        this.templateId = hardwareInfoPrivate.templateId;\n        this.heartbeatInterval = hardwareInfoPrivate.heartbeatInterval;\n        this.buffIn = hardwareInfoPrivate.buffIn <= 0 ? DEFAULT_HARDWARE_BUFFER_SIZE : hardwareInfoPrivate.buffIn;\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/device/HardwareInfoPrivate.java",
    "content": "package cc.blynk.server.core.model.device;\n\n//utility class to make fields of HardwareInfo final, used instead of hashmap\npublic final class HardwareInfoPrivate {\n\n    public String version;\n    public String blynkVersion;\n    public String boardType;\n    public String cpuType;\n    public String connectionType;\n    public String templateId;\n    public String build;\n    public int heartbeatInterval;\n    public int buffIn;\n\n    public HardwareInfoPrivate(String[] info) {\n        for (int i = 0; i < info.length; i++) {\n            if (i < info.length - 1) {\n                intiField(info[i], info[++i]);\n            }\n        }\n    }\n\n    private void intiField(final String key, final String value) {\n        switch (key) {\n            case \"h-beat\" :\n                try {\n                    this.heartbeatInterval = Integer.parseInt(value);\n                } catch (NumberFormatException nfe) {\n                    this.heartbeatInterval = -1;\n                }\n                break;\n            case \"ver\" :\n                this.blynkVersion = value;\n                break;\n            case \"fw\" :\n                this.version = value;\n                break;\n            case \"dev\" :\n                this.boardType = value;\n                break;\n            case \"cpu\" :\n                this.cpuType = value;\n                break;\n            case \"con\" :\n                this.connectionType = value;\n                break;\n            case \"tmpl\" :\n                this.templateId = value;\n                break;\n            case \"build\" :\n                this.build = value;\n                break;\n            case \"buff-in\" :\n                try {\n                    this.buffIn = Integer.parseInt(value);\n                } catch (NumberFormatException nfe) {\n                    this.buffIn = 0;\n                }\n                break;\n        }\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/device/Status.java",
    "content": "package cc.blynk.server.core.model.device;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 16.12.16.\n */\npublic enum Status {\n\n    ONLINE,\n    OFFLINE\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/device/Tag.java",
    "content": "package cc.blynk.server.core.model.device;\n\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.model.widgets.DeviceCleaner;\nimport cc.blynk.server.core.model.widgets.Target;\nimport cc.blynk.utils.ArrayUtil;\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport static cc.blynk.server.internal.EmptyArraysUtil.EMPTY_INTS;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 16.11.16.\n */\npublic class Tag implements Target, DeviceCleaner {\n\n    public static final int START_TAG_ID = 100_000;\n    private static final int MAX_NUMBER_OF_DEVICE_PER_TAG = 25;\n\n    public final int id;\n\n    public volatile String name;\n\n    public volatile int[] deviceIds;\n\n    public boolean isNotValid() {\n        return name == null || name.isEmpty() || name.length() > 40\n                || id < START_TAG_ID || deviceIds.length > MAX_NUMBER_OF_DEVICE_PER_TAG;\n    }\n\n    public Tag(int id, String name) {\n        this.id = id;\n        this.name = name;\n        this.deviceIds = EMPTY_INTS;\n    }\n\n    @JsonCreator\n    public Tag(@JsonProperty(\"id\") int id,\n               @JsonProperty(\"name\") String name,\n               @JsonProperty(\"deviceIds\") int[] deviceIds) {\n        this.id = id;\n        this.name = name;\n        this.deviceIds = deviceIds == null ? EMPTY_INTS : deviceIds;\n    }\n\n    @Override\n    public int[] getDeviceIds() {\n        return deviceIds;\n    }\n\n    @Override\n    public boolean isSelected(int deviceId) {\n        return ArrayUtil.contains(deviceIds, deviceId);\n    }\n\n    @Override\n    public int[] getAssignedDeviceIds() {\n        return deviceIds;\n    }\n\n    @Override\n    public boolean contains(int deviceId) {\n        return ArrayUtil.contains(this.deviceIds, deviceId);\n    }\n\n    @Override\n    public int getDeviceId() {\n        return deviceIds[0];\n    }\n\n    @Override\n    public boolean isTag() {\n        return true;\n    }\n\n    public void update(Tag tag) {\n        this.name = tag.name;\n        this.deviceIds = tag.deviceIds;\n    }\n\n    public Tag copy() {\n        return new Tag(id, name, deviceIds);\n    }\n\n    @Override\n    public void deleteDevice(int deviceId) {\n        this.deviceIds = ArrayUtil.deleteFromArray(this.deviceIds, deviceId);\n    }\n\n    @Override\n    public String toString() {\n        return JsonParser.toJson(this);\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/enums/PinMode.java",
    "content": "package cc.blynk.server.core.model.enums;\n\n/**\n * Defines type of widget.\n * OUT means that widget writes to pin.\n * IN means that widget reads from pin.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 20.11.17.\n */\npublic enum PinMode {\n\n    out,\n    in\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/enums/PinType.java",
    "content": "package cc.blynk.server.core.model.enums;\n\n/**\n * User: ddumanskiy\n * Date: 10.12.13\n * Time: 10:15\n */\npublic enum PinType {\n\n    DIGITAL('d'),\n    VIRTUAL('v'),\n    ANALOG('a');\n\n    public final char pintTypeChar;\n    public final String pinTypeString;\n\n    PinType(char pinType) {\n        this.pintTypeChar = pinType;\n        this.pinTypeString = String.valueOf(pinType);\n    }\n\n    public static PinType getPinType(char pinTypeChar) {\n        switch (pinTypeChar) {\n            case 'a' :\n            case 'A' :\n                return ANALOG;\n            case 'v' :\n            case 'V' :\n                return VIRTUAL;\n            case 'd' :\n            case 'D' :\n                return DIGITAL;\n            default:\n                //NumberFormatException is used for parsing errors\n                throw new NumberFormatException(\"Invalid pin type.\");\n        }\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/enums/ProvisionType.java",
    "content": "package cc.blynk.server.core.model.enums;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 20.03.17.\n */\npublic enum ProvisionType {\n\n    STATIC,\n    DYNAMIC\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/enums/Theme.java",
    "content": "package cc.blynk.server.core.model.enums;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 20.03.17.\n */\npublic enum Theme {\n\n    blynk,\n    Blynk,\n    BlynkLight,\n    SparkFun,\n    AppTheme, //should be removed in future\n    CustomTheme, //should be removed in future\n    AppExport //should be removed in future\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/enums/WidgetProperty.java",
    "content": "package cc.blynk.server.core.model.enums;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 19.11.17.\n */\npublic enum WidgetProperty {\n\n    LABEL(\"label\"),\n    COLOR(\"color\"),\n    ON_BACK_COLOR(\"onBackColor\"),\n    OFF_BACK_COLOR(\"offBackColor\"),\n    ON_COLOR(\"onColor\"),\n    OFF_COLOR(\"offColor\"),\n    ON_LABEL(\"onLabel\"),\n    OFF_LABEL(\"offLabel\"),\n    LABELS(\"labels\"),\n    MIN(\"min\"),\n    MAX(\"max\"),\n    IS_ON_PLAY(\"isOnPlay\"),\n    URL(\"url\"),\n    URLS(\"urls\"),\n    STEP(\"step\"),\n    VALUE_FORMATTING(\"valueFormatting\"),\n    SUFFIX(\"suffix\"),\n    FRACTION(\"maximumFractionDigits\"),\n    OPACITY(\"opacity\"),\n    SCALE(\"scale\"),\n    ROTATION(\"rotation\");\n\n    public final String label;\n\n    private static final WidgetProperty[] values = values();\n\n    WidgetProperty(String label) {\n        this.label = label;\n    }\n\n    public static WidgetProperty getProperty(String value) {\n        switch (value) {\n            case \"label\" :\n                return LABEL;\n            case \"color\" :\n                return COLOR;\n            case \"onLabel\" :\n                return ON_LABEL;\n            case \"onColor\" :\n                return ON_COLOR;\n            case \"onBackColor\" :\n                return ON_BACK_COLOR;\n            case \"offLabel\" :\n                return OFF_LABEL;\n            case \"offColor\" :\n                return OFF_COLOR;\n            case \"offBackColor\" :\n                return OFF_BACK_COLOR;\n            case \"labels\" :\n                return LABELS;\n            case \"min\" :\n                return MIN;\n            case \"max\" :\n                return MAX;\n            case \"isOnPlay\" :\n                return IS_ON_PLAY;\n            case \"url\" :\n                return URL;\n            case \"urls\" :\n                return URLS;\n            case \"step\" :\n                return STEP;\n            case \"valueFormatting\" :\n                return VALUE_FORMATTING;\n            case \"suffix\" :\n                return SUFFIX;\n            case \"maximumFractionDigits\" :\n                return FRACTION;\n            case \"opacity\" :\n                return OPACITY;\n            case \"scale\" :\n                return SCALE;\n            case \"rotation\" :\n                return ROTATION;\n            default:\n                return null;\n        }\n    }\n\n    @Override\n    public String toString() {\n        return label;\n    }\n\n    public static WidgetProperty[] getValues() {\n        return values;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/graph/GraphKey.java",
    "content": "package cc.blynk.server.core.model.graph;\n\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.utils.NumberUtil;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 07.07.15.\n */\npublic class GraphKey {\n\n    public final int dashId;\n\n    public final short pin;\n\n    public final PinType pinType;\n\n    public final String value;\n\n    public final long ts;\n\n    public GraphKey(int dashId, String[] bodyParts, long ts) {\n        this.dashId = dashId;\n        this.pinType = PinType.getPinType(bodyParts[0].charAt(0));\n        this.pin = NumberUtil.parsePin(bodyParts[1]);\n        this.value = bodyParts[2];\n        this.ts = ts;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (!(o instanceof GraphKey)) {\n            return false;\n        }\n\n        GraphKey graphKey = (GraphKey) o;\n\n        if (dashId != graphKey.dashId) {\n            return false;\n        }\n        if (pin != graphKey.pin) {\n            return false;\n        }\n        return pinType == graphKey.pinType;\n    }\n\n    @Override\n    public int hashCode() {\n        int result = dashId;\n        result = 31 * result + (int) pin;\n        result = 31 * result + (pinType != null ? pinType.hashCode() : 0);\n        return result;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/serialization/CopyUtil.java",
    "content": "package cc.blynk.server.core.model.serialization;\n\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.Profile;\nimport cc.blynk.server.core.model.device.Tag;\nimport com.fasterxml.jackson.databind.util.TokenBuffer;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\n/**\n *\n * Used to deep copy objects via Jackson serialization\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 10.09.18.\n */\npublic final class CopyUtil {\n\n    private static final Logger log = LogManager.getLogger(CopyUtil.class);\n\n    private CopyUtil() {\n    }\n\n    public static Tag[] copyTags(Tag[] tagsToCopy) {\n        if (tagsToCopy.length == 0) {\n            return tagsToCopy;\n        }\n        Tag[] copy = new Tag[tagsToCopy.length];\n        for (int i = 0; i < copy.length; i++) {\n            copy[i] = tagsToCopy[i].copy();\n        }\n        return copy;\n    }\n\n    public static DashBoard deepCopy(DashBoard dash) {\n        if (dash == null) {\n            return null;\n        }\n        try {\n            TokenBuffer tb = new TokenBuffer(JsonParser.MAPPER, false);\n            JsonParser.MAPPER.writeValue(tb, dash);\n            return JsonParser.MAPPER.readValue(tb.asParser(), DashBoard.class);\n        } catch (Exception e) {\n            log.error(\"Error during deep copy of dashboard. Reason : {}\", e.getMessage());\n            log.debug(e);\n        }\n        return null;\n    }\n\n    public static Profile deepCopy(Profile profile) {\n        if (profile == null) {\n            return null;\n        }\n        try {\n            TokenBuffer tb = new TokenBuffer(JsonParser.MAPPER, false);\n            JsonParser.MAPPER.writeValue(tb, profile);\n            return JsonParser.MAPPER.readValue(tb.asParser(), Profile.class);\n        } catch (Exception e) {\n            log.error(\"Error during deep copy of profile. Reason : {}\", e.getMessage());\n            log.debug(e);\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/serialization/JsonParser.java",
    "content": "package cc.blynk.server.core.model.serialization;\n\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.DashboardSettings;\nimport cc.blynk.server.core.model.Profile;\nimport cc.blynk.server.core.model.auth.App;\nimport cc.blynk.server.core.model.auth.FacebookTokenResponse;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.device.Tag;\nimport cc.blynk.server.core.model.storage.value.SinglePinStorageValue;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.ui.reporting.Report;\nimport cc.blynk.server.core.model.widgets.ui.tiles.TileTemplate;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandBodyException;\nimport cc.blynk.server.core.stats.model.Stat;\nimport com.fasterxml.jackson.annotation.JsonAutoDetect;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.PropertyAccessor;\nimport com.fasterxml.jackson.databind.DeserializationFeature;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.ObjectReader;\nimport com.fasterxml.jackson.databind.ObjectWriter;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Collection;\nimport java.util.StringJoiner;\nimport java.util.zip.DeflaterOutputStream;\n\nimport static cc.blynk.utils.StringUtils.BODY_SEPARATOR_STRING;\n\n/**\n * User: ddumanskiy\n * Date: 21.11.13\n * Time: 15:31\n */\npublic final class JsonParser {\n\n    private static final Logger log = LogManager.getLogger(JsonParser.class);\n\n    private JsonParser() {\n    }\n\n    //it is threadsafe\n    public static final ObjectMapper MAPPER = init();\n\n    private static final ObjectReader userReader = MAPPER.readerFor(User.class);\n    private static final ObjectReader profileReader = MAPPER.readerFor(Profile.class);\n    private static final ObjectReader dashboardReader = MAPPER.readerFor(DashBoard.class);\n    private static final ObjectReader dashboardSettingsReader = MAPPER.readerFor(DashboardSettings.class);\n    private static final ObjectReader widgetReader = MAPPER.readerFor(Widget.class);\n    private static final ObjectReader tileTemplateReader = MAPPER.readerFor(TileTemplate.class);\n    private static final ObjectReader appReader = MAPPER.readerFor(App.class);\n    private static final ObjectReader deviceReader = MAPPER.readerFor(Device.class);\n    private static final ObjectReader tagReader = MAPPER.readerFor(Tag.class);\n    private static final ObjectReader facebookTokenReader = MAPPER.readerFor(FacebookTokenResponse.class);\n    private static final ObjectReader reportReader = MAPPER.readerFor(Report.class);\n\n    private static final ObjectWriter userWriter = MAPPER.writerFor(User.class);\n    private static final ObjectWriter profileWriter = MAPPER.writerFor(Profile.class);\n    private static final ObjectWriter dashboardWriter = MAPPER.writerFor(DashBoard.class);\n    private static final ObjectWriter deviceWriter = MAPPER.writerFor(Device.class);\n    private static final ObjectWriter appWriter = MAPPER.writerFor(App.class);\n    private static final ObjectWriter reportWriter = MAPPER.writerFor(Report.class);\n\n    public static final ObjectWriter restrictiveDashWriter = init()\n            .writerFor(DashBoard.class).withView(View.PublicOnly.class);\n\n    private static final ObjectWriter restrictiveDashWriterForHttp = init()\n            .writerFor(DashBoard.class).withView(View.PublicOnly.class).withView(View.HttpAPIField.class);\n\n    private static final ObjectWriter restrictiveProfileWriter = init()\n            .writerFor(Profile.class).withView(View.PublicOnly.class);\n\n    private static final ObjectWriter restrictiveWidgetWriter = init()\n            .writerFor(Widget.class).withView(View.PublicOnly.class);\n\n    private static final ObjectWriter statWriter = init().writerWithDefaultPrettyPrinter().forType(Stat.class);\n\n    public static ObjectMapper init() {\n        return new ObjectMapper()\n                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)\n                .setSerializationInclusion(JsonInclude.Include.NON_NULL)\n                .setSerializationInclusion(JsonInclude.Include.NON_EMPTY)\n                .setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE)\n                .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);\n    }\n\n    public static String toJson(User user) {\n        return toJson(userWriter, user);\n    }\n\n    public static String toJson(Profile profile) {\n        return toJson(profileWriter, profile);\n    }\n\n    public static String toJson(DashBoard dashBoard) {\n        return toJson(dashboardWriter, dashBoard);\n    }\n\n    public static byte[] gzipDash(DashBoard dash) {\n        return writeJsonAsCompressedBytes(dashboardWriter, dash);\n    }\n\n    public static byte[] gzipDashRestrictive(DashBoard dash) {\n        return writeJsonAsCompressedBytes(restrictiveDashWriter, dash);\n    }\n\n    public static byte[] gzipProfileRestrictive(Profile profile) {\n        return writeJsonAsCompressedBytes(restrictiveProfileWriter, profile);\n    }\n\n    public static byte[] gzipProfile(Profile profile) {\n        return writeJsonAsCompressedBytes(profileWriter, profile);\n    }\n\n    private static byte[] writeJsonAsCompressedBytes(ObjectWriter objectWriter, Object o) {\n        ByteArrayOutputStream baos = new ByteArrayOutputStream();\n        try (OutputStream out = new DeflaterOutputStream(baos)) {\n            objectWriter.writeValue(out, o);\n        } catch (Exception e) {\n            log.error(\"Error compressing data.\", e);\n            return null;\n        }\n        return baos.toByteArray();\n    }\n\n    public static String toJsonRestrictiveDashboard(DashBoard dashBoard) {\n        return toJson(restrictiveDashWriter, dashBoard);\n    }\n\n    public static String toJsonRestrictiveDashboardForHTTP(DashBoard dashBoard) {\n        return toJson(restrictiveDashWriterForHttp, dashBoard);\n    }\n\n    public static String toJson(Device device) {\n        return toJson(deviceWriter, device);\n    }\n\n    public static String toJson(App app) {\n        return toJson(appWriter, app);\n    }\n\n    public static String toJson(Report report) {\n        return toJson(reportWriter, report);\n    }\n\n    public static String toJson(Stat stat) {\n        return toJson(statWriter, stat);\n    }\n\n    public static void writeUser(File file, User user) throws IOException {\n        userWriter.writeValue(file, user);\n    }\n\n    private static String toJson(ObjectWriter writer, Object o) {\n        try {\n            return writer.writeValueAsString(o);\n        } catch (Exception e) {\n            log.error(\"Error jsoning object.\", e);\n        }\n        return \"{}\";\n    }\n\n    public static String toJson(Widget widget) {\n        try {\n            return restrictiveWidgetWriter.writeValueAsString(widget);\n        } catch (Exception e) {\n            log.error(\"Error jsoning widget.\", e);\n        }\n        return null;\n    }\n\n    public static String toJson(Object o) {\n        try {\n            return MAPPER.writeValueAsString(o);\n        } catch (Exception e) {\n            log.error(\"Error jsoning object.\", e);\n        }\n        return null;\n    }\n\n    public static <T> T readAny(String val, Class<T> c) {\n        try {\n            return MAPPER.readValue(val, c);\n        } catch (Exception e) {\n            log.error(\"Error reading json object.\", e);\n        }\n        return null;\n    }\n\n    public static User parseUserFromFile(Path path) throws IOException {\n        try (InputStream is = Files.newInputStream(path)) {\n            return userReader.readValue(is);\n        }\n    }\n\n    public static User parseUserFromFile(File userFile) throws IOException {\n        return userReader.readValue(userFile);\n    }\n\n    public static User parseUserFromString(String userString) throws IOException {\n        return userReader.readValue(userString);\n    }\n\n    public static Profile parseProfileFromString(String profileString) throws IOException {\n        return profileReader.readValue(profileString);\n    }\n\n    public static FacebookTokenResponse parseFacebookTokenResponse(String response) throws IOException {\n        return facebookTokenReader.readValue(response);\n    }\n\n    public static DashboardSettings parseDashboardSettings(String json, int msgId) {\n        return parse(dashboardSettingsReader, json, \"Error parsing dashboard settings.\", msgId);\n    }\n\n    public static DashBoard parseDashboard(String json, int msgId) {\n        return parse(dashboardReader, json, \"Error parsing dashboard.\", msgId);\n    }\n\n    public static TileTemplate parseTileTemplate(String json, int msgId) {\n        return parse(tileTemplateReader, json, \"Error parsing tile template.\", msgId);\n    }\n\n    public static Widget parseWidget(String reader) throws IOException {\n        return widgetReader.readValue(reader);\n    }\n\n    public static Report parseReport(String json, int msgId) {\n        return parse(reportReader, json, \"Error parsing report.\", msgId);\n    }\n\n    public static Widget parseWidget(String json, int msgId) {\n        return parse(widgetReader, json, \"Error parsing widget.\", msgId);\n    }\n\n    public static App parseApp(String json, int msgId) {\n        return parse(appReader, json, \"Error parsing app.\", msgId);\n    }\n\n    public static Device parseDevice(String json, int msgId) {\n        return parse(deviceReader, json, \"Error parsing device.\", msgId);\n    }\n\n    public static Tag parseTag(String json, int msgId) {\n        return parse(tagReader, json, \"Error parsing tag.\", msgId);\n    }\n\n    private static <T> T parse(ObjectReader objectReader, String json, String errorMessage, int msgId) {\n        try {\n            return objectReader.readValue(json);\n        } catch (IOException e) {\n            log.error(e.getMessage());\n            throw new IllegalCommandBodyException(errorMessage, msgId);\n        }\n    }\n\n    public static String valueToJsonAsString(Collection<String> values) {\n        StringJoiner sj = new StringJoiner(\",\", \"[\", \"]\");\n        for (String value : values) {\n            sj.add(makeJsonStringValue(value));\n        }\n        return sj.toString();\n    }\n\n    public static String valueToJsonAsString(SinglePinStorageValue singlePinStorageValue) {\n        Collection<String> singleValueList = singlePinStorageValue.values();\n        if (singleValueList.size() == 0) {\n            return \"[]\";\n        }\n        String[] values = singleValueList.iterator().next().split(BODY_SEPARATOR_STRING);\n        return valueToJsonAsString(values);\n    }\n\n    private static String valueToJsonAsString(String[] values) {\n        StringJoiner sj = new StringJoiner(\",\", \"[\", \"]\");\n        for (String value : values) {\n            sj.add(makeJsonStringValue(value));\n        }\n        return sj.toString();\n    }\n\n    public static String valueToJsonAsString(String value) {\n        return \"[\\\"\" + value  + \"\\\"]\";\n    }\n\n    private static String makeJsonStringValue(String value) {\n        return \"\\\"\" + value  + \"\\\"\";\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/serialization/View.java",
    "content": "package cc.blynk.server.core.model.serialization;\n\npublic class View {\n\n    /**\n     * special marker class that used to serialize all fields expect those one\n     * that marked with Private annotation\n     */\n    public static class PublicOnly {\n    }\n\n    /**\n     * special utility class that is used to mark private fields\n     * that should not always be visible to the end users.\n     */\n    public static class Private {\n    }\n\n    public static class HttpAPIField {\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/storage/DashPinStorageKeyDeserializer.java",
    "content": "package cc.blynk.server.core.model.storage;\n\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.enums.WidgetProperty;\nimport cc.blynk.server.core.model.storage.key.DashPinPropertyStorageKey;\nimport cc.blynk.server.core.model.storage.key.DashPinStorageKey;\nimport cc.blynk.utils.NumberUtil;\nimport cc.blynk.utils.StringUtils;\nimport com.fasterxml.jackson.databind.DeserializationContext;\nimport com.fasterxml.jackson.databind.KeyDeserializer;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.11.18.\n */\npublic class DashPinStorageKeyDeserializer extends KeyDeserializer {\n\n    @Override\n    public DashPinStorageKey deserializeKey(String key, DeserializationContext ctx) {\n        //parsing \"0-123-v24\"\n        //or\n        //parsing \"0-123-v24-property\"\n        String[] split = key.split(StringUtils.DEVICE_SEPARATOR_STRING);\n\n        int dashId = Integer.parseInt(split[0]);\n        int deviceId = Integer.parseInt(split[1]);\n        PinType pinType = PinType.getPinType(split[2].charAt(0));\n        short pin = NumberUtil.parsePin(split[2].substring(1));\n\n        if (split.length == 4) {\n            WidgetProperty widgetProperty = WidgetProperty.getProperty(split[3]);\n            if (widgetProperty == null) {\n                widgetProperty = WidgetProperty.LABEL;\n            }\n            return new DashPinPropertyStorageKey(dashId, deviceId, pinType, pin, widgetProperty);\n        } else {\n            return new DashPinStorageKey(dashId, deviceId, pinType, pin);\n        }\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/storage/PinStorageKeyDeserializer.java",
    "content": "package cc.blynk.server.core.model.storage;\n\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.enums.WidgetProperty;\nimport cc.blynk.server.core.model.storage.key.PinPropertyStorageKey;\nimport cc.blynk.server.core.model.storage.key.PinStorageKey;\nimport cc.blynk.utils.NumberUtil;\nimport cc.blynk.utils.StringUtils;\nimport com.fasterxml.jackson.databind.DeserializationContext;\nimport com.fasterxml.jackson.databind.KeyDeserializer;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 19.11.16.\n */\n@Deprecated\npublic class PinStorageKeyDeserializer extends KeyDeserializer {\n\n    @Override\n    public PinStorageKey deserializeKey(String key, DeserializationContext ctx) {\n        //parsing \"123-v24\"\n        String[] split = StringUtils.split3(StringUtils.DEVICE_SEPARATOR, key);\n\n        int deviceId = Integer.parseInt(split[0]);\n        PinType pinType = PinType.getPinType(split[1].charAt(0));\n        short pin = 0;\n        try {\n            pin = NumberUtil.parsePin(split[1].substring(1));\n        } catch (NumberFormatException e) {\n            //special case for outdated data format.\n            return new PinStorageKey(deviceId, pinType, pin);\n        }\n        if (split.length == 3) {\n            WidgetProperty widgetProperty = WidgetProperty.getProperty(split[2]);\n            if (widgetProperty == null) {\n                widgetProperty = WidgetProperty.LABEL;\n            }\n            return new PinPropertyStorageKey(deviceId, pinType, pin, widgetProperty);\n        } else {\n            return new PinStorageKey(deviceId, pinType, pin);\n        }\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/storage/PinStorageValueDeserializer.java",
    "content": "package cc.blynk.server.core.model.storage;\n\nimport cc.blynk.server.core.model.storage.value.MultiPinStorageValue;\nimport cc.blynk.server.core.model.storage.value.MultiPinStorageValueType;\nimport cc.blynk.server.core.model.storage.value.SinglePinStorageValue;\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jackson.core.JsonToken;\nimport com.fasterxml.jackson.databind.DeserializationContext;\nimport com.fasterxml.jackson.databind.JsonDeserializer;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static com.fasterxml.jackson.core.JsonToken.START_OBJECT;\nimport static com.fasterxml.jackson.core.JsonToken.VALUE_STRING;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 26.04.18.\n */\npublic class PinStorageValueDeserializer extends JsonDeserializer {\n\n    private static final Logger log = LogManager.getLogger(PinStorageValueDeserializer.class);\n\n    @Override\n    public Object deserialize(JsonParser p, DeserializationContext ctxt) {\n        try {\n            JsonToken jsonToken = p.currentToken();\n\n            if (jsonToken == VALUE_STRING) {\n                return new SinglePinStorageValue(p.getValueAsString());\n            }\n\n            if (jsonToken == START_OBJECT) {\n                JsonNode multiValueNode = p.getCodec().readTree(p);\n                JsonNode type = multiValueNode.get(\"type\");\n                if (type != null) {\n                    MultiPinStorageValue multiPinStorageValue =\n                            new MultiPinStorageValue(MultiPinStorageValueType.valueOf(type.textValue()));\n\n                    JsonNode values = multiValueNode.get(\"values\");\n                    if (values != null) {\n                        if (values.isArray()) {\n                            for (var objNode : values) {\n                                multiPinStorageValue.values.add(objNode.textValue());\n                            }\n                        }\n                    }\n                    return multiPinStorageValue;\n                }\n            }\n        } catch (Exception e) {\n            log.error(\"Error reading pin storage value.\", e);\n        }\n\n        return null;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/storage/key/DashPinPropertyStorageKey.java",
    "content": "package cc.blynk.server.core.model.storage.key;\n\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.enums.WidgetProperty;\nimport cc.blynk.utils.StringUtils;\n\nimport static cc.blynk.server.core.model.DataStream.makePropertyHardwareBody;\nimport static cc.blynk.server.core.protocol.enums.Command.SET_WIDGET_PROPERTY;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 10.06.17.\n */\npublic final class DashPinPropertyStorageKey extends DashPinStorageKey {\n\n    private final WidgetProperty property;\n\n    private DashPinPropertyStorageKey(int dashId, int deviceId, char pinTypeChar, short pin, WidgetProperty property) {\n        super(dashId, deviceId, pinTypeChar, pin);\n        this.property = property;\n    }\n\n    public DashPinPropertyStorageKey(int dashId, int deviceId, PinType pinType, short pin, WidgetProperty property) {\n        super(dashId, deviceId, pinType, pin);\n        this.property = property;\n    }\n\n    public DashPinPropertyStorageKey(int dashId, PinPropertyStorageKey pinPropertyStorageKey) {\n        this(dashId, pinPropertyStorageKey.deviceId,\n                pinPropertyStorageKey.pinTypeChar,\n                pinPropertyStorageKey.pin,\n                pinPropertyStorageKey.property);\n    }\n\n    @Override\n    public String makeHardwareBody(String value) {\n        return makePropertyHardwareBody(pin, property, value);\n    }\n\n    @Override\n    public short getCmdType() {\n        return SET_WIDGET_PROPERTY;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n        if (!super.equals(o)) {\n            return false;\n        }\n\n        DashPinPropertyStorageKey that = (DashPinPropertyStorageKey) o;\n\n        return property == that.property;\n    }\n\n    @Override\n    public int hashCode() {\n        int result = super.hashCode();\n        result = 31 * result + (property != null ? property.hashCode() : 0);\n        return result;\n    }\n\n    @Override\n    public String toString() {\n        return super.toString() + StringUtils.DEVICE_SEPARATOR + property.label;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/storage/key/DashPinStorageKey.java",
    "content": "package cc.blynk.server.core.model.storage.key;\n\nimport cc.blynk.server.core.model.DataStream;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.widgets.MultiPinWidget;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\nimport com.fasterxml.jackson.annotation.JsonValue;\n\nimport static cc.blynk.server.core.protocol.enums.Command.APP_SYNC;\nimport static cc.blynk.utils.StringUtils.DEVICE_SEPARATOR;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.11.18.\n */\npublic class DashPinStorageKey {\n\n    public final int dashId;\n\n    public final int deviceId;\n\n    public final short pin;\n\n    public final char pinTypeChar;\n\n    public DashPinStorageKey(int dashId, int deviceId, char pintTypeChar, short pin) {\n        this.dashId = dashId;\n        this.deviceId = deviceId;\n        this.pinTypeChar = pintTypeChar;\n        this.pin = pin;\n    }\n\n    public DashPinStorageKey(int dashId, int deviceId, PinType pinType, short pin) {\n        this(dashId, deviceId, pinType.pintTypeChar, pin);\n    }\n\n    public DashPinStorageKey(int dashId, PinStorageKey pinStorageKey) {\n        this(dashId, pinStorageKey.deviceId, pinStorageKey.pinTypeChar, pinStorageKey.pin);\n    }\n\n    public boolean isSame(int dashId, OnePinWidget onePinWidget) {\n        return this.dashId == dashId\n                && this.pin == onePinWidget.pin\n                && this.pinTypeChar == onePinWidget.pinType.pintTypeChar;\n    }\n\n    public boolean isSame(int dashId, MultiPinWidget multiPinWidget) {\n        if (multiPinWidget.dataStreams == null || this.dashId != dashId) {\n            return false;\n        }\n        for (DataStream dataStream : multiPinWidget.dataStreams) {\n            if (dataStream.isSame(this.pin, PinType.getPinType(this.pinTypeChar))) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    public String makeHardwareBody(String value) {\n        return DataStream.makeHardwareBody(pinTypeChar, pin, value);\n    }\n\n    public short getCmdType() {\n        return APP_SYNC;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n\n        DashPinStorageKey that = (DashPinStorageKey) o;\n\n        if (dashId != that.dashId) {\n            return false;\n        }\n        if (deviceId != that.deviceId) {\n            return false;\n        }\n        if (pin != that.pin) {\n            return false;\n        }\n        return pinTypeChar == that.pinTypeChar;\n    }\n\n    @Override\n    public int hashCode() {\n        int result = dashId;\n        result = 31 * result + deviceId;\n        result = 31 * result + (int) pin;\n        result = 31 * result + (int) pinTypeChar;\n        return result;\n    }\n\n    @Override\n    @JsonValue\n    public String toString() {\n        return \"\" + dashId + DEVICE_SEPARATOR + deviceId + DEVICE_SEPARATOR + pinTypeChar + pin;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/storage/key/PinPropertyStorageKey.java",
    "content": "package cc.blynk.server.core.model.storage.key;\n\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.enums.WidgetProperty;\nimport cc.blynk.utils.StringUtils;\n\nimport static cc.blynk.server.core.model.DataStream.makePropertyHardwareBody;\nimport static cc.blynk.server.core.protocol.enums.Command.SET_WIDGET_PROPERTY;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 10.06.17.\n */\n@Deprecated\npublic final class PinPropertyStorageKey extends PinStorageKey {\n\n    public final WidgetProperty property;\n\n    public PinPropertyStorageKey(int deviceId, PinType pinType, short pin, WidgetProperty property) {\n        super(deviceId, pinType, pin);\n        this.property = property;\n    }\n\n    @Override\n    public String makeHardwareBody(String value) {\n        return makePropertyHardwareBody(pin, property, value);\n    }\n\n    @Override\n    public short getCmdType() {\n        return SET_WIDGET_PROPERTY;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (!(o instanceof PinPropertyStorageKey)) {\n            return false;\n        }\n        if (!super.equals(o)) {\n            return false;\n        }\n\n        PinPropertyStorageKey that = (PinPropertyStorageKey) o;\n\n        return property == that.property;\n    }\n\n    @Override\n    public int hashCode() {\n        int result = super.hashCode();\n        result = 31 * result + (property != null ? property.hashCode() : 0);\n        return result;\n    }\n\n    @Override\n    public String toString() {\n        return super.toString() + StringUtils.DEVICE_SEPARATOR + property.label;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/storage/key/PinStorageKey.java",
    "content": "package cc.blynk.server.core.model.storage.key;\n\nimport cc.blynk.server.core.model.DataStream;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.widgets.MultiPinWidget;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\nimport com.fasterxml.jackson.annotation.JsonValue;\n\nimport static cc.blynk.server.core.protocol.enums.Command.APP_SYNC;\nimport static cc.blynk.utils.StringUtils.DEVICE_SEPARATOR;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 19.11.16.\n */\n@Deprecated\npublic class PinStorageKey {\n\n    public final int deviceId;\n\n    public final short pin;\n\n    public final char pinTypeChar;\n\n    public PinStorageKey(int deviceId, PinType pinType, short pin) {\n        this.deviceId = deviceId;\n        this.pinTypeChar = pinType.pintTypeChar;\n        this.pin = pin;\n    }\n\n    public boolean isSamePin(OnePinWidget onePinWidget) {\n        return this.pin == onePinWidget.pin && this.pinTypeChar == onePinWidget.pinType.pintTypeChar;\n    }\n\n    public boolean isSamePin(MultiPinWidget multiPinWidget) {\n        if (multiPinWidget.dataStreams == null) {\n            return false;\n        }\n        for (var dataStream : multiPinWidget.dataStreams) {\n           if (dataStream.isSame(this.pin, PinType.getPinType(this.pinTypeChar))) {\n               return true;\n           }\n        }\n        return false;\n    }\n\n    public String makeHardwareBody(String value) {\n        return DataStream.makeHardwareBody(pinTypeChar, pin, value);\n    }\n\n    public short getCmdType() {\n        return APP_SYNC;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n\n        PinStorageKey that = (PinStorageKey) o;\n\n        if (deviceId != that.deviceId) {\n            return false;\n        }\n        if (pin != that.pin) {\n            return false;\n        }\n        return pinTypeChar == that.pinTypeChar;\n    }\n\n    @Override\n    public int hashCode() {\n        int result = deviceId;\n        result = 31 * result + (int) pin;\n        result = 31 * result + (int) pinTypeChar;\n        return result;\n    }\n\n    @Override\n    @JsonValue\n    public String toString() {\n        return \"\" + deviceId + DEVICE_SEPARATOR + pinTypeChar + pin;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/storage/value/MultiPinStorageValue.java",
    "content": "package cc.blynk.server.core.model.storage.value;\n\nimport cc.blynk.server.core.model.storage.key.DashPinStorageKey;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.utils.structure.BaseLimitedQueue;\nimport io.netty.channel.Channel;\n\nimport java.util.Collection;\nimport java.util.Iterator;\n\nimport static cc.blynk.server.core.model.widgets.MobileSyncWidget.SYNC_DEFAULT_MESSAGE_ID;\nimport static cc.blynk.server.core.protocol.enums.Command.APP_SYNC;\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeUTF8StringMessage;\nimport static cc.blynk.utils.StringUtils.BODY_SEPARATOR;\nimport static cc.blynk.utils.StringUtils.DEVICE_SEPARATOR;\nimport static cc.blynk.utils.StringUtils.prependDashIdAndDeviceId;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 27/04/2018.\n */\npublic class MultiPinStorageValue extends PinStorageValue {\n\n    public final MultiPinStorageValueType type;\n\n    public final BaseLimitedQueue<String> values;\n\n    public MultiPinStorageValue(MultiPinStorageValueType multiPinStorageValueType) {\n        this.type = multiPinStorageValueType;\n        this.values = multiPinStorageValueType.getQueue();\n    }\n\n    @Override\n    public void sendAppSync(Channel appChannel, int dashId, DashPinStorageKey key) {\n        if (values.size() > 0) {\n            Iterator<String> valIterator = values.iterator();\n            if (valIterator.hasNext()) {\n                String last = null;\n                StringBuilder sb = new StringBuilder();\n                sb.append(dashId).append(DEVICE_SEPARATOR).append(key.deviceId).append(BODY_SEPARATOR)\n                        .append(key.pinTypeChar).append('m').append(BODY_SEPARATOR).append(key.pin);\n                while (valIterator.hasNext()) {\n                    last = valIterator.next();\n                    sb.append(BODY_SEPARATOR).append(last);\n                }\n\n                appChannel.write(makeUTF8StringMessage(APP_SYNC, SYNC_DEFAULT_MESSAGE_ID, sb.toString()));\n\n                //special case, when few widgets are on the same pin\n                String body = prependDashIdAndDeviceId(dashId, key.deviceId, key.makeHardwareBody(last));\n                StringMessage message = makeUTF8StringMessage(APP_SYNC, SYNC_DEFAULT_MESSAGE_ID, body);\n                appChannel.write(message, appChannel.voidPromise());\n            }\n        }\n    }\n\n    @Override\n    public Collection<String> values() {\n        return values;\n    }\n\n    @Override\n    public void update(String value) {\n        values.add(value);\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/storage/value/MultiPinStorageValueType.java",
    "content": "package cc.blynk.server.core.model.storage.value;\n\nimport cc.blynk.utils.structure.BaseLimitedQueue;\nimport cc.blynk.utils.structure.LCDLimitedQueue;\nimport cc.blynk.utils.structure.TableLimitedQueue;\nimport cc.blynk.utils.structure.TerminalLimitedQueue;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 27/04/2018.\n *\n */\npublic enum MultiPinStorageValueType {\n\n    //WARNING: order change is not allowed!\n    LCD,\n    TERMINAL,\n    TABLE;\n\n    public BaseLimitedQueue<String> getQueue() {\n        switch (this) {\n            case LCD:\n                return new LCDLimitedQueue<>();\n            case TERMINAL:\n                return new TerminalLimitedQueue<>();\n            case TABLE:\n                return new TableLimitedQueue<>();\n            default:\n                throw new RuntimeException(\"not supported\");\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/storage/value/PinStorageValue.java",
    "content": "package cc.blynk.server.core.model.storage.value;\n\nimport cc.blynk.server.core.model.storage.key.DashPinStorageKey;\nimport io.netty.channel.Channel;\n\nimport java.util.Collection;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 27/04/2018.\n *\n */\npublic abstract class PinStorageValue {\n\n    public abstract void update(String value);\n\n    public abstract Collection<String> values();\n\n    public abstract void sendAppSync(Channel appChannel, int dashId, DashPinStorageKey key);\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/storage/value/SinglePinStorageValue.java",
    "content": "package cc.blynk.server.core.model.storage.value;\n\nimport cc.blynk.server.core.model.storage.key.DashPinStorageKey;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport com.fasterxml.jackson.annotation.JsonValue;\nimport io.netty.channel.Channel;\n\nimport java.util.Collection;\nimport java.util.Collections;\n\nimport static cc.blynk.server.core.model.widgets.MobileSyncWidget.SYNC_DEFAULT_MESSAGE_ID;\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeUTF8StringMessage;\nimport static cc.blynk.utils.StringUtils.prependDashIdAndDeviceId;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 27/04/2018.\n *\n */\npublic class SinglePinStorageValue extends PinStorageValue {\n\n    public volatile String value;\n\n    public SinglePinStorageValue() {\n    }\n\n    public SinglePinStorageValue(String value) {\n        this.value = value;\n    }\n\n    @Override\n    public void update(String value) {\n        this.value = value;\n    }\n\n    @Override\n    public Collection<String> values() {\n        if (value == null) {\n            return Collections.emptyList();\n        }\n        return Collections.singletonList(value);\n    }\n\n    @Override\n    public void sendAppSync(Channel appChannel, int dashId, DashPinStorageKey key) {\n        if (value != null) {\n            String body = key.makeHardwareBody(value);\n            String finalBody = prependDashIdAndDeviceId(key.dashId, key.deviceId, body);\n            //special case for setProperty\n            short cmdType = key.getCmdType();\n            StringMessage message = makeUTF8StringMessage(cmdType, SYNC_DEFAULT_MESSAGE_ID, finalBody);\n            appChannel.write(message, appChannel.voidPromise());\n        }\n    }\n\n    @Override\n    @JsonValue\n    public String toString() {\n        return value == null ? \"\" : value;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/CopyObject.java",
    "content": "package cc.blynk.server.core.model.widgets;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 24.08.17.\n */\npublic interface CopyObject<T> {\n\n    T copy();\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/DeviceCleaner.java",
    "content": "package cc.blynk.server.core.model.widgets;\n\npublic interface DeviceCleaner {\n\n    void deleteDevice(int deviceId);\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/FrequencyWidget.java",
    "content": "package cc.blynk.server.core.model.widgets;\n\nimport io.netty.channel.Channel;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 02.12.15.\n */\npublic interface FrequencyWidget {\n\n    int READING_MSG_ID = 7778;\n\n    void writeReadingCommand(Channel channel);\n\n    int getDeviceId();\n\n    boolean isTicked(long now);\n\n    boolean hasReadingInterval();\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/HardwareSyncWidget.java",
    "content": "package cc.blynk.server.core.model.widgets;\n\nimport io.netty.channel.ChannelHandlerContext;\n\n/**\n * Marker interface. Used in order to define if pin value from this widget should be sent back\n * to hardware on HARDWARE_SYNC command.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 03.12.15.\n */\npublic interface HardwareSyncWidget {\n\n    void sendHardSync(ChannelHandlerContext ctx, int msgId, int deviceId);\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/MobileSyncWidget.java",
    "content": "package cc.blynk.server.core.model.widgets;\n\nimport io.netty.channel.Channel;\n\n/**\n * Interface defines if pin value of widget should be send to application on activate.\n * Usually all widgets that have pins should implement this interface otherwise widget\n * state may be outdated on application side.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 23.08.16.\n */\npublic interface MobileSyncWidget {\n\n    int SYNC_DEFAULT_MESSAGE_ID = 1111;\n    int ANY_TARGET = -1;\n\n    //todo remove useNewFormat in future. leave it for a while for back compatibility\n    void sendAppSync(Channel appChannel, int dashId, int targetId);\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/MultiPinWidget.java",
    "content": "package cc.blynk.server.core.model.widgets;\n\nimport cc.blynk.server.core.model.DataStream;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.widgets.ui.DeviceSelector;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport java.util.StringJoiner;\n\nimport static cc.blynk.utils.StringUtils.BODY_SEPARATOR;\nimport static cc.blynk.utils.StringUtils.BODY_SEPARATOR_STRING;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 02.11.15.\n */\npublic abstract class MultiPinWidget extends Widget implements MobileSyncWidget {\n\n    public int deviceId;\n\n    @JsonProperty(\"pins\") //todo \"pins\" for back compatibility\n    public DataStream[] dataStreams;\n\n    @Override\n    public boolean updateIfSame(int deviceId, short pinIn, PinType type, String value) {\n        boolean isSame = false;\n        if (this.dataStreams != null && this.deviceId == deviceId) {\n            for (DataStream dataStream : this.dataStreams) {\n                if (dataStream.isSame(pinIn, type)) {\n                    dataStream.value = value;\n                    isSame = true;\n                }\n            }\n        }\n        return isSame;\n    }\n\n    @Override\n    public boolean isSame(int deviceId, short pinIn, PinType pinType) {\n        if (dataStreams != null && this.deviceId == deviceId) {\n            for (DataStream dataStream : dataStreams) {\n                if (dataStream.isSame(pinIn, pinType)) {\n                    return true;\n                }\n            }\n        }\n        return false;\n    }\n\n    public abstract boolean isSplitMode();\n\n    public boolean isAssignedToDeviceSelector() {\n        return this.deviceId >= DeviceSelector.DEVICE_SELECTOR_STARTING_ID;\n    }\n\n    public String makeHardwareBody(short pinIn, PinType pinType) {\n        if (dataStreams == null) {\n            return null;\n        }\n        if (isSplitMode()) {\n            for (DataStream dataStream : dataStreams) {\n                if (dataStream.isSame(pinIn, pinType)) {\n                    return dataStream.makeHardwareBody();\n                }\n            }\n        } else {\n            if (dataStreams[0].notEmptyAndIsValid()) {\n                StringBuilder sb = new StringBuilder(dataStreams[0].makeHardwareBody());\n                for (int i = 1; i < dataStreams.length; i++) {\n                    if (dataStreams[i].notEmptyAndIsValid()) {\n                        sb.append(BODY_SEPARATOR).append(dataStreams[i].value);\n                    }\n                }\n                return sb.toString();\n            }\n        }\n        return null;\n    }\n\n    @Override\n    public void append(StringBuilder sb, int deviceId) {\n        if (dataStreams != null && this.deviceId == deviceId) {\n            for (DataStream dataStream : dataStreams) {\n                append(sb, dataStream.pin, dataStream.pinType);\n            }\n        }\n    }\n\n    @Override\n    public String getJsonValue() {\n        if (dataStreams == null) {\n            return \"[]\";\n        }\n\n        StringJoiner sj = new StringJoiner(\",\", \"[\", \"]\");\n        if (isSplitMode()) {\n            for (DataStream dataStream : dataStreams) {\n                if (dataStream.value == null) {\n                    sj.add(\"\\\"\\\"\");\n                } else {\n                    sj.add(\"\\\"\" + dataStream.value + \"\\\"\");\n                }\n            }\n        } else {\n            if (dataStreams[0].notEmptyAndIsValid()) {\n                for (String pinValue : dataStreams[0].value.split(BODY_SEPARATOR_STRING)) {\n                    sj.add(\"\\\"\" + pinValue + \"\\\"\");\n                }\n            }\n        }\n        return sj.toString();\n    }\n\n    @Override\n    public void updateValue(Widget oldWidget) {\n        if (oldWidget instanceof MultiPinWidget) {\n            MultiPinWidget multiPinWidget = (MultiPinWidget) oldWidget;\n            if (multiPinWidget.dataStreams != null) {\n                for (DataStream dataStream : multiPinWidget.dataStreams) {\n                    updateIfSame(multiPinWidget.deviceId,\n                            dataStream.pin, dataStream.pinType, dataStream.value);\n                }\n            }\n        }\n    }\n\n    @Override\n    public void erase() {\n        if (dataStreams != null) {\n            for (DataStream dataStream : this.dataStreams) {\n                if (dataStream != null) {\n                    dataStream.value = null;\n                }\n            }\n        }\n    }\n\n    @Override\n    public boolean isAssignedToDevice(int deviceId) {\n        return this.deviceId == deviceId;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/NoPinWidget.java",
    "content": "package cc.blynk.server.core.model.widgets;\n\nimport cc.blynk.server.core.model.enums.PinMode;\n\n/**\n * All widgets that doesn't have pins on UI should extend this class.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.04.16.\n */\npublic abstract class NoPinWidget extends Widget {\n\n    @Override\n    public PinMode getModeType() {\n        return null;\n    }\n\n    @Override\n    public void erase() {\n    }\n\n    @Override\n    public void updateValue(Widget oldWidget) {\n    }\n\n    @Override\n    public boolean isAssignedToDevice(int deviceId) {\n        return false;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/OnePinReadingWidget.java",
    "content": "package cc.blynk.server.core.model.widgets;\n\nimport cc.blynk.server.core.model.DataStream;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport io.netty.channel.Channel;\n\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE;\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeUTF8StringMessage;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 02.02.17.\n */\npublic abstract class OnePinReadingWidget extends OnePinWidget implements FrequencyWidget {\n\n    public int frequency;\n\n    private transient long lastRequestTS;\n\n    @Override\n    public boolean isTicked(long now) {\n        if (hasReadingInterval() && now >= lastRequestTS + frequency) {\n            this.lastRequestTS = now;\n            return true;\n        }\n        return false;\n    }\n\n    @Override\n    public boolean hasReadingInterval() {\n        return frequency > 0;\n    }\n\n    @Override\n    public int getDeviceId() {\n        return deviceId;\n    }\n\n    @Override\n    public void writeReadingCommand(Channel channel) {\n        if (isNotValid()) {\n            return;\n        }\n        StringMessage msg = makeUTF8StringMessage(HARDWARE, READING_MSG_ID,\n                DataStream.makeReadingHardwareBody(pinType.pintTypeChar, pin));\n        channel.write(msg, channel.voidPromise());\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/OnePinWidget.java",
    "content": "package cc.blynk.server.core.model.widgets;\n\nimport cc.blynk.server.core.model.DataStream;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.enums.WidgetProperty;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.model.widgets.ui.DeviceSelector;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelHandlerContext;\n\nimport java.util.Iterator;\n\nimport static cc.blynk.server.core.protocol.enums.Command.APP_SYNC;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE;\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeUTF8StringMessage;\nimport static cc.blynk.utils.StringUtils.BODY_SEPARATOR;\nimport static cc.blynk.utils.StringUtils.DEVICE_SEPARATOR;\nimport static cc.blynk.utils.StringUtils.prependDashIdAndDeviceId;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 02.12.15.\n */\n//todo all this should be replaced with 1 Pin field.\npublic abstract class OnePinWidget extends Widget implements MobileSyncWidget, HardwareSyncWidget {\n\n    public int deviceId;\n\n    public PinType pinType;\n\n    public short pin = -1;\n\n    public boolean pwmMode;\n\n    public boolean rangeMappingOn;\n\n    public float min;\n\n    public float max;\n\n    public volatile String value;\n\n    public static String makeMultiValueHardwareBody(int dashId, int deviceId,\n                                                       char pintTypeChar, short pin, Iterator<?> values) {\n        StringBuilder sb = new StringBuilder();\n        sb.append(dashId).append(DEVICE_SEPARATOR).append(deviceId).append(BODY_SEPARATOR)\n          .append(pintTypeChar).append('m').append(BODY_SEPARATOR).append(pin);\n        while (values.hasNext()) {\n            String value = values.next().toString();\n            sb.append(BODY_SEPARATOR).append(value);\n        }\n        return sb.toString();\n    }\n\n    public static String makeHardwareBody(PinType pinType, short pin, String value) {\n        return makeHardwareBody(pinType.pintTypeChar, pin, value);\n    }\n\n    public static String makeHardwareBody(char pintTypeChar, short pin, String value) {\n        return \"\" + pintTypeChar + 'w' + BODY_SEPARATOR + pin + BODY_SEPARATOR + value;\n    }\n\n    @Override\n    public void sendAppSync(Channel appChannel, int dashId, int targetId) {\n        //do not send SYNC message for widgets assigned to device selector\n        //as it will be duplicated later.\n        if (isAssignedToDeviceSelector()) {\n            return;\n        }\n        if (targetId == ANY_TARGET || this.deviceId == targetId) {\n            String hardBody = makeHardwareBody();\n            if (hardBody != null) {\n                String body = prependDashIdAndDeviceId(dashId, this.deviceId, hardBody);\n                appChannel.write(makeUTF8StringMessage(APP_SYNC, SYNC_DEFAULT_MESSAGE_ID, body));\n            }\n        }\n    }\n\n    public boolean isAssignedToDeviceSelector() {\n        return this.deviceId >= DeviceSelector.DEVICE_SELECTOR_STARTING_ID;\n    }\n\n    public boolean isValid() {\n        return DataStream.isValid(pin, pinType);\n    }\n\n    public boolean isNotValid() {\n        return !DataStream.isValid(pin, pinType);\n    }\n\n    public String makeHardwareBody() {\n        if (isNotValid() || value == null) {\n            return null;\n        }\n        return pwmMode ? makeHardwareBody(PinType.ANALOG, pin, value) : makeHardwareBody(pinType, pin, value);\n    }\n\n    @Override\n    public boolean updateIfSame(int deviceId, short pin, PinType type, String value) {\n        if (isSame(deviceId, pin, type)) {\n            this.value = value;\n            return true;\n        }\n        return false;\n    }\n\n    @Override\n    public void sendHardSync(ChannelHandlerContext ctx, int msgId, int deviceId) {\n        if (this.deviceId == deviceId) {\n            String body = makeHardwareBody();\n            if (body != null) {\n                ctx.write(makeUTF8StringMessage(HARDWARE, msgId, body), ctx.voidPromise());\n            }\n        }\n    }\n\n    @Override\n    public boolean isSame(int deviceId, short pin, PinType type) {\n        return this.deviceId == deviceId && this.pin == pin && (\n                (type == this.pinType)\n                        || (this.pwmMode && type == PinType.ANALOG)\n                        || (type == PinType.DIGITAL && this.pinType == PinType.ANALOG)\n        );\n    }\n\n    @Override\n    public String getJsonValue() {\n        if (value == null) {\n            return \"[]\";\n        }\n        return JsonParser.valueToJsonAsString(value);\n    }\n\n    @Override\n    public void append(StringBuilder sb, int deviceId) {\n        if (this.deviceId == deviceId) {\n            append(sb, pin, pinType);\n        }\n    }\n\n    @Override\n    public boolean setProperty(WidgetProperty property, String propertyValue) {\n        switch (property) {\n            case MIN :\n                //accepting floats as valid, but using int for min/max due to back compatibility\n                this.min = Float.parseFloat(propertyValue);\n                return true;\n            case MAX :\n                //accepting floats as valid, but using int for min/max due to back compatibility\n                this.max = Float.parseFloat(propertyValue);\n                return true;\n            default:\n                return super.setProperty(property, propertyValue);\n        }\n    }\n\n    @Override\n    public void updateValue(Widget oldWidget) {\n        if (oldWidget instanceof OnePinWidget) {\n            OnePinWidget onePinWidget = (OnePinWidget) oldWidget;\n            if (onePinWidget.value != null) {\n                updateIfSame(onePinWidget.deviceId,\n                        onePinWidget.pin, onePinWidget.pinType, onePinWidget.value);\n            }\n        }\n    }\n\n    @Override\n    public void erase() {\n        this.value = null;\n    }\n\n    @Override\n    public boolean isAssignedToDevice(int deviceId) {\n        return this.deviceId == deviceId;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/Target.java",
    "content": "package cc.blynk.server.core.model.widgets;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 05.01.17.\n */\npublic interface Target {\n\n    //device ids that target should operate with\n    int[] getDeviceIds();\n\n    boolean isSelected(int deviceId);\n\n    int[] getAssignedDeviceIds();\n\n    int getDeviceId();\n\n    boolean contains(int deviceId);\n\n    default boolean isTag() {\n        return false;\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/Widget.java",
    "content": "package cc.blynk.server.core.model.widgets;\n\nimport cc.blynk.server.core.model.DataStream;\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.enums.WidgetProperty;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.model.storage.value.PinStorageValue;\nimport cc.blynk.server.core.model.storage.value.SinglePinStorageValue;\nimport cc.blynk.server.core.model.widgets.controls.Button;\nimport cc.blynk.server.core.model.widgets.controls.LinkButton;\nimport cc.blynk.server.core.model.widgets.controls.NumberInput;\nimport cc.blynk.server.core.model.widgets.controls.QR;\nimport cc.blynk.server.core.model.widgets.controls.RGB;\nimport cc.blynk.server.core.model.widgets.controls.SegmentedControl;\nimport cc.blynk.server.core.model.widgets.controls.Slider;\nimport cc.blynk.server.core.model.widgets.controls.Step;\nimport cc.blynk.server.core.model.widgets.controls.StyledButton;\nimport cc.blynk.server.core.model.widgets.controls.Switch;\nimport cc.blynk.server.core.model.widgets.controls.Terminal;\nimport cc.blynk.server.core.model.widgets.controls.TextInput;\nimport cc.blynk.server.core.model.widgets.controls.Timer;\nimport cc.blynk.server.core.model.widgets.controls.TwoAxisJoystick;\nimport cc.blynk.server.core.model.widgets.controls.VerticalSlider;\nimport cc.blynk.server.core.model.widgets.controls.VerticalStep;\nimport cc.blynk.server.core.model.widgets.notifications.Mail;\nimport cc.blynk.server.core.model.widgets.notifications.Notification;\nimport cc.blynk.server.core.model.widgets.notifications.SMS;\nimport cc.blynk.server.core.model.widgets.notifications.Twitter;\nimport cc.blynk.server.core.model.widgets.others.Bluetooth;\nimport cc.blynk.server.core.model.widgets.others.BluetoothSerial;\nimport cc.blynk.server.core.model.widgets.others.Bridge;\nimport cc.blynk.server.core.model.widgets.others.Player;\nimport cc.blynk.server.core.model.widgets.others.TextWidget;\nimport cc.blynk.server.core.model.widgets.others.Video;\nimport cc.blynk.server.core.model.widgets.others.eventor.Eventor;\nimport cc.blynk.server.core.model.widgets.others.rtc.RTC;\nimport cc.blynk.server.core.model.widgets.others.webhook.WebHook;\nimport cc.blynk.server.core.model.widgets.outputs.Gauge;\nimport cc.blynk.server.core.model.widgets.outputs.LCD;\nimport cc.blynk.server.core.model.widgets.outputs.LED;\nimport cc.blynk.server.core.model.widgets.outputs.LabeledValueDisplay;\nimport cc.blynk.server.core.model.widgets.outputs.LevelDisplay;\nimport cc.blynk.server.core.model.widgets.outputs.Map;\nimport cc.blynk.server.core.model.widgets.outputs.ValueDisplay;\nimport cc.blynk.server.core.model.widgets.outputs.VerticalLevelDisplay;\nimport cc.blynk.server.core.model.widgets.outputs.graph.Superchart;\nimport cc.blynk.server.core.model.widgets.sensors.Accelerometer;\nimport cc.blynk.server.core.model.widgets.sensors.Barometer;\nimport cc.blynk.server.core.model.widgets.sensors.GPSStreaming;\nimport cc.blynk.server.core.model.widgets.sensors.GPSTrigger;\nimport cc.blynk.server.core.model.widgets.sensors.Gravity;\nimport cc.blynk.server.core.model.widgets.sensors.Humidity;\nimport cc.blynk.server.core.model.widgets.sensors.Light;\nimport cc.blynk.server.core.model.widgets.sensors.Proximity;\nimport cc.blynk.server.core.model.widgets.sensors.Temperature;\nimport cc.blynk.server.core.model.widgets.ui.DeviceSelector;\nimport cc.blynk.server.core.model.widgets.ui.Menu;\nimport cc.blynk.server.core.model.widgets.ui.Tabs;\nimport cc.blynk.server.core.model.widgets.ui.TimeInput;\nimport cc.blynk.server.core.model.widgets.ui.image.Image;\nimport cc.blynk.server.core.model.widgets.ui.reporting.ReportingWidget;\nimport cc.blynk.server.core.model.widgets.ui.table.Table;\nimport cc.blynk.server.core.model.widgets.ui.tiles.DeviceTiles;\nimport cc.blynk.utils.ByteUtils;\nimport com.fasterxml.jackson.annotation.JsonSubTypes;\nimport com.fasterxml.jackson.annotation.JsonTypeInfo;\n\nimport java.io.IOException;\n\nimport static cc.blynk.utils.StringUtils.BODY_SEPARATOR;\n\n/**\n * User: ddumanskiy\n * Date: 21.11.13\n * Time: 13:08\n */\n@JsonTypeInfo(\n        use = JsonTypeInfo.Id.NAME,\n        property = \"type\",\n        defaultImpl = Button.class)\n@JsonSubTypes({\n\n        //controls\n        @JsonSubTypes.Type(value = Button.class, name = \"BUTTON\"),\n        @JsonSubTypes.Type(value = StyledButton.class, name = \"STYLED_BUTTON\"),\n        @JsonSubTypes.Type(value = LinkButton.class, name = \"LINK_BUTTON\"),\n        @JsonSubTypes.Type(value = TextInput.class, name = \"TEXT_INPUT\"),\n        @JsonSubTypes.Type(value = NumberInput.class, name = \"NUMBER_INPUT\"),\n        @JsonSubTypes.Type(value = Slider.class, name = \"SLIDER\"),\n        @JsonSubTypes.Type(value = VerticalSlider.class, name = \"VERTICAL_SLIDER\"),\n        @JsonSubTypes.Type(value = RGB.class, name = \"RGB\"),\n        @JsonSubTypes.Type(value = Timer.class, name = \"TIMER\"),\n        @JsonSubTypes.Type(value = TwoAxisJoystick.class, name = \"TWO_AXIS_JOYSTICK\"),\n        @JsonSubTypes.Type(value = Terminal.class, name = \"TERMINAL\"),\n        @JsonSubTypes.Type(value = Step.class, name = \"STEP\"),\n        @JsonSubTypes.Type(value = VerticalStep.class, name = \"VERTICAL_STEP\"),\n        @JsonSubTypes.Type(value = QR.class, name = \"QR\"),\n        @JsonSubTypes.Type(value = TimeInput.class, name = \"TIME_INPUT\"),\n        @JsonSubTypes.Type(value = SegmentedControl.class, name = \"SEGMENTED_CONTROL\"),\n        @JsonSubTypes.Type(value = Switch.class, name = \"SWITCH\"),\n\n        //outputs\n        @JsonSubTypes.Type(value = LED.class, name = \"LED\"),\n        @JsonSubTypes.Type(value = ValueDisplay.class, name = \"DIGIT4_DISPLAY\"),\n        @JsonSubTypes.Type(value = LabeledValueDisplay.class, name = \"LABELED_VALUE_DISPLAY\"),\n        @JsonSubTypes.Type(value = Gauge.class, name = \"GAUGE\"),\n        @JsonSubTypes.Type(value = LCD.class, name = \"LCD\"),\n        @JsonSubTypes.Type(value = LevelDisplay.class, name = \"LEVEL_DISPLAY\"),\n        @JsonSubTypes.Type(value = VerticalLevelDisplay.class, name = \"VERTICAL_LEVEL_DISPLAY\"),\n        @JsonSubTypes.Type(value = Video.class, name = \"VIDEO\"),\n        @JsonSubTypes.Type(value = Superchart.class, name = \"ENHANCED_GRAPH\"),\n\n        //sensors\n        @JsonSubTypes.Type(value = GPSTrigger.class, name = \"GPS_TRIGGER\"),\n        @JsonSubTypes.Type(value = GPSStreaming.class, name = \"GPS_STREAMING\"),\n        @JsonSubTypes.Type(value = Light.class, name = \"LIGHT\"),\n        @JsonSubTypes.Type(value = Proximity.class, name = \"PROXIMITY\"),\n        @JsonSubTypes.Type(value = Temperature.class, name = \"TEMPERATURE\"),\n        @JsonSubTypes.Type(value = Accelerometer.class, name = \"ACCELEROMETER\"),\n        @JsonSubTypes.Type(value = Gravity.class, name = \"GRAVITY\"),\n        @JsonSubTypes.Type(value = Barometer.class, name = \"BAROMETER\"),\n        @JsonSubTypes.Type(value = Humidity.class, name = \"HUMIDITY\"),\n\n        //notifications\n        @JsonSubTypes.Type(value = Twitter.class, name = \"TWITTER\"),\n        @JsonSubTypes.Type(value = Mail.class, name = \"EMAIL\"),\n        @JsonSubTypes.Type(value = Notification.class, name = \"NOTIFICATION\"),\n        @JsonSubTypes.Type(value = SMS.class, name = \"SMS\"),\n\n        //interface\n        @JsonSubTypes.Type(value = Menu.class, name = \"MENU\"),\n        @JsonSubTypes.Type(value = Tabs.class, name = \"TABS\"),\n        @JsonSubTypes.Type(value = Player.class, name = \"PLAYER\"),\n        @JsonSubTypes.Type(value = Table.class, name = \"TABLE\"),\n        @JsonSubTypes.Type(value = Image.class, name = \"IMAGE\"),\n        @JsonSubTypes.Type(value = ReportingWidget.class, name = \"REPORT\"),\n\n        //others\n        @JsonSubTypes.Type(value = RTC.class, name = \"RTC\"),\n        @JsonSubTypes.Type(value = Bridge.class, name = \"BRIDGE\"),\n        @JsonSubTypes.Type(value = Bluetooth.class, name = \"BLUETOOTH\"),\n        @JsonSubTypes.Type(value = BluetoothSerial.class, name = \"BLUETOOTH_SERIAL\"),\n        @JsonSubTypes.Type(value = Eventor.class, name = \"EVENTOR\"),\n        @JsonSubTypes.Type(value = Map.class, name = \"MAP\"),\n        @JsonSubTypes.Type(value = DeviceSelector.class, name = \"DEVICE_SELECTOR\"),\n        @JsonSubTypes.Type(value = DeviceTiles.class, name = \"DEVICE_TILES\"),\n        @JsonSubTypes.Type(value = TextWidget.class, name = \"TEXT\"),\n\n        @JsonSubTypes.Type(value = WebHook.class, name = \"WEBHOOK\")\n\n})\npublic abstract class Widget implements CopyObject<Widget> {\n\n    public long id;\n\n    public int x;\n\n    public int y;\n\n    public volatile int color;\n\n    public int width;\n\n    public int height;\n\n    public int tabId = 0;\n\n    public volatile String label;\n\n    public boolean isDefaultColor;\n\n    public abstract PinMode getModeType();\n\n    public abstract int getPrice();\n\n    public abstract void updateValue(Widget oldWidget);\n\n    public abstract void erase();\n\n    /**\n     * WARNING: this method has one exclusion for DeviceTiles, as\n     * Device for Tiles not assigned directly, but assigned via provisioning\n     */\n    public abstract boolean isAssignedToDevice(int deviceId);\n\n    protected void append(StringBuilder sb, short pin, PinType pinType) {\n        if (pin != DataStream.NO_PIN && pinType != PinType.VIRTUAL) {\n            PinMode pinMode = getModeType();\n            if (pinMode != null) {\n                sb.append(BODY_SEPARATOR)\n                        .append(pin)\n                        .append(BODY_SEPARATOR)\n                        .append(pinMode);\n            }\n        }\n    }\n\n    public boolean updateIfSame(int deviceId, short pin, PinType type, String value) {\n        return false;\n    }\n\n    public boolean isSame(int deviceId, short pin, PinType type) {\n        return false;\n    }\n\n    public String getJsonValue() {\n        return null;\n    }\n\n    /**\n     * This method should be overridden by every widget that supports direct pins (analog, digital) control\n     */\n    public void append(StringBuilder sb, int deviceId) {\n    }\n\n    //todo this is ugly and not effective. refactor\n    @Override\n    public Widget copy() {\n        String copyWidgetString = JsonParser.toJson(this);\n        try {\n            return JsonParser.parseWidget(copyWidgetString);\n        } catch (IOException ioe) {\n            throw new RuntimeException(ioe);\n        }\n    }\n\n    public PinStorageValue getPinStorageValue() {\n        return new SinglePinStorageValue();\n    }\n\n    public boolean isMultiValueWidget() {\n        return false;\n    }\n\n    public boolean setProperty(WidgetProperty property, String propertyValue) {\n        switch (property) {\n            case LABEL :\n                this.label = propertyValue;\n                return true;\n            case COLOR :\n                this.color = ByteUtils.parseColor(propertyValue);\n                this.isDefaultColor = false;\n                return true;\n            default:\n                return false;\n        }\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/controls/Button.java",
    "content": "package cc.blynk.server.core.model.widgets.controls;\n\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.enums.WidgetProperty;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\nimport cc.blynk.server.core.model.widgets.outputs.graph.FontSize;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.03.15.\n */\npublic class Button extends OnePinWidget {\n\n    public boolean pushMode;\n\n    public volatile String onLabel;\n\n    public volatile String offLabel;\n\n    public FontSize fontSize;\n\n    @Override\n    public String makeHardwareBody() {\n        if (isNotValid() || value == null) {\n            return null;\n        }\n        return makeHardwareBody(pinType, pin, value);\n    }\n\n    @Override\n    public PinMode getModeType() {\n        return PinMode.out;\n    }\n\n    @Override\n    public int getPrice() {\n        return 200;\n    }\n\n    @Override\n    public boolean setProperty(WidgetProperty property, String propertyValue) {\n        switch (property) {\n            case ON_LABEL :\n                this.onLabel = propertyValue;\n                return true;\n            case OFF_LABEL :\n                this.offLabel = propertyValue;\n                return true;\n            default:\n                return super.setProperty(property, propertyValue);\n        }\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/controls/ButtonState.java",
    "content": "package cc.blynk.server.core.model.widgets.controls;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 09.03.18.\n */\npublic class ButtonState {\n\n    public volatile String text;\n\n    public volatile int textColor;\n\n    public volatile int backgroundColor;\n\n    public String iconName;\n\n    @JsonCreator\n    public ButtonState(@JsonProperty(\"text\") String text,\n                       @JsonProperty(\"textColor\") int textColor,\n                       @JsonProperty(\"backgroundColor\") int backgroundColor,\n                       @JsonProperty(\"iconName\") String iconName) {\n        this.text = text;\n        this.textColor = textColor;\n        this.backgroundColor = backgroundColor;\n        this.iconName = iconName;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/controls/ButtonStyle.java",
    "content": "package cc.blynk.server.core.model.widgets.controls;\n\npublic enum ButtonStyle {\n\n    SOLID, OUTLINE\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/controls/Edge.java",
    "content": "package cc.blynk.server.core.model.widgets.controls;\n\npublic enum Edge {\n\n    ROUNDED, SHARP, PILL, TEXT\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/controls/LinkButton.java",
    "content": "package cc.blynk.server.core.model.widgets.controls;\n\nimport cc.blynk.server.core.model.widgets.NoPinWidget;\nimport cc.blynk.server.core.model.widgets.outputs.graph.FontSize;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.03.15.\n */\npublic class LinkButton extends NoPinWidget {\n\n    public String url;\n\n    public ButtonState onButtonState;\n\n    public ButtonState offButtonState;\n\n    public FontSize fontSize;\n\n    public Edge edge;\n\n    public ButtonStyle buttonStyle;\n\n    public boolean lockSize;\n\n    public boolean showAddressBar;\n\n    public boolean showNavigationBar;\n\n    public boolean allowInBrowser;\n\n    @Override\n    public int getPrice() {\n        return 500;\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/controls/NumberInput.java",
    "content": "package cc.blynk.server.core.model.widgets.controls;\n\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.enums.WidgetProperty;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\nimport cc.blynk.server.core.model.widgets.outputs.graph.FontSize;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.03.15.\n */\npublic class NumberInput extends OnePinWidget {\n\n    public String suffix;\n\n    public float step;\n\n    public boolean isLoopOn;\n\n    public FontSize fontSize;\n\n    @Override\n    public boolean setProperty(WidgetProperty property, String propertyValue) {\n        switch (property) {\n            case SUFFIX :\n                this.suffix = propertyValue;\n                return true;\n            default:\n                return super.setProperty(property, propertyValue);\n        }\n    }\n\n    @Override\n    public PinMode getModeType() {\n        return PinMode.out;\n    }\n\n    @Override\n    public int getPrice() {\n        return 400;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/controls/QR.java",
    "content": "package cc.blynk.server.core.model.widgets.controls;\n\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.03.15.\n */\npublic class QR extends OnePinWidget {\n\n    @Override\n    public PinMode getModeType() {\n        return PinMode.out;\n    }\n\n    @Override\n    public int getPrice() {\n        return 200;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/controls/RGB.java",
    "content": "package cc.blynk.server.core.model.widgets.controls;\n\nimport cc.blynk.server.core.model.DataStream;\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.widgets.HardwareSyncWidget;\nimport cc.blynk.server.core.model.widgets.MultiPinWidget;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelHandlerContext;\n\nimport static cc.blynk.server.core.protocol.enums.Command.APP_SYNC;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE;\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeUTF8StringMessage;\nimport static cc.blynk.utils.StringUtils.prependDashIdAndDeviceId;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.03.15.\n */\npublic class RGB extends MultiPinWidget implements HardwareSyncWidget {\n\n    public boolean splitMode;\n\n    public boolean sendOnReleaseOn;\n\n    public int frequency;\n\n    @Override\n    public void sendHardSync(ChannelHandlerContext ctx, int msgId, int deviceId) {\n        if (dataStreams == null || this.deviceId != deviceId) {\n            return;\n        }\n        if (isSplitMode()) {\n            for (DataStream dataStream : dataStreams) {\n                if (dataStream.notEmptyAndIsValid()) {\n                    ctx.write(makeUTF8StringMessage(HARDWARE, msgId,\n                            dataStream.makeHardwareBody()), ctx.voidPromise());\n                }\n            }\n        } else {\n            if (dataStreams[0].notEmptyAndIsValid()) {\n                ctx.write(makeUTF8StringMessage(HARDWARE, msgId,\n                        dataStreams[0].makeHardwareBody()), ctx.voidPromise());\n            }\n        }\n    }\n\n    @Override\n    public void sendAppSync(Channel appChannel, int dashId, int targetId) {\n        if (dataStreams == null) {\n            return;\n        }\n        if (targetId == ANY_TARGET || this.deviceId == targetId) {\n            if (isSplitMode()) {\n                for (DataStream dataStream : dataStreams) {\n                    if (dataStream.notEmptyAndIsValid()) {\n                        String body = prependDashIdAndDeviceId(dashId, deviceId, dataStream.makeHardwareBody());\n                        appChannel.write(makeUTF8StringMessage(APP_SYNC, SYNC_DEFAULT_MESSAGE_ID, body),\n                                appChannel.voidPromise());\n                    }\n                }\n            } else {\n                if (dataStreams[0].notEmptyAndIsValid()) {\n                    String body = prependDashIdAndDeviceId(dashId, deviceId, dataStreams[0].makeHardwareBody());\n                    appChannel.write(makeUTF8StringMessage(APP_SYNC, SYNC_DEFAULT_MESSAGE_ID, body),\n                            appChannel.voidPromise());\n                }\n            }\n        }\n    }\n\n    public boolean isSplitMode() {\n        return splitMode;\n    }\n\n    @Override\n    public PinMode getModeType() {\n        return PinMode.out;\n    }\n\n    @Override\n    public int getPrice() {\n        return 400;\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/controls/SegmentedControl.java",
    "content": "package cc.blynk.server.core.model.widgets.controls;\n\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.enums.WidgetProperty;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\nimport cc.blynk.server.core.model.widgets.outputs.graph.FontSize;\nimport cc.blynk.utils.StringUtils;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 28.03.16.\n */\npublic class SegmentedControl extends OnePinWidget {\n\n    public volatile String[] labels;\n\n    public FontSize fontSize;\n\n    @Override\n    public PinMode getModeType() {\n        return PinMode.out;\n    }\n\n    @Override\n    public int getPrice() {\n        return 400;\n    }\n\n    @Override\n    public boolean setProperty(WidgetProperty property, String propertyValue) {\n        switch (property) {\n            case LABELS :\n                this.labels = propertyValue.split(StringUtils.BODY_SEPARATOR_STRING);\n                return true;\n            default:\n                return super.setProperty(property, propertyValue);\n        }\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/controls/Slider.java",
    "content": "package cc.blynk.server.core.model.widgets.controls;\n\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.enums.WidgetProperty;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.03.15.\n */\npublic class Slider extends OnePinWidget {\n\n    public boolean sendOnReleaseOn;\n\n    public int frequency;\n\n    public int maximumFractionDigits;\n\n    public boolean showValueOn = true;\n\n    @Override\n    public PinMode getModeType() {\n        return PinMode.out;\n    }\n\n    @Override\n    public int getPrice() {\n        return 200;\n    }\n\n    @Override\n    public boolean setProperty(WidgetProperty property, String propertyValue) {\n        switch (property) {\n            case FRACTION :\n                this.maximumFractionDigits = Integer.parseInt(propertyValue);\n                return true;\n            default:\n                return super.setProperty(property, propertyValue);\n        }\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/controls/Step.java",
    "content": "package cc.blynk.server.core.model.widgets.controls;\n\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.enums.WidgetProperty;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.04.15.\n */\npublic class Step extends OnePinWidget {\n\n    public volatile float step;\n\n    public boolean isArrowsOn;\n\n    public boolean isLoopOn;\n\n    public boolean isSendStep;\n\n    public int frequency;\n\n    public boolean showValueOn = true;\n\n    @Override\n    public PinMode getModeType() {\n        return PinMode.out;\n    }\n\n    @Override\n    public int getPrice() {\n        return 500;\n    }\n\n    @Override\n    public boolean setProperty(WidgetProperty property, String propertyValue) {\n        switch (property) {\n            case STEP :\n                this.step = Float.parseFloat(propertyValue);\n                return true;\n            default:\n                return super.setProperty(property, propertyValue);\n        }\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/controls/StyledButton.java",
    "content": "package cc.blynk.server.core.model.widgets.controls;\n\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.enums.WidgetProperty;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\nimport cc.blynk.server.core.model.widgets.outputs.graph.FontSize;\nimport cc.blynk.utils.ByteUtils;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.03.15.\n */\npublic class StyledButton extends OnePinWidget {\n\n    public boolean pushMode;\n\n    public ButtonState onButtonState;\n\n    public ButtonState offButtonState;\n\n    public FontSize fontSize;\n\n    public Edge edge;\n\n    public ButtonStyle buttonStyle;\n\n    public boolean lockSize;\n\n    @Override\n    public String makeHardwareBody() {\n        if (isNotValid() || value == null) {\n            return null;\n        }\n        return makeHardwareBody(pinType, pin, value);\n    }\n\n    @Override\n    public PinMode getModeType() {\n        return PinMode.out;\n    }\n\n    @Override\n    public int getPrice() {\n        return 300;\n    }\n\n    @Override\n    public boolean setProperty(WidgetProperty property, String propertyValue) {\n        switch (property) {\n            case ON_BACK_COLOR :\n                if (this.onButtonState != null) {\n                    this.onButtonState.backgroundColor = ByteUtils.parseColor(propertyValue);\n                }\n                return true;\n            case OFF_BACK_COLOR :\n                if (this.offButtonState != null) {\n                    this.offButtonState.backgroundColor = ByteUtils.parseColor(propertyValue);\n                }\n                return true;\n            case ON_COLOR :\n                if (this.onButtonState != null) {\n                    this.onButtonState.textColor = ByteUtils.parseColor(propertyValue);\n                }\n                return true;\n            case OFF_COLOR :\n                if (this.offButtonState != null) {\n                    this.offButtonState.textColor = ByteUtils.parseColor(propertyValue);\n                }\n                return true;\n            case ON_LABEL :\n                if (this.onButtonState != null) {\n                    this.onButtonState.text = propertyValue;\n                }\n                return true;\n            case OFF_LABEL :\n                if (this.offButtonState != null) {\n                    this.offButtonState.text = propertyValue;\n                }\n                return true;\n            default:\n                return super.setProperty(property, propertyValue);\n        }\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/controls/Switch.java",
    "content": "package cc.blynk.server.core.model.widgets.controls;\n\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.enums.WidgetProperty;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\nimport cc.blynk.server.core.model.widgets.outputs.graph.FontSize;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 31.05.19.\n */\npublic class Switch extends OnePinWidget {\n\n    public volatile String onLabel;\n\n    public volatile String offLabel;\n\n    public int offBackgroundColor;\n\n    public int onBackgroundColor;\n\n    public int handleColor;\n\n    public FontSize fontSize;\n\n    @Override\n    public String makeHardwareBody() {\n        if (isNotValid() || value == null) {\n            return null;\n        }\n        return makeHardwareBody(pinType, pin, value);\n    }\n\n    @Override\n    public PinMode getModeType() {\n        return PinMode.out;\n    }\n\n    @Override\n    public int getPrice() {\n        return 200;\n    }\n\n    @Override\n    public boolean setProperty(WidgetProperty property, String propertyValue) {\n        switch (property) {\n            case ON_LABEL :\n                this.onLabel = propertyValue;\n                return true;\n            case OFF_LABEL :\n                this.offLabel = propertyValue;\n                return true;\n            default:\n                return super.setProperty(property, propertyValue);\n        }\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/controls/Terminal.java",
    "content": "package cc.blynk.server.core.model.widgets.controls;\n\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.model.storage.value.MultiPinStorageValue;\nimport cc.blynk.server.core.model.storage.value.MultiPinStorageValueType;\nimport cc.blynk.server.core.model.storage.value.PinStorageValue;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\nimport cc.blynk.utils.structure.LimitedArrayDeque;\nimport cc.blynk.utils.structure.TerminalLimitedQueue;\nimport io.netty.channel.Channel;\n\nimport java.util.Iterator;\n\nimport static cc.blynk.server.core.protocol.enums.Command.APP_SYNC;\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeUTF8StringMessage;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.03.15.\n */\npublic class Terminal extends OnePinWidget {\n\n    //todo move to persistent LCDLimitedQueue?\n    private transient final LimitedArrayDeque<String> lastCommands =\n            new LimitedArrayDeque<>(TerminalLimitedQueue.POOL_SIZE);\n\n    public boolean autoScrollOn;\n\n    public boolean terminalInputOn;\n\n    public boolean textLightOn;\n\n    public boolean attachNewLine;\n\n    @Override\n    public boolean updateIfSame(int deviceId, short pin, PinType type, String value) {\n        if (isSame(deviceId, pin, type)) {\n            if (\"clr\".equals(value)) {\n                this.lastCommands.clear();\n            } else {\n                this.lastCommands.add(value);\n            }\n            return true;\n        }\n        return false;\n    }\n\n    @Override\n    public void sendAppSync(Channel appChannel, int dashId, int targetId) {\n        if (isNotValid() || lastCommands.size() == 0) {\n            return;\n        }\n        if (targetId == ANY_TARGET || this.deviceId == targetId) {\n            Iterator<String> valIterator = lastCommands.iterator();\n            if (valIterator.hasNext()) {\n                String body = makeMultiValueHardwareBody(dashId, deviceId, pinType.pintTypeChar, pin, valIterator);\n                appChannel.write(makeUTF8StringMessage(APP_SYNC, SYNC_DEFAULT_MESSAGE_ID, body));\n            }\n        }\n    }\n\n    @Override\n    public String makeHardwareBody() {\n        if (isNotValid() || lastCommands.size() == 0) {\n            return null;\n        }\n        //terminal supports only virtual pins\n        return makeHardwareBody(pinType, pin, lastCommands.getLast());\n    }\n\n    @Override\n    public PinStorageValue getPinStorageValue() {\n        return new MultiPinStorageValue(MultiPinStorageValueType.TERMINAL);\n    }\n\n    @Override\n    public boolean isMultiValueWidget() {\n        return true;\n    }\n\n    @Override\n    public String getJsonValue() {\n        return JsonParser.toJson(lastCommands);\n    }\n\n    @Override\n    //terminal supports only virtual pins\n    public PinMode getModeType() {\n        return null;\n    }\n\n    @Override\n    public int getPrice() {\n        return 200;\n    }\n\n    @Override\n    public void erase() {\n        this.lastCommands.clear();\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/controls/TextInput.java",
    "content": "package cc.blynk.server.core.model.widgets.controls;\n\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\nimport cc.blynk.server.core.model.widgets.outputs.graph.FontSize;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.03.15.\n */\npublic class TextInput extends OnePinWidget {\n\n    public String hint;\n\n    public int limit;\n\n    public FontSize fontSize;\n\n    @Override\n    public PinMode getModeType() {\n        return PinMode.out;\n    }\n\n    @Override\n    public int getPrice() {\n        return 400;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/controls/Timer.java",
    "content": "package cc.blynk.server.core.model.widgets.controls;\n\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.outputs.graph.FontSize;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.03.15.\n */\npublic class Timer extends OnePinWidget {\n\n    public int startTime = -1;\n\n    public String startValue;\n\n    public int stopTime = -1;\n\n    public String stopValue;\n\n    public FontSize fontSize;\n\n    public boolean isValidStart() {\n        return isValidTime(startTime) && isValidValue(startValue);\n    }\n\n    public boolean isValidStop() {\n        return isValidTime(stopTime) && isValidValue(stopValue);\n    }\n\n    public static boolean isValidTime(int time) {\n        return time > -1 && time < 86400;\n    }\n\n    private static boolean isValidValue(String value) {\n        return value != null && !value.isEmpty();\n    }\n\n    @Override\n    public PinMode getModeType() {\n        return PinMode.out;\n    }\n\n    @Override\n    public int getPrice() {\n        return 200;\n    }\n\n    @Override\n    public void erase() {\n        super.erase();\n        this.startValue = null;\n        this.stopValue = null;\n        this.startTime = -1;\n        this.stopTime = -1;\n    }\n\n    @Override\n    public void updateValue(Widget oldWidget) {\n        if (oldWidget instanceof Timer) {\n            Timer oldTimer = (Timer) oldWidget;\n            if (isSame(oldTimer.deviceId, oldTimer.pin, oldTimer.pinType)) {\n                if (oldTimer.value != null) {\n                    this.value = oldTimer.value;\n                }\n                if (oldTimer.startTime != -1) {\n                    this.startTime = oldTimer.startTime;\n                }\n                if (oldTimer.stopTime != -1) {\n                    this.stopTime = oldTimer.stopTime;\n                }\n            }\n        }\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (!(o instanceof Timer)) {\n            return false;\n        }\n\n        Timer timer = (Timer) o;\n\n        return id == timer.id;\n\n    }\n\n    @Override\n    public int hashCode() {\n        return (int) (id ^ (id >>> 32));\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/controls/TwoAxisJoystick.java",
    "content": "package cc.blynk.server.core.model.widgets.controls;\n\nimport cc.blynk.server.core.model.DataStream;\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.widgets.HardwareSyncWidget;\nimport cc.blynk.server.core.model.widgets.MultiPinWidget;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelHandlerContext;\n\nimport static cc.blynk.server.core.protocol.enums.Command.APP_SYNC;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE;\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeUTF8StringMessage;\nimport static cc.blynk.utils.StringUtils.prependDashIdAndDeviceId;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.03.15.\n */\npublic class TwoAxisJoystick extends MultiPinWidget implements HardwareSyncWidget {\n\n    public boolean split;\n\n    public boolean autoReturnOn;\n\n    public boolean portraitLocked;\n\n    public int frequency;\n\n    @Override\n    public void sendHardSync(ChannelHandlerContext ctx, int msgId, int deviceId) {\n        if (dataStreams == null || this.deviceId != deviceId) {\n            return;\n        }\n        if (split) {\n            for (DataStream dataStream : dataStreams) {\n                if (dataStream.notEmptyAndIsValid()) {\n                    ctx.write(makeUTF8StringMessage(HARDWARE, msgId,\n                            dataStream.makeHardwareBody()), ctx.voidPromise());\n                }\n            }\n        } else {\n            if (dataStreams[0].notEmptyAndIsValid()) {\n                ctx.write(makeUTF8StringMessage(HARDWARE, msgId,\n                        dataStreams[0].makeHardwareBody()), ctx.voidPromise());\n            }\n        }\n    }\n\n    @Override\n    public void sendAppSync(Channel appChannel, int dashId, int targetId) {\n        if (dataStreams == null) {\n            return;\n        }\n        if (targetId == ANY_TARGET || this.deviceId == targetId) {\n            if (split) {\n                for (DataStream dataStream : dataStreams) {\n                    if (dataStream.notEmptyAndIsValid()) {\n                        String body = prependDashIdAndDeviceId(dashId, deviceId, dataStream.makeHardwareBody());\n                        appChannel.write(makeUTF8StringMessage(APP_SYNC, SYNC_DEFAULT_MESSAGE_ID, body),\n                                appChannel.voidPromise());\n                    }\n                }\n            } else {\n                if (dataStreams[0].notEmptyAndIsValid()) {\n                    String body = prependDashIdAndDeviceId(dashId, deviceId, dataStreams[0].makeHardwareBody());\n                    appChannel.write(makeUTF8StringMessage(APP_SYNC, SYNC_DEFAULT_MESSAGE_ID, body),\n                            appChannel.voidPromise());\n                }\n            }\n        }\n    }\n\n    public boolean isSplitMode() {\n        return split;\n    }\n\n    @Override\n    public PinMode getModeType() {\n        return PinMode.out;\n    }\n\n\n    @Override\n    public int getPrice() {\n        return 400;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/controls/VerticalSlider.java",
    "content": "package cc.blynk.server.core.model.widgets.controls;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.03.15.\n */\npublic class VerticalSlider extends Slider {\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/controls/VerticalStep.java",
    "content": "package cc.blynk.server.core.model.widgets.controls;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.04.15.\n */\npublic class VerticalStep extends Step {\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/notifications/Mail.java",
    "content": "package cc.blynk.server.core.model.widgets.notifications;\n\nimport cc.blynk.server.core.model.widgets.NoPinWidget;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.utils.http.ContentType;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.03.15.\n */\npublic class Mail extends NoPinWidget {\n\n    public volatile String to;\n\n    public volatile ContentType contentType = ContentType.TEXT_HTML;\n\n    @Override\n    public void updateValue(Widget oldWidget) {\n        if (oldWidget instanceof Mail) {\n            Mail oldMailWidget = (Mail) oldWidget;\n            this.to = oldMailWidget.to;\n            this.contentType = oldMailWidget.contentType;\n        }\n    }\n\n    @Override\n    public void erase() {\n        this.to = null;\n    }\n\n    @Override\n    public int getPrice() {\n        return 100;\n    }\n\n    public boolean isText() {\n        return contentType == ContentType.TEXT_PLAIN;\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/notifications/Notification.java",
    "content": "package cc.blynk.server.core.model.widgets.notifications;\n\nimport cc.blynk.server.core.model.serialization.View;\nimport cc.blynk.server.core.model.widgets.NoPinWidget;\nimport cc.blynk.server.notifications.push.GCMWrapper;\nimport cc.blynk.server.notifications.push.android.AndroidGCMMessage;\nimport cc.blynk.server.notifications.push.enums.Priority;\nimport cc.blynk.server.notifications.push.ios.IOSGCMMessage;\nimport com.fasterxml.jackson.annotation.JsonView;\n\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.03.15.\n */\npublic class Notification extends NoPinWidget {\n\n    private static final int MAX_PUSH_BODY_SIZE = 255;\n\n    @JsonView({View.Private.class, View.HttpAPIField.class})\n    public volatile ConcurrentHashMap<String, String> androidTokens = new ConcurrentHashMap<>();\n\n    @JsonView({View.Private.class, View.HttpAPIField.class})\n    public volatile ConcurrentHashMap<String, String> iOSTokens = new ConcurrentHashMap<>();\n\n    public boolean notifyWhenOffline;\n\n    public int notifyWhenOfflineIgnorePeriod;\n\n    public Priority priority = Priority.normal;\n\n    public String soundUri;\n\n    public static boolean isWrongBody(String body) {\n        return body == null || body.isEmpty() || body.length() > MAX_PUSH_BODY_SIZE;\n    }\n\n    public boolean hasNoToken() {\n        return iOSTokens.size() == 0 && androidTokens.size() == 0;\n    }\n\n    @Override\n    public int getPrice() {\n        return 400;\n    }\n\n    public void push(GCMWrapper gcmWrapper, String body, int dashId) {\n        if (androidTokens.size() != 0) {\n            for (Map.Entry<String, String> entry : androidTokens.entrySet()) {\n                gcmWrapper.send(\n                        new AndroidGCMMessage(entry.getValue(), priority, body, dashId),\n                        androidTokens,\n                        entry.getKey()\n                );\n            }\n        }\n\n        if (iOSTokens.size() != 0) {\n            for (Map.Entry<String, String> entry : iOSTokens.entrySet()) {\n                gcmWrapper.send(new IOSGCMMessage(entry.getValue(), priority, body, dashId),\n                        iOSTokens,\n                        entry.getKey()\n                );\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/notifications/SMS.java",
    "content": "package cc.blynk.server.core.model.widgets.notifications;\n\nimport cc.blynk.server.core.model.widgets.NoPinWidget;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.03.15.\n */\npublic class SMS extends NoPinWidget {\n\n    public String to;\n\n    @Override\n    public int getPrice() {\n        return 100;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/notifications/Twitter.java",
    "content": "package cc.blynk.server.core.model.widgets.notifications;\n\nimport cc.blynk.server.core.model.serialization.View;\nimport cc.blynk.server.core.model.widgets.NoPinWidget;\nimport com.fasterxml.jackson.annotation.JsonView;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.03.15.\n */\npublic class Twitter extends NoPinWidget {\n\n    private static final int MAX_TWITTER_BODY_SIZE = 140;\n\n    @JsonView(View.Private.class)\n    public String token;\n\n    @JsonView(View.Private.class)\n    public String secret;\n\n    @JsonView(View.Private.class)\n    public String username;\n\n    public static boolean isWrongBody(String body) {\n       return body == null || body.isEmpty() || body.length() > MAX_TWITTER_BODY_SIZE;\n    }\n\n    @Override\n    public int getPrice() {\n        return 0;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/others/Bluetooth.java",
    "content": "package cc.blynk.server.core.model.widgets.others;\n\nimport cc.blynk.server.core.model.widgets.NoPinWidget;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.03.15.\n */\npublic class Bluetooth extends NoPinWidget {\n\n    public String name;\n\n    public int deviceId;\n\n    @Override\n    public int getPrice() {\n        return 0;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/others/BluetoothSerial.java",
    "content": "package cc.blynk.server.core.model.widgets.others;\n\nimport cc.blynk.server.core.model.widgets.NoPinWidget;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.03.15.\n */\npublic class BluetoothSerial extends NoPinWidget {\n\n    public String name;\n\n    public int deviceId;\n\n    @Override\n    public int getPrice() {\n        return 0;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/others/Bridge.java",
    "content": "package cc.blynk.server.core.model.widgets.others;\n\nimport cc.blynk.server.core.model.widgets.NoPinWidget;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.03.15.\n */\npublic class Bridge extends NoPinWidget {\n\n    @Override\n    public int getPrice() {\n        return 100;\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/others/Player.java",
    "content": "package cc.blynk.server.core.model.widgets.others;\n\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.enums.WidgetProperty;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 28.03.16.\n */\npublic class Player extends OnePinWidget {\n\n    public volatile boolean isOnPlay;\n\n    @Override\n    public boolean updateIfSame(int deviceId, short pin, PinType type, String value) {\n        if (isSame(deviceId, pin, type)) {\n            this.value = value;\n            switch (value) {\n                case \"play\" :\n                    isOnPlay = true;\n                    break;\n                case \"stop\" :\n                    isOnPlay = false;\n                    break;\n            }\n            return true;\n        }\n        return false;\n    }\n\n    @Override\n    public PinMode getModeType() {\n        return PinMode.out;\n    }\n\n    @Override\n    public int getPrice() {\n        return 400;\n    }\n\n    @Override\n    public boolean setProperty(WidgetProperty property, String propertyValue) {\n        switch (property) {\n            case IS_ON_PLAY :\n                this.isOnPlay = Boolean.parseBoolean(propertyValue);\n                return true;\n            default:\n                return super.setProperty(property, propertyValue);\n        }\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/others/TextWidget.java",
    "content": "package cc.blynk.server.core.model.widgets.others;\n\nimport cc.blynk.server.core.model.widgets.NoPinWidget;\nimport cc.blynk.server.core.model.widgets.outputs.TextAlignment;\nimport cc.blynk.server.core.model.widgets.outputs.graph.FontSize;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 31.05.19.\n */\npublic class TextWidget extends NoPinWidget {\n\n    public FontSize textSize = FontSize.AUTO;\n\n    public TextAlignment alignment;\n\n    public String text;\n\n    @Override\n    public int getPrice() {\n        return 100;\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/others/Video.java",
    "content": "package cc.blynk.server.core.model.widgets.others;\n\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.enums.WidgetProperty;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelHandlerContext;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.03.15.\n */\npublic class Video extends OnePinWidget {\n\n    public String url;\n\n    public boolean forceTCP;\n\n    @Override\n    public void sendAppSync(Channel appChannel, int dashId, int targetId) {\n    }\n\n    @Override\n    public void sendHardSync(ChannelHandlerContext ctx, int msgId, int deviceId) {\n    }\n\n    @Override\n    public boolean setProperty(WidgetProperty property, String propertyValue) {\n        switch (property) {\n            case URL :\n                this.url = propertyValue;\n                return true;\n            default:\n                return super.setProperty(property, propertyValue);\n        }\n    }\n\n    @Override\n    //supports only virtual pins\n    public PinMode getModeType() {\n        return null;\n    }\n\n    @Override\n    public int getPrice() {\n        return 500;\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/others/eventor/Eventor.java",
    "content": "package cc.blynk.server.core.model.widgets.others.eventor;\n\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.widgets.NoPinWidget;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.action.BaseAction;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.action.SetPinAction;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.08.16.\n */\npublic class Eventor extends NoPinWidget {\n\n    public Rule[] rules;\n\n    public int deviceId;\n\n    public Eventor() {\n        this.width = 2;\n        this.height = 1;\n    }\n\n    public Eventor(Rule[] rules) {\n        this();\n        this.rules = rules;\n    }\n\n    @Override\n    public PinMode getModeType() {\n        return PinMode.out;\n    }\n\n    @Override\n    public void append(StringBuilder sb, int deviceId) {\n        if (rules != null && this.deviceId == deviceId) {\n            for (Rule rule : rules) {\n                if (rule.actions != null) {\n                    for (BaseAction action : rule.actions) {\n                        if (action instanceof SetPinAction) {\n                            SetPinAction setPinActionAction = (SetPinAction) action;\n                            if (setPinActionAction.dataStream != null) {\n                                append(sb, setPinActionAction.dataStream.pin,\n                                        setPinActionAction.dataStream.pinType);\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    @Override\n    public int getPrice() {\n        return 500;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/others/eventor/Rule.java",
    "content": "package cc.blynk.server.core.model.widgets.others.eventor;\n\nimport cc.blynk.server.core.model.DataStream;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.widgets.controls.Timer;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.action.BaseAction;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.condition.BaseCondition;\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.08.16.\n */\npublic class Rule {\n\n    @JsonProperty(\"triggerPin\") //todo \"triggerPin\" for back compatibility\n    public final DataStream triggerDataStream;\n\n    public final TimerTime triggerTime;\n\n    public final BaseCondition condition;\n\n    public final BaseAction[] actions;\n\n    public final boolean isActive;\n\n    public transient boolean isProcessed;\n\n    @JsonCreator\n    public Rule(@JsonProperty(\"triggerPin\") DataStream triggerDataStream,\n                @JsonProperty(\"triggerTime\") TimerTime triggerTime,\n                @JsonProperty(\"condition\") BaseCondition condition,\n                @JsonProperty(\"actions\") BaseAction[] actions,\n                @JsonProperty(\"isActive\") boolean isActive) {\n        this.triggerDataStream = triggerDataStream;\n        this.triggerTime = triggerTime;\n        this.condition = condition;\n        this.actions = actions;\n        this.isActive = isActive;\n    }\n\n    private boolean notEmpty() {\n        return triggerDataStream != null && condition != null && actions != null;\n    }\n\n    public boolean isReady(short pin, PinType pinType) {\n        return isActive && notEmpty() && triggerDataStream.isSame(pin, pinType);\n    }\n\n    public boolean isValidTimerRule() {\n        return isActive && triggerTime != null && Timer.isValidTime(triggerTime.time)\n         && actions != null && actions.length > 0 && actions[0].isValid();\n    }\n\n    public boolean matchesCondition(String inValue, double parsedInValueToDouble) {\n        return condition.matches(inValue, parsedInValueToDouble);\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/others/eventor/TimerTime.java",
    "content": "package cc.blynk.server.core.model.widgets.others.eventor;\n\nimport cc.blynk.server.core.model.widgets.others.rtc.StringToZoneId;\nimport cc.blynk.server.core.model.widgets.others.rtc.ZoneIdToString;\nimport cc.blynk.server.internal.EmptyArraysUtil;\nimport cc.blynk.utils.ArrayUtil;\nimport cc.blynk.utils.DateTimeUtils;\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\n\nimport java.time.LocalDate;\nimport java.time.ZoneId;\nimport java.time.ZonedDateTime;\nimport java.util.Arrays;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 30.01.17.\n */\npublic class TimerTime {\n\n    private static final int[] ALL_DAYS = new int[] {1, 2, 3, 4, 5, 6, 7};\n\n    public final int id;\n\n    public final int[] days;\n\n    public final int time;\n\n    @JsonSerialize(using = ZoneIdToString.class)\n    @JsonDeserialize(using = StringToZoneId.class, as = ZoneId.class)\n    public final ZoneId tzName;\n\n    @JsonCreator\n    public TimerTime(@JsonProperty(\"id\") int id,\n                     @JsonProperty(\"days\") int[] days,\n                     @JsonProperty(\"time\") int time,\n                     @JsonProperty(\"tzName\") ZoneId tzName) {\n        this.id = id;\n        this.days = days == null ? EmptyArraysUtil.EMPTY_INTS : days;\n        this.time = time;\n        this.tzName = tzName;\n    }\n\n    public TimerTime(int time) {\n        this(0, ALL_DAYS, time, DateTimeUtils.UTC);\n    }\n\n    public boolean isTickTime(ZonedDateTime currentDateTime) {\n        LocalDate userDate = currentDateTime.withZoneSameInstant(tzName).toLocalDate();\n        int dayOfWeek = userDate.getDayOfWeek().getValue();\n        return ArrayUtil.contains(days, dayOfWeek);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (!(o instanceof TimerTime)) {\n            return false;\n        }\n\n        TimerTime timerTime = (TimerTime) o;\n\n        if (id != timerTime.id) {\n            return false;\n        }\n        if (time != timerTime.time) {\n            return false;\n        }\n        if (!Arrays.equals(days, timerTime.days)) {\n            return false;\n        }\n        return !(tzName != null ? !tzName.equals(timerTime.tzName) : timerTime.tzName != null);\n\n    }\n\n    @Override\n    public int hashCode() {\n        int result = id;\n        result = 31 * result + (days != null ? Arrays.hashCode(days) : 0);\n        result = 31 * result + time;\n        result = 31 * result + (tzName != null ? tzName.hashCode() : 0);\n        return result;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/others/eventor/model/action/BaseAction.java",
    "content": "package cc.blynk.server.core.model.widgets.others.eventor.model.action;\n\nimport cc.blynk.server.core.model.widgets.others.eventor.model.action.notification.MailAction;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.action.notification.NotifyAction;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.action.notification.TwitAction;\nimport com.fasterxml.jackson.annotation.JsonSubTypes;\nimport com.fasterxml.jackson.annotation.JsonTypeInfo;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.08.16.\n */\n@JsonTypeInfo(\n        use = JsonTypeInfo.Id.NAME,\n        property = \"type\")\n@JsonSubTypes({\n        @JsonSubTypes.Type(value = SetPropertyPinAction.class, name = \"SET_PROP\"),\n        @JsonSubTypes.Type(value = SetPinAction.class, name = \"SETPIN\"),\n        @JsonSubTypes.Type(value = NotifyAction.class, name = \"NOTIFY\"),\n        @JsonSubTypes.Type(value = MailAction.class, name = \"MAIL\"),\n        @JsonSubTypes.Type(value = TwitAction.class, name = \"TWIT\"),\n})\npublic abstract class BaseAction {\n\n    public abstract boolean isValid();\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/others/eventor/model/action/SetPinAction.java",
    "content": "package cc.blynk.server.core.model.widgets.others.eventor.model.action;\n\nimport cc.blynk.server.core.model.DataStream;\nimport cc.blynk.server.core.model.enums.PinType;\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.08.16.\n */\npublic class SetPinAction extends BaseAction {\n\n    @JsonProperty(\"pin\") //todo \"pin\" for back compatibility\n    public final DataStream dataStream;\n\n    public final String value;\n\n    private final SetPinActionType setPinType;\n\n    @JsonCreator\n    public SetPinAction(@JsonProperty(\"pin\") DataStream dataStream,\n                        @JsonProperty(\"value\") String value,\n                        @JsonProperty(\"setPinType\") SetPinActionType setPinType) {\n        this.dataStream = dataStream;\n        this.value = value;\n        this.setPinType = setPinType;\n    }\n\n    public SetPinAction(short pin, PinType pinType, String value) {\n        this.dataStream = new DataStream(pin, pinType);\n        this.value = value;\n        this.setPinType = SetPinActionType.CUSTOM;\n    }\n\n    public String makeHardwareBody() {\n        return DataStream.makeHardwareBody(dataStream.pwmMode, dataStream.pinType, dataStream.pin, value);\n    }\n\n    @Override\n    public boolean isValid() {\n        return dataStream != null && dataStream.pinType != null && dataStream.pin > -1 && value != null;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/others/eventor/model/action/SetPinActionType.java",
    "content": "package cc.blynk.server.core.model.widgets.others.eventor.model.action;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 12.02.17.\n */\npublic enum SetPinActionType {\n\n    ON, OFF, CUSTOM\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/others/eventor/model/action/SetPropertyPinAction.java",
    "content": "package cc.blynk.server.core.model.widgets.others.eventor.model.action;\n\nimport cc.blynk.server.core.model.DataStream;\nimport cc.blynk.server.core.model.enums.WidgetProperty;\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 02.11.18.\n */\npublic class SetPropertyPinAction extends BaseAction {\n\n    @JsonProperty(\"pin\") //todo \"pin\" for back compatibility\n    public final DataStream dataStream;\n\n    public final WidgetProperty property;\n\n    public final String value;\n\n    @JsonCreator\n    public SetPropertyPinAction(@JsonProperty(\"pin\") DataStream dataStream,\n                                @JsonProperty(\"property\") WidgetProperty property,\n                                @JsonProperty(\"value\") String value) {\n        this.dataStream = dataStream;\n        this.property = property;\n        this.value = value;\n    }\n\n    public String makeHardwareBody() {\n        return DataStream.makePropertyHardwareBody(dataStream.pin, property, value);\n    }\n\n    @Override\n    public boolean isValid() {\n        return dataStream != null\n                && dataStream.pinType != null\n                && dataStream.pin > -1\n                && value != null && !value.isEmpty();\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/others/eventor/model/action/notification/MailAction.java",
    "content": "package cc.blynk.server.core.model.widgets.others.eventor.model.action.notification;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.08.16.\n */\npublic class MailAction extends NotificationAction {\n\n    public final String subject;\n\n    @JsonCreator\n    public MailAction(@JsonProperty(\"subject\") String subject,\n                      @JsonProperty(\"message\") String message) {\n        super(message);\n        this.subject = subject;\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/others/eventor/model/action/notification/NotificationAction.java",
    "content": "package cc.blynk.server.core.model.widgets.others.eventor.model.action.notification;\n\nimport cc.blynk.server.core.model.widgets.others.eventor.model.action.BaseAction;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 23.08.16.\n */\npublic abstract class NotificationAction extends BaseAction {\n\n    public final String message;\n\n    NotificationAction(String message) {\n        this.message = message;\n    }\n\n    @Override\n    public boolean isValid() {\n        return message != null && !message.isEmpty();\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/others/eventor/model/action/notification/NotifyAction.java",
    "content": "package cc.blynk.server.core.model.widgets.others.eventor.model.action.notification;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.08.16.\n */\npublic class NotifyAction extends NotificationAction {\n\n    @JsonCreator\n    public NotifyAction(@JsonProperty(\"message\") String message) {\n        super(message);\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/others/eventor/model/action/notification/TwitAction.java",
    "content": "package cc.blynk.server.core.model.widgets.others.eventor.model.action.notification;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.08.16.\n */\npublic class TwitAction extends NotificationAction {\n\n    @JsonCreator\n    public TwitAction(@JsonProperty(\"message\") String message) {\n        super(message);\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/others/eventor/model/condition/BaseCondition.java",
    "content": "package cc.blynk.server.core.model.widgets.others.eventor.model.condition;\n\nimport cc.blynk.server.core.model.widgets.others.eventor.model.condition.number.Between;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.condition.number.Equal;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.condition.number.GreaterThan;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.condition.number.GreaterThanOrEqual;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.condition.number.LessThan;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.condition.number.LessThanOrEqual;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.condition.number.NotBetween;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.condition.number.NotEqual;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.condition.string.StringEqual;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.condition.string.StringNotEqual;\nimport com.fasterxml.jackson.annotation.JsonSubTypes;\nimport com.fasterxml.jackson.annotation.JsonTypeInfo;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.08.16.\n */\n@JsonTypeInfo(\n        use = JsonTypeInfo.Id.NAME,\n        property = \"type\")\n@JsonSubTypes({\n        @JsonSubTypes.Type(value = GreaterThan.class, name = \"GT\"),\n        @JsonSubTypes.Type(value = GreaterThanOrEqual.class, name = \"GTE\"),\n        @JsonSubTypes.Type(value = LessThan.class, name = \"LT\"),\n        @JsonSubTypes.Type(value = LessThanOrEqual.class, name = \"LTE\"),\n        @JsonSubTypes.Type(value = Equal.class, name = \"EQ\"),\n        @JsonSubTypes.Type(value = NotEqual.class, name = \"NEQ\"),\n        @JsonSubTypes.Type(value = Between.class, name = \"BETWEEN\"),\n        @JsonSubTypes.Type(value = NotBetween.class, name = \"NOT_BETWEEN\"),\n        @JsonSubTypes.Type(value = ValueChanged.class, name = \"CHANGED\"),\n\n        @JsonSubTypes.Type(value = StringEqual.class, name = \"STR_EQUAL\"),\n        @JsonSubTypes.Type(value = StringNotEqual.class, name = \"STR_NOT_EQUAL\")\n})\npublic abstract class BaseCondition {\n\n    public abstract boolean matches(String inString, double in);\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/others/eventor/model/condition/ValueChanged.java",
    "content": "package cc.blynk.server.core.model.widgets.others.eventor.model.condition;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.08.16.\n */\npublic class ValueChanged extends BaseCondition {\n\n    private volatile String value;\n\n    @JsonCreator\n    public ValueChanged(@JsonProperty(\"value\") String value) {\n        this.value = value;\n    }\n\n    @Override\n    public boolean matches(String inString, double in) {\n        if (inString.equals(value)) {\n            return false;\n        }\n        value = inString;\n        return true;\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/others/eventor/model/condition/number/Between.java",
    "content": "package cc.blynk.server.core.model.widgets.others.eventor.model.condition.number;\n\nimport cc.blynk.server.core.model.widgets.others.eventor.model.condition.BaseCondition;\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.08.16.\n */\npublic class Between extends BaseCondition {\n\n    private final double left;\n\n    private final double right;\n\n    @JsonCreator\n    public Between(@JsonProperty(\"left\") double left,\n                   @JsonProperty(\"right\") double right) {\n        this.left = left;\n        this.right = right;\n    }\n\n    @Override\n    public boolean matches(String inString, double in) {\n        return (left < in) && (in < right);\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/others/eventor/model/condition/number/Equal.java",
    "content": "package cc.blynk.server.core.model.widgets.others.eventor.model.condition.number;\n\nimport cc.blynk.server.core.model.widgets.others.eventor.model.condition.BaseCondition;\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.08.16.\n */\npublic class Equal extends BaseCondition {\n\n    private final double value;\n\n    @JsonCreator\n    public Equal(@JsonProperty(\"value\") double value) {\n        this.value = value;\n    }\n\n    @Override\n    public boolean matches(String inString, double in) {\n        return Double.compare(in, value) == 0;\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/others/eventor/model/condition/number/GreaterThan.java",
    "content": "package cc.blynk.server.core.model.widgets.others.eventor.model.condition.number;\n\nimport cc.blynk.server.core.model.widgets.others.eventor.model.condition.BaseCondition;\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.08.16.\n */\npublic class GreaterThan extends BaseCondition {\n\n    private final double value;\n\n    @JsonCreator\n    public GreaterThan(@JsonProperty(\"value\") double value) {\n        this.value = value;\n    }\n\n    @Override\n    public boolean matches(String inString, double in) {\n        return in > value;\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/others/eventor/model/condition/number/GreaterThanOrEqual.java",
    "content": "package cc.blynk.server.core.model.widgets.others.eventor.model.condition.number;\n\nimport cc.blynk.server.core.model.widgets.others.eventor.model.condition.BaseCondition;\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.08.16.\n */\npublic class GreaterThanOrEqual extends BaseCondition {\n\n    private final double value;\n\n    @JsonCreator\n    public GreaterThanOrEqual(@JsonProperty(\"value\") double value) {\n        this.value = value;\n    }\n\n    @Override\n    public boolean matches(String inString, double in) {\n        return in >= value;\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/others/eventor/model/condition/number/LessThan.java",
    "content": "package cc.blynk.server.core.model.widgets.others.eventor.model.condition.number;\n\nimport cc.blynk.server.core.model.widgets.others.eventor.model.condition.BaseCondition;\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.08.16.\n */\npublic class LessThan extends BaseCondition {\n\n    private final double value;\n\n    @JsonCreator\n    public LessThan(@JsonProperty(\"value\") double value) {\n        this.value = value;\n    }\n\n    @Override\n    public boolean matches(String inString, double in) {\n        return in < value;\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/others/eventor/model/condition/number/LessThanOrEqual.java",
    "content": "package cc.blynk.server.core.model.widgets.others.eventor.model.condition.number;\n\nimport cc.blynk.server.core.model.widgets.others.eventor.model.condition.BaseCondition;\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.08.16.\n */\npublic class LessThanOrEqual extends BaseCondition {\n\n    private final double value;\n\n    @JsonCreator\n    public LessThanOrEqual(@JsonProperty(\"value\") double value) {\n        this.value = value;\n    }\n\n    @Override\n    public boolean matches(String inString, double in) {\n        return in <= value;\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/others/eventor/model/condition/number/NotBetween.java",
    "content": "package cc.blynk.server.core.model.widgets.others.eventor.model.condition.number;\n\nimport cc.blynk.server.core.model.widgets.others.eventor.model.condition.BaseCondition;\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.08.16.\n */\npublic class NotBetween extends BaseCondition {\n\n    private final double left;\n\n    private final double right;\n\n    @JsonCreator\n    public NotBetween(@JsonProperty(\"left\") double left,\n                      @JsonProperty(\"right\") double right) {\n        this.left = left;\n        this.right = right;\n    }\n\n    @Override\n    public boolean matches(String inString, double in) {\n        return !((left < in) && (in < right));\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/others/eventor/model/condition/number/NotEqual.java",
    "content": "package cc.blynk.server.core.model.widgets.others.eventor.model.condition.number;\n\nimport cc.blynk.server.core.model.widgets.others.eventor.model.condition.BaseCondition;\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.08.16.\n */\npublic class NotEqual extends BaseCondition {\n\n    private final double value;\n\n    @JsonCreator\n    public NotEqual(@JsonProperty(\"value\") double value) {\n        this.value = value;\n    }\n\n    @Override\n    public boolean matches(String inString, double in) {\n        return in != value;\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/others/eventor/model/condition/string/StringEqual.java",
    "content": "package cc.blynk.server.core.model.widgets.others.eventor.model.condition.string;\n\nimport cc.blynk.server.core.model.widgets.others.eventor.model.condition.BaseCondition;\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.08.16.\n */\npublic class StringEqual extends BaseCondition {\n\n    private final String value;\n\n    @JsonCreator\n    public StringEqual(@JsonProperty(\"value\") String value) {\n        this.value = value;\n    }\n\n    @Override\n    public boolean matches(String inString, double in) {\n        return inString.equals(value);\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/others/eventor/model/condition/string/StringNotEqual.java",
    "content": "package cc.blynk.server.core.model.widgets.others.eventor.model.condition.string;\n\nimport cc.blynk.server.core.model.widgets.others.eventor.model.condition.BaseCondition;\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.08.16.\n */\npublic class StringNotEqual extends BaseCondition {\n\n    private final String value;\n\n    @JsonCreator\n    public StringNotEqual(@JsonProperty(\"value\") String value) {\n        this.value = value;\n    }\n\n    @Override\n    public boolean matches(String inString, double in) {\n        return !inString.equals(value);\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/others/rtc/RTC.java",
    "content": "package cc.blynk.server.core.model.widgets.others.rtc;\n\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.widgets.NoPinWidget;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.utils.DateTimeUtils;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\n\nimport java.time.LocalDateTime;\nimport java.time.ZoneId;\nimport java.time.ZoneOffset;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.03.15.\n */\npublic class RTC extends NoPinWidget {\n\n    @JsonSerialize(using = ZoneIdToString.class)\n    @JsonDeserialize(using = StringToZoneId.class, as = ZoneId.class)\n    public ZoneId tzName;\n\n    @Override\n    //supports only virtual pins\n    public PinMode getModeType() {\n        return null;\n    }\n\n    @Override\n    public int getPrice() {\n        return 100;\n    }\n\n    @Override\n    public void updateValue(Widget oldWidget) {\n        if (oldWidget instanceof RTC) {\n            this.tzName = ((RTC) oldWidget).tzName;\n        }\n    }\n\n    public long getTime() {\n        ZoneId zone;\n        if (tzName != null) {\n            zone = tzName;\n        } else {\n            zone = DateTimeUtils.UTC;\n        }\n\n        LocalDateTime ldt = LocalDateTime.now(zone);\n        return ldt.toEpochSecond(ZoneOffset.UTC);\n    }\n\n    @Override\n    public String getJsonValue() {\n        return \"[\" + getTime() + \"]\";\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/others/rtc/StringToZoneId.java",
    "content": "package cc.blynk.server.core.model.widgets.others.rtc;\n\nimport cc.blynk.utils.DateTimeUtils;\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jackson.databind.DeserializationContext;\nimport com.fasterxml.jackson.databind.JsonDeserializer;\n\nimport java.io.IOException;\nimport java.time.ZoneId;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 04.09.16.\n */\npublic class StringToZoneId extends JsonDeserializer<ZoneId> {\n\n    public static ZoneId parseZoneId(String zoneString) {\n        try {\n            return ZoneId.of(zoneString);\n        } catch (Exception e) {\n            switch (zoneString) {\n                case \"Canada/East-Saskatchewan\" :\n                    return DateTimeUtils.AMERICA_REGINA;\n                case \"Asia/Hanoi\" :\n                    return DateTimeUtils.ASIA_HO_CHI;\n                default :\n                    return DateTimeUtils.UTC;\n            }\n        }\n    }\n\n    @Override\n    public ZoneId deserialize(JsonParser p, DeserializationContext ctx) throws IOException {\n        String zoneString = p.readValueAs(String.class);\n        return parseZoneId(zoneString);\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/others/rtc/ZoneIdToString.java",
    "content": "package cc.blynk.server.core.model.widgets.others.rtc;\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.databind.JsonSerializer;\nimport com.fasterxml.jackson.databind.SerializerProvider;\n\nimport java.io.IOException;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 04.09.16.\n */\npublic class ZoneIdToString extends JsonSerializer<Object> {\n\n    @Override\n    public void serialize(Object value, JsonGenerator jsonGenerator,\n                          SerializerProvider serializers) throws IOException {\n        String result = value.toString();\n        jsonGenerator.writeObject(result);\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/others/webhook/Header.java",
    "content": "package cc.blynk.server.core.model.widgets.others.webhook;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 05.09.16.\n */\npublic class Header {\n\n    public final String name;\n\n    public final String value;\n\n    @JsonCreator\n    public Header(@JsonProperty(\"name\") String name,\n                  @JsonProperty(\"value\") String value) {\n        this.name = name;\n        this.value = value;\n    }\n\n    public boolean isValid() {\n        return name != null && value != null;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/others/webhook/SupportedWebhookMethod.java",
    "content": "package cc.blynk.server.core.model.widgets.others.webhook;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 05.09.16.\n */\npublic enum SupportedWebhookMethod {\n\n    POST,\n    GET,\n    PUT,\n    DELETE\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/others/webhook/WebHook.java",
    "content": "package cc.blynk.server.core.model.widgets.others.webhook;\n\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelHandlerContext;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 05.09.16.\n */\npublic class WebHook extends OnePinWidget {\n\n    public String url;\n\n    //GET is always default so we don't do null checks\n    public SupportedWebhookMethod method = SupportedWebhookMethod.GET;\n\n    public Header[] headers;\n\n    public String body;\n\n    public transient volatile int failureCounter = 0;\n\n    public static boolean isValidUrl(String url) {\n        return url != null && !url.isEmpty() && url.regionMatches(true, 0, \"http\", 0, 4);\n    }\n\n    public boolean isNotFailed(int webhookFailureLimit) {\n        return failureCounter < webhookFailureLimit;\n    }\n\n    //a bit ugly but as quick fix ok\n    public boolean isSameWebHook(int deviceId, short pin, PinType type) {\n        return super.isSame(deviceId, pin, type);\n    }\n\n    @Override\n    public void sendAppSync(Channel appChannel, int dashId, int targetId) {\n    }\n\n    @Override\n    public void sendHardSync(ChannelHandlerContext ctx, int msgId, int deviceId) {\n    }\n\n    @Override\n    public boolean updateIfSame(int deviceId, short pin, PinType type, String value) {\n        return false;\n    }\n\n    @Override\n    public boolean isSame(int deviceId, short pin, PinType type) {\n        return false;\n    }\n\n    @Override\n    //supports only virtual pins\n    public PinMode getModeType() {\n        return null;\n    }\n\n    @Override\n    public int getPrice() {\n        return 500;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/outputs/Gauge.java",
    "content": "package cc.blynk.server.core.model.widgets.outputs;\n\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.enums.WidgetProperty;\nimport cc.blynk.server.core.model.widgets.OnePinReadingWidget;\nimport cc.blynk.server.core.model.widgets.outputs.graph.FontSize;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.03.15.\n */\npublic class Gauge extends OnePinReadingWidget {\n\n    private String valueFormatting;\n\n    public FontSize fontSize;\n\n    @Override\n    public PinMode getModeType() {\n        return PinMode.in;\n    }\n\n    @Override\n    public boolean setProperty(WidgetProperty property, String propertyValue) {\n        switch (property) {\n            case VALUE_FORMATTING :\n                this.valueFormatting = propertyValue;\n                return true;\n            default:\n                return super.setProperty(property, propertyValue);\n        }\n    }\n\n    @Override\n    public int getPrice() {\n        return 300;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/outputs/LCD.java",
    "content": "package cc.blynk.server.core.model.widgets.outputs;\n\nimport cc.blynk.server.core.model.DataStream;\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.storage.value.MultiPinStorageValue;\nimport cc.blynk.server.core.model.storage.value.MultiPinStorageValueType;\nimport cc.blynk.server.core.model.storage.value.PinStorageValue;\nimport cc.blynk.server.core.model.widgets.FrequencyWidget;\nimport cc.blynk.server.core.model.widgets.MultiPinWidget;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.utils.structure.LCDLimitedQueue;\nimport cc.blynk.utils.structure.LimitedArrayDeque;\nimport io.netty.channel.Channel;\n\nimport static cc.blynk.server.core.protocol.enums.Command.APP_SYNC;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE;\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeUTF8StringMessage;\nimport static cc.blynk.utils.StringUtils.prependDashIdAndDeviceId;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.03.15.\n */\npublic class LCD extends MultiPinWidget implements FrequencyWidget {\n\n    public boolean advancedMode;\n\n    public String textFormatLine1;\n    public String textFormatLine2;\n\n    public boolean textLight;\n\n    private int frequency;\n\n    private transient long lastRequestTS;\n\n    //todo move to persistent LCDLimitedQueue?\n    private transient final LimitedArrayDeque<String> lastCommands = new LimitedArrayDeque<>(LCDLimitedQueue.POOL_SIZE);\n\n    private static void sendSyncOnActivate(DataStream dataStream, int dashId, int deviceId, Channel appChannel) {\n        if (dataStream.notEmptyAndIsValid()) {\n            String body = prependDashIdAndDeviceId(dashId, deviceId, dataStream.makeHardwareBody());\n            appChannel.write(makeUTF8StringMessage(APP_SYNC, SYNC_DEFAULT_MESSAGE_ID, body),\n                    appChannel.voidPromise());\n        }\n    }\n\n    @Override\n    public boolean updateIfSame(int deviceId, short pinIn, PinType type, String value) {\n        boolean isSame = false;\n        if (dataStreams != null && this.deviceId == deviceId) {\n            for (DataStream dataStream : dataStreams) {\n                if (dataStream.isSame(pinIn, type)) {\n                    dataStream.value = value;\n                    isSame = true;\n                }\n            }\n            if (advancedMode && isSame && value != null) {\n                lastCommands.add(value);\n            }\n        }\n        return isSame;\n    }\n\n    @Override\n    public void sendAppSync(Channel appChannel, int dashId, int targetId) {\n        if (dataStreams == null) {\n            return;\n        }\n\n        //do not send SYNC message for widgets assigned to device selector\n        //as it will be duplicated later.\n        if (isAssignedToDeviceSelector()) {\n            return;\n        }\n\n        if (targetId == ANY_TARGET || this.deviceId == targetId) {\n            if (advancedMode) {\n                for (String command : lastCommands) {\n                    dataStreams[0].value = command;\n                    sendSyncOnActivate(dataStreams[0], dashId, deviceId, appChannel);\n                }\n            } else {\n                for (DataStream dataStream : dataStreams) {\n                    sendSyncOnActivate(dataStream, dashId, deviceId, appChannel);\n                }\n            }\n        }\n    }\n\n    @Override\n    public boolean isSplitMode() {\n        return !advancedMode;\n    }\n\n    @Override\n    public boolean isTicked(long now) {\n        if (hasReadingInterval() && now >= lastRequestTS + frequency) {\n            this.lastRequestTS = now;\n            return true;\n        }\n        return false;\n    }\n\n    @Override\n    public boolean hasReadingInterval() {\n        return frequency > 0;\n    }\n\n    @Override\n    public void writeReadingCommand(Channel channel) {\n        if (dataStreams == null) {\n            return;\n        }\n        for (DataStream dataStream : dataStreams) {\n            if (dataStream.isValid()) {\n                StringMessage msg = makeUTF8StringMessage(HARDWARE, READING_MSG_ID,\n                        DataStream.makeReadingHardwareBody(dataStream.pinType.pintTypeChar, dataStream.pin));\n                channel.write(msg, channel.voidPromise());\n            }\n        }\n    }\n\n    @Override\n    public PinStorageValue getPinStorageValue() {\n        return new MultiPinStorageValue(MultiPinStorageValueType.LCD);\n    }\n\n    @Override\n    public boolean isMultiValueWidget() {\n        return true;\n    }\n\n    @Override\n    public int getDeviceId() {\n        return deviceId;\n    }\n\n    @Override\n    public PinMode getModeType() {\n        return PinMode.in;\n    }\n\n    @Override\n    public int getPrice() {\n        return 400;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/outputs/LED.java",
    "content": "package cc.blynk.server.core.model.widgets.outputs;\n\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.03.15.\n */\npublic class LED extends OnePinWidget {\n\n    @Override\n    public PinMode getModeType() {\n        return PinMode.in;\n    }\n\n    @Override\n    public int getPrice() {\n        return 100;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/outputs/LabeledValueDisplay.java",
    "content": "package cc.blynk.server.core.model.widgets.outputs;\n\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.enums.WidgetProperty;\nimport cc.blynk.server.core.model.widgets.OnePinReadingWidget;\nimport cc.blynk.server.core.model.widgets.outputs.graph.FontSize;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.03.15.\n */\npublic class LabeledValueDisplay extends OnePinReadingWidget {\n\n    private TextAlignment textAlignment;\n\n    private String valueFormatting;\n\n    private FontSize fontSize;\n\n    @Override\n    public boolean setProperty(WidgetProperty property, String propertyValue) {\n        switch (property) {\n            case VALUE_FORMATTING :\n                this.valueFormatting = propertyValue;\n                return true;\n            default:\n                return super.setProperty(property, propertyValue);\n        }\n    }\n\n    @Override\n    public PinMode getModeType() {\n        return PinMode.in;\n    }\n\n    @Override\n    public int getPrice() {\n        return 400;\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/outputs/LevelDisplay.java",
    "content": "package cc.blynk.server.core.model.widgets.outputs;\n\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.widgets.OnePinReadingWidget;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.03.15.\n */\npublic class LevelDisplay extends OnePinReadingWidget {\n\n    public boolean isAxisFlipOn;\n\n    public boolean showValueOn = true;\n\n    @Override\n    public PinMode getModeType() {\n        return PinMode.in;\n    }\n\n    @Override\n    public int getPrice() {\n        return 200;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/outputs/Map.java",
    "content": "package cc.blynk.server.core.model.widgets.outputs;\n\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\nimport cc.blynk.utils.structure.MapLimitedQueue;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelHandlerContext;\n\nimport static cc.blynk.server.core.protocol.enums.Command.APP_SYNC;\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeUTF8StringMessage;\nimport static cc.blynk.utils.StringUtils.prependDashIdAndDeviceId;\n\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.03.15.\n */\npublic class Map extends OnePinWidget {\n\n    private transient final MapLimitedQueue<String> lastCommands = new MapLimitedQueue<>();\n\n    public boolean isPinToLatestPoint;\n\n    public boolean isMyLocationSupported;\n\n    public boolean isSatelliteMode;\n\n    public String labelFormat;\n\n    public int radius; //zoom level / radius which user selected.\n\n    public float lat; // last user position on map\n\n    public float lon; // last user position on map\n\n    @Override\n    public boolean updateIfSame(int deviceId, short pin, PinType type, String value) {\n        if (isSame(deviceId, pin, type)) {\n            switch (value) {\n                case \"clr\" :\n                    this.value = null;\n                    this.lastCommands.clear();\n                    break;\n                default:\n                    this.value = value;\n                    this.lastCommands.add(value);\n                    break;\n            }\n            return true;\n        }\n        return false;\n    }\n\n    @Override\n    public void sendAppSync(Channel appChannel, int dashId, int targetId) {\n        if (isNotValid() || lastCommands.size() == 0) {\n            return;\n        }\n        if (targetId == ANY_TARGET || this.deviceId == targetId) {\n            for (String storedValue : lastCommands) {\n                String body = prependDashIdAndDeviceId(dashId, deviceId, makeHardwareBody(pinType, pin, storedValue));\n                appChannel.write(makeUTF8StringMessage(APP_SYNC, SYNC_DEFAULT_MESSAGE_ID, body));\n            }\n        }\n    }\n\n    @Override\n    public void sendHardSync(ChannelHandlerContext ctx, int msgId, int deviceId) {\n    }\n\n    @Override\n    public String getJsonValue() {\n        return JsonParser.toJson(lastCommands);\n    }\n\n    @Override\n    public PinMode getModeType() {\n        return PinMode.in;\n    }\n\n    @Override\n    public int getPrice() {\n        return 600;\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/outputs/TextAlignment.java",
    "content": "package cc.blynk.server.core.model.widgets.outputs;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 28.03.16.\n */\npublic enum TextAlignment {\n\n    LEFT, RIGHT, MIDDLE\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/outputs/ValueDisplay.java",
    "content": "package cc.blynk.server.core.model.widgets.outputs;\n\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.widgets.OnePinReadingWidget;\nimport cc.blynk.server.core.model.widgets.outputs.graph.FontSize;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.03.15.\n */\npublic class ValueDisplay extends OnePinReadingWidget {\n\n    public FontSize fontSize;\n\n    @Override\n    public PinMode getModeType() {\n        return PinMode.in;\n    }\n\n    @Override\n    public int getPrice() {\n        return 200;\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/outputs/VerticalLevelDisplay.java",
    "content": "package cc.blynk.server.core.model.widgets.outputs;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.03.15.\n */\npublic class VerticalLevelDisplay extends LevelDisplay {\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/outputs/graph/AggregationFunctionType.java",
    "content": "package cc.blynk.server.core.model.widgets.outputs.graph;\n\nimport cc.blynk.server.core.dao.functions.AverageGraphFunction;\nimport cc.blynk.server.core.dao.functions.GraphFunction;\nimport cc.blynk.server.core.dao.functions.MaxGraphFunction;\nimport cc.blynk.server.core.dao.functions.MedianGraphFunction;\nimport cc.blynk.server.core.dao.functions.MinGraphFunction;\nimport cc.blynk.server.core.dao.functions.SumGraphFunction;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.07.17.\n */\npublic enum AggregationFunctionType {\n\n    MIN,\n    MAX,\n    AVG,\n    SUM,\n    MED;\n\n    public GraphFunction produce() {\n        switch (this) {\n            case MIN :\n                return new MinGraphFunction();\n            case MAX :\n                return new MaxGraphFunction();\n            case SUM :\n                return new SumGraphFunction();\n            case MED :\n                return new MedianGraphFunction();\n            default:\n                return new AverageGraphFunction();\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/outputs/graph/FontSize.java",
    "content": "package cc.blynk.server.core.model.widgets.outputs.graph;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 28.03.16.\n */\npublic enum FontSize {\n\n    LARGE, MEDIUM, SMALL, AUTO\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/outputs/graph/GoalLine.java",
    "content": "package cc.blynk.server.core.model.widgets.outputs.graph;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 28.03.16.\n */\npublic enum GoalLine {\n\n    GOAL, ABOVE_GOAL, BELOW_GOAL\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/outputs/graph/GraphDataStream.java",
    "content": "package cc.blynk.server.core.model.widgets.outputs.graph;\n\nimport cc.blynk.server.core.model.DataStream;\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.06.17.\n */\npublic class GraphDataStream {\n\n    private final String title;\n\n    private final GraphType graphType;\n\n    private final int color;\n\n    public final int targetId;\n\n    @JsonProperty(\"pin\") //todo \"pin\" for back compatibility\n    public final DataStream dataStream;\n\n    public final AggregationFunctionType functionType;\n\n    private final int flip;\n\n    private final String low;\n\n    private final String high;\n\n    private final String mathFormula;\n\n    private final float yAxisMin;\n\n    private final float yAxisMax;\n\n    private final boolean showYAxis;\n\n    private final String suffix;\n\n    private final boolean cubicSmoothingEnabled;\n\n    private final boolean connectMissingPointsEnabled;\n\n    private final boolean isPercentMaxMin;\n\n    private final YAxisScale yAxisScale;\n\n    private final float delta;\n\n    private final boolean userDeltaModifyAllowed;\n\n    private final int maximumFractionDigits;\n\n    @JsonCreator\n    public GraphDataStream(@JsonProperty(\"title\") String title,\n                           @JsonProperty(\"graphType\") GraphType graphType,\n                           @JsonProperty(\"color\") int color,\n                           @JsonProperty(\"targetId\") int targetId,\n                           @JsonProperty(\"pin\") DataStream dataStream,\n                           @JsonProperty(\"functionType\") AggregationFunctionType functionType,\n                           @JsonProperty(\"flip\") int flip,\n                           @JsonProperty(\"low\") String low,\n                           @JsonProperty(\"high\") String high,\n                           @JsonProperty(\"mathFormula\") String mathFormula,\n                           @JsonProperty(\"yAxisMin\") float yAxisMin,\n                           @JsonProperty(\"yAxisMax\") float yAxisMax,\n                           @JsonProperty(\"showYAxis\") boolean showYAxis,\n                           @JsonProperty(\"suffix\") String suffix,\n                           @JsonProperty(\"cubicSmoothingEnabled\") boolean cubicSmoothingEnabled,\n                           @JsonProperty(\"connectMissingPointsEnabled\") boolean connectMissingPointsEnabled,\n                           @JsonProperty(\"isPercentMaxMin\") boolean isPercentMaxMin,\n                           @JsonProperty(\"yAxisScale\") YAxisScale yAxisScale,\n                           @JsonProperty(\"delta\") float delta,\n                           @JsonProperty(\"userDeltaModifyAllowed\") boolean userDeltaModifyAllowed,\n                           @JsonProperty(\"maximumFractionDigits\") int maximumFractionDigits) {\n        this.title = title;\n        this.graphType = graphType;\n        this.color = color;\n        this.targetId = targetId;\n        this.dataStream = dataStream;\n        this.functionType = functionType;\n        this.flip = flip;\n        this.low = low;\n        this.high = high;\n        this.mathFormula = mathFormula;\n        this.yAxisMin = yAxisMin;\n        this.yAxisMax = yAxisMax;\n        this.showYAxis = showYAxis;\n        this.suffix = suffix;\n        this.cubicSmoothingEnabled = cubicSmoothingEnabled;\n        this.connectMissingPointsEnabled = connectMissingPointsEnabled;\n        this.isPercentMaxMin = isPercentMaxMin;\n        this.yAxisScale = yAxisScale == null ? YAxisScale.UNSET : yAxisScale;\n        this.delta = delta;\n        this.userDeltaModifyAllowed = userDeltaModifyAllowed;\n        this.maximumFractionDigits = maximumFractionDigits;\n    }\n\n    public int getTargetId(int targetIdOverride) {\n        return targetIdOverride == -1 ? this.targetId : targetIdOverride;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/outputs/graph/GraphGranularityType.java",
    "content": "package cc.blynk.server.core.model.widgets.outputs.graph;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 12.08.15.\n */\npublic enum GraphGranularityType {\n\n    MINUTE(\"minute\", 'm', 60 * 1000),\n    HOURLY(\"hourly\", 'h', 60 * 60 * 1000),\n    DAILY(\"daily\", 'd', 24 * 60 * 60 * 1000);\n\n    public final String label;\n    public final char type;\n    public final long period;\n\n    private static final GraphGranularityType[] values = values();\n\n    GraphGranularityType(String label, char type, long period) {\n        this.label = label;\n        this.type = type;\n        this.period = period;\n    }\n\n    public static GraphGranularityType[] getValues() {\n        return values;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/outputs/graph/GraphPeriod.java",
    "content": "package cc.blynk.server.core.model.widgets.outputs.graph;\n\nimport static cc.blynk.server.core.model.widgets.outputs.graph.GraphGranularityType.DAILY;\nimport static cc.blynk.server.core.model.widgets.outputs.graph.GraphGranularityType.HOURLY;\nimport static cc.blynk.server.core.model.widgets.outputs.graph.GraphGranularityType.MINUTE;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 12.08.15.\n */\npublic enum GraphPeriod {\n\n    LIVE(60, MINUTE),\n    FIFTEEN_MINUTES(15, MINUTE),\n    THIRTY_MINUTES(30, MINUTE),\n    ONE_HOUR(60, MINUTE),\n    THREE_HOURS(3 * 60, MINUTE),\n    SIX_HOURS(6 * 60, MINUTE),\n    TWELVE_HOURS(12 * 60, MINUTE),\n\n    DAY(24 * 60, MINUTE),\n    @Deprecated\n    THREE_DAYS(24 * 3, HOURLY),\n    WEEK(24 * 7, HOURLY),\n    TWO_WEEKS(24 * 14, HOURLY),\n    MONTH(30 * 24, HOURLY),\n    THREE_MONTHS(3 * 30 * 24, HOURLY),\n    @Deprecated\n    ALL(12 * 30 * 24, HOURLY),\n\n    N_DAY(24, HOURLY),\n    TWO_DAYS(2 * 24, HOURLY),\n    N_THREE_DAYS(3 * 24, HOURLY),\n    N_WEEK(7, DAILY),\n    N_TWO_WEEKS(14, DAILY),\n    N_MONTH(30, DAILY),\n    N_THREE_MONTHS(3 * 30, DAILY),\n    SIX_MONTHS(6 * 30, DAILY),\n    ONE_YEAR(12 * 30, DAILY);\n\n    public final int numberOfPoints;\n    public final GraphGranularityType granularityType;\n\n    GraphPeriod(int numberOfPoints, GraphGranularityType granularityType) {\n        this.numberOfPoints = numberOfPoints;\n        this.granularityType = granularityType;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/outputs/graph/GraphType.java",
    "content": "package cc.blynk.server.core.model.widgets.outputs.graph;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.06.17.\n */\npublic enum GraphType {\n\n    LINE, FILLED_LINE, BAR, BINARY\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/outputs/graph/LineType.java",
    "content": "package cc.blynk.server.core.model.widgets.outputs.graph;\n\npublic enum LineType {\n\n    LINE, DASH\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/outputs/graph/Stacking.java",
    "content": "package cc.blynk.server.core.model.widgets.outputs.graph;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 28.03.16.\n */\npublic enum Stacking {\n\n    NO_STACKING, STACK, STACK_100\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/outputs/graph/Superchart.java",
    "content": "package cc.blynk.server.core.model.widgets.outputs.graph;\n\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.outputs.TextAlignment;\n\nimport static cc.blynk.server.core.model.widgets.outputs.graph.GraphPeriod.LIVE;\nimport static cc.blynk.server.core.model.widgets.outputs.graph.GraphPeriod.N_DAY;\nimport static cc.blynk.server.core.model.widgets.outputs.graph.GraphPeriod.N_MONTH;\nimport static cc.blynk.server.core.model.widgets.outputs.graph.GraphPeriod.N_THREE_MONTHS;\nimport static cc.blynk.server.core.model.widgets.outputs.graph.GraphPeriod.N_WEEK;\nimport static cc.blynk.server.core.model.widgets.outputs.graph.GraphPeriod.ONE_HOUR;\nimport static cc.blynk.server.core.model.widgets.outputs.graph.GraphPeriod.SIX_HOURS;\nimport static cc.blynk.server.internal.EmptyArraysUtil.EMPTY_GRAPH_DATA_STREAMS;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 12.08.15.\n */\npublic class Superchart extends Widget {\n\n    private static final GraphPeriod[] DEFAULT_PERIODS = new GraphPeriod[] {\n            LIVE, ONE_HOUR, SIX_HOURS, N_DAY, N_WEEK, N_MONTH, N_THREE_MONTHS\n    };\n\n    public GraphDataStream[] dataStreams = EMPTY_GRAPH_DATA_STREAMS;\n\n    public GraphPeriod period;\n\n    public TextAlignment textAlignment;\n\n    public FontSize fontSize;\n\n    public Stacking stacking;\n\n    public boolean showTitle;\n\n    public boolean showLegend;\n\n    public boolean yAxisValues;\n\n    public boolean xAxisValues;\n\n    public boolean showXAxis;\n\n    public boolean allowFullScreen;\n\n    public boolean overrideYAxis;\n\n    public boolean hideGradient;\n\n    public float yAxisMin;\n\n    public float yAxisMax;\n\n    public boolean isPercentMaxMin;\n\n    public String goalText;\n\n    public GoalLine goalLine;\n\n    public LineType lineType;\n\n    public GraphPeriod[] selectedPeriods = DEFAULT_PERIODS;\n\n    public boolean hasLivePeriodsSelected() {\n        for (GraphPeriod graphPeriod : selectedPeriods) {\n            if (graphPeriod == LIVE) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    @Override\n    //do not performs any direct pin operations\n    public PinMode getModeType() {\n        return null;\n    }\n\n    @Override\n    public int getPrice() {\n        return 900;\n    }\n\n    @Override\n    public void updateValue(Widget oldWidget) {\n    }\n\n    @Override\n    public void erase() {\n    }\n\n    @Override\n    public boolean isAssignedToDevice(int deviceId) {\n        return false;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/outputs/graph/YAxisScale.java",
    "content": "package cc.blynk.server.core.model.widgets.outputs.graph;\n\npublic enum YAxisScale {\n\n    UNSET,\n    AUTO,\n    MINMAX,\n    HEIGHT,\n    DELTA\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/sensors/Accelerometer.java",
    "content": "package cc.blynk.server.core.model.widgets.sensors;\n\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\nimport io.netty.channel.ChannelHandlerContext;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 06.09.16.\n */\npublic class Accelerometer extends OnePinWidget {\n\n    private int frequency;\n\n    @Override\n    public PinMode getModeType() {\n        return PinMode.out;\n    }\n\n    @Override\n    public void sendHardSync(ChannelHandlerContext ctx, int msgId, int deviceId) {\n    }\n\n    @Override\n    public int getPrice() {\n        return 400;\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/sensors/Barometer.java",
    "content": "package cc.blynk.server.core.model.widgets.sensors;\n\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\nimport io.netty.channel.ChannelHandlerContext;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 06.09.16.\n */\npublic class Barometer extends OnePinWidget {\n\n    private int frequency;\n\n    @Override\n    public PinMode getModeType() {\n        return PinMode.out;\n    }\n\n    @Override\n    public void sendHardSync(ChannelHandlerContext ctx, int msgId, int deviceId) {\n    }\n\n    @Override\n    public int getPrice() {\n        return 300;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/sensors/GPSStreaming.java",
    "content": "package cc.blynk.server.core.model.widgets.sensors;\n\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\nimport io.netty.channel.ChannelHandlerContext;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.03.15.\n */\npublic class GPSStreaming extends OnePinWidget {\n\n    public int accuracy;\n\n    private int frequency;\n\n    private long interval;\n\n    @Override\n    public PinMode getModeType() {\n        return PinMode.out;\n    }\n\n    @Override\n    public void sendHardSync(ChannelHandlerContext ctx, int msgId, int deviceId) {\n    }\n\n    @Override\n    public int getPrice() {\n        return 500;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/sensors/GPSTrigger.java",
    "content": "package cc.blynk.server.core.model.widgets.sensors;\n\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\nimport io.netty.channel.ChannelHandlerContext;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.03.15.\n */\npublic class GPSTrigger extends OnePinWidget {\n\n    public boolean triggerOnEnter;\n\n    public float triggerLat;\n\n    public float triggerLon;\n\n    public int triggerRadius;\n\n    public int accuracy;\n\n    @Override\n    public PinMode getModeType() {\n        return PinMode.out;\n    }\n\n    @Override\n    public void sendHardSync(ChannelHandlerContext ctx, int msgId, int deviceId) {\n    }\n\n    @Override\n    public int getPrice() {\n        return 500;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/sensors/Gravity.java",
    "content": "package cc.blynk.server.core.model.widgets.sensors;\n\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\nimport io.netty.channel.ChannelHandlerContext;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 06.09.16.\n */\npublic class Gravity extends OnePinWidget {\n\n    private int frequency;\n\n    @Override\n    public PinMode getModeType() {\n        return PinMode.out;\n    }\n\n    @Override\n    public void sendHardSync(ChannelHandlerContext ctx, int msgId, int deviceId) {\n    }\n\n    @Override\n    public int getPrice() {\n        return 300;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/sensors/Humidity.java",
    "content": "package cc.blynk.server.core.model.widgets.sensors;\n\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\nimport io.netty.channel.ChannelHandlerContext;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 06.09.16.\n */\npublic class Humidity extends OnePinWidget {\n\n    private int frequency;\n\n    @Override\n    public PinMode getModeType() {\n        return PinMode.out;\n    }\n\n    @Override\n    public void sendHardSync(ChannelHandlerContext ctx, int msgId, int deviceId) {\n    }\n\n    @Override\n    public int getPrice() {\n        return 300;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/sensors/Light.java",
    "content": "package cc.blynk.server.core.model.widgets.sensors;\n\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\nimport io.netty.channel.ChannelHandlerContext;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 06.09.16.\n */\npublic class Light extends OnePinWidget {\n\n    private int frequency;\n\n    @Override\n    public PinMode getModeType() {\n        return PinMode.out;\n    }\n\n    @Override\n    public void sendHardSync(ChannelHandlerContext ctx, int msgId, int deviceId) {\n    }\n\n    @Override\n    public int getPrice() {\n        return 300;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/sensors/Proximity.java",
    "content": "package cc.blynk.server.core.model.widgets.sensors;\n\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\nimport io.netty.channel.ChannelHandlerContext;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 06.09.16.\n */\npublic class Proximity extends OnePinWidget {\n\n    private int frequency;\n\n    @Override\n    public PinMode getModeType() {\n        return PinMode.out;\n    }\n\n    @Override\n    public void sendHardSync(ChannelHandlerContext ctx, int msgId, int deviceId) {\n    }\n\n    @Override\n    public int getPrice() {\n        return 300;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/sensors/Temperature.java",
    "content": "package cc.blynk.server.core.model.widgets.sensors;\n\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\nimport io.netty.channel.ChannelHandlerContext;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 06.09.16.\n */\npublic class Temperature extends OnePinWidget {\n\n    public boolean isCelsius;\n\n    private int frequency;\n\n    @Override\n    public PinMode getModeType() {\n        return PinMode.out;\n    }\n\n    @Override\n    public void sendHardSync(ChannelHandlerContext ctx, int msgId, int deviceId) {\n    }\n\n    @Override\n    public int getPrice() {\n        return 300;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/DeviceSelector.java",
    "content": "package cc.blynk.server.core.model.widgets.ui;\n\nimport cc.blynk.server.core.model.widgets.DeviceCleaner;\nimport cc.blynk.server.core.model.widgets.NoPinWidget;\nimport cc.blynk.server.core.model.widgets.Target;\nimport cc.blynk.server.core.model.widgets.outputs.graph.FontSize;\nimport cc.blynk.utils.ArrayUtil;\n\nimport static cc.blynk.server.internal.EmptyArraysUtil.EMPTY_INTS;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 02.02.17.\n */\npublic class DeviceSelector extends NoPinWidget implements Target, DeviceCleaner {\n\n    public static final int DEVICE_SELECTOR_STARTING_ID = 200_000;\n\n    //this is selected deviceId in widget\n    public volatile int value = 0;\n\n    public volatile int[] deviceIds = EMPTY_INTS;\n\n    public FontSize fontSize;\n\n    public int iconColor;\n\n    public boolean showIcon;\n\n    public String hint;\n\n    @Override\n    public int[] getDeviceIds() {\n        return new int[] {value};\n    }\n\n    @Override\n    public boolean isSelected(int deviceId) {\n        return value == deviceId;\n    }\n\n    @Override\n    public int[] getAssignedDeviceIds() {\n        return deviceIds;\n    }\n\n    @Override\n    public boolean contains(int deviceId) {\n        return ArrayUtil.contains(this.deviceIds, deviceId);\n    }\n\n    @Override\n    public int getDeviceId() {\n        return value;\n    }\n\n    @Override\n    public int getPrice() {\n        return 1900;\n    }\n\n    @Override\n    public boolean isAssignedToDevice(int deviceId) {\n        return ArrayUtil.contains(this.deviceIds, deviceId);\n    }\n\n    @Override\n    public void deleteDevice(int deviceId) {\n        this.deviceIds = ArrayUtil.deleteFromArray(this.deviceIds, deviceId);\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/Menu.java",
    "content": "package cc.blynk.server.core.model.widgets.ui;\n\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.enums.WidgetProperty;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\nimport cc.blynk.server.core.model.widgets.outputs.TextAlignment;\nimport cc.blynk.server.core.model.widgets.outputs.graph.FontSize;\nimport cc.blynk.utils.StringUtils;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 28.03.16.\n */\npublic class Menu extends OnePinWidget {\n\n    public volatile String[] labels;\n\n    public String hint;\n\n    public TextAlignment alignment;\n\n    public FontSize fontSize;\n\n    public int iconColor;\n\n    @Override\n    public PinMode getModeType() {\n        return PinMode.out;\n    }\n\n    @Override\n    public int getPrice() {\n        return 400;\n    }\n\n    @Override\n    public boolean setProperty(WidgetProperty property, String propertyValue) {\n        switch (property) {\n            case LABELS :\n                this.labels = propertyValue.split(StringUtils.BODY_SEPARATOR_STRING);\n                return true;\n            default:\n                return super.setProperty(property, propertyValue);\n        }\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/Tab.java",
    "content": "package cc.blynk.server.core.model.widgets.ui;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 10.03.16.\n */\npublic class Tab {\n\n    public final int id;\n\n    public final String label;\n\n    @JsonCreator\n    public Tab(@JsonProperty(\"id\") int id,\n               @JsonProperty(\"label\") String label) {\n        this.id = id;\n        this.label = label;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/Tabs.java",
    "content": "package cc.blynk.server.core.model.widgets.ui;\n\nimport cc.blynk.server.core.model.widgets.NoPinWidget;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 07.02.16.\n */\npublic class Tabs extends NoPinWidget {\n\n    public Tab[] tabs;\n\n    public volatile int color;\n\n    public int activeTxtColor;\n\n    public int underlineColor;\n\n    public int textColor;\n\n    public Tabs() {\n        this.tabId = -1;\n    }\n\n    @Override\n    public int getPrice() {\n        return 0;\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/TimeInput.java",
    "content": "package cc.blynk.server.core.model.widgets.ui;\n\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.others.rtc.StringToZoneId;\nimport cc.blynk.server.core.model.widgets.others.rtc.ZoneIdToString;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\n\nimport java.time.ZoneId;\n\nimport static cc.blynk.utils.StringUtils.BODY_SEPARATOR_STRING;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 27.08.16.\n */\npublic class TimeInput extends OnePinWidget {\n\n    private static final int NEVER = -1;\n    private static final int SUNSET = -2;\n    private static final int SUNRISE = -3;\n\n    public String format;\n\n    //from 1 to 7, starts from MONDAY (1)\n    public volatile int[] days;\n\n    public volatile int startAt = -1;\n\n    public volatile int stopAt = -1;\n\n    @JsonSerialize(using = ZoneIdToString.class)\n    @JsonDeserialize(using = StringToZoneId.class, as = ZoneId.class)\n    public volatile ZoneId tzName;\n\n    public boolean isStartStopAllowed;\n\n    public boolean isDayOfWeekAllowed;\n\n    public boolean isSunsetSunriseAllowed;\n\n    public boolean isTimezoneAllowed;\n\n    @Override\n    public boolean updateIfSame(int deviceId, short pin, PinType type, String value) {\n        if (super.updateIfSame(deviceId, pin, type, value)) {\n            String[] values = value.split(BODY_SEPARATOR_STRING);\n            if (values.length > 2) {\n                startAt = calcTime(values[0]);\n                stopAt = calcTime(values[1]);\n                tzName = StringToZoneId.parseZoneId(values[2]);\n                if (values.length == 3 || values[3].isEmpty()) {\n                    days = null;\n                } else {\n                    String[] daysString = values[3].split(\",\");\n                    days = new int[daysString.length];\n                    for (int i = 0; i < daysString.length; i++) {\n                        days[i] = Integer.parseInt(daysString[i]);\n                    }\n                }\n            }\n            return true;\n        }\n        return false;\n    }\n\n    private static int calcTime(String value) {\n        switch (value) {\n            case \"ss\" :\n                return SUNSET;\n            case \"sr\" :\n                return SUNRISE;\n            case \"\" :\n                return NEVER;\n            default :\n                return Integer.parseInt(value);\n        }\n    }\n\n    @Override\n    public void updateValue(Widget oldWidget) {\n        if (oldWidget instanceof TimeInput) {\n            TimeInput oldTimeInput = (TimeInput) oldWidget;\n            if (oldTimeInput.value != null) {\n                this.updateIfSame(oldTimeInput.deviceId, oldTimeInput.pin, oldTimeInput.pinType, oldTimeInput.value);\n            }\n        }\n    }\n\n    @Override\n    public void erase() {\n        super.erase();\n        this.tzName = null;\n    }\n\n    @Override\n    public PinMode getModeType() {\n        return PinMode.out;\n    }\n\n    @Override\n    public int getPrice() {\n        return 200;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/image/Image.java",
    "content": "package cc.blynk.server.core.model.widgets.ui.image;\n\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.enums.WidgetProperty;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\nimport cc.blynk.utils.ArrayUtil;\nimport cc.blynk.utils.StringUtils;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 13.09.16.\n */\npublic class Image extends OnePinWidget {\n\n    public ImageSource source;\n\n    public ImageScaling scaling;\n\n    public volatile String[] urls;\n\n    public volatile int opacity;\n\n    public volatile int scale;\n\n    public volatile int rotation;\n\n    @Override\n    public boolean setProperty(WidgetProperty property, String propertyValue) {\n        switch (property) {\n            case OPACITY :\n                this.opacity = Integer.parseInt(propertyValue);\n                return true;\n            case SCALE :\n                this.scale = Integer.parseInt(propertyValue);\n                return true;\n            case ROTATION :\n                this.rotation = Integer.parseInt(propertyValue);\n                return true;\n            case URLS :\n                this.urls = propertyValue.split(StringUtils.BODY_SEPARATOR_STRING);\n                return true;\n            case URL :\n                String[] split = StringUtils.split2(propertyValue);\n                if (split.length == 2) {\n                    int index = Integer.parseInt(split[0]) - 1;\n                    if (index >= 0 && index < urls.length) {\n                        this.urls = ArrayUtil.copyAndReplace(this.urls, split[1], index);\n                        return true;\n                    }\n                }\n                return false;\n            default:\n                return super.setProperty(property, propertyValue);\n        }\n    }\n\n    @Override\n    public PinMode getModeType() {\n        return PinMode.in;\n    }\n\n    @Override\n    public int getPrice() {\n        return 600;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/image/ImageScaling.java",
    "content": "package cc.blynk.server.core.model.widgets.ui.image;\n\npublic enum ImageScaling {\n\n    NONE, FILL, FIT\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/image/ImageSource.java",
    "content": "package cc.blynk.server.core.model.widgets.ui.image;\n\npublic enum ImageSource {\n\n    URL, ALBUM, PREDEFINED\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/reporting/BaseReportTask.java",
    "content": "package cc.blynk.server.core.model.widgets.ui.reporting;\n\nimport cc.blynk.server.core.dao.ReportingDiskDao;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.Profile;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.widgets.ui.reporting.source.ReportDataStream;\nimport cc.blynk.server.core.model.widgets.ui.reporting.source.ReportSource;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandException;\nimport cc.blynk.server.notifications.mail.MailWrapper;\nimport cc.blynk.utils.FileUtils;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.io.BufferedWriter;\nimport java.io.IOException;\nimport java.io.OutputStreamWriter;\nimport java.nio.ByteBuffer;\nimport java.nio.charset.Charset;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.time.LocalDate;\nimport java.util.concurrent.TimeUnit;\nimport java.util.zip.ZipEntry;\nimport java.util.zip.ZipException;\nimport java.util.zip.ZipOutputStream;\n\nimport static cc.blynk.utils.StringUtils.truncateFileName;\nimport static java.nio.charset.StandardCharsets.UTF_16;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 31/05/2018.\n *\n */\npublic abstract class BaseReportTask implements Runnable {\n\n    static final Logger log = LogManager.getLogger(BaseReportTask.class);\n\n    public final ReportTaskKey key;\n\n    final Report report;\n\n    private final MailWrapper mailWrapper;\n\n    private final ReportingDiskDao reportingDiskDao;\n\n    private final String downloadUrl;\n\n    private static final Charset REPORT_ENCODING = UTF_16;\n    private static final int size = 64 * 1024;\n\n    protected BaseReportTask(User user, int dashId, Report report,\n                             MailWrapper mailWrapper, ReportingDiskDao reportingDiskDao,\n                             String downloadUrl) {\n        this.key = new ReportTaskKey(user, dashId, report.id);\n        this.report = report;\n        this.mailWrapper = mailWrapper;\n        this.reportingDiskDao = reportingDiskDao;\n        this.downloadUrl = downloadUrl;\n    }\n\n    private static String deviceAndPinFileName(String deviceName, int deviceId, ReportDataStream reportDataStream) {\n        String pinLabel = reportDataStream.formatForFileName();\n        return deviceName + \"_\" + deviceId + \"_\" + pinLabel + \".csv\";\n    }\n\n    private static String deviceFileName(String deviceName, int deviceId) {\n        return deviceName + \"_\" + deviceId + \".csv\";\n    }\n\n    @Override\n    public void run() {\n        try {\n            report.lastReportAt = generateReport();\n            log.debug(report);\n        } catch (Exception e) {\n            log.debug(\"Error generating report {} for {}.\", report, key.user.email, e);\n        }\n    }\n\n    private void sendEmail(Path output) throws Exception {\n        String durationLabel = report.reportType.getDurationLabel().toLowerCase();\n        String subj = \"Your \" + durationLabel + \" \" + report.getReportName() + \" is ready\";\n        String gzipDownloadUrl = downloadUrl + output.getFileName();\n        String dynamicSection = report.buildDynamicSection();\n        mailWrapper.sendReportEmail(report.recipients, subj, gzipDownloadUrl, dynamicSection);\n    }\n\n    protected long generateReport() {\n        long now = System.currentTimeMillis();\n\n        String date = LocalDate.now(report.tzName).toString();\n        Path userCsvFolder = FileUtils.getUserReportDir(\n                key.user.email, key.user.appName, key.reportId, date);\n\n        try {\n            Profile profile = key.user.profile;\n            DashBoard dash = profile.getDashByIdOrThrow(key.dashId);\n            report.lastRunResult = generateReport(userCsvFolder, profile, dash, now);\n        } catch (IllegalCommandException illegalState) {\n            report.lastRunResult = ReportResult.ERROR;\n            log.debug(\"Dashboard is not exists anymore for the report {} for user {}. \", report.id, key.user.email);\n        } catch (Exception e) {\n            report.lastRunResult = ReportResult.ERROR;\n            log.error(\"Error generating report {} for user {}. \", report.id, key.user.email);\n            log.error(\"Error: \", e);\n        }\n\n        long newNow = System.currentTimeMillis();\n\n        log.info(\"Processed report for {}, time {} ms.\", key.user.email, newNow - now);\n        return newNow;\n    }\n\n    private ReportResult generateReport(Path userCsvFolder, Profile profile,\n                                        DashBoard dash, long now) throws Exception {\n        int fetchCount = (int) report.reportType.getFetchCount(report.granularityType);\n        long startFrom = now - TimeUnit.DAYS.toMillis(report.reportType.getDuration());\n        //truncate second, minute, hour, depending of granularity in order to do not filter first point.\n        //https://github.com/blynkkk/blynk-server/issues/1149\n        startFrom = (startFrom / report.granularityType.period) * report.granularityType.period;\n        Path output = Paths.get(userCsvFolder.toString() + \".zip\");\n\n        boolean hasData = generateReport(output, profile, dash, fetchCount, startFrom);\n        if (hasData) {\n            sendEmail(output);\n            return ReportResult.OK;\n        }\n\n        log.info(\"No data for report for user {} and reportId {}.\", key.user.email, report.id);\n        return ReportResult.NO_DATA;\n    }\n\n    private boolean generateReport(Path output, Profile profile,\n                                   DashBoard dash, int fetchCount, long startFrom) throws Exception {\n        switch (report.reportOutput) {\n            case MERGED_CSV:\n                return merged(output, profile, dash, fetchCount, startFrom);\n            case CSV_FILE_PER_DEVICE:\n                return filePerDevice(output, profile, dash, fetchCount, startFrom);\n            case CSV_FILE_PER_DEVICE_PER_PIN:\n            case EXCEL_TAB_PER_DEVICE:\n            default:\n                return filePerDevicePerPin(output, profile, dash, fetchCount, startFrom);\n        }\n    }\n\n    private boolean merged(Path output, Profile profile, DashBoard dash,\n                           int fetchCount, long startFrom) throws Exception {\n        boolean atLeastOne = false;\n        try (ZipOutputStream zipStream = new ZipOutputStream(Files.newOutputStream(output));\n             BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(zipStream, REPORT_ENCODING), size)) {\n            String fileName = truncateFileName(report.getReportName()) + \".csv\";\n            ZipEntry zipEntry = new ZipEntry(fileName);\n            zipStream.putNextEntry(zipEntry);\n            for (ReportSource reportSource : report.reportSources) {\n                if (reportSource.isValid()) {\n                    for (int deviceId : reportSource.getDeviceIds()) {\n                        String deviceName = profile.getCSVDeviceName(dash, deviceId);\n                        for (ReportDataStream reportDataStream : reportSource.reportDataStreams) {\n                            if (reportDataStream.isValid()) {\n                                ByteBuffer onePinData = reportingDiskDao.getByteBufferFromDisk(key.user,\n                                        key.dashId, deviceId, reportDataStream.pinType,\n                                        reportDataStream.pin, fetchCount, report.granularityType, 0);\n\n                                if (onePinData != null) {\n                                    String pin = reportDataStream.formatAndEscapePin();\n                                    atLeastOne = FileUtils.writeBufToCsvFilterAndFormat(writer,\n                                            onePinData, pin, deviceName, startFrom, report.makeFormatter());\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n            zipStream.closeEntry();\n        }\n        return atLeastOne;\n    }\n\n    private boolean filePerDevice(Path output, Profile profile,\n                                  DashBoard dash, int fetchCount, long startFrom) throws Exception {\n        boolean atLeastOne = false;\n        try (ZipOutputStream zipStream = new ZipOutputStream(Files.newOutputStream(output));\n             BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(zipStream, REPORT_ENCODING), size)) {\n            for (ReportSource reportSource : report.reportSources) {\n                if (reportSource.isValid()) {\n                    for (int deviceId : reportSource.getDeviceIds()) {\n                        String deviceName = profile.getDeviceName(dash, deviceId);\n                        String deviceFileName = deviceFileName(deviceName, deviceId);\n                        ZipEntry zipEntry = new ZipEntry(deviceFileName);\n                        zipStream.putNextEntry(zipEntry);\n                        for (ReportDataStream reportDataStream : reportSource.reportDataStreams) {\n                            if (reportDataStream.isValid()) {\n                                ByteBuffer onePinData = reportingDiskDao.getByteBufferFromDisk(key.user,\n                                        key.dashId, deviceId, reportDataStream.pinType,\n                                        reportDataStream.pin, fetchCount, report.granularityType, 0);\n\n                                if (onePinData != null) {\n                                    String pin = reportDataStream.formatAndEscapePin();\n                                    atLeastOne = FileUtils.writeBufToCsvFilterAndFormat(writer,\n                                            onePinData, pin, startFrom, report.makeFormatter());\n                                }\n                            }\n                        }\n                        zipStream.closeEntry();\n                    }\n                }\n            }\n        }\n        return atLeastOne;\n    }\n\n    private boolean filePerDevicePerPin(Path output, Profile profile,\n                                        DashBoard dash, int fetchCount, long startFrom) throws Exception {\n        boolean atLeastOne = false;\n        try (ZipOutputStream zipStream = new ZipOutputStream(Files.newOutputStream(output))) {\n            for (ReportSource reportSource : report.reportSources) {\n                if (reportSource.isValid()) {\n                    for (int deviceId : reportSource.getDeviceIds()) {\n                        String deviceName = profile.getDeviceName(dash, deviceId);\n                        for (ReportDataStream reportDataStream : reportSource.reportDataStreams) {\n                            if (reportDataStream.isValid()) {\n                                ByteBuffer onePinData = reportingDiskDao.getByteBufferFromDisk(key.user,\n                                        key.dashId, deviceId, reportDataStream.pinType,\n                                        reportDataStream.pin, fetchCount, report.granularityType, 0);\n\n                                if (onePinData != null) {\n                                    String onePinDataCsv = FileUtils.writeBufToCsvFilterAndFormat(onePinData,\n                                            startFrom, report.makeFormatter());\n                                    if (onePinDataCsv.length() > 0) {\n                                        String onePinFileName =\n                                                deviceAndPinFileName(deviceName, deviceId, reportDataStream);\n                                        addZipEntryAndWrite(zipStream, onePinFileName,\n                                                onePinDataCsv.getBytes(REPORT_ENCODING));\n                                        atLeastOne = true;\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n        return atLeastOne;\n    }\n\n    private void addZipEntryAndWrite(ZipOutputStream zipStream,\n                                        String onePinFileName, byte[] onePinDataCsv) throws IOException {\n        ZipEntry zipEntry = new ZipEntry(onePinFileName);\n        try {\n            zipStream.putNextEntry(zipEntry);\n            zipStream.write(onePinDataCsv);\n            zipStream.closeEntry();\n        } catch (ZipException zipException) {\n            String message = zipException.getMessage();\n            if (message != null && message.contains(\"duplicate\")) {\n                log.warn(\"Duplicate zip entry {}. Wrong report configuration.\", onePinFileName);\n            } else {\n                log.error(\"Error compressing report file.\", message);\n                throw zipException;\n            }\n        } catch (IOException e) {\n            log.error(\"Error compressing report file.\", e.getMessage());\n            throw e;\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/reporting/Format.java",
    "content": "package cc.blynk.server.core.model.widgets.ui.reporting;\n\npublic enum Format {\n\n    ISO_SIMPLE(\"yyyy-MM-dd HH:mm:ss\"), //2018-06-07 22:01:20\n    ISO_INSTANT(\"yyyy-MM-dd'T'HH:mm:ssXXX\"), //2018-06-07T22:01:20+03:00\n    ISO_INSTANT_Z(\"yyyy-MM-dd'T'HH:mm:ssz\"), //2018-06-07T22:01:20EEST\n    TS(null);\n\n    public final String pattern;\n\n    Format(String pattern) {\n        this.pattern = pattern;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/reporting/PeriodicReportTask.java",
    "content": "package cc.blynk.server.core.model.widgets.ui.reporting;\n\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandBodyException;\n\nimport java.util.concurrent.TimeUnit;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 31/05/2018.\n *\n */\npublic class PeriodicReportTask extends BaseReportTask {\n\n    private final ReportScheduler reportScheduler;\n\n    PeriodicReportTask(User user, int dashId, Report report, ReportScheduler reportScheduler) {\n        super(user, dashId, report,\n                reportScheduler.mailWrapper, reportScheduler.reportingDao,\n                reportScheduler.downloadUrl);\n        this.reportScheduler = reportScheduler;\n    }\n\n    @Override\n    public void run() {\n        try {\n            long finishedAt = generateReport();\n            report.lastReportAt = finishedAt;\n            reschedule(finishedAt);\n            log.debug(\"After rescheduling: {}\", report);\n        } catch (IllegalCommandBodyException ice) {\n            log.info(\"Seems like report is expired for {}.\", key.user.email);\n            report.lastRunResult = ReportResult.EXPIRED;\n        } catch (Exception e) {\n            log.debug(\"Error generating report {} for {}.\", report, key.user.email, e);\n        }\n    }\n\n    private void reschedule(long reportFinishedAt) {\n        long initialDelaySeconds = report.calculateDelayInSeconds();\n        report.nextReportAt = reportFinishedAt + initialDelaySeconds * 1000;\n\n        //rescheduling report\n        log.info(\"Rescheduling report for {} with delay {}.\", key.user.email, initialDelaySeconds);\n        reportScheduler.schedule(this, initialDelaySeconds, TimeUnit.SECONDS);\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/reporting/Report.java",
    "content": "package cc.blynk.server.core.model.widgets.ui.reporting;\n\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.model.widgets.others.rtc.StringToZoneId;\nimport cc.blynk.server.core.model.widgets.others.rtc.ZoneIdToString;\nimport cc.blynk.server.core.model.widgets.outputs.graph.GraphGranularityType;\nimport cc.blynk.server.core.model.widgets.ui.reporting.source.ReportSource;\nimport cc.blynk.server.core.model.widgets.ui.reporting.type.BaseReportType;\nimport cc.blynk.server.core.model.widgets.ui.reporting.type.DailyReport;\nimport cc.blynk.server.core.model.widgets.ui.reporting.type.OneTimeReport;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandBodyException;\nimport cc.blynk.server.internal.EmptyArraysUtil;\nimport cc.blynk.utils.validators.BlynkEmailValidator;\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\n\nimport java.time.Duration;\nimport java.time.ZoneId;\nimport java.time.ZonedDateTime;\nimport java.time.format.DateTimeFormatter;\n\nimport static cc.blynk.server.core.model.widgets.ui.reporting.ReportOutput.CSV_FILE_PER_DEVICE_PER_PIN;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 22.05.18.\n */\npublic class Report {\n\n    public final int id;\n\n    public final String name;\n\n    public final ReportSource[] reportSources;\n\n    public final BaseReportType reportType;\n\n    public final String recipients;\n\n    public final GraphGranularityType granularityType;\n\n    public final boolean isActive;\n\n    public final ReportOutput reportOutput;\n\n    public final Format format;\n\n    @JsonSerialize(using = ZoneIdToString.class)\n    @JsonDeserialize(using = StringToZoneId.class, as = ZoneId.class)\n    public final ZoneId tzName;\n\n    public volatile long nextReportAt;\n\n    public volatile long lastReportAt;\n\n    public volatile ReportResult lastRunResult;\n\n    @JsonCreator\n    public Report(@JsonProperty(\"id\") int id,\n                  @JsonProperty(\"name\") String name,\n                  @JsonProperty(\"reportSources\") ReportSource[] reportSources,\n                  @JsonProperty(\"reportType\") BaseReportType reportType,\n                  @JsonProperty(\"recipients\") String recipients,\n                  @JsonProperty(\"granularityType\") GraphGranularityType granularityType,\n                  @JsonProperty(\"isActive\") boolean isActive,\n                  @JsonProperty(\"reportOutput\") ReportOutput reportOutput,\n                  @JsonProperty(\"format\") Format format,\n                  @JsonProperty(\"tzName\") ZoneId tzName,\n                  @JsonProperty(\"nextReportAt\") long nextReportAt,\n                  @JsonProperty(\"lastReportAt\") long lastReportAt,\n                  @JsonProperty(\"lastRunResult\") ReportResult lastRunResult) {\n        this.id = id;\n        this.name = name;\n        this.reportSources = reportSources == null ? EmptyArraysUtil.EMPTY_REPORT_SOURCES : reportSources;\n        this.reportType = reportType;\n        this.recipients = recipients;\n        this.granularityType = granularityType == null ? GraphGranularityType.MINUTE : granularityType;\n        this.isActive = isActive;\n        this.reportOutput = reportOutput == null ? CSV_FILE_PER_DEVICE_PER_PIN : reportOutput;\n        this.format = format;\n        this.tzName = tzName;\n        this.nextReportAt = nextReportAt;\n        this.lastReportAt = lastReportAt;\n        this.lastRunResult = lastRunResult;\n    }\n\n    public boolean isValid() {\n        return reportType != null && reportType.isValid()\n                && reportSources != null && reportSources.length > 0\n                && BlynkEmailValidator.isValidEmails(recipients);\n    }\n\n    public boolean isPeriodic() {\n        return !(reportType instanceof OneTimeReport);\n    }\n\n    public static int getPrice() {\n        return 2900;\n    }\n\n    public long calculateDelayInSeconds() throws IllegalCommandBodyException {\n        DailyReport basePeriodicReportType = (DailyReport) reportType;\n\n        ZonedDateTime zonedNow = ZonedDateTime.now(tzName);\n        ZonedDateTime zonedStartAt = basePeriodicReportType.getNextTriggerTime(zonedNow, tzName);\n        if (basePeriodicReportType.isExpired(zonedStartAt, tzName)) {\n            throw new IllegalCommandBodyException(\"Report is expired.\");\n        }\n\n        Duration duration = Duration.between(zonedNow, zonedStartAt);\n        long initialDelaySeconds = duration.getSeconds();\n\n        if (initialDelaySeconds < 0) {\n            throw new IllegalCommandBodyException(\"Initial delay in less than zero.\");\n        }\n\n        return initialDelaySeconds;\n    }\n\n    String buildDynamicSection() {\n        StringBuilder sb = new StringBuilder();\n        sb.append(\"Report name: \").append(getReportName()).append(\"<br>\");\n        reportType.buildDynamicSection(sb, tzName);\n        return sb.toString();\n    }\n\n    public String getReportName() {\n        return (name == null || name.isEmpty()) ? \"Report\" : name;\n    }\n\n    public DateTimeFormatter makeFormatter() {\n        return (format == null || format == Format.TS)\n                ? null\n                : DateTimeFormatter.ofPattern(format.pattern).withZone(tzName);\n    }\n\n    @Override\n    public String toString() {\n        return JsonParser.toJson(this);\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/reporting/ReportOutput.java",
    "content": "package cc.blynk.server.core.model.widgets.ui.reporting;\n\npublic enum ReportOutput {\n\n    EXCEL_TAB_PER_DEVICE,\n    CSV_FILE_PER_DEVICE_PER_PIN,\n    CSV_FILE_PER_DEVICE,\n    MERGED_CSV\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/reporting/ReportResult.java",
    "content": "package cc.blynk.server.core.model.widgets.ui.reporting;\n\npublic enum ReportResult {\n\n    NO_DATA, ERROR, OK, EXPIRED\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/reporting/ReportScheduler.java",
    "content": "package cc.blynk.server.core.model.widgets.ui.reporting;\n\nimport cc.blynk.server.core.dao.ReportingDiskDao;\nimport cc.blynk.server.core.dao.UserKey;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandBodyException;\nimport cc.blynk.server.notifications.mail.MailWrapper;\nimport cc.blynk.utils.BlynkTPFactory;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.util.Iterator;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ScheduledFuture;\nimport java.util.concurrent.ScheduledThreadPoolExecutor;\nimport java.util.concurrent.TimeUnit;\n\nimport static cc.blynk.server.core.model.widgets.ui.reporting.ReportResult.EXPIRED;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 31/05/2018.\n *\n */\npublic class ReportScheduler extends ScheduledThreadPoolExecutor {\n\n    private static final Logger log = LogManager.getLogger(ReportScheduler.class);\n\n    public final Map<ReportTaskKey, ScheduledFuture<?>> map;\n    public final MailWrapper mailWrapper;\n    public final ReportingDiskDao reportingDao;\n    public final String downloadUrl;\n\n    public ReportScheduler(int corePoolSize, String downloadUrl,\n                           MailWrapper mailWrapper, ReportingDiskDao reportingDao, Map<UserKey, User> users) {\n        super(corePoolSize,  BlynkTPFactory.build(\"report\"));\n        setRemoveOnCancelPolicy(true);\n        setExecuteExistingDelayedTasksAfterShutdownPolicy(false);\n        this.map = new ConcurrentHashMap<>();\n        this.downloadUrl = downloadUrl;\n        this.mailWrapper = mailWrapper;\n        this.reportingDao = reportingDao;\n        init(users);\n    }\n\n    private void init(Map<UserKey, User> users) {\n        int counter = 0;\n        for (Map.Entry<UserKey, User> entry : users.entrySet()) {\n            User user = entry.getValue();\n            for (DashBoard dashBoard : user.profile.dashBoards) {\n                for (Widget widget : dashBoard.widgets) {\n                    if (widget instanceof ReportingWidget) {\n                        ReportingWidget reportingWidget = (ReportingWidget) widget;\n                        for (Report report : reportingWidget.reports) {\n                            if (report.isValid() && report.isPeriodic() && report.isActive) {\n                                try {\n                                    long now = System.currentTimeMillis();\n                                    long initialDelaySeconds;\n\n                                    if (report.nextReportAt < now && report.lastRunResult != EXPIRED) {\n                                        //this is special case, when we restart server we may miss some reports\n                                        //while the server is down, so we perform checks and run those reports,\n                                        //so we are sure we didn't miss any report.\n                                        log.warn(\"Rescheduling missed report {} for {}.\", report, user.email);\n                                        initialDelaySeconds = 0;\n                                    } else {\n                                        initialDelaySeconds = report.calculateDelayInSeconds();\n                                        log.trace(\"Adding periodic report for user {} with delay {} to scheduler.\",\n                                                user.email, initialDelaySeconds);\n                                        report.nextReportAt = now + initialDelaySeconds * 1000;\n                                    }\n                                    schedule(user, dashBoard.id, report, initialDelaySeconds);\n                                    counter++;\n                                } catch (IllegalCommandBodyException e) {\n                                    report.lastRunResult = EXPIRED;\n                                    log.debug(\"Report is expired for {}, {}\", user.email, report.id);\n                                } catch (Exception e) {\n                                    report.lastRunResult = ReportResult.ERROR;\n                                    log.debug(\"Error scheduling report for {}, {}\", user.email, report.id);\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n        log.info(\"Reports : {}\", counter);\n    }\n\n    public void schedule(User user, int dashId, Report report, long delayInSeconds) {\n        schedule(\n                new PeriodicReportTask(user, dashId, report, this),\n                delayInSeconds,\n                TimeUnit.SECONDS\n        );\n    }\n\n    @Override\n    public ScheduledFuture<?> schedule(Runnable task, long delay, TimeUnit unit) {\n        ScheduledFuture<?> scheduledFuture = super.schedule(task, delay, unit);\n        if (task instanceof PeriodicReportTask) {\n            BaseReportTask baseReportTask = (PeriodicReportTask) task;\n            map.put(baseReportTask.key, scheduledFuture);\n        }\n        return scheduledFuture;\n    }\n\n    public void cancelStoredFuture(User user, int dashId) {\n        Iterator<Map.Entry<ReportTaskKey, ScheduledFuture<?>>> iter = map.entrySet().iterator();\n        while (iter.hasNext()) {\n            Map.Entry<ReportTaskKey, ScheduledFuture<?>> entry = iter.next();\n            ReportTaskKey reportTaskKey = entry.getKey();\n            if (reportTaskKey.dashId == dashId && reportTaskKey.user.equals(user)) {\n                iter.remove();\n                ScheduledFuture<?> scheduledFuture = entry.getValue();\n                if (scheduledFuture != null) {\n                    scheduledFuture.cancel(true);\n                }\n            }\n        }\n    }\n\n    public boolean cancelStoredFuture(User user, int dashId, int reportId) {\n        ReportTaskKey key = new ReportTaskKey(user, dashId, reportId);\n        ScheduledFuture<?> scheduledFuture = map.remove(key);\n        if (scheduledFuture == null) {\n            return false;\n        }\n        return scheduledFuture.cancel(true);\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/reporting/ReportTaskKey.java",
    "content": "package cc.blynk.server.core.model.widgets.ui.reporting;\n\nimport cc.blynk.server.core.model.auth.User;\n\npublic class ReportTaskKey {\n\n    public final User user;\n\n    public final int dashId;\n\n    public final int reportId;\n\n    public ReportTaskKey(User user, int dashId, int reportId) {\n        this.user = user;\n        this.dashId = dashId;\n        this.reportId = reportId;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n\n        ReportTaskKey that = (ReportTaskKey) o;\n\n        if (dashId != that.dashId) {\n            return false;\n        }\n        if (reportId != that.reportId) {\n            return false;\n        }\n        return user != null ? user.equals(that.user) : that.user == null;\n    }\n\n    @Override\n    public int hashCode() {\n        int result = user != null ? user.hashCode() : 0;\n        result = 31 * result + dashId;\n        result = 31 * result + reportId;\n        return result;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/reporting/ReportingWidget.java",
    "content": "package cc.blynk.server.core.model.widgets.ui.reporting;\n\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.widgets.DeviceCleaner;\nimport cc.blynk.server.core.model.widgets.NoPinWidget;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.controls.ButtonStyle;\nimport cc.blynk.server.core.model.widgets.controls.Edge;\nimport cc.blynk.server.core.model.widgets.outputs.graph.FontSize;\nimport cc.blynk.server.core.model.widgets.ui.reporting.source.ReportSource;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandException;\n\nimport static cc.blynk.server.internal.EmptyArraysUtil.EMPTY_REPORTS;\nimport static cc.blynk.server.internal.EmptyArraysUtil.EMPTY_REPORT_SOURCES;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 22.05.18.\n */\npublic class ReportingWidget extends NoPinWidget implements DeviceCleaner {\n\n    public ReportSource[] reportSources = EMPTY_REPORT_SOURCES;\n\n    public boolean allowEndUserToDeleteDataOn;\n\n    public FontSize fontSize = FontSize.MEDIUM;\n\n    public Edge edge = Edge.ROUNDED;\n\n    public ButtonStyle buttonStyle = ButtonStyle.SOLID;\n\n    public int textColor;\n\n    public volatile Report[] reports = EMPTY_REPORTS;\n\n    public void validateId(int id) {\n        Report report = getReportById(id);\n        if (report != null) {\n            throw new IllegalCommandException(\"Report with passed id already exists.\");\n        }\n    }\n\n    public Report getReportById(int id) {\n        for (Report report : reports) {\n            if (report.id == id) {\n                return report;\n            }\n        }\n        return null;\n    }\n\n    public int getReportIndexById(int id) {\n        for (int i = 0; i < reports.length; i++) {\n            if (id == reports[i].id) {\n                return i;\n            }\n        }\n        return -1;\n    }\n\n    public boolean hasPin(short pin, PinType pinType) {\n        for (ReportSource reportSource : reportSources) {\n            if (reportSource.isSame(pin, pinType)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    @Override\n    public void deleteDevice(int deviceId) {\n        for (Report report : reports) {\n            if (report.reportSources != null) {\n                for (ReportSource reportSource : report.reportSources) {\n                    reportSource.deleteDevice(deviceId);\n                }\n            }\n        }\n    }\n\n    @Override\n    public int getPrice() {\n        return Report.getPrice() * reports.length;\n    }\n\n    @Override\n    public void erase() {\n        this.reports = EMPTY_REPORTS;\n    }\n\n    @Override\n    public void updateValue(Widget oldWidget) {\n        if (oldWidget instanceof ReportingWidget) {\n            this.reports = ((ReportingWidget) oldWidget).reports;\n        }\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/reporting/source/DeviceReportSource.java",
    "content": "package cc.blynk.server.core.model.widgets.ui.reporting.source;\n\nimport cc.blynk.utils.ArrayUtil;\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport static cc.blynk.server.internal.EmptyArraysUtil.EMPTY_INTS;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 22.05.18.\n */\npublic class DeviceReportSource extends ReportSource {\n\n    public volatile int[] deviceIds;\n\n    @JsonCreator\n    public DeviceReportSource(@JsonProperty(\"dataStreams\") ReportDataStream[] reportDataStream,\n                              @JsonProperty(\"deviceIds\") int[] deviceIds) {\n        super(reportDataStream);\n        this.deviceIds = deviceIds == null ? EMPTY_INTS : deviceIds;\n    }\n\n    @Override\n    public boolean isValid() {\n        return deviceIds.length > 0 && super.isValid();\n    }\n\n    @Override\n    public int[] getDeviceIds() {\n        return deviceIds;\n    }\n\n    @Override\n    public void deleteDevice(int deviceId) {\n        this.deviceIds = ArrayUtil.deleteFromArray(this.deviceIds, deviceId);\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/reporting/source/ReportDataStream.java",
    "content": "package cc.blynk.server.core.model.widgets.ui.reporting.source;\n\nimport cc.blynk.server.core.model.DataStream;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.utils.StringUtils;\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 03.07.15.\n */\npublic class ReportDataStream {\n\n    public final short pin;\n\n    public final PinType pinType;\n\n    public final String label;\n\n    public final boolean isSelected;\n\n    @JsonCreator\n    public ReportDataStream(@JsonProperty(\"pin\") short pin,\n                            @JsonProperty(\"pinType\") PinType pinType,\n                            @JsonProperty(\"label\") String label,\n                            @JsonProperty(\"isSelected\") boolean isSelected) {\n        this.pin = pin;\n        this.pinType = pinType;\n        this.label = label;\n        this.isSelected = isSelected;\n    }\n\n    public boolean isSame(short pin, PinType pinType) {\n        return this.pin == pin && this.pinType == pinType;\n    }\n\n    public boolean isValid() {\n        return this.isSelected && DataStream.isValid(pin, pinType);\n    }\n\n    public String formatForFileName() {\n        if (label == null || label.isEmpty()) {\n            return pinType.pinTypeString + pin;\n        }\n        return StringUtils.truncate(label.replaceAll(\"[^a-zA-Z0-9]\", \"\"), 16);\n    }\n\n    public String formatAndEscapePin() {\n        if (label == null || label.isEmpty()) {\n            return pinType.pinTypeString + pin;\n        }\n        String truncated = StringUtils.truncate(label, 16);\n        return StringUtils.escapeCSV(truncated);\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/reporting/source/ReportSource.java",
    "content": "package cc.blynk.server.core.model.widgets.ui.reporting.source;\n\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.widgets.DeviceCleaner;\nimport com.fasterxml.jackson.annotation.JsonSubTypes;\nimport com.fasterxml.jackson.annotation.JsonTypeInfo;\n\nimport static cc.blynk.server.internal.EmptyArraysUtil.EMPTY_REPORT_DATA_STREAMS;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 22.05.18.\n */\n@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = \"type\")\n@JsonSubTypes({\n        @JsonSubTypes.Type(value = TileTemplateReportSource.class, name = \"TILE_TEMPLATE\"),\n        @JsonSubTypes.Type(value = DeviceReportSource.class, name = \"DEVICE\")\n})\npublic abstract class ReportSource implements DeviceCleaner {\n\n    public final ReportDataStream[] reportDataStreams;\n\n    ReportSource(ReportDataStream[] reportDataStreams) {\n        this.reportDataStreams = reportDataStreams == null ? EMPTY_REPORT_DATA_STREAMS : reportDataStreams;\n    }\n\n    public abstract int[] getDeviceIds();\n\n    public boolean isValid() {\n        return reportDataStreams.length > 0;\n    }\n\n    public boolean isSame(short pin, PinType pinType) {\n        for (ReportDataStream reportDataStream : reportDataStreams) {\n            if (reportDataStream.isSame(pin, pinType)) {\n                return true;\n            }\n        }\n        return false;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/reporting/source/TileTemplateReportSource.java",
    "content": "package cc.blynk.server.core.model.widgets.ui.reporting.source;\n\nimport cc.blynk.utils.ArrayUtil;\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport static cc.blynk.server.internal.EmptyArraysUtil.EMPTY_INTS;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 22.05.18.\n */\npublic class TileTemplateReportSource extends ReportSource {\n\n    public final int templateId;\n\n    public volatile int[] deviceIds;\n\n    @JsonCreator\n    public TileTemplateReportSource(@JsonProperty(\"dataStreams\") ReportDataStream[] reportDataStream,\n                                    @JsonProperty(\"templateId\") int templateId,\n                                    @JsonProperty(\"deviceIds\") int[] deviceIds) {\n        super(reportDataStream);\n        this.templateId = templateId;\n        this.deviceIds = deviceIds == null ? EMPTY_INTS : deviceIds;\n    }\n\n    @Override\n    public boolean isValid() {\n        return deviceIds.length > 0 && super.isValid();\n    }\n\n    @Override\n    public int[] getDeviceIds() {\n        return deviceIds;\n    }\n\n    @Override\n    public void deleteDevice(int deviceId) {\n        this.deviceIds = ArrayUtil.deleteFromArray(this.deviceIds, deviceId);\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/reporting/type/BaseReportType.java",
    "content": "package cc.blynk.server.core.model.widgets.ui.reporting.type;\n\nimport cc.blynk.server.core.model.widgets.outputs.graph.GraphGranularityType;\nimport com.fasterxml.jackson.annotation.JsonSubTypes;\nimport com.fasterxml.jackson.annotation.JsonTypeInfo;\n\nimport java.time.ZoneId;\nimport java.time.ZonedDateTime;\nimport java.util.concurrent.TimeUnit;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 22.05.18.\n */\n@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = \"type\")\n@JsonSubTypes({\n        @JsonSubTypes.Type(value = OneTimeReport.class, name = \"ONE_TIME\"),\n        @JsonSubTypes.Type(value = DailyReport.class, name = \"DAILY\"),\n        @JsonSubTypes.Type(value = WeeklyReport.class, name = \"WEEKLY\"),\n        @JsonSubTypes.Type(value = MonthlyReport.class, name = \"MONTHLY\")\n})\npublic abstract class BaseReportType {\n\n    public abstract ZonedDateTime getNextTriggerTime(ZonedDateTime zonedNow, ZoneId zoneId);\n\n    public abstract boolean isValid();\n\n    public abstract long getDuration();\n\n    public abstract String getDurationLabel();\n\n    public abstract void buildDynamicSection(StringBuilder sb, ZoneId zoneId);\n\n    public long getFetchCount(GraphGranularityType granularity) {\n        switch (granularity) {\n            case DAILY:\n                return TimeUnit.DAYS.toDays(getDuration());\n            case HOURLY:\n                return TimeUnit.DAYS.toHours(getDuration());\n            default:\n                return TimeUnit.DAYS.toMinutes(getDuration());\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/reporting/type/DailyReport.java",
    "content": "package cc.blynk.server.core.model.widgets.ui.reporting.type;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport java.time.Instant;\nimport java.time.LocalTime;\nimport java.time.ZoneId;\nimport java.time.ZonedDateTime;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 22.05.18.\n */\npublic class DailyReport extends BaseReportType {\n\n    private final long atTime;\n\n    private final ReportDurationType durationType;\n\n    private final long startTs;\n\n    private final long endTs;\n\n    @JsonCreator\n    public DailyReport(@JsonProperty(\"atTime\") long atTime,\n                       @JsonProperty(\"durationType\") ReportDurationType durationType,\n                       @JsonProperty(\"startTs\") long startTs,\n                       @JsonProperty(\"endTs\")long endTs) {\n        this.atTime = atTime;\n        this.durationType = durationType;\n        this.startTs = startTs;\n        this.endTs = endTs;\n    }\n\n    @Override\n    public boolean isValid() {\n        return startTs <= endTs;\n    }\n\n    @Override\n    public long getDuration() {\n        return 1;\n    }\n\n    static ZonedDateTime getZonedFromTs(long ts, ZoneId zoneId) {\n        return ZonedDateTime.ofInstant(Instant.ofEpochMilli(ts), zoneId);\n    }\n\n    @Override\n    public String getDurationLabel() {\n        return \"Daily\";\n    }\n\n    @Override\n    public void buildDynamicSection(StringBuilder sb, ZoneId zoneId) {\n        sb.append(\"Period: \").append(getDurationLabel());\n        addReportSpecificAtTime(sb, zoneId);\n\n        if (durationType == ReportDurationType.CUSTOM) {\n            sb.append(\"<br>\");\n\n            ZonedDateTime date;\n\n            date = getZonedFromTs(startTs, zoneId);\n            sb.append(\"Start date: \").append(date.toLocalDate()).append(\"<br>\");\n\n            date = ZonedDateTime.ofInstant(Instant.ofEpochMilli(endTs), zoneId);\n            sb.append(\"End date: \").append(date.toLocalDate()).append(\"<br>\");\n        }\n    }\n\n    public void addReportSpecificAtTime(StringBuilder sb, ZoneId zoneId) {\n        ZonedDateTime zonedAt = getZonedFromTs(atTime, zoneId);\n        LocalTime localTime = zonedAt.toLocalTime();\n        sb.append(\", \").append(\"at \").append(LocalTime.of(localTime.getHour(), localTime.getMinute()));\n    }\n\n    ZonedDateTime buildZonedStartAt(ZonedDateTime zonedNow, ZoneId zoneId) {\n        ZonedDateTime zonedStartAt = getZonedFromTs(atTime, zoneId);\n        zonedStartAt = zonedNow\n                .withHour(zonedStartAt.getHour())\n                .withMinute(zonedStartAt.getMinute())\n                .withSecond(zonedStartAt.getSecond());\n\n        return adjustToStartDate(zonedStartAt, zonedNow, zoneId);\n    }\n\n    private ZonedDateTime adjustToStartDate(ZonedDateTime zonedStartAt, ZonedDateTime zonedNow, ZoneId zoneId) {\n        if (durationType == ReportDurationType.CUSTOM) {\n            ZonedDateTime zonedStartDate = getZonedFromTs(startTs, zoneId).with(LocalTime.MIN);\n            if (zonedStartDate.isAfter(zonedNow)) {\n                zonedStartAt = zonedStartAt\n                        .withDayOfMonth(zonedStartDate.getDayOfMonth())\n                        .withMonth(zonedStartDate.getMonthValue())\n                        .withYear(zonedStartDate.getYear());\n            }\n        }\n        return zonedStartAt;\n    }\n\n    public boolean isExpired(ZonedDateTime zonedNow, ZoneId zoneId) {\n        if (durationType == ReportDurationType.CUSTOM) {\n            ZonedDateTime zonedEndDate = getZonedFromTs(endTs, zoneId).with(LocalTime.MAX);\n            return zonedEndDate.isBefore(zonedNow);\n        }\n        return false;\n    }\n\n    @Override\n    public ZonedDateTime getNextTriggerTime(ZonedDateTime zonedNow, ZoneId zoneId) {\n        ZonedDateTime zonedStartAt = buildZonedStartAt(zonedNow, zoneId);\n        return zonedStartAt.isAfter(zonedNow) ? zonedStartAt : zonedStartAt.plusDays(1);\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/reporting/type/DayOfMonth.java",
    "content": "package cc.blynk.server.core.model.widgets.ui.reporting.type;\n\npublic enum DayOfMonth {\n\n    FIRST(\"at the first day of every month\"),\n    LAST(\"at the last day of every month\");\n\n    public final String label;\n\n    DayOfMonth(String label) {\n        this.label = label;\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/reporting/type/MonthlyReport.java",
    "content": "package cc.blynk.server.core.model.widgets.ui.reporting.type;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport java.time.ZoneId;\nimport java.time.ZonedDateTime;\nimport java.time.temporal.TemporalAdjusters;\n\nimport static cc.blynk.server.core.model.widgets.ui.reporting.type.DayOfMonth.FIRST;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 22.05.18.\n */\npublic class MonthlyReport extends DailyReport {\n\n    private final DayOfMonth dayOfMonth;\n\n    @JsonCreator\n    public MonthlyReport(@JsonProperty(\"atTime\") long atTime,\n                         @JsonProperty(\"durationType\") ReportDurationType durationType,\n                         @JsonProperty(\"startTs\") long startTs,\n                         @JsonProperty(\"endTs\") long endTs,\n                         @JsonProperty(\"dayOfMonth\") DayOfMonth dayOfMonth) {\n        super(atTime, durationType, startTs, endTs);\n        this.dayOfMonth = dayOfMonth == null ? FIRST : dayOfMonth;\n    }\n\n    @Override\n    public long getDuration() {\n        return 30;\n    }\n\n    @Override\n    public String getDurationLabel() {\n        return \"Monthly\";\n    }\n\n    @Override\n    public void addReportSpecificAtTime(StringBuilder sb, ZoneId zoneId) {\n        super.addReportSpecificAtTime(sb, zoneId);\n        sb.append(\" \").append(dayOfMonth.label);\n    }\n\n    @Override\n    public ZonedDateTime getNextTriggerTime(ZonedDateTime zonedNow, ZoneId zoneId) {\n        ZonedDateTime zonedStartAt = buildZonedStartAt(zonedNow, zoneId);\n\n        switch (dayOfMonth) {\n            case LAST:\n                zonedStartAt = zonedStartAt.with(TemporalAdjusters.lastDayOfMonth());\n                return zonedStartAt.isAfter(zonedNow)\n                        ? zonedStartAt\n                        : zonedStartAt.plusDays(1).with(TemporalAdjusters.lastDayOfMonth());\n            case FIRST:\n            default:\n                zonedStartAt = zonedStartAt.with(TemporalAdjusters.firstDayOfMonth());\n                return zonedStartAt.isAfter(zonedNow)\n                        ? zonedStartAt\n                        : zonedStartAt.with(TemporalAdjusters.firstDayOfNextMonth());\n\n        }\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/reporting/type/OneTimeReport.java",
    "content": "package cc.blynk.server.core.model.widgets.ui.reporting.type;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport java.time.ZoneId;\nimport java.time.ZonedDateTime;\nimport java.util.concurrent.TimeUnit;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 22.05.18.\n */\npublic class OneTimeReport extends BaseReportType {\n\n    private final long rangeMillis;\n\n    @JsonCreator\n    public OneTimeReport(@JsonProperty(\"rangeMillis\") long rangeMillis) {\n        this.rangeMillis = rangeMillis;\n    }\n\n    @Override\n    public boolean isValid() {\n        return getDuration() > 0;\n    }\n\n    @Override\n    public String getDurationLabel() {\n        return \"One time\";\n    }\n\n    @Override\n    public void buildDynamicSection(StringBuilder sb, ZoneId zoneId) {\n        sb.append(\"Period: \").append(getDurationLabel());\n    }\n\n    @Override\n    public long getDuration() {\n        return rangeMillis / TimeUnit.DAYS.toMillis(1);\n    }\n\n    @Override\n    public ZonedDateTime getNextTriggerTime(ZonedDateTime zonedNow, ZoneId zoneId) {\n        return zonedNow;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/reporting/type/ReportDurationType.java",
    "content": "package cc.blynk.server.core.model.widgets.ui.reporting.type;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 22.05.18.\n */\npublic enum ReportDurationType {\n\n    CUSTOM, INFINITE\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/reporting/type/WeeklyReport.java",
    "content": "package cc.blynk.server.core.model.widgets.ui.reporting.type;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport java.time.DayOfWeek;\nimport java.time.ZoneId;\nimport java.time.ZonedDateTime;\nimport java.time.format.TextStyle;\nimport java.time.temporal.TemporalAdjusters;\nimport java.util.Locale;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 22.05.18.\n */\npublic class WeeklyReport extends DailyReport {\n\n    //starts from MONDAY (1)\n    private final int dayOfTheWeek;\n\n    @JsonCreator\n    public WeeklyReport(@JsonProperty(\"atTime\") long atTime,\n                        @JsonProperty(\"durationType\") ReportDurationType durationType,\n                        @JsonProperty(\"startTs\") long startTs,\n                        @JsonProperty(\"endTs\") long endTs,\n                        @JsonProperty(\"dayOfTheWeek\") int dayOfTheWeek) {\n        super(atTime, durationType, startTs, endTs);\n        this.dayOfTheWeek = dayOfTheWeek;\n    }\n\n    @Override\n    public long getDuration() {\n        return 7;\n    }\n\n    @Override\n    public String getDurationLabel() {\n        return \"Weekly\";\n    }\n\n    @Override\n    public void addReportSpecificAtTime(StringBuilder sb, ZoneId zoneId) {\n        super.addReportSpecificAtTime(sb, zoneId);\n\n        DayOfWeek dayOfWeek = DayOfWeek.of(dayOfTheWeek);\n        String dayOfWeekDisplayName = dayOfWeek.getDisplayName(TextStyle.FULL, Locale.US);\n        sb.append(\" every \").append(dayOfWeekDisplayName);\n    }\n\n    @Override\n    public ZonedDateTime getNextTriggerTime(ZonedDateTime zonedNow, ZoneId zoneId) {\n        ZonedDateTime zonedStartAt = buildZonedStartAt(zonedNow, zoneId);\n\n        DayOfWeek dayOfWeek = DayOfWeek.of(dayOfTheWeek);\n        zonedStartAt = zonedStartAt.with(TemporalAdjusters.nextOrSame(dayOfWeek));\n        return zonedStartAt.isAfter(zonedNow)\n                ? zonedStartAt\n                : zonedStartAt.with(TemporalAdjusters.next(dayOfWeek));\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/table/Column.java",
    "content": "package cc.blynk.server.core.model.widgets.ui.table;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 07.09.16.\n */\npublic class Column {\n\n    public final String name;\n\n    @JsonCreator\n    public Column(@JsonProperty(\"name\") String name) {\n        this.name = name;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/table/Row.java",
    "content": "package cc.blynk.server.core.model.widgets.ui.table;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport static cc.blynk.utils.StringUtils.BODY_SEPARATOR;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 07.09.16.\n */\npublic class Row {\n\n    public final int id;\n\n    public volatile String name;\n\n    public volatile String value;\n\n    public volatile boolean isSelected;\n\n    @JsonCreator\n    public Row(@JsonProperty(\"id\") int id,\n               @JsonProperty(\"name\") String name,\n               @JsonProperty(\"value\") String value,\n               @JsonProperty(\"isSelected\") boolean isSelected) {\n        this.id = id;\n        this.name = name;\n        this.value = value;\n        this.isSelected = isSelected;\n    }\n\n    public void update(String name, String value) {\n        this.name = name;\n        this.value = value;\n    }\n\n    @Override\n    public String toString() {\n        return \"add\" + BODY_SEPARATOR\n                + id + BODY_SEPARATOR\n                + name + BODY_SEPARATOR\n                + value + BODY_SEPARATOR\n                + isSelected;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/table/Table.java",
    "content": "package cc.blynk.server.core.model.widgets.ui.table;\n\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.storage.value.MultiPinStorageValue;\nimport cc.blynk.server.core.model.storage.value.MultiPinStorageValueType;\nimport cc.blynk.server.core.model.storage.value.PinStorageValue;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\nimport cc.blynk.utils.structure.TableLimitedQueue;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelHandlerContext;\n\nimport java.util.Iterator;\n\nimport static cc.blynk.server.core.protocol.enums.Command.APP_SYNC;\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeUTF8StringMessage;\nimport static cc.blynk.utils.StringUtils.BODY_SEPARATOR_STRING;\n\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 28.03.16.\n */\npublic class Table extends OnePinWidget {\n\n    public Column[] columns;\n\n    public final TableLimitedQueue<Row> rows = new TableLimitedQueue<>();\n\n    public volatile int currentRowIndex;\n\n    public boolean isReoderingAllowed;\n\n    public boolean isClickableRows;\n\n    @Override\n    public void sendHardSync(ChannelHandlerContext ctx, int msgId, int deviceId) {\n    }\n\n    @Override\n    public boolean updateIfSame(int deviceId, short pin, PinType type, String value) {\n        if (isSame(deviceId, pin, type)) {\n            var values = value.split(BODY_SEPARATOR_STRING);\n            if (values.length > 0) {\n                String tableCommand = values[0];\n                switch (tableCommand) {\n                    case \"clr\" :\n                        rows.clear();\n                        currentRowIndex = 0;\n                        break;\n                    case \"add\" :\n                        if (values.length > 3) {\n                            int id = Integer.parseInt(values[1]);\n                            String rowName = values[2];\n                            String rowValue = values[3];\n                            Row existingRow = get(id);\n                            if (existingRow == null) {\n                                rows.add(new Row(id, rowName, rowValue, true));\n                            } else {\n                                existingRow.update(rowName, rowValue);\n                            }\n                        }\n                        break;\n                    case \"update\" :\n                        if (values.length > 3) {\n                            int id = Integer.parseInt(values[1]);\n                            String rowName = values[2];\n                            String rowValue = values[3];\n                            Row existingRow = get(id);\n                            if (existingRow != null) {\n                                existingRow.update(rowName, rowValue);\n                            }\n                        }\n                        break;\n                    case \"pick\" :\n                        if (values.length > 1) {\n                            currentRowIndex = Integer.parseInt(values[1]);\n                        }\n                        break;\n                    case \"select\" :\n                        if (values.length > 1) {\n                            selectRow(values[1], true);\n                        }\n                        break;\n                    case \"deselect\" :\n                        if (values.length > 1) {\n                            selectRow(values[1], false);\n                        }\n                        break;\n                }\n                this.value = value;\n            }\n            return true;\n        }\n        return false;\n    }\n\n    @Override\n    public void sendAppSync(Channel appChannel, int dashId, int targetId) {\n        if (isNotValid() || rows.size() == 0) {\n            return;\n        }\n        if (targetId == ANY_TARGET || this.deviceId == targetId) {\n            Iterator<Row> valIterator = rows.iterator();\n            if (valIterator.hasNext()) {\n                String body = makeMultiValueHardwareBody(dashId, deviceId, pinType.pintTypeChar, pin, valIterator);\n                appChannel.write(makeUTF8StringMessage(APP_SYNC, SYNC_DEFAULT_MESSAGE_ID, body));\n            }\n        }\n    }\n\n    private void selectRow(String idString, boolean select) {\n        int id = Integer.parseInt(idString);\n        Row row = get(id);\n        if (row != null) {\n            row.isSelected = select;\n        }\n    }\n\n    public Row get(int id) {\n        for (Row row : rows) {\n            if (id == row.id) {\n                return row;\n            }\n        }\n        return null;\n    }\n\n    @Override\n    public PinStorageValue getPinStorageValue() {\n        return new MultiPinStorageValue(MultiPinStorageValueType.TABLE);\n    }\n\n    @Override\n    public boolean isMultiValueWidget() {\n        return true;\n    }\n\n    @Override\n    public PinMode getModeType() {\n        return PinMode.out;\n    }\n\n    @Override\n    public int getPrice() {\n        return 800;\n    }\n\n    @Override\n    public void erase() {\n        super.erase();\n        rows.clear();\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/tiles/DeviceTiles.java",
    "content": "package cc.blynk.server.core.model.widgets.ui.tiles;\n\nimport cc.blynk.server.core.model.DataStream;\nimport cc.blynk.server.core.model.enums.PinMode;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.widgets.DeviceCleaner;\nimport cc.blynk.server.core.model.widgets.HardwareSyncWidget;\nimport cc.blynk.server.core.model.widgets.MobileSyncWidget;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.outputs.TextAlignment;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandException;\nimport cc.blynk.utils.ArrayUtil;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelHandlerContext;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\n\nimport static cc.blynk.server.core.protocol.enums.Command.APP_SYNC;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE;\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeUTF8StringMessage;\nimport static cc.blynk.server.internal.EmptyArraysUtil.EMPTY_DEVICE_TILES;\nimport static cc.blynk.server.internal.EmptyArraysUtil.EMPTY_TEMPLATES;\nimport static cc.blynk.utils.StringUtils.prependDashIdAndDeviceId;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 02.10.17.\n */\npublic class DeviceTiles extends Widget implements MobileSyncWidget, HardwareSyncWidget, DeviceCleaner {\n\n    public volatile TileTemplate[] templates = EMPTY_TEMPLATES;\n\n    public volatile Tile[] tiles = EMPTY_DEVICE_TILES;\n\n    public int rows;\n\n    public int columns;\n\n    public SortType sortType;\n\n    public TextAlignment alignment = TextAlignment.LEFT;\n\n    public boolean disableWhenOffline;\n\n    public boolean stretchToBottom;\n\n    public void deleteDeviceTilesByTemplateId(long deviceTileId) {\n        ArrayList<Tile> list = new ArrayList<>();\n        for (Tile tile : tiles) {\n            if (tile.templateId != deviceTileId) {\n                list.add(tile);\n            }\n        }\n        tiles = list.toArray(new Tile[0]);\n    }\n\n    public void recreateTilesIfNecessary(TileTemplate newTileTemplate, TileTemplate existingTileTemplate) {\n        //no changes. do nothing.\n        if (existingTileTemplate != null\n                && Arrays.equals(newTileTemplate.deviceIds, existingTileTemplate.deviceIds)\n                && newTileTemplate.dataStream != null\n                && newTileTemplate.dataStream.equals(existingTileTemplate.dataStream)) {\n            return;\n        }\n\n        Tile[] existingTiles = this.tiles;\n\n        ArrayList<Tile> list = new ArrayList<>();\n        for (TileTemplate tileTemplate : this.templates) {\n            //creating new device tiles for updated TileTemplate\n            if (tileTemplate.id == newTileTemplate.id) {\n                for (int deviceId : newTileTemplate.deviceIds) {\n                    Tile newTile = new Tile(deviceId, tileTemplate.id, null,\n                            newTileTemplate.dataStream == null\n                                    ? null\n                                    : new DataStream(newTileTemplate.dataStream)\n                    );\n                    preserveOldValueIfPossible(existingTiles, newTile);\n                    list.add(newTile);\n                }\n                //leaving untouched device tiles that are not updated\n            } else {\n                for (Tile tile : existingTiles) {\n                    if (tile.templateId == tileTemplate.id) {\n                        list.add(tile);\n                    }\n                }\n            }\n        }\n        this.tiles = list.toArray(new Tile[0]);\n    }\n\n    private void preserveOldValueIfPossible(Tile[] existingTiles, Tile newTile) {\n        for (Tile existingTile : existingTiles) {\n            if (existingTile.templateId == newTile.templateId\n                    && newTile.updateIfSame(existingTile.deviceId, existingTile.dataStream)) {\n                return;\n            }\n        }\n    }\n\n    public TileTemplate getTileTemplateByIdOrThrow(long id) {\n        return templates[getTileTemplateIndexByIdOrThrow(id)];\n    }\n\n    public TileTemplate getTileTemplateById(long id) {\n        for (TileTemplate tileTemplate : templates) {\n            if (tileTemplate.id == id) {\n                return tileTemplate;\n            }\n        }\n        return null;\n    }\n\n    public int getTileTemplateIndexByIdOrThrow(long id) {\n        for (int i = 0; i < templates.length; i++) {\n            if (templates[i].id == id) {\n                return i;\n            }\n        }\n        throw new IllegalCommandException(\"Tile template with passed id not found.\");\n    }\n\n    public Widget getWidgetById(long widgetId) {\n        for (TileTemplate tileTemplate : templates) {\n            for (Widget widget : tileTemplate.widgets) {\n                if (widget.id == widgetId) {\n                    return widget;\n                }\n            }\n        }\n        return null;\n    }\n\n    public TileTemplate getTileTemplateByWidgetIdOrThrow(long widgetId) {\n        for (TileTemplate tileTemplate : templates) {\n            for (Widget tileTemplateWidget : tileTemplate.widgets) {\n                if (tileTemplateWidget.id == widgetId) {\n                    return tileTemplate;\n                }\n            }\n        }\n        throw new IllegalCommandException(\"Widget template not found for passed widget id.\");\n    }\n\n    @Override\n    public boolean isSame(int deviceId, short pin, PinType pinType) {\n        for (Tile tile : tiles) {\n            if (tile.isSame(deviceId, pin, pinType)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    @Override\n    public boolean updateIfSame(int deviceId, short pin, PinType pinType, String value) {\n        for (Tile tile : tiles) {\n            if (tile.updateIfSame(deviceId, pin, pinType, value)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    @Override\n    public void sendAppSync(Channel appChannel, int dashId, int targetId) {\n        for (Tile tile : tiles) {\n            if ((targetId == ANY_TARGET || tile.deviceId == targetId)\n                    && tile.isValidDataStream() && tile.dataStream.isNotEmpty()) {\n                String hardBody = tile.dataStream.makeHardwareBody();\n                String body = prependDashIdAndDeviceId(dashId, tile.deviceId, hardBody);\n                appChannel.write(makeUTF8StringMessage(APP_SYNC, SYNC_DEFAULT_MESSAGE_ID, body));\n            }\n        }\n    }\n\n    @Override\n    public void sendHardSync(ChannelHandlerContext ctx, int msgId, int deviceId) {\n        for (Tile tile : tiles) {\n            if (tile.deviceId == deviceId && tile.isValidDataStream() && tile.dataStream.isNotEmpty()) {\n                String body = tile.dataStream.makeHardwareBody();\n                if (body != null) {\n                    ctx.write(makeUTF8StringMessage(HARDWARE, msgId, body), ctx.voidPromise());\n                }\n            }\n        }\n    }\n\n    @Override\n    public PinMode getModeType() {\n        return null;\n    }\n\n    @Override\n    public int getPrice() {\n        int sum = 1700; //price for DeviceTiles widget itself\n        for (TileTemplate template : templates) {\n            sum += template.getPrice();\n        }\n        return sum;\n    }\n\n    @Override\n    public void updateValue(Widget oldWidget) {\n        if (oldWidget instanceof DeviceTiles) {\n            DeviceTiles oldDeviceTiles = (DeviceTiles) oldWidget;\n            this.tiles = oldDeviceTiles.tiles;\n            for (TileTemplate tileTemplate : templates) {\n                TileTemplate oldTileTemplate = oldDeviceTiles.getTileTemplateById(tileTemplate.id);\n                if (oldTileTemplate != null) {\n                    tileTemplate.deviceIds = oldTileTemplate.deviceIds;\n                }\n            }\n        }\n    }\n\n    @Override\n    public void erase() {\n        //for export apps tiles are fully removed\n        //tiles will be created during provisioning.\n        tiles = EMPTY_DEVICE_TILES;\n        if (templates != null) {\n            for (TileTemplate tileTemplate : templates) {\n                tileTemplate.erase();\n            }\n        }\n    }\n\n    public String getValue(int deviceId, short pin, PinType pinType) {\n        for (Tile tile : tiles) {\n            if (tile.isSame(deviceId, pin, pinType)) {\n                return tile.dataStream.value;\n            }\n        }\n        return null;\n    }\n\n    @Override\n    public boolean isAssignedToDevice(int deviceId) {\n        return false;\n    }\n\n    private static int getTileIndexByDeviceId(Tile[] tiles, int deviceId) {\n        for (int i = 0; i < tiles.length; i++) {\n            if (tiles[i].deviceId == deviceId) {\n                return i;\n            }\n        }\n        return -1;\n    }\n\n    public void replaceTileTemplate(TileTemplate newTileTemplate, int existingTileTemplateIndex) {\n        TileTemplate[] updatedTemplates = Arrays.copyOf(templates, templates.length);\n        TileTemplate existingTileTemplate = templates[existingTileTemplateIndex];\n        updatedTemplates[existingTileTemplateIndex] = newTileTemplate;\n        //do not override widgets field, as we have separate commands for it.\n\n        newTileTemplate.widgets = existingTileTemplate.widgets;\n        this.templates = updatedTemplates;\n    }\n\n    @Override\n    public void deleteDevice(int deviceId) {\n        Tile[] localTiles = this.tiles;\n        int index = getTileIndexByDeviceId(localTiles, deviceId);\n        if (index != -1) {\n            this.tiles = localTiles.length == 1 ? EMPTY_DEVICE_TILES : ArrayUtil.remove(localTiles, index, Tile.class);\n        }\n\n        for (TileTemplate tileTemplate : this.templates) {\n            tileTemplate.deviceIds = ArrayUtil.deleteFromArray(tileTemplate.deviceIds, deviceId);\n        }\n\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/tiles/SortType.java",
    "content": "package cc.blynk.server.core.model.widgets.ui.tiles;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 02.10.17.\n */\npublic enum SortType {\n\n    CUSTOM,\n    DEVICE_NAME_ASC, DEVICE_NAME_DESC,\n    TEMPLATE_NAME_ASC, TEMPLATE_NAME_DESC,\n    STATUS_CRITICAL, STATUS_OK\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/tiles/Tile.java",
    "content": "package cc.blynk.server.core.model.widgets.ui.tiles;\n\nimport cc.blynk.server.core.model.DataStream;\nimport cc.blynk.server.core.model.enums.PinType;\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 02.10.17.\n */\npublic class Tile {\n\n    public final int deviceId;\n\n    public final long templateId;\n\n    public final String iconName;\n\n    @JsonProperty(\"pin\")\n    public final DataStream dataStream;\n\n    private transient long lastRequestTS;\n\n    @JsonCreator\n    public Tile(@JsonProperty(\"deviceId\") int deviceId,\n                @JsonProperty(\"templateId\") long templateId,\n                @JsonProperty(\"iconName\") String iconName,\n                @JsonProperty(\"pin\") DataStream dataStream) {\n        this.deviceId = deviceId;\n        this.templateId = templateId;\n        this.iconName = iconName;\n        this.dataStream = dataStream;\n    }\n\n    public boolean isSame(int deviceId, short pin, PinType pinType) {\n        return this.deviceId == deviceId && dataStream != null && dataStream.isSame(pin, pinType);\n    }\n\n    public boolean updateIfSame(int deviceId, DataStream dataStream) {\n        if (dataStream != null) {\n            return updateIfSame(deviceId, dataStream.pin, dataStream.pinType, dataStream.value);\n        }\n        return false;\n    }\n\n    public boolean updateIfSame(int deviceId, short pin, PinType pinType, String value) {\n        if (isSame(deviceId, pin, pinType)) {\n            this.dataStream.value = value;\n            return true;\n        }\n        return false;\n    }\n\n    public boolean isValidDataStream() {\n        return dataStream != null && dataStream.isValid();\n    }\n\n    public void erase() {\n        if (dataStream != null) {\n            dataStream.value = null;\n        }\n    }\n\n    public boolean isTicked(long now) {\n        //todo 1000 is hardcoded for now\n        if (now >= lastRequestTS + 1000) {\n            this.lastRequestTS = now;\n            return true;\n        }\n        return false;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/tiles/TileMode.java",
    "content": "package cc.blynk.server.core.model.widgets.ui.tiles;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 02.10.17.\n */\npublic enum TileMode {\n\n    PAGE, BUTTON, ICON, DIMMER\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/tiles/TileTemplate.java",
    "content": "package cc.blynk.server.core.model.widgets.ui.tiles;\n\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.DataStream;\nimport cc.blynk.server.core.model.device.BoardType;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.ui.tiles.templates.ButtonTileTemplate;\nimport cc.blynk.server.core.model.widgets.ui.tiles.templates.DimmerTileTemplate;\nimport cc.blynk.server.core.model.widgets.ui.tiles.templates.PageTileTemplate;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonSubTypes;\nimport com.fasterxml.jackson.annotation.JsonTypeInfo;\n\nimport static cc.blynk.server.internal.EmptyArraysUtil.EMPTY_INTS;\nimport static cc.blynk.server.internal.EmptyArraysUtil.EMPTY_WIDGETS;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 02.10.17.\n */\n@JsonTypeInfo(\n        use = JsonTypeInfo.Id.NAME,\n        property = \"mode\",\n        defaultImpl = PageTileTemplate.class)\n@JsonSubTypes({\n        @JsonSubTypes.Type(value = PageTileTemplate.class, name = \"PAGE\"),\n        @JsonSubTypes.Type(value = ButtonTileTemplate.class, name = \"BUTTON\"),\n        @JsonSubTypes.Type(value = DimmerTileTemplate.class, name = \"DIMMER\")\n})\npublic abstract class TileTemplate {\n\n    public final long id;\n\n    public volatile Widget[] widgets;\n\n    public volatile int[] deviceIds;\n\n    public String templateId;\n\n    public final String name;\n\n    public final String iconName;\n\n    public final BoardType boardType;\n\n    @JsonProperty(\"pin\")\n    public final DataStream dataStream;\n\n    public final boolean showDeviceName;\n\n    public TileTemplate(long id,\n                        Widget[] widgets,\n                        int[] deviceIds,\n                        String templateId,\n                        String name,\n                        String iconName,\n                        BoardType boardType,\n                        DataStream dataStream,\n                        boolean showDeviceName) {\n        this.id = id;\n        this.widgets = widgets == null ? EMPTY_WIDGETS : widgets;\n        this.deviceIds = deviceIds == null ? EMPTY_INTS : deviceIds;\n        this.templateId = templateId;\n        this.name = name;\n        this.iconName = iconName;\n        this.boardType = boardType;\n        this.dataStream = dataStream;\n        this.showDeviceName = showDeviceName;\n    }\n\n    public int getPrice() {\n        int sum = 0;\n        for (Widget widget : widgets) {\n            sum += widget.getPrice();\n        }\n        return sum;\n    }\n\n    public void erase() {\n        if (dataStream != null) {\n            dataStream.value = null;\n        }\n        this.deviceIds = EMPTY_INTS;\n        for (Widget widget : widgets) {\n            widget.erase();\n        }\n    }\n\n    public boolean isEmptyTemplateId() {\n        return templateId == null || templateId.isEmpty();\n    }\n\n    public int getWidgetIndexByIdOrThrow(long widgetId) {\n        return DashBoard.getWidgetIndexByIdOrThrow(widgets, widgetId);\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/tiles/templates/ButtonTileTemplate.java",
    "content": "package cc.blynk.server.core.model.widgets.ui.tiles.templates;\n\nimport cc.blynk.server.core.model.DataStream;\nimport cc.blynk.server.core.model.device.BoardType;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.ui.tiles.TileTemplate;\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 09.03.18.\n */\npublic class ButtonTileTemplate extends TileTemplate {\n\n    private final boolean pushMode;\n\n    private final boolean labelsVisibility;\n\n    private final State stateOn;\n\n    private final State stateOff;\n\n    @JsonCreator\n    public ButtonTileTemplate(@JsonProperty(\"id\") long id,\n                              @JsonProperty(\"widgets\") Widget[] widgets,\n                              @JsonProperty(\"deviceIds\") int[] deviceIds,\n                              @JsonProperty(\"templateId\") String templateId,\n                              @JsonProperty(\"name\") String name,\n                              @JsonProperty(\"iconName\") String iconName,\n                              @JsonProperty(\"boardType\") BoardType boardType,\n                              @JsonProperty(\"dataStream\") DataStream dataStream,\n                              @JsonProperty(\"showDeviceName\") boolean showDeviceName,\n                              @JsonProperty(\"pushMode\")boolean pushMode,\n                              @JsonProperty(\"labelsVisibility\") boolean labelsVisibility,\n                              @JsonProperty(\"stateOn\") State stateOn,\n                              @JsonProperty(\"stateOff\") State stateOff) {\n        super(id, widgets, deviceIds, templateId, name, iconName, boardType, dataStream, showDeviceName);\n        this.pushMode = pushMode;\n        this.labelsVisibility = labelsVisibility;\n        this.stateOn = stateOn;\n        this.stateOff = stateOff;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/tiles/templates/DimmerTileTemplate.java",
    "content": "package cc.blynk.server.core.model.widgets.ui.tiles.templates;\n\nimport cc.blynk.server.core.model.DataStream;\nimport cc.blynk.server.core.model.device.BoardType;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.ui.tiles.TileTemplate;\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 09.03.18.\n */\npublic class DimmerTileTemplate extends TileTemplate {\n\n    private final String valueName;\n\n    private final String valueFormatting;\n\n    private final String valueSuffix;\n\n    public final int color;\n\n    public final int tileColor;\n\n    private final boolean showTileLabel;\n\n    private final int maximumFractionDigits;\n\n    private final int levelColor;\n\n    private Interaction interaction = Interaction.PAGE;\n\n    private float step = 1;\n\n    @JsonCreator\n    public DimmerTileTemplate(@JsonProperty(\"id\") long id,\n                              @JsonProperty(\"widgets\") Widget[] widgets,\n                              @JsonProperty(\"deviceIds\") int[] deviceIds,\n                              @JsonProperty(\"templateId\") String templateId,\n                              @JsonProperty(\"name\") String name,\n                              @JsonProperty(\"iconName\") String iconName,\n                              @JsonProperty(\"boardType\") BoardType boardType,\n                              @JsonProperty(\"dataStream\") DataStream dataStream,\n                              @JsonProperty(\"showDeviceName\") boolean showDeviceName,\n                              @JsonProperty(\"valueName\") String valueName,\n                              @JsonProperty(\"valueFormatting\") String valueFormatting,\n                              @JsonProperty(\"valueSuffix\") String valueSuffix,\n                              @JsonProperty(\"color\") int color,\n                              @JsonProperty(\"tileColor\")int tileColor,\n                              @JsonProperty(\"showTileLabel\")boolean showTileLabel,\n                              @JsonProperty(\"maximumFractionDigits\") int maximumFractionDigits,\n                              @JsonProperty(\"levelColor\") int levelColor) {\n        super(id, widgets, deviceIds, templateId, name, iconName, boardType, dataStream, showDeviceName);\n        this.valueName = valueName;\n        this.valueFormatting = valueFormatting;\n        this.valueSuffix = valueSuffix;\n        this.color = color;\n        this.tileColor = tileColor;\n        this.showTileLabel = showTileLabel;\n        this.maximumFractionDigits = maximumFractionDigits;\n        this.levelColor = levelColor;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/tiles/templates/Interaction.java",
    "content": "package cc.blynk.server.core.model.widgets.ui.tiles.templates;\n\npublic enum Interaction {\n\n    BUTTON, PAGE, STEP\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/tiles/templates/PageTileTemplate.java",
    "content": "package cc.blynk.server.core.model.widgets.ui.tiles.templates;\n\nimport cc.blynk.server.core.model.DataStream;\nimport cc.blynk.server.core.model.device.BoardType;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.outputs.graph.FontSize;\nimport cc.blynk.server.core.model.widgets.ui.tiles.TileTemplate;\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 09.03.18.\n */\npublic class PageTileTemplate extends TileTemplate {\n\n    private final String valueName;\n\n    private final String valueFormatting;\n\n    private final String valueSuffix;\n\n    public final int color;\n\n    public final int tileColor;\n\n    private final FontSize fontSize;\n\n    private final boolean showTileLabel;\n\n    private final int maximumFractionDigits;\n\n    @JsonCreator\n    public PageTileTemplate(@JsonProperty(\"id\") long id,\n                            @JsonProperty(\"widgets\") Widget[] widgets,\n                            @JsonProperty(\"deviceIds\") int[] deviceIds,\n                            @JsonProperty(\"templateId\") String templateId,\n                            @JsonProperty(\"name\") String name,\n                            @JsonProperty(\"iconName\") String iconName,\n                            @JsonProperty(\"boardType\") BoardType boardType,\n                            @JsonProperty(\"dataStream\") DataStream dataStream,\n                            @JsonProperty(\"showDeviceName\") boolean showDeviceName,\n                            @JsonProperty(\"valueName\") String valueName,\n                            @JsonProperty(\"valueFormatting\") String valueFormatting,\n                            @JsonProperty(\"valueSuffix\") String valueSuffix,\n                            @JsonProperty(\"color\") int color,\n                            @JsonProperty(\"tileColor\")int tileColor,\n                            @JsonProperty(\"fontSize\") FontSize fontSize,\n                            @JsonProperty(\"showTileLabel\")boolean showTileLabel,\n                            @JsonProperty(\"maximumFractionDigits\") int maximumFractionDigits) {\n        super(id, widgets, deviceIds, templateId, name, iconName, boardType, dataStream, showDeviceName);\n        this.valueName = valueName;\n        this.valueFormatting = valueFormatting;\n        this.valueSuffix = valueSuffix;\n        this.color = color;\n        this.tileColor = tileColor;\n        this.fontSize = fontSize;\n        this.showTileLabel = showTileLabel;\n        this.maximumFractionDigits = maximumFractionDigits;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/model/widgets/ui/tiles/templates/State.java",
    "content": "package cc.blynk.server.core.model.widgets.ui.tiles.templates;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 09.03.18.\n */\npublic class State {\n\n    private final int tileColor;\n\n    private final String iconName;\n\n    private final int iconColor;\n\n    private final String text;\n\n    private final int textColor;\n\n    @JsonCreator\n    public State(@JsonProperty(\"tileColor\") int tileColor,\n                 @JsonProperty(\"iconName\") String iconName,\n                 @JsonProperty(\"iconColor\") int iconColor,\n                 @JsonProperty(\"text\") String text,\n                 @JsonProperty(\"textColor\") int textColor) {\n        this.tileColor = tileColor;\n        this.iconName = iconName;\n        this.iconColor = iconColor;\n        this.text = text;\n        this.textColor = textColor;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/processors/BaseProcessorHandler.java",
    "content": "package cc.blynk.server.core.processors;\n\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.Session;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.protocol.exceptions.QuotaLimitException;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\n/**\n * Simple abstract class for handling all processor engines.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 25.08.17.\n */\npublic abstract class BaseProcessorHandler {\n\n    protected static final Logger log = LogManager.getLogger(BaseProcessorHandler.class);\n\n    private final EventorProcessor eventorProcessor;\n    private final WebhookProcessor webhookProcessor;\n\n    protected BaseProcessorHandler(EventorProcessor eventorProcessor, WebhookProcessor webhookProcessor) {\n        this.eventorProcessor = eventorProcessor;\n        this.webhookProcessor = webhookProcessor;\n    }\n\n    protected void processEventorAndWebhook(User user, DashBoard dash, int deviceId, Session session, short pin,\n                                            PinType pinType, String value, long now) {\n        try {\n            eventorProcessor.process(user, session, dash, deviceId, pin, pinType, value, now);\n            webhookProcessor.process(user, session, dash, deviceId, pin, pinType, value, now);\n        } catch (QuotaLimitException qle) {\n            log.debug(\"User {} reached notification limit for eventor/webhook.\", user.name);\n        } catch (IllegalArgumentException iae) {\n            log.debug(\"Error processing webhook for {}. Reason : {}\", user.email, iae.getMessage());\n        } catch (Exception e) {\n            log.error(\"Error processing eventor/webhook.\", e);\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/processors/EventorProcessor.java",
    "content": "package cc.blynk.server.core.processors;\n\nimport cc.blynk.server.core.BlockingIOProcessor;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.Profile;\nimport cc.blynk.server.core.model.auth.Session;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.notifications.Mail;\nimport cc.blynk.server.core.model.widgets.notifications.Notification;\nimport cc.blynk.server.core.model.widgets.notifications.Twitter;\nimport cc.blynk.server.core.model.widgets.others.eventor.Eventor;\nimport cc.blynk.server.core.model.widgets.others.eventor.Rule;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.action.BaseAction;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.action.SetPinAction;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.action.SetPropertyPinAction;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.action.notification.MailAction;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.action.notification.NotificationAction;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.action.notification.NotifyAction;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.action.notification.TwitAction;\nimport cc.blynk.server.core.stats.GlobalStats;\nimport cc.blynk.server.notifications.mail.MailWrapper;\nimport cc.blynk.server.notifications.push.GCMWrapper;\nimport cc.blynk.server.notifications.twitter.TwitterWrapper;\nimport cc.blynk.utils.NumberUtil;\nimport cc.blynk.utils.validators.BlynkEmailValidator;\nimport io.netty.handler.codec.http.HttpResponseStatus;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\nimport org.asynchttpclient.AsyncCompletionHandler;\nimport org.asynchttpclient.Response;\n\nimport static cc.blynk.server.core.protocol.enums.Command.EVENTOR;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE;\nimport static cc.blynk.server.core.protocol.enums.Command.SET_WIDGET_PROPERTY;\nimport static cc.blynk.utils.StringUtils.PIN_PATTERN;\n\n/**\n * Class responsible for handling eventor logic.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 24.08.16.\n */\npublic class EventorProcessor {\n\n    private static final Logger log = LogManager.getLogger(EventorProcessor.class);\n\n    private final GCMWrapper gcmWrapper;\n    private final TwitterWrapper twitterWrapper;\n    private final MailWrapper mailWrapper;\n    private final BlockingIOProcessor blockingIOProcessor;\n    private final GlobalStats globalStats;\n\n    public EventorProcessor(GCMWrapper gcmWrapper, MailWrapper mailWrapper, TwitterWrapper twitterWrapper,\n                            BlockingIOProcessor blockingIOProcessor, GlobalStats stats) {\n        this.gcmWrapper = gcmWrapper;\n        this.mailWrapper = mailWrapper;\n        this.twitterWrapper = twitterWrapper;\n        this.blockingIOProcessor = blockingIOProcessor;\n        this.globalStats = stats;\n    }\n\n    public static void push(GCMWrapper gcmWrapper, DashBoard dash, String body) {\n        if (Notification.isWrongBody(body)) {\n            log.debug(\"Wrong push body.\");\n            return;\n        }\n\n        Notification widget = dash.getNotificationWidget();\n\n        if (widget == null || widget.hasNoToken()) {\n            log.debug(\"User has no access token provided for eventor push.\");\n            return;\n        }\n\n        widget.push(gcmWrapper, body, dash.id);\n    }\n\n    private void execute(User user, DashBoard dash, String triggerValue, NotificationAction notificationAction) {\n        String body = PIN_PATTERN.matcher(notificationAction.message).replaceAll(triggerValue);\n        if (notificationAction instanceof NotifyAction) {\n            push(gcmWrapper, dash, body);\n        } else if (notificationAction instanceof TwitAction) {\n            twit(dash, body);\n        } else if (notificationAction instanceof MailAction) {\n            MailAction mailAction = (MailAction) notificationAction;\n            email(user, dash, mailAction.subject, body);\n        }\n    }\n\n    public void process(User user, Session session, DashBoard dash, int deviceId, short pin,\n                        PinType type, String triggerValue, long now) {\n        Eventor eventor = dash.getEventorWidget();\n        if (eventor == null || eventor.rules == null\n                || eventor.deviceId != deviceId || !dash.isActive) {\n            return;\n        }\n\n        double valueParsed = NumberUtil.parseDouble(triggerValue);\n\n        for (Rule rule : eventor.rules) {\n            if (rule.isReady(pin, type)) {\n                if (rule.matchesCondition(triggerValue, valueParsed)) {\n                    if (!rule.isProcessed) {\n                        for (BaseAction action : rule.actions) {\n                            if (action.isValid()) {\n                                if (action instanceof SetPinAction) {\n                                    execute(session, user.profile, dash, deviceId, (SetPinAction) action, now);\n                                } else if (action instanceof SetPropertyPinAction) {\n                                    execute(session, user.profile, dash, deviceId, (SetPropertyPinAction) action, now);\n                                } else if (action instanceof NotificationAction) {\n                                    execute(user, dash, triggerValue, (NotificationAction) action);\n                                }\n                                globalStats.mark(EVENTOR);\n                            }\n                        }\n                        rule.isProcessed = true;\n                    }\n                } else {\n                    rule.isProcessed = false;\n                }\n            }\n        }\n    }\n\n    private void email(User user, DashBoard dash, String subject, String body) {\n        Mail mail = dash.getMailWidget();\n\n        user.checkDailyEmailLimit();\n\n        String to = (mail == null || mail.to == null || mail.to.isEmpty()) ? user.email : mail.to;\n\n        if (BlynkEmailValidator.isNotValidEmail(to)) {\n            log.error(\"Invalid mail receiver: {}.\", to);\n            return;\n        }\n\n        blockingIOProcessor.execute(() -> {\n            try {\n                mailWrapper.sendText(to, subject, body);\n            } catch (Exception e) {\n                log.warn(\"Error sending email from eventor. From user {}, to : {}. Reason : {}\",\n                        user.email, to, e.getMessage());\n            }\n        });\n        user.emailMessages++;\n    }\n\n    private void twit(DashBoard dash, String body) {\n        if (Twitter.isWrongBody(body)) {\n            log.debug(\"Wrong twit body.\");\n            return;\n        }\n\n        Twitter twitterWidget = dash.getTwitterWidget();\n\n        if (twitterWidget == null\n                || twitterWidget.token == null\n                || twitterWidget.token.isEmpty()\n                || twitterWidget.secret == null\n                || twitterWidget.secret.isEmpty()) {\n            log.debug(\"User has no access token provided for eventor twit.\");\n            return;\n        }\n\n        twitterWrapper.send(twitterWidget.token, twitterWidget.secret, body,\n                new AsyncCompletionHandler<>() {\n                    @Override\n                    public Response onCompleted(Response response) {\n                        if (response.getStatusCode() != HttpResponseStatus.OK.code()) {\n                            log.debug(\"Error sending twit from eventor. Reason : {}.\", response.getResponseBody());\n                        }\n                        return response;\n                    }\n\n                    @Override\n                    public void onThrowable(Throwable t) {\n                        log.debug(\"Error sending twit from eventor.\", t);\n                    }\n                }\n        );\n    }\n\n    private void execute(Session session, Profile profile, DashBoard dash,\n                         int deviceId, SetPinAction action, long now) {\n        String body = action.makeHardwareBody();\n        session.sendMessageToHardware(dash.id, HARDWARE, 888, body, deviceId);\n        session.sendToApps(HARDWARE, 888, dash.id, deviceId, body);\n\n        profile.update(dash, deviceId, action.dataStream.pin, action.dataStream.pinType, action.value, now);\n    }\n\n    private void execute(Session session, Profile profile, DashBoard dash,\n                         int deviceId, SetPropertyPinAction action, long now) {\n        String body = action.makeHardwareBody();\n        session.sendToApps(SET_WIDGET_PROPERTY, 888, dash.id, deviceId, body);\n\n        Widget widget = dash.updateProperty(deviceId, action.dataStream.pin, action.property, action.value);\n        //this is possible case for device selector\n        if (widget == null) {\n            profile.putPinPropertyStorageValue(dash, deviceId,\n                    PinType.VIRTUAL, action.dataStream.pin, action.property, action.value);\n        }\n        dash.updatedAt = now;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/processors/NotificationBase.java",
    "content": "package cc.blynk.server.core.processors;\n\nimport cc.blynk.server.core.protocol.exceptions.QuotaLimitException;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 29.07.15.\n */\npublic abstract class NotificationBase {\n\n    private final long notificationQuotaLimit;\n    private long lastSentTs;\n    public final static QuotaLimitException EXCEPTION_CACHE = new QuotaLimitException(\"Notification limit reached.\");\n\n    public NotificationBase(long defaultNotificationQuotaLimit) {\n        this.notificationQuotaLimit = defaultNotificationQuotaLimit;\n    }\n\n    protected void checkIfNotificationQuotaLimitIsNotReached() {\n        checkIfNotificationQuotaLimitIsNotReached(System.currentTimeMillis());\n    }\n\n    protected void checkIfNotificationQuotaLimitIsNotReached(final long currentTs) {\n        final long timePassedSinceLastMessage = (currentTs - lastSentTs);\n        if (timePassedSinceLastMessage < notificationQuotaLimit) {\n            throw EXCEPTION_CACHE;\n        }\n        this.lastSentTs = currentTs;\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/processors/WebhookProcessor.java",
    "content": "package cc.blynk.server.core.processors;\n\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.DataStream;\nimport cc.blynk.server.core.model.auth.Session;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.widgets.others.webhook.Header;\nimport cc.blynk.server.core.model.widgets.others.webhook.WebHook;\nimport cc.blynk.server.core.protocol.enums.Command;\nimport cc.blynk.server.core.stats.GlobalStats;\nimport cc.blynk.utils.StringUtils;\nimport io.netty.util.CharsetUtil;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\nimport org.asynchttpclient.AsyncCompletionHandler;\nimport org.asynchttpclient.AsyncHttpClient;\nimport org.asynchttpclient.BoundRequestBuilder;\nimport org.asynchttpclient.DefaultAsyncHttpClient;\nimport org.asynchttpclient.HttpResponseBodyPart;\nimport org.asynchttpclient.Response;\n\nimport java.time.Instant;\nimport java.util.regex.Matcher;\n\nimport static cc.blynk.server.core.protocol.enums.Command.WEB_HOOKS;\nimport static cc.blynk.utils.StringUtils.DATETIME_PATTERN;\nimport static cc.blynk.utils.StringUtils.DEVICE_OWNER_EMAIL;\nimport static cc.blynk.utils.StringUtils.GENERIC_PLACEHOLDER;\nimport static cc.blynk.utils.StringUtils.PIN_PATTERN;\nimport static cc.blynk.utils.StringUtils.PIN_PATTERN_0;\nimport static cc.blynk.utils.StringUtils.PIN_PATTERN_1;\nimport static cc.blynk.utils.StringUtils.PIN_PATTERN_2;\nimport static cc.blynk.utils.StringUtils.PIN_PATTERN_3;\nimport static cc.blynk.utils.StringUtils.PIN_PATTERN_4;\nimport static cc.blynk.utils.StringUtils.PIN_PATTERN_5;\nimport static cc.blynk.utils.StringUtils.PIN_PATTERN_6;\nimport static cc.blynk.utils.StringUtils.PIN_PATTERN_7;\nimport static cc.blynk.utils.StringUtils.PIN_PATTERN_8;\nimport static cc.blynk.utils.StringUtils.PIN_PATTERN_9;\n\n/**\n * Handles all webhooks logic.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 05.09.16.\n */\npublic class WebhookProcessor extends NotificationBase {\n\n    private static final Logger log = LogManager.getLogger(WebhookProcessor.class);\n    private static final String CONTENT_TYPE = \"Content-Type\";\n\n    private final AsyncHttpClient httpclient;\n    private final GlobalStats globalStats;\n    private final int responseSizeLimit;\n    private final String email;\n    private final int webhookFailureLimit;\n\n    public WebhookProcessor(DefaultAsyncHttpClient httpclient,\n                            long quotaFrequencyLimit,\n                            int responseSizeLimit,\n                            int failureLimit,\n                            GlobalStats stats, String email) {\n        super(quotaFrequencyLimit);\n        this.httpclient = httpclient;\n        this.globalStats = stats;\n        this.responseSizeLimit = responseSizeLimit;\n        this.email = email;\n        this.webhookFailureLimit = failureLimit;\n    }\n\n    public void process(User user, Session session, DashBoard dash, int deviceId, short pin,\n                        PinType pinType, String triggerValue, long now) {\n        WebHook webhook = dash.findWebhookByPin(deviceId, pin, pinType);\n        if (webhook == null) {\n            return;\n        }\n\n        checkIfNotificationQuotaLimitIsNotReached(now);\n\n        if (webhook.isNotFailed(webhookFailureLimit) && webhook.url != null) {\n            process(user, session, dash.id, deviceId, webhook, triggerValue);\n        }\n    }\n\n    private void process(User user, Session session, int dashId, int deviceId,  WebHook webHook, String triggerValue) {\n        String newUrl = format(webHook.url, triggerValue, user.email);\n\n        if (!WebHook.isValidUrl(newUrl)) {\n            return;\n        }\n\n        BoundRequestBuilder builder;\n        try {\n            builder = httpclient.prepare(webHook.method.name(), newUrl);\n        } catch (NumberFormatException nfe) {\n            //this is known possible error due to malformed input\n            //https://github.com/blynkkk/blynk-server/issues/1001\n            log.debug(\"Error during webhook initialization.\", nfe);\n            return;\n        } catch (IllegalArgumentException iae) {\n            String error = iae.getMessage();\n            if (error != null && error.contains(\"missing scheme\")) {\n                //this is known possible error due to malformed input\n                log.debug(\"Error during webhook initialization.\", iae);\n                return;\n            } else {\n                throw iae;\n            }\n        }\n\n        if (webHook.headers != null) {\n            for (Header header : webHook.headers) {\n                if (header.isValid()) {\n                    builder.setHeader(header.name, header.value);\n                    if (webHook.body != null && !webHook.body.isEmpty()) {\n                        if (CONTENT_TYPE.equals(header.name)) {\n                            String newBody = format(webHook.body, triggerValue, user.email);\n                            log.trace(\"Webhook formatted body : {}\", newBody);\n                            builder.setBody(newBody);\n                        }\n                    }\n                }\n            }\n        }\n\n        log.trace(\"Sending webhook. {}\", webHook);\n        builder.execute(new AsyncCompletionHandler<Response>() {\n\n            private int length = 0;\n\n            @Override\n            public State onBodyPartReceived(HttpResponseBodyPart content) throws Exception {\n                length += content.length();\n\n                if (length > responseSizeLimit) {\n                    log.warn(\"Response from webhook is too big for {}. Skipping. Size : {}\", email, length);\n                    return State.ABORT;\n                }\n                return super.onBodyPartReceived(content);\n            }\n\n            @Override\n            public Response onCompleted(Response response) {\n                if (isValidResponseCode(response.getStatusCode())) {\n                    webHook.failureCounter = 0;\n                    if (response.hasResponseBody()) {\n                        //todo could be optimized with response.getResponseBodyAsByteBuffer()\n                        String body = DataStream.makeHardwareBody(webHook.pinType, webHook.pin,\n                                response.getResponseBody(CharsetUtil.UTF_8));\n                        log.trace(\"Sending webhook to hardware. {}\", body);\n                        session.sendMessageToHardware(dashId, Command.HARDWARE, 888, body, deviceId);\n                    }\n                } else {\n                    webHook.failureCounter++;\n                    log.debug(\"Error sending webhook for {}. Code {}.\", email, response.getStatusCode());\n                    if (log.isDebugEnabled()) {\n                        log.debug(\"Reason {}\", response.getResponseBody());\n                    }\n                }\n\n                return null;\n            }\n\n            @Override\n            public void onThrowable(Throwable t) {\n                webHook.failureCounter++;\n                log.debug(\"Error sending webhook for {}.\", email);\n                if (log.isDebugEnabled()) {\n                    log.debug(\"Reason {}\", t.getMessage());\n                }\n            }\n        });\n        globalStats.mark(WEB_HOOKS);\n    }\n\n    private static boolean isValidResponseCode(int responseCode) {\n        switch (responseCode) {\n            case 200:\n            case 201:\n            case 202:\n            case 204:\n            case 302:\n                return true;\n            default:\n                return false;\n        }\n    }\n\n    private static String format(String data, String triggerValue, String ownerEmail) {\n        //this is an ugly hack to make it work with Blynk HTTP API.\n        String quotedValue = Matcher.quoteReplacement(triggerValue);\n        data = PIN_PATTERN.matcher(data).replaceFirst(quotedValue);\n\n        String[] splitted = quotedValue.split(StringUtils.BODY_SEPARATOR_STRING);\n        switch (splitted.length) {\n            case 10 :\n                data = PIN_PATTERN_9.matcher(data).replaceFirst(splitted[9]);\n            case 9 :\n                data = PIN_PATTERN_8.matcher(data).replaceFirst(splitted[8]);\n            case 8 :\n                data = PIN_PATTERN_7.matcher(data).replaceFirst(splitted[7]);\n            case 7 :\n                data = PIN_PATTERN_6.matcher(data).replaceFirst(splitted[6]);\n            case 6 :\n                data = PIN_PATTERN_5.matcher(data).replaceFirst(splitted[5]);\n            case 5 :\n                data = PIN_PATTERN_4.matcher(data).replaceFirst(splitted[4]);\n            case 4 :\n                data = PIN_PATTERN_3.matcher(data).replaceFirst(splitted[3]);\n            case 3 :\n                data = PIN_PATTERN_2.matcher(data).replaceFirst(splitted[2]);\n            case 2 :\n                data = PIN_PATTERN_1.matcher(data).replaceFirst(splitted[1]);\n            case 1 :\n                data = PIN_PATTERN_0.matcher(data).replaceFirst(splitted[0]);\n            default :\n                data = GENERIC_PLACEHOLDER.matcher(data).replaceFirst(quotedValue);\n                data = DATETIME_PATTERN.matcher(data).replaceFirst(Instant.now().toString());\n                data = DEVICE_OWNER_EMAIL.matcher(data).replaceFirst(ownerEmail);\n        }\n        return data;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/protocol/enums/Command.java",
    "content": "package cc.blynk.server.core.protocol.enums;\n\nimport java.util.Map;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n */\npublic final class Command {\n\n    public static final short RESPONSE = 0;\n\n    //app commands\n    public static final short REGISTER = 1;\n    public static final short LOGIN = 2;\n    public static final short REDEEM = 3;\n    public static final short HARDWARE_CONNECTED = 4;\n\n    public static final short PING = 6;\n    public static final short ACTIVATE_DASHBOARD = 7;\n    public static final short DEACTIVATE_DASHBOARD = 8;\n    public static final short REFRESH_TOKEN = 9;\n    //HARDWARE commands\n    public static final short TWEET = 12;\n    public static final short EMAIL = 13;\n    public static final short PUSH_NOTIFICATION = 14;\n    public static final short BRIDGE = 15;\n    public static final short HARDWARE_SYNC = 16;\n    public static final short BLYNK_INTERNAL = 17;\n    public static final short SMS = 18;\n    public static final short SET_WIDGET_PROPERTY = 19;\n    public static final short HARDWARE = 20;\n    //app commands\n    public static final short CREATE_DASH = 21;\n    public static final short UPDATE_DASH = 22;\n    public static final short DELETE_DASH = 23;\n    public static final short LOAD_PROFILE_GZIPPED = 24;\n    public static final short APP_SYNC = 25;\n    public static final short SHARING = 26;\n    public static final short ADD_PUSH_TOKEN = 27;\n    public static final short EXPORT_GRAPH_DATA = 28;\n\n    public static final short HARDWARE_LOGIN = 29;\n    //app sharing commands\n    public static final short GET_SHARE_TOKEN = 30;\n    public static final short REFRESH_SHARE_TOKEN = 31;\n    public static final short SHARE_LOGIN = 32;\n    //app commands\n    public static final short CREATE_WIDGET = 33;\n    public static final short UPDATE_WIDGET = 34;\n    public static final short DELETE_WIDGET = 35;\n\n    //energy commands\n    public static final short GET_ENERGY = 36;\n    public static final short ADD_ENERGY = 37;\n\n    public static final short UPDATE_PROJECT_SETTINGS = 38;\n\n    public static final short ASSIGN_TOKEN = 39;\n\n    public static final short GET_SERVER = 40;\n    public static final short CONNECT_REDIRECT = 41;\n\n    public static final short CREATE_DEVICE = 42;\n    public static final short UPDATE_DEVICE = 43;\n    public static final short DELETE_DEVICE = 44;\n    public static final short GET_DEVICES = 45;\n\n    public static final short CREATE_TAG = 46;\n    public static final short UPDATE_TAG = 47;\n    public static final short DELETE_TAG = 48;\n    public static final short GET_TAGS = 49;\n    public static final short MOBILE_GET_DEVICE = 50;\n\n    public static final short UPDATE_FACE = 51;\n\n    //------------------------------------------\n\n    //web sockets\n    public static final short WEB_SOCKETS = 52;\n\n    public static final short EVENTOR = 53;\n    public static final short WEB_HOOKS = 54;\n\n    public static final short CREATE_APP = 55;\n    public static final short UPDATE_APP = 56;\n    public static final short DELETE_APP = 57;\n    public static final short GET_PROJECT_BY_TOKEN = 58;\n    public static final short EMAIL_QR = 59;\n    public static final short GET_ENHANCED_GRAPH_DATA = 60;\n    public static final short DELETE_ENHANCED_GRAPH_DATA = 61;\n\n    public static final short GET_CLONE_CODE = 62;\n    public static final short GET_PROJECT_BY_CLONE_CODE = 63;\n\n    public static final short HARDWARE_LOG_EVENT = 64;\n    public static final short HARDWARE_RESEND_FROM_BLUETOOTH = 65;\n    public static final short LOGOUT = 66;\n\n    public static final short CREATE_TILE_TEMPLATE = 67;\n    public static final short UPDATE_TILE_TEMPLATE = 68;\n    public static final short DELETE_TILE_TEMPLATE = 69;\n    public static final short GET_WIDGET = 70;\n    public static final short DEVICE_OFFLINE = 71;\n    public static final short OUTDATED_APP_NOTIFICATION = 72;\n    public static final short TRACK_DEVICE = 73;\n    public static final short GET_PROVISION_TOKEN = 74;\n    public static final short RESOLVE_EVENT = 75;\n    public static final short DELETE_DEVICE_DATA = 76;\n\n    public static final short CREATE_REPORT = 77;\n    public static final short UPDATE_REPORT = 78;\n    public static final short DELETE_REPORT = 79;\n    public static final short EXPORT_REPORT = 80;\n\n    public static final short RESET_PASSWORD = 81;\n\n    //http codes. Used only for stats\n    public static final short HTTP_IS_HARDWARE_CONNECTED = 82;\n    public static final short HTTP_IS_APP_CONNECTED = 83;\n    public static final short HTTP_GET_PIN_DATA = 84;\n    public static final short HTTP_UPDATE_PIN_DATA = 85;\n    public static final short HTTP_NOTIFY = 86;\n    public static final short HTTP_EMAIL = 87;\n    public static final short HTTP_GET_PROJECT = 88;\n    public static final short HTTP_QR = 89;\n    public static final short HTTP_GET_HISTORY_DATA = 90;\n    public static final short HTTP_START_OTA = 91;\n    public static final short HTTP_STOP_OTA = 92;\n    public static final short HTTP_CLONE = 93;\n    public static final short HTTP_TOTAL = 94;\n\n    //right now we have less than 100 commands\n    public static final int LAST_COMMAND_INDEX = 100;\n\n    private Command() {\n    }\n\n    //all this code just to make logging more user-friendly\n    public static final Map<Short, String> VALUES_NAME = Map.ofEntries(\n            Map.entry(RESPONSE, \"Response\"),\n            Map.entry(REDEEM, \"Redeem\"),\n            Map.entry(HARDWARE_CONNECTED, \"HardwareConnected\"),\n            Map.entry(REGISTER, \"Register\"),\n            Map.entry(LOGIN, \"Login\"),\n            Map.entry(HARDWARE_LOGIN, \"LoginHardware\"),\n            Map.entry(LOGOUT, \"Logout\"),\n            Map.entry(LOAD_PROFILE_GZIPPED, \"LoadProfile\"),\n            Map.entry(APP_SYNC, \"AppSync\"),\n            Map.entry(SHARING, \"Sharing\"),\n            Map.entry(ASSIGN_TOKEN, \"AssignToken\"),\n            Map.entry(PING, \"Ping\"), Map.entry(SMS, \"Sms\"),\n            Map.entry(ACTIVATE_DASHBOARD, \"Activate\"),\n            Map.entry(DEACTIVATE_DASHBOARD, \"Deactivate\"),\n            Map.entry(REFRESH_TOKEN, \"RefreshToken\"),\n            Map.entry(GET_ENHANCED_GRAPH_DATA, \"GetEnhancedGraphDataRequest\"),\n            Map.entry(DELETE_ENHANCED_GRAPH_DATA, \"DeleteEnhancedGraphDataRequest\"),\n            Map.entry(EXPORT_GRAPH_DATA, \"ExportGraphData\"),\n            Map.entry(SET_WIDGET_PROPERTY, \"setWidgetProperty\"),\n            Map.entry(BRIDGE, \"Bridge\"),\n            Map.entry(HARDWARE, \"Hardware\"),\n            Map.entry(GET_SHARE_TOKEN, \"GetShareToken\"),\n            Map.entry(REFRESH_SHARE_TOKEN, \"RefreshShareToken\"),\n            Map.entry(SHARE_LOGIN, \"ShareLogin\"),\n            Map.entry(CREATE_DASH, \"CreateProject\"),\n            Map.entry(UPDATE_DASH, \"UpdateProject\"),\n            Map.entry(DELETE_DASH, \"DeleteProject\"),\n            Map.entry(HARDWARE_SYNC, \"HardwareSync\"),\n            Map.entry(BLYNK_INTERNAL, \"Internal\"),\n            Map.entry(ADD_PUSH_TOKEN, \"AddPushToken\"),\n            Map.entry(TWEET, \"Tweet\"), Map.entry(EMAIL, \"Email\"),\n            Map.entry(PUSH_NOTIFICATION, \"Push\"),\n            Map.entry(CREATE_WIDGET, \"CreateWidget\"),\n            Map.entry(UPDATE_WIDGET, \"UpdateWidget\"),\n            Map.entry(DELETE_WIDGET, \"DeleteWidget\"),\n            Map.entry(GET_WIDGET, \"GetWidget\"),\n            Map.entry(CREATE_TILE_TEMPLATE, \"CreateTileTemplate\"),\n            Map.entry(UPDATE_TILE_TEMPLATE, \"UpdateTileTemplate\"),\n            Map.entry(DELETE_TILE_TEMPLATE, \"DeleteTileTemplate\"),\n            Map.entry(CREATE_DEVICE, \"CreateDevice\"),\n            Map.entry(UPDATE_DEVICE, \"UpdateDevice\"),\n            Map.entry(DELETE_DEVICE, \"DeleteDevice\"),\n            Map.entry(MOBILE_GET_DEVICE, \"GetDevice\"),\n            Map.entry(GET_DEVICES, \"GetDevices\"),\n            Map.entry(ADD_ENERGY, \"AddEnergy\"),\n            Map.entry(GET_ENERGY, \"GetEnergy\"),\n            Map.entry(UPDATE_PROJECT_SETTINGS, \"UpdateProjectSettings\"),\n            Map.entry(GET_SERVER, \"GetServer\"),\n            Map.entry(CONNECT_REDIRECT, \"ConnectRedirect\"),\n            Map.entry(CREATE_APP, \"CreateApp\"),\n            Map.entry(UPDATE_APP, \"UpdateApp\"),\n            Map.entry(DELETE_APP, \"DeleteApp\"),\n            Map.entry(GET_PROJECT_BY_TOKEN, \"GetProjectByToken\"),\n            Map.entry(EMAIL_QR, \"MailQRs\"),\n            Map.entry(UPDATE_FACE, \"UpdateFace\"),\n            Map.entry(GET_PROVISION_TOKEN, \"getProvisionToken\"),\n            Map.entry(RESOLVE_EVENT, \"resolveEvent\"),\n            Map.entry(DELETE_DEVICE_DATA, \"deleteDeviceData\"),\n            Map.entry(HARDWARE_LOG_EVENT, \"HardwareLogEvent\"),\n            Map.entry(HARDWARE_RESEND_FROM_BLUETOOTH, \"HardwareResendFromBluetooth\"),\n            Map.entry(GET_CLONE_CODE, \"GetCloneCode\"),\n            Map.entry(GET_PROJECT_BY_CLONE_CODE, \"GetProjectByCloneCode\"),\n            Map.entry(DEVICE_OFFLINE, \"deviceOffline\"),\n            Map.entry(OUTDATED_APP_NOTIFICATION, \"outdatedAppNotification\"),\n            Map.entry(TRACK_DEVICE, \"trackDevice\"),\n            Map.entry(CREATE_REPORT, \"createReport\"),\n            Map.entry(UPDATE_REPORT, \"updateReport\"),\n            Map.entry(DELETE_REPORT, \"deleteReport\"),\n            Map.entry(EXPORT_REPORT, \"exportReport\"),\n            Map.entry(RESET_PASSWORD, \"resetPass\"),\n            Map.entry(HTTP_IS_HARDWARE_CONNECTED, \"HttpIsHardwareConnected\"),\n            Map.entry(HTTP_IS_APP_CONNECTED, \"HttpIsAppConnected\"),\n            Map.entry(HTTP_GET_PIN_DATA, \"HttpGetPinData\"),\n            Map.entry(HTTP_UPDATE_PIN_DATA, \"HttpUpdatePinData\"),\n            Map.entry(HTTP_NOTIFY, \"HttpNotify\"),\n            Map.entry(HTTP_EMAIL, \"HttpEmail\"),\n            Map.entry(HTTP_GET_PROJECT, \"HttpGetProject\"),\n            Map.entry(HTTP_QR, \"QR\"),\n            Map.entry(HTTP_CLONE, \"Clone\"),\n            Map.entry(HTTP_GET_HISTORY_DATA, \"HttpGetHistoryData\"),\n            Map.entry(HTTP_START_OTA, \"HttpStartOTA\"),\n            Map.entry(HTTP_TOTAL, \"HttpTotal\"),\n            Map.entry(WEB_SOCKETS, \"WebSockets\"),\n            Map.entry(EVENTOR, \"Eventor\"),\n            Map.entry(WEB_HOOKS, \"WebHooks\")\n    );\n\n    public static String getNameByValue(short val) {\n        return VALUES_NAME.get(val);\n    }\n    //--------------------------------------------------------\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/protocol/enums/Response.java",
    "content": "package cc.blynk.server.core.protocol.enums;\n\nimport cc.blynk.utils.ReflectionUtil;\n\nimport java.util.Map;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n */\npublic final class Response {\n\n    public static final int OK = 200;\n    public static final int QUOTA_LIMIT = 1;\n    public static final int ILLEGAL_COMMAND = 2;\n    public static final int USER_NOT_REGISTERED = 3;\n    public static final int USER_ALREADY_REGISTERED = 4;\n    public static final int USER_NOT_AUTHENTICATED = 5;\n    public static final int NOT_ALLOWED = 6;\n    public static final int DEVICE_NOT_IN_NETWORK = 7;\n    public static final int NO_ACTIVE_DASHBOARD = 8;\n    public static final int INVALID_TOKEN = 9;\n    public static final int ILLEGAL_COMMAND_BODY = 11;\n\n    public static final int NOTIFICATION_INVALID_BODY = 13;\n    public static final int NOTIFICATION_NOT_AUTHORIZED = 14;\n    public static final int NOTIFICATION_ERROR = 15;\n\n    public static final int NO_DATA = 17;\n    public static final int SERVER_ERROR = 19;\n    public static final int ENERGY_LIMIT = 21;\n    public static final int FACEBOOK_USER_LOGIN_WITH_PASS = 22;\n\n    //all this code just to make logging more user-friendly\n    private final static Map<Integer, String> valuesName = ReflectionUtil.generateMapOfValueNameInteger(Response.class);\n\n    private Response() {\n    }\n\n    public static String getNameByValue(int val) {\n        return valuesName.get(val);\n    }\n    //--------------------------------------------------------\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/protocol/exceptions/BaseServerException.java",
    "content": "package cc.blynk.server.core.protocol.exceptions;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/3/2015.\n */\npublic class BaseServerException extends RuntimeException {\n\n    public final int msgId;\n    public final int errorCode;\n\n    /**\n     * Should be used for handlers that are not processed within BaseSimpleChannelInboundHandler\n     */\n    BaseServerException(String message, int msgId, int errorCode) {\n        super(message);\n        this.msgId = msgId;\n        this.errorCode = errorCode;\n    }\n\n    BaseServerException(String message, int errorCode) {\n        super(message);\n        this.msgId = -1;\n        this.errorCode = errorCode;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/protocol/exceptions/IllegalCommandBodyException.java",
    "content": "package cc.blynk.server.core.protocol.exceptions;\n\nimport cc.blynk.server.core.protocol.enums.Response;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/3/2015.\n */\npublic class IllegalCommandBodyException extends BaseServerException {\n\n    public IllegalCommandBodyException(String message, int msgId) {\n        super(message, msgId, Response.ILLEGAL_COMMAND_BODY);\n    }\n\n    public IllegalCommandBodyException(String message) {\n        super(message, Response.ILLEGAL_COMMAND_BODY);\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/protocol/exceptions/IllegalCommandException.java",
    "content": "package cc.blynk.server.core.protocol.exceptions;\n\nimport cc.blynk.server.core.protocol.enums.Response;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/3/2015.\n */\npublic class IllegalCommandException extends BaseServerException {\n\n    public IllegalCommandException(String message) {\n        super(message, Response.ILLEGAL_COMMAND);\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/protocol/exceptions/NoDataException.java",
    "content": "package cc.blynk.server.core.protocol.exceptions;\n\n/**\n * This exception doesn't inherit BaseServerException\n * as it is only used as marker.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/3/2015.\n */\npublic class NoDataException extends Exception {\n\n    public NoDataException() {\n        super(\"No data.\");\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/protocol/exceptions/NotAllowedException.java",
    "content": "package cc.blynk.server.core.protocol.exceptions;\n\nimport cc.blynk.server.core.protocol.enums.Response;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/23/2015.\n */\npublic class NotAllowedException extends BaseServerException {\n\n    public NotAllowedException(String message, int msgId) {\n        super(message, msgId, Response.NOT_ALLOWED);\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/protocol/exceptions/QuotaLimitException.java",
    "content": "package cc.blynk.server.core.protocol.exceptions;\n\nimport cc.blynk.server.core.protocol.enums.Response;\n\npublic class QuotaLimitException extends BaseServerException {\n\n    public QuotaLimitException(String message) {\n        super(message, Response.QUOTA_LIMIT);\n    }\n\n    public QuotaLimitException(String message, int msgId) {\n        super(message, msgId, Response.QUOTA_LIMIT);\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/protocol/exceptions/UnsupportedCommandException.java",
    "content": "package cc.blynk.server.core.protocol.exceptions;\n\nimport cc.blynk.server.core.protocol.enums.Response;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/3/2015.\n */\npublic class UnsupportedCommandException extends BaseServerException {\n\n    public UnsupportedCommandException(String message, int msgId) {\n        super(message, msgId, Response.ILLEGAL_COMMAND);\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/protocol/handlers/DefaultExceptionHandler.java",
    "content": "package cc.blynk.server.core.protocol.handlers;\n\nimport cc.blynk.server.core.protocol.exceptions.BaseServerException;\nimport cc.blynk.server.core.protocol.exceptions.UnsupportedCommandException;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.handler.codec.DecoderException;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport javax.net.ssl.SSLException;\nimport java.io.IOException;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeResponse;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/11/2015.\n */\npublic abstract class DefaultExceptionHandler {\n\n    private final static Logger log = LogManager.getLogger(DefaultExceptionHandler.class);\n\n    public static void handleBaseServerException(ChannelHandlerContext ctx,\n                                           BaseServerException baseServerException, int msgId) {\n        log.debug(baseServerException.getMessage());\n        if (ctx.channel().isWritable()) {\n            ctx.writeAndFlush(makeResponse(msgId, baseServerException.errorCode), ctx.voidPromise());\n        }\n    }\n\n    public static void handleGeneralException(ChannelHandlerContext ctx, Throwable cause) {\n        if (cause instanceof BaseServerException) {\n            BaseServerException baseServerException = (BaseServerException) cause;\n            handleBaseServerException(ctx, baseServerException, baseServerException.msgId);\n        } else {\n            handleUnexpectedException(ctx, cause);\n        }\n    }\n\n    public static void handleUnexpectedException(ChannelHandlerContext ctx, Throwable cause) {\n        if (cause instanceof DecoderException) {\n            Throwable t = cause.getCause();\n            if (t instanceof UnsupportedCommandException) {\n                log.debug(\"Input command is invalid. Closing socket. Reason {}. Address {}\",\n                        cause.getMessage(), ctx.channel().remoteAddress());\n            } else if (t instanceof SSLException) {\n                log.debug(\"Unsecured connection attempt or not supported protocol. Channel : {}. Reason : {}\",\n                        ctx.channel().remoteAddress(), cause.getMessage());\n            } else {\n                log.error(\"DecoderException. Pipeline : {}.\", ctx.pipeline().names(), cause);\n            }\n            ctx.close();\n        } else if (cause instanceof SSLException) {\n            log.debug(\"SSL exception. {}. {}\", cause.getMessage(), ctx.channel().remoteAddress());\n            ctx.close();\n        } else if (cause instanceof IOException) {\n            log.trace(\"Blynk server IOException.\", cause);\n        } else {\n            String message = cause == null ? \"\" : cause.getMessage();\n            if (message != null && message.contains(\"OutOfDirectMemoryError\")) {\n                log.error(\"OutOfDirectMemoryError!!!\");\n            } else {\n                log.error(\"Unexpected error! Handler class : {}. Name : {}. Reason : {}. Channel : {}.\",\n                        ctx.handler().getClass(), ctx.name(), message, ctx.channel());\n                //additional logging for rare NPE.\n                if (message == null) {\n                    log.error(\"Error.\", cause);\n                } else {\n                    log.debug(cause);\n                }\n            }\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/protocol/handlers/decoders/MessageDecoder.java",
    "content": "package cc.blynk.server.core.protocol.handlers.decoders;\n\nimport cc.blynk.server.Limits;\nimport cc.blynk.server.core.protocol.enums.Command;\nimport cc.blynk.server.core.protocol.model.messages.MessageBase;\nimport cc.blynk.server.core.protocol.model.messages.ResponseMessage;\nimport cc.blynk.server.core.stats.GlobalStats;\nimport cc.blynk.server.core.stats.metrics.InstanceLoadMeter;\nimport cc.blynk.server.internal.QuotaLimitChecker;\nimport io.netty.buffer.ByteBuf;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.handler.codec.ByteToMessageDecoder;\nimport io.netty.util.CharsetUtil;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.util.List;\n\nimport static cc.blynk.server.core.protocol.model.messages.MessageFactory.produce;\n\n/**\n * Decodes input byte array into java message.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n */\npublic class MessageDecoder extends ByteToMessageDecoder {\n\n    private static final Logger log = LogManager.getLogger(MessageDecoder.class);\n\n    private final GlobalStats stats;\n    private final QuotaLimitChecker limitChecker;\n\n    public MessageDecoder(GlobalStats stats, Limits limits) {\n        this.stats = stats;\n        this.limitChecker = new QuotaLimitChecker(limits.userQuotaLimit);\n    }\n\n    @Override\n    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {\n        if (in.readableBytes() < 5) {\n            return;\n        }\n\n        in.markReaderIndex();\n\n        short command = in.readUnsignedByte();\n        int messageId = in.readUnsignedShort();\n        int codeOrLength = in.readUnsignedShort();\n\n        if (limitChecker.quotaReached(ctx, messageId)) {\n            return;\n        }\n\n        MessageBase message;\n        if (command == Command.RESPONSE) {\n            message = new ResponseMessage(messageId, codeOrLength);\n        } else {\n            if (in.readableBytes() < codeOrLength) {\n                in.resetReaderIndex();\n                return;\n            }\n\n            message = produce(messageId, command, (String) in.readCharSequence(codeOrLength, CharsetUtil.UTF_8));\n        }\n\n        log.trace(\"Incoming {}\", message);\n\n        stats.mark(command);\n\n        out.add(message);\n    }\n\n    public InstanceLoadMeter getQuotaMeter() {\n        return limitChecker.quotaMeter;\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/protocol/handlers/decoders/MobileMessageDecoder.java",
    "content": "package cc.blynk.server.core.protocol.handlers.decoders;\n\nimport cc.blynk.server.Limits;\nimport cc.blynk.server.core.protocol.enums.Command;\nimport cc.blynk.server.core.protocol.exceptions.UnsupportedCommandException;\nimport cc.blynk.server.core.protocol.model.messages.MessageBase;\nimport cc.blynk.server.core.protocol.model.messages.ResponseMessage;\nimport cc.blynk.server.core.stats.GlobalStats;\nimport cc.blynk.server.core.stats.metrics.InstanceLoadMeter;\nimport cc.blynk.server.internal.QuotaLimitChecker;\nimport io.netty.buffer.ByteBuf;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.handler.codec.ByteToMessageDecoder;\nimport io.netty.handler.codec.DecoderException;\nimport io.netty.util.CharsetUtil;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.util.List;\n\nimport static cc.blynk.server.core.protocol.model.messages.MessageFactory.produce;\n\n/**\n * Decodes input byte array into java message.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 18/1/2018.\n */\npublic class MobileMessageDecoder extends ByteToMessageDecoder {\n\n    private static final Logger log = LogManager.getLogger(MobileMessageDecoder.class);\n\n    private final GlobalStats stats;\n    public static final int PROTOCOL_APP_HEADER_SIZE = 7;\n    private static final DecoderException decoderException =\n            new DecoderException(new UnsupportedCommandException(\"Length field is wrong.\", 1));\n    private final QuotaLimitChecker limitChecker;\n\n    public MobileMessageDecoder(GlobalStats stats, Limits limits) {\n        this.stats = stats;\n        this.limitChecker = new QuotaLimitChecker(limits.userQuotaLimit);\n    }\n\n    @Override\n    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {\n        if (in.readableBytes() < PROTOCOL_APP_HEADER_SIZE) {\n            return;\n        }\n\n        in.markReaderIndex();\n\n        short command = in.readUnsignedByte();\n        int messageId = in.readUnsignedShort();\n        //actually here should be long. but we do not expect this number to be large\n        //so it should perfectly fit int\n        int codeOrLength = (int) in.readUnsignedInt();\n\n        if (limitChecker.quotaReached(ctx, messageId)) {\n            return;\n        }\n\n        MessageBase message;\n        if (command == Command.RESPONSE) {\n            message = new ResponseMessage(messageId, codeOrLength);\n        } else {\n            if (in.readableBytes() < codeOrLength) {\n                in.resetReaderIndex();\n                return;\n            }\n\n            validateLength(codeOrLength);\n\n            message = produce(messageId, command, (String) in.readCharSequence(codeOrLength, CharsetUtil.UTF_8));\n        }\n\n        log.trace(\"Incoming {}\", message);\n\n        stats.mark(command);\n\n        out.add(message);\n    }\n\n    private static void validateLength(int length) {\n        if (length < 0 || length > 10_000_000) {\n            throw decoderException;\n        }\n    }\n\n    public InstanceLoadMeter getQuotaMeter() {\n        return limitChecker.quotaMeter;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/protocol/handlers/decoders/WSMessageDecoder.java",
    "content": "package cc.blynk.server.core.protocol.handlers.decoders;\n\nimport cc.blynk.server.Limits;\nimport cc.blynk.server.core.protocol.enums.Command;\nimport cc.blynk.server.core.protocol.model.messages.MessageBase;\nimport cc.blynk.server.core.protocol.model.messages.ResponseMessage;\nimport cc.blynk.server.core.stats.GlobalStats;\nimport cc.blynk.server.core.stats.metrics.InstanceLoadMeter;\nimport cc.blynk.server.internal.QuotaLimitChecker;\nimport io.netty.buffer.ByteBuf;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.ChannelInboundHandlerAdapter;\nimport io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;\nimport io.netty.handler.codec.http.websocketx.WebSocketHandshakeException;\nimport io.netty.util.ReferenceCountUtil;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.core.protocol.model.messages.MessageFactory.produce;\nimport static java.nio.charset.StandardCharsets.UTF_8;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 11.01.16.\n */\npublic class WSMessageDecoder extends ChannelInboundHandlerAdapter {\n\n    private static final Logger log = LogManager.getLogger(WSMessageDecoder.class);\n\n    private final GlobalStats stats;\n    private final QuotaLimitChecker limitChecker;\n\n    public WSMessageDecoder(GlobalStats globalStats, Limits limits) {\n        this.stats = globalStats;\n        this.limitChecker = new QuotaLimitChecker(limits.userQuotaLimit);\n    }\n\n    @Override\n    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {\n        log.debug(\"In webappdecoder. {}\", msg);\n        if (msg instanceof BinaryWebSocketFrame) {\n            try {\n                ByteBuf in = ((BinaryWebSocketFrame) msg).content();\n\n                short command = in.readUnsignedByte();\n                int messageId = in.readUnsignedShort();\n\n                if (limitChecker.quotaReached(ctx, messageId)) {\n                    return;\n                }\n\n                MessageBase message;\n                if (command == Command.RESPONSE) {\n                    message = new ResponseMessage(messageId, (int) in.readUnsignedInt());\n                } else {\n                    int codeOrLength = in.capacity() - 3;\n                    message = produce(messageId, command, (String) in.readCharSequence(codeOrLength, UTF_8));\n                }\n\n                log.trace(\"Incoming websocket msg {}\", message);\n                stats.markWithoutGlobal(Command.WEB_SOCKETS);\n                ctx.fireChannelRead(message);\n            } finally {\n                ReferenceCountUtil.release(msg);\n            }\n        } else {\n            super.channelRead(ctx, msg);\n        }\n    }\n\n    @Override\n    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {\n        if (cause instanceof WebSocketHandshakeException) {\n            log.debug(\"Web Socket Handshake Exception.\", cause);\n        }\n    }\n\n    public InstanceLoadMeter getQuotaMeter() {\n        return limitChecker.quotaMeter;\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/protocol/handlers/encoders/MessageEncoder.java",
    "content": "package cc.blynk.server.core.protocol.handlers.encoders;\n\nimport cc.blynk.server.core.protocol.model.messages.MessageBase;\nimport cc.blynk.server.core.protocol.model.messages.ResponseMessage;\nimport cc.blynk.server.core.stats.GlobalStats;\nimport io.netty.buffer.ByteBuf;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.handler.codec.MessageToByteEncoder;\n\n/**\n * Encodes java message into a bytes array.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n */\npublic class MessageEncoder extends MessageToByteEncoder<MessageBase> {\n\n    private final GlobalStats stats;\n\n    public MessageEncoder(GlobalStats stats) {\n        this.stats = stats;\n    }\n\n    @Override\n    protected void encode(ChannelHandlerContext ctx, MessageBase message, ByteBuf out) {\n        out.writeByte(message.command);\n        out.writeShort(message.id);\n\n        if (message instanceof ResponseMessage) {\n            out.writeShort(((ResponseMessage) message).code);\n        } else {\n            stats.mark(message.command);\n\n            byte[] body = message.getBytes();\n            out.writeShort(body.length);\n            if (body.length > 0) {\n                out.writeBytes(body);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/protocol/handlers/encoders/MobileMessageEncoder.java",
    "content": "package cc.blynk.server.core.protocol.handlers.encoders;\n\nimport cc.blynk.server.core.protocol.model.messages.MessageBase;\nimport cc.blynk.server.core.protocol.model.messages.ResponseMessage;\nimport cc.blynk.server.core.stats.GlobalStats;\nimport io.netty.buffer.ByteBuf;\nimport io.netty.channel.ChannelHandler;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.handler.codec.MessageToByteEncoder;\n\n/**\n * Encodes java message into a bytes array.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 18/1/2018.\n */\n@ChannelHandler.Sharable\npublic class MobileMessageEncoder extends MessageToByteEncoder<MessageBase> {\n\n    private final GlobalStats stats;\n\n    public MobileMessageEncoder(GlobalStats stats) {\n        this.stats = stats;\n    }\n\n    @Override\n    protected void encode(ChannelHandlerContext ctx, MessageBase message, ByteBuf out) {\n        out.writeByte(message.command);\n        out.writeShort(message.id);\n\n        if (message instanceof ResponseMessage) {\n            out.writeInt(((ResponseMessage) message).code);\n        } else {\n            stats.mark(message.command);\n\n            byte[] body = message.getBytes();\n            out.writeInt(body.length);\n            if (body.length > 0) {\n                out.writeBytes(body);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/protocol/handlers/encoders/WSMessageEncoder.java",
    "content": "package cc.blynk.server.core.protocol.handlers.encoders;\n\nimport cc.blynk.server.core.protocol.model.messages.MessageBase;\nimport cc.blynk.server.core.protocol.model.messages.ResponseMessage;\nimport io.netty.buffer.ByteBuf;\nimport io.netty.buffer.ByteBufAllocator;\nimport io.netty.channel.ChannelHandler;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.ChannelOutboundHandlerAdapter;\nimport io.netty.channel.ChannelPromise;\nimport io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\n/**\n * Just wraps ByteBuf into WebSockets frame.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 15.01.16.\n */\n@ChannelHandler.Sharable\npublic class WSMessageEncoder extends ChannelOutboundHandlerAdapter {\n\n    private static final Logger log = LogManager.getLogger(WSMessageEncoder.class);\n\n    @Override\n    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {\n        log.debug(\"In webapp socket encoder {}\", msg);\n        if (msg instanceof MessageBase) {\n            MessageBase message = (MessageBase) msg;\n            ByteBuf out = ByteBufAllocator.DEFAULT.buffer();\n            out.writeByte(message.command);\n            out.writeShort(message.id);\n\n            if (message instanceof ResponseMessage) {\n                out.writeInt(((ResponseMessage) message).code);\n            } else {\n                byte[] body = message.getBytes();\n                if (body.length > 0) {\n                    out.writeBytes(body);\n                }\n            }\n            super.write(ctx, new BinaryWebSocketFrame(out), promise);\n        } else {\n            super.write(ctx, msg, promise);\n        }\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/protocol/model/messages/BinaryMessage.java",
    "content": "package cc.blynk.server.core.protocol.model.messages;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n */\npublic class BinaryMessage extends MessageBase {\n\n    private final byte[] data;\n\n    public BinaryMessage(int messageId, short command, byte[] data) {\n        super(messageId, command);\n        this.data = data;\n    }\n\n    @Override\n    public byte[] getBytes() {\n        return data;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/protocol/model/messages/MessageBase.java",
    "content": "package cc.blynk.server.core.protocol.model.messages;\n\nimport cc.blynk.server.core.protocol.enums.Command;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n * Yes, I don't use getters and setters, inlining is not always works as expected.\n *\n * IMPORTANT : have in mind, in body we retrieve always unsigned bytes, shorts, while in java\n * is only signed types, so we require 2 times larger types.\n */\npublic abstract class MessageBase {\n\n    public final short command;\n\n    public final int id;\n\n    public MessageBase(int id, short command) {\n        this.command = command;\n        this.id = id;\n    }\n\n    public abstract byte[] getBytes();\n\n    @Override\n    public String toString() {\n        return \"id=\" + id\n                + \", command=\" + Command.getNameByValue(command);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n\n        MessageBase that = (MessageBase) o;\n\n        if (command != that.command) {\n            return false;\n        }\n        return id == that.id;\n    }\n\n    @Override\n    public int hashCode() {\n        int result = (int) command;\n        result = 31 * result + id;\n        return result;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/protocol/model/messages/MessageFactory.java",
    "content": "package cc.blynk.server.core.protocol.model.messages;\n\nimport cc.blynk.server.core.protocol.enums.Command;\nimport cc.blynk.server.core.protocol.exceptions.UnsupportedCommandException;\nimport cc.blynk.server.core.protocol.model.messages.appllication.GetServerMessage;\nimport cc.blynk.server.core.protocol.model.messages.appllication.LoginMessage;\nimport cc.blynk.server.core.protocol.model.messages.appllication.RegisterMessage;\nimport cc.blynk.server.core.protocol.model.messages.appllication.ResetPasswordMessage;\nimport cc.blynk.server.core.protocol.model.messages.appllication.sharing.ShareLoginMessage;\nimport cc.blynk.server.core.protocol.model.messages.common.HardwareMessage;\nimport cc.blynk.server.core.protocol.model.messages.hardware.HardwareLoginMessage;\n\nimport static cc.blynk.server.core.protocol.enums.Command.GET_SERVER;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE_LOGIN;\nimport static cc.blynk.server.core.protocol.enums.Command.LOGIN;\nimport static cc.blynk.server.core.protocol.enums.Command.REGISTER;\nimport static cc.blynk.server.core.protocol.enums.Command.RESET_PASSWORD;\nimport static cc.blynk.server.core.protocol.enums.Command.SHARE_LOGIN;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n */\npublic final class MessageFactory {\n\n    private MessageFactory() {\n    }\n\n    public static MessageBase produce(int messageId, short commandId, String body) {\n        switch (commandId) {\n            case REGISTER :\n                return new RegisterMessage(messageId, body);\n            case LOGIN :\n                return new LoginMessage(messageId, body);\n            case HARDWARE_LOGIN :\n                return new HardwareLoginMessage(messageId, body);\n            case SHARE_LOGIN :\n                return new ShareLoginMessage(messageId, body);\n            case HARDWARE :\n                return new HardwareMessage(messageId, body);\n            case GET_SERVER :\n                return new GetServerMessage(messageId, body);\n            case RESET_PASSWORD :\n                return new ResetPasswordMessage(messageId, body);\n            default:\n                if (commandId < Command.LAST_COMMAND_INDEX) {\n                    return new StringMessage(messageId, commandId, body);\n                }\n                throw new UnsupportedCommandException(\"Command not supported. Code : \" + commandId, messageId);\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/protocol/model/messages/ResponseMessage.java",
    "content": "package cc.blynk.server.core.protocol.model.messages;\n\nimport cc.blynk.server.core.protocol.enums.Command;\nimport cc.blynk.server.core.protocol.enums.Response;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n */\npublic class ResponseMessage extends MessageBase {\n\n    public final int code;\n\n    public ResponseMessage(int messageId, int responseCode) {\n        super(messageId, Command.RESPONSE);\n        this.code = responseCode;\n    }\n\n    @Override\n    public byte[] getBytes() {\n        return null;\n    }\n\n    @Override\n    public String toString() {\n        return \"ResponseMessage{id=\" + id\n                + \", command=\" + Command.getNameByValue(command)\n                + \", responseCode=\" + Response.getNameByValue(code) + \"}\";\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (!(o instanceof ResponseMessage)) {\n            return false;\n        }\n        if (!super.equals(o)) {\n            return false;\n        }\n\n        ResponseMessage that = (ResponseMessage) o;\n\n        return code == that.code;\n    }\n\n    @Override\n    public int hashCode() {\n        int result = super.hashCode();\n        result = 31 * result + code;\n        return result;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/protocol/model/messages/StringMessage.java",
    "content": "package cc.blynk.server.core.protocol.model.messages;\n\nimport java.nio.charset.Charset;\nimport java.nio.charset.StandardCharsets;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n */\npublic class StringMessage extends MessageBase {\n\n    public final String body;\n    private final Charset charset;\n\n    public StringMessage(int messageId, short command, String body, Charset charset) {\n        super(messageId, command);\n        this.body = body;\n        this.charset = charset;\n    }\n\n    public StringMessage(int messageId, short command, String body) {\n        this(messageId, command, body, StandardCharsets.UTF_8);\n    }\n\n    @Override\n    public byte[] getBytes() {\n        return body.getBytes(charset);\n    }\n\n    @Override\n    public String toString() {\n        return super.toString() + \", body='\" + body + \"'\";\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n        if (!super.equals(o)) {\n            return false;\n        }\n\n        StringMessage that = (StringMessage) o;\n\n        return !(body != null ? !body.equals(that.body) : that.body != null);\n\n    }\n\n    @Override\n    public int hashCode() {\n        int result = super.hashCode();\n        result = 31 * result + (body != null ? body.hashCode() : 0);\n        return result;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/protocol/model/messages/appllication/GetServerMessage.java",
    "content": "package cc.blynk.server.core.protocol.model.messages.appllication;\n\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\n\nimport static cc.blynk.server.core.protocol.enums.Command.GET_SERVER;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n */\npublic class GetServerMessage extends StringMessage {\n\n    public GetServerMessage(int messageId, String body) {\n        super(messageId, GET_SERVER, body);\n    }\n\n    @Override\n    public String toString() {\n        return \"GetServerMessage{\" + super.toString() + \"}\";\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/protocol/model/messages/appllication/LoginMessage.java",
    "content": "package cc.blynk.server.core.protocol.model.messages.appllication;\n\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\n\nimport static cc.blynk.server.core.protocol.enums.Command.LOGIN;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n */\npublic class LoginMessage extends StringMessage {\n\n    public LoginMessage(int messageId, String body) {\n        super(messageId, LOGIN, body);\n    }\n\n    public LoginMessage(int messageId, short command, String body) {\n        super(messageId, command, body);\n    }\n\n    @Override\n    public String toString() {\n        return \"LoginMessage{\" + super.toString() + \"}\";\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/protocol/model/messages/appllication/RegisterMessage.java",
    "content": "package cc.blynk.server.core.protocol.model.messages.appllication;\n\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\n\nimport static cc.blynk.server.core.protocol.enums.Command.REGISTER;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n */\npublic class RegisterMessage extends StringMessage {\n\n    public RegisterMessage(int messageId, String body) {\n        super(messageId, REGISTER, body);\n    }\n\n    @Override\n    public String toString() {\n        return \"RegisterMessage{\" + super.toString() + \"}\";\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/protocol/model/messages/appllication/ResetPasswordMessage.java",
    "content": "package cc.blynk.server.core.protocol.model.messages.appllication;\n\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\n\nimport static cc.blynk.server.core.protocol.enums.Command.RESET_PASSWORD;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n */\npublic class ResetPasswordMessage extends StringMessage {\n\n    public ResetPasswordMessage(int messageId, String body) {\n        super(messageId, RESET_PASSWORD, body);\n    }\n\n    @Override\n    public String toString() {\n        return \"ResetPasswordMessage{\" + super.toString() + \"}\";\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/protocol/model/messages/appllication/sharing/ShareLoginMessage.java",
    "content": "package cc.blynk.server.core.protocol.model.messages.appllication.sharing;\n\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\n\nimport static cc.blynk.server.core.protocol.enums.Command.SHARE_LOGIN;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n */\npublic class ShareLoginMessage extends StringMessage {\n\n    public ShareLoginMessage(int messageId, String body) {\n        super(messageId, SHARE_LOGIN, body);\n    }\n\n    @Override\n    public String toString() {\n        return \"ShareLoginMessage{\" + super.toString() + \"}\";\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/protocol/model/messages/common/HardwareMessage.java",
    "content": "package cc.blynk.server.core.protocol.model.messages.common;\n\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\n\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n */\npublic class HardwareMessage extends StringMessage {\n\n    public HardwareMessage(int messageId, String body) {\n        super(messageId, HARDWARE, body);\n    }\n\n    @Override\n    public String toString() {\n        return \"HardwareMessage{\" + super.toString() + \"}\";\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/protocol/model/messages/hardware/HardwareLoginMessage.java",
    "content": "package cc.blynk.server.core.protocol.model.messages.hardware;\n\nimport cc.blynk.server.core.protocol.model.messages.appllication.LoginMessage;\n\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE_LOGIN;\n\npublic class HardwareLoginMessage extends LoginMessage {\n\n    public HardwareLoginMessage(int messageId, String body) {\n        super(messageId, HARDWARE_LOGIN, body);\n    }\n\n    @Override\n    public String toString() {\n        return \"HardwareLoginMessage{\" + super.toString() + \"}\";\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/reporting/GraphPinRequest.java",
    "content": "package cc.blynk.server.core.reporting;\n\nimport cc.blynk.server.core.model.DataStream;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.widgets.outputs.graph.AggregationFunctionType;\nimport cc.blynk.server.core.model.widgets.outputs.graph.GraphGranularityType;\nimport cc.blynk.server.core.model.widgets.outputs.graph.GraphPeriod;\n\nimport java.util.Arrays;\n\nimport static cc.blynk.server.core.model.widgets.outputs.graph.GraphPeriod.LIVE;\nimport static cc.blynk.server.internal.EmptyArraysUtil.EMPTY_INTS;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 23.10.15.\n */\npublic class GraphPinRequest {\n\n    public final int dashId;\n\n    public final int deviceId;\n\n    public final int[] deviceIds;\n\n    public final boolean isTag;\n\n    public final PinType pinType;\n\n    public final short pin;\n\n    private final GraphPeriod graphPeriod;\n\n    public final AggregationFunctionType functionType;\n\n    public final int count;\n\n    public final GraphGranularityType type;\n\n    public final int skipCount;\n\n    public GraphPinRequest(int dashId, int[] deviceIds, DataStream dataStream,\n                           GraphPeriod graphPeriod, int skipCount, AggregationFunctionType function) {\n        this.dashId = dashId;\n        this.deviceId = -1;\n        this.deviceIds = deviceIds == null ? EMPTY_INTS : deviceIds;\n        this.isTag = true;\n        if (dataStream == null) {\n            this.pinType = PinType.VIRTUAL;\n            this.pin = (short) DataStream.NO_PIN;\n        } else {\n            this.pinType = (dataStream.pinType == null ? PinType.VIRTUAL : dataStream.pinType);\n            this.pin = dataStream.pin;\n        }\n        this.graphPeriod = graphPeriod;\n        this.functionType = function;\n        this.count = graphPeriod.numberOfPoints;\n        this.type = graphPeriod.granularityType;\n        this.skipCount = skipCount;\n    }\n\n    public GraphPinRequest(int dashId, int deviceId, DataStream dataStream,\n                           GraphPeriod graphPeriod, int skipCount, AggregationFunctionType function) {\n        this.dashId = dashId;\n        this.deviceId = deviceId;\n        this.deviceIds = EMPTY_INTS;\n        this.isTag = false;\n        if (dataStream == null) {\n            this.pinType = PinType.VIRTUAL;\n            this.pin = (short) DataStream.NO_PIN;\n        } else {\n            this.pinType = (dataStream.pinType == null ? PinType.VIRTUAL : dataStream.pinType);\n            this.pin = dataStream.pin;\n        }\n        this.graphPeriod = graphPeriod;\n        this.functionType = (function == null ? AggregationFunctionType.AVG : function);\n        this.count = graphPeriod.numberOfPoints;\n        this.type = graphPeriod.granularityType;\n        this.skipCount = skipCount;\n    }\n\n    public boolean isLiveData() {\n        return graphPeriod == LIVE;\n    }\n\n    public boolean isValid() {\n        return deviceId != -1 || deviceIds.length > 0;\n    }\n\n    @Override\n    public String toString() {\n        return \"GraphPinRequest{\"\n                + \"dashId=\" + dashId\n                + \", deviceId=\" + deviceId\n                + \", deviceIds=\" + Arrays.toString(deviceIds)\n                + \", isTag=\" + isTag\n                + \", pinType=\" + pinType\n                + \", pin=\" + pin\n                + \", graphPeriod=\" + graphPeriod\n                + \", functionType=\" + functionType\n                + \", count=\" + count\n                + \", type=\" + type\n                + \", skipCount=\" + skipCount\n                + '}';\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/reporting/average/AggregationKey.java",
    "content": "package cc.blynk.server.core.reporting.average;\n\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.widgets.outputs.graph.GraphGranularityType;\nimport cc.blynk.server.core.reporting.raw.BaseReportingKey;\n\nimport java.io.Serializable;\nimport java.util.Comparator;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 10.08.15.\n */\npublic final class AggregationKey implements Serializable {\n\n    public static final Comparator<AggregationKey> AGGREGATION_KEY_COMPARATOR = (o1, o2) -> (int) (o1.ts - o2.ts);\n\n    private final BaseReportingKey baseReportingKey;\n    public final long ts;\n\n    public AggregationKey(String email, String appName, int dashId, int deviceId, PinType pinType, short pin, long ts) {\n        this(new BaseReportingKey(email, appName, dashId, deviceId, pinType, pin), ts);\n    }\n\n    public AggregationKey(BaseReportingKey baseReportingKey, long ts) {\n        this.baseReportingKey = baseReportingKey;\n        this.ts = ts;\n    }\n\n    public long getTs(GraphGranularityType type) {\n        return ts * type.period;\n    }\n\n    public boolean isOutdated(long nowTruncatedToPeriod) {\n        return ts < nowTruncatedToPeriod;\n    }\n\n    public String getEmail() {\n        return baseReportingKey.email;\n    }\n\n    public String getAppName() {\n        return baseReportingKey.appName;\n    }\n\n    public int getDashId() {\n        return baseReportingKey.dashId;\n    }\n\n    public int getDeviceId() {\n        return baseReportingKey.deviceId;\n    }\n\n    public PinType getPinType() {\n        return baseReportingKey.pinType;\n    }\n\n    public short getPin() {\n        return baseReportingKey.pin;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (!(o instanceof AggregationKey)) {\n            return false;\n        }\n\n        AggregationKey that = (AggregationKey) o;\n\n        if (ts != that.ts) {\n            return false;\n        }\n        return baseReportingKey != null\n                ? baseReportingKey.equals(that.baseReportingKey)\n                : that.baseReportingKey == null;\n    }\n\n    @Override\n    public int hashCode() {\n        int result = baseReportingKey != null\n                ? baseReportingKey.hashCode()\n                : 0;\n        result = 31 * result + (int) (ts ^ (ts >>> 32));\n        return result;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/reporting/average/AggregationValue.java",
    "content": "package cc.blynk.server.core.reporting.average;\n\nimport java.io.Serializable;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 10.08.15.\n */\npublic class AggregationValue implements Serializable {\n\n    private double values = 0;\n    private long count = 0;\n\n    public AggregationValue() {\n    }\n\n    AggregationValue(double value) {\n        this.values = value;\n        this.count = 1;\n    }\n\n    public void update(double val) {\n        values += val;\n        count++;\n    }\n\n    public double calcAverage() {\n        return values / count;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/reporting/average/AverageAggregatorProcessor.java",
    "content": "package cc.blynk.server.core.reporting.average;\n\nimport cc.blynk.server.core.reporting.raw.BaseReportingKey;\nimport cc.blynk.utils.FileUtils;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.io.Closeable;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\n\nimport static cc.blynk.server.internal.SerializationUtil.deserialize;\nimport static cc.blynk.server.internal.SerializationUtil.serialize;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 10.08.15.\n */\npublic class AverageAggregatorProcessor implements Closeable {\n\n    private static final Logger log = LogManager.getLogger(AverageAggregatorProcessor.class);\n\n    public static final long MINUTE = 1000 * 60;\n    public static final long HOUR = 60 * MINUTE;\n    public static final long DAY = 24 * HOUR;\n    static final String MINUTE_TEMP_FILENAME = \"minute_temp.bin\";\n    static final String HOURLY_TEMP_FILENAME = \"hourly_temp.bin\";\n    static final String DAILY_TEMP_FILENAME = \"daily_temp.bin\";\n    private final String dataFolder;\n    private final ConcurrentHashMap<AggregationKey, AggregationValue> minute;\n    private final ConcurrentHashMap<AggregationKey, AggregationValue> hourly;\n    private final ConcurrentHashMap<AggregationKey, AggregationValue> daily;\n\n    @SuppressWarnings(\"unchecked\")\n    public AverageAggregatorProcessor(String dataFolder) {\n        this.dataFolder = dataFolder;\n\n        Path path;\n\n        path = Paths.get(dataFolder, MINUTE_TEMP_FILENAME);\n        this.minute = (ConcurrentHashMap<AggregationKey, AggregationValue>) deserialize(path);\n        FileUtils.deleteQuietly(path);\n\n        path = Paths.get(dataFolder, HOURLY_TEMP_FILENAME);\n        this.hourly = (ConcurrentHashMap<AggregationKey, AggregationValue>) deserialize(path);\n        FileUtils.deleteQuietly(path);\n\n        path = Paths.get(dataFolder, DAILY_TEMP_FILENAME);\n        this.daily = (ConcurrentHashMap<AggregationKey, AggregationValue>) deserialize(path);\n        FileUtils.deleteQuietly(path);\n    }\n\n    private static void aggregate(Map<AggregationKey, AggregationValue> map, AggregationKey key, double value) {\n        AggregationValue aggregationValue = map.get(key);\n        if (aggregationValue == null) {\n            aggregationValue = map.putIfAbsent(key, new AggregationValue(value));\n            if (aggregationValue == null) {\n                return;\n            }\n        }\n\n        aggregationValue.update(value);\n    }\n\n    public void collect(BaseReportingKey baseReportingKey, long ts, double val) {\n        aggregate(minute, new AggregationKey(baseReportingKey, ts / MINUTE), val);\n        aggregate(hourly, new AggregationKey(baseReportingKey, ts / HOUR), val);\n        aggregate(daily, new AggregationKey(baseReportingKey, ts / DAY), val);\n    }\n\n    public ConcurrentHashMap<AggregationKey, AggregationValue> getMinute() {\n        return minute;\n    }\n\n    public ConcurrentHashMap<AggregationKey, AggregationValue> getHourly() {\n        return hourly;\n    }\n\n    public ConcurrentHashMap<AggregationKey, AggregationValue> getDaily() {\n        return daily;\n    }\n\n    @Override\n    public void close() {\n        if (minute.size() > 100_000) {\n            log.info(\"Too many minute records ({}). \"\n                    + \"This may cause performance issues on server start. Skipping.\", minute.size());\n        } else {\n            serialize(Paths.get(dataFolder, MINUTE_TEMP_FILENAME), minute);\n        }\n        serialize(Paths.get(dataFolder, HOURLY_TEMP_FILENAME), hourly);\n        serialize(Paths.get(dataFolder, DAILY_TEMP_FILENAME), daily);\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/reporting/raw/BaseReportingKey.java",
    "content": "package cc.blynk.server.core.reporting.raw;\n\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.reporting.GraphPinRequest;\n\nimport java.io.Serializable;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 20.07.17.\n */\npublic final class BaseReportingKey implements Serializable {\n\n    public final String email;\n    public final String appName;\n    public final int dashId;\n    public final int deviceId;\n    public final PinType pinType;\n    public final short pin;\n\n    public BaseReportingKey(User user, GraphPinRequest graphPinRequest) {\n        this(user.email, user.appName,\n             graphPinRequest.dashId, graphPinRequest.deviceId,\n             graphPinRequest.pinType, graphPinRequest.pin);\n    }\n\n    public BaseReportingKey(String email, String appName, int dashId, int deviceId, PinType pinType, short pin) {\n        this.email = email;\n        this.appName = appName;\n        this.dashId = dashId;\n        this.deviceId = deviceId;\n        this.pinType = pinType;\n        this.pin = pin;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (!(o instanceof BaseReportingKey)) {\n            return false;\n        }\n\n        BaseReportingKey that = (BaseReportingKey) o;\n\n        if (dashId != that.dashId) {\n            return false;\n        }\n        if (deviceId != that.deviceId) {\n            return false;\n        }\n        if (pin != that.pin) {\n            return false;\n        }\n        if (email != null ? !email.equals(that.email) : that.email != null) {\n            return false;\n        }\n        if (appName != null ? !appName.equals(that.appName) : that.appName != null) {\n            return false;\n        }\n        return pinType == that.pinType;\n    }\n\n    @Override\n    public int hashCode() {\n        int result = email != null ? email.hashCode() : 0;\n        result = 31 * result + (appName != null ? appName.hashCode() : 0);\n        result = 31 * result + dashId;\n        result = 31 * result + deviceId;\n        result = 31 * result + (pinType != null ? pinType.hashCode() : 0);\n        result = 31 * result + (int) pin;\n        return result;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/reporting/raw/GraphValue.java",
    "content": "package cc.blynk.server.core.reporting.raw;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 20.07.17.\n */\npublic final class GraphValue {\n\n    public final double value;\n\n    public final long ts;\n\n    public GraphValue(double value, long ts) {\n        this.value = value;\n        this.ts = ts;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/reporting/raw/RawDataCacheForGraphProcessor.java",
    "content": "package cc.blynk.server.core.reporting.raw;\n\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.reporting.GraphPinRequest;\nimport cc.blynk.utils.structure.LimitedArrayDeque;\n\nimport java.nio.ByteBuffer;\nimport java.util.concurrent.ConcurrentHashMap;\n\nimport static cc.blynk.utils.FileUtils.SIZE_OF_REPORT_ENTRY;\n\n/**\n * Raw data storage for graph LIVE stream.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 25.01.17.\n */\npublic class RawDataCacheForGraphProcessor {\n\n    private static final int GRAPH_CACHE_SIZE = 60;\n\n    public final ConcurrentHashMap<BaseReportingKey, LimitedArrayDeque<GraphValue>> rawStorage;\n\n    public RawDataCacheForGraphProcessor() {\n        rawStorage = new ConcurrentHashMap<>();\n    }\n\n    public void collect(BaseReportingKey baseReportingKey, GraphValue graphCacheValue) {\n        LimitedArrayDeque<GraphValue> cache = rawStorage.get(baseReportingKey);\n        if (cache == null) {\n            cache = new LimitedArrayDeque<>(GRAPH_CACHE_SIZE);\n            rawStorage.put(baseReportingKey, cache);\n        }\n        cache.add(graphCacheValue);\n    }\n\n    public ByteBuffer getLiveGraphData(User user, GraphPinRequest graphPinRequest) {\n        LimitedArrayDeque<GraphValue> cache = rawStorage.get(new BaseReportingKey(user, graphPinRequest));\n\n        if (cache != null && cache.size() > graphPinRequest.skipCount) {\n            return toByteBuffer(cache, graphPinRequest.count, graphPinRequest.skipCount);\n        }\n\n        return null;\n    }\n\n    private ByteBuffer toByteBuffer(LimitedArrayDeque<GraphValue> cache, int count, int skipCount) {\n        int size = cache.size();\n        int expectedMinimumLength = count + skipCount;\n        int diff = size - expectedMinimumLength;\n        int startReadIndex = Math.max(0, diff);\n        int expectedResultSize = diff < 0 ? count + diff : count;\n        if (expectedResultSize <= 0) {\n            return null;\n        }\n\n        ByteBuffer byteBuffer = ByteBuffer.allocate(expectedResultSize * SIZE_OF_REPORT_ENTRY);\n\n        int i = 0;\n        int counter = 0;\n        for (GraphValue graphValue : cache) {\n            if (startReadIndex <= i && counter < expectedResultSize) {\n                counter++;\n                byteBuffer.putDouble(graphValue.value)\n                        .putLong(graphValue.ts);\n            }\n            i++;\n        }\n        return byteBuffer;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/reporting/raw/RawDataProcessor.java",
    "content": "package cc.blynk.server.core.reporting.raw;\n\nimport cc.blynk.server.core.reporting.average.AggregationKey;\nimport cc.blynk.utils.NumberUtil;\n\nimport java.util.Collections;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\n\n/**\n * Simply stores every record in memory that should be stored in reporting DB lately.\n * Could cause OOM at high request rate. However we don't use it very high loads.\n * So this is fine for now.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 25.01.17.\n */\npublic class RawDataProcessor {\n\n    public final Map<AggregationKey, Object> rawStorage;\n\n    public RawDataProcessor(boolean enable) {\n        if (enable) {\n            rawStorage = new ConcurrentHashMap<>();\n        } else {\n            rawStorage = Collections.emptyMap();\n        }\n    }\n\n    //todo 2 millis is minimum allowed interval for data pushing.\n    public void collect(BaseReportingKey key, long ts, String stringValue, double doubleValue) {\n        final AggregationKey aggregationKey = new AggregationKey(key, ts);\n        if (doubleValue == NumberUtil.NO_RESULT) {\n            rawStorage.put(aggregationKey, stringValue);\n        } else {\n            rawStorage.put(aggregationKey, doubleValue);\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/session/HardwareStateHolder.java",
    "content": "package cc.blynk.server.core.session;\n\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Device;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 13.09.15.\n */\npublic final class HardwareStateHolder extends StateHolderBase {\n\n    public final DashBoard dash;\n    public final Device device;\n\n    public HardwareStateHolder(User user, DashBoard dash, Device device) {\n        super(user);\n        this.dash = dash;\n        this.device = device;\n    }\n\n    @Override\n    public boolean contains(String sharedToken) {\n        return false;\n    }\n\n    @Override\n    public boolean isSameDash(int inDashId) {\n        return dash.id == inDashId;\n    }\n\n    @Override\n    public boolean isSameDevice(int deviceId) {\n        return device.id == deviceId;\n    }\n\n    @Override\n    public boolean isSameDashAndDeviceId(int inDashId, int deviceId) {\n        return isSameDash(inDashId) && isSameDevice(deviceId);\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/session/StateHolderBase.java",
    "content": "package cc.blynk.server.core.session;\n\nimport cc.blynk.server.core.dao.UserKey;\nimport cc.blynk.server.core.model.auth.User;\n\n/**\n * Base class for user session state.\n * Every connection has it's own info like user, tokem .deviceId, etc.\n * All info that requires quick access without any lookups.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 05.01.16.\n */\npublic abstract class StateHolderBase {\n\n    public final User user;\n    public final UserKey userKey;\n\n    public StateHolderBase(User user) {\n        this.user = user;\n        this.userKey = new UserKey(user);\n    }\n\n    public abstract boolean contains(String sharedToken);\n\n    public abstract boolean isSameDash(int inDashId);\n\n    public abstract boolean isSameDevice(int deviceId);\n\n    public abstract boolean isSameDashAndDeviceId(int inDashId, int deviceId);\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/stats/EWMA.java",
    "content": "package cc.blynk.server.core.stats;\n\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.atomic.LongAdder;\n\n/**\n * copy paste from codahale metrics\n */\npublic class EWMA {\n\n    private final LongAdder uncounted = new LongAdder();\n    private final double alpha, interval;\n    private volatile boolean initialized = false;\n    private volatile double rate = 0.0;\n\n\n    /**\n     * Create a new EWMA with a specific smoothing constant.\n     *\n     * @param alpha        the smoothing constant\n     * @param interval     the expected tick interval\n     * @param intervalUnit the time unit of the tick interval\n     */\n    EWMA(double alpha, long interval, TimeUnit intervalUnit) {\n        this.interval = intervalUnit.toNanos(interval);\n        this.alpha = alpha;\n    }\n\n    /**\n     * Update the moving average with a new value.\n     *\n     * @param n the new value\n     */\n    public void update(long n) {\n        uncounted.add(n);\n    }\n\n    /**\n     * Mark the passage of time and decay the current rate accordingly.\n     */\n    void tick() {\n        final long count = uncounted.sumThenReset();\n        final double instantRate = count / interval;\n        if (initialized) {\n            rate += (alpha * (instantRate - rate));\n        } else {\n            rate = instantRate;\n            initialized = true;\n        }\n    }\n\n    /**\n     * Returns the rate in the given units of time.\n     *\n     * @param rateUnit the unit of time\n     * @return the rate\n     */\n    double getRate(TimeUnit rateUnit) {\n        return rate * (double) rateUnit.toNanos(1);\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/stats/GlobalStats.java",
    "content": "package cc.blynk.server.core.stats;\n\nimport cc.blynk.server.core.protocol.enums.Command;\n\nimport java.util.concurrent.atomic.LongAdder;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/13/2015.\n */\npublic class GlobalStats {\n\n    private static final int APP_STAT_COUNTER_INDEX = Command.LAST_COMMAND_INDEX - 1;\n    private static final int MQTT_STAT_COUNTER_INDEX = Command.LAST_COMMAND_INDEX - 2;\n\n    //separate by income/outcome?\n    public final Meter totalMessages;\n\n    //2 last load adders are used as separate counters\n    public final LongAdder[] specificCounters;\n\n    public GlobalStats() {\n        this.totalMessages = new Meter();\n\n        //yeah, this is a bit ugly code, but as fast as possible =).\n        this.specificCounters = new LongAdder[Command.LAST_COMMAND_INDEX];\n        for (int i = 0; i < Command.LAST_COMMAND_INDEX; i++) {\n            specificCounters[i] = new LongAdder();\n        }\n    }\n\n    public void markWithoutGlobal(short cmd) {\n        specificCounters[cmd].increment();\n    }\n\n    public void mark(short cmd) {\n        totalMessages.mark(1);\n        markWithoutGlobal(cmd);\n    }\n\n    public void markSpecificCounterOnly(short cmd) {\n        specificCounters[cmd].increment();\n    }\n\n    public void incrementAppStat() {\n        specificCounters[APP_STAT_COUNTER_INDEX].increment();\n    }\n\n    public void incrementMqttStat() {\n        specificCounters[MQTT_STAT_COUNTER_INDEX].increment();\n    }\n\n    public long getTotalAppCounter(boolean reset) {\n        LongAdder longAdder = specificCounters[APP_STAT_COUNTER_INDEX];\n        return reset ? longAdder.sumThenReset() : longAdder.sum();\n    }\n\n    public long getTotalMqttCounter(boolean reset) {\n        LongAdder longAdder = specificCounters[MQTT_STAT_COUNTER_INDEX];\n        return reset ? longAdder.sumThenReset() : longAdder.sum();\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/stats/Meter.java",
    "content": "package cc.blynk.server.core.stats;\n\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.atomic.AtomicLong;\nimport java.util.concurrent.atomic.LongAdder;\n\nimport static java.lang.Math.exp;\n\n/**\n * copy paste from codahale metrics\n */\npublic class Meter {\n\n    private static final int INTERVAL = 5;\n    private static final long TICK_INTERVAL = TimeUnit.SECONDS.toNanos(INTERVAL);\n    private static final double SECONDS_PER_MINUTE = 60.0;\n    private static final int ONE_MINUTE = 1;\n\n    private static final double M1_ALPHA = 1 - exp(-INTERVAL / SECONDS_PER_MINUTE / ONE_MINUTE);\n    private final EWMA m1Rate = new EWMA(M1_ALPHA, INTERVAL, TimeUnit.SECONDS);\n\n    private final LongAdder count = new LongAdder();\n    private final AtomicLong lastTick;\n\n    /**\n     * Creates a new {@link Meter}.\n     *\n     */\n    Meter() {\n        this.lastTick = new AtomicLong(System.nanoTime());\n    }\n\n    /**\n     * Mark the occurrence of a given number of events.\n     *\n     * @param n the number of events\n     */\n    public void mark(long n) {\n        tickIfNecessary();\n        count.add(n);\n        m1Rate.update(n);\n    }\n\n    private void tickIfNecessary() {\n        final long oldTick = lastTick.get();\n        final long newTick = System.nanoTime();\n        final long age = newTick - oldTick;\n        if (age > TICK_INTERVAL) {\n            final long newIntervalStartTick = newTick - age % TICK_INTERVAL;\n            if (lastTick.compareAndSet(oldTick, newIntervalStartTick)) {\n                final long requiredTicks = age / TICK_INTERVAL;\n                for (long i = 0; i < requiredTicks; i++) {\n                    m1Rate.tick();\n                }\n            }\n        }\n    }\n\n    public long getCount() {\n        return count.sum();\n    }\n\n    public double getOneMinuteRate() {\n        tickIfNecessary();\n        return m1Rate.getRate(TimeUnit.SECONDS);\n    }\n}\n\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/stats/metrics/InstanceLoadMeter.java",
    "content": "package cc.blynk.server.core.stats.metrics;\n\nimport java.util.concurrent.TimeUnit;\n\nimport static java.lang.Math.exp;\n\n/**\n * Class used to restrict user in request rate. Mostly copied from codahale metrics for performance improvement.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 16.03.15.\n */\npublic class InstanceLoadMeter {\n\n    private static final long TICK_INTERVAL = TimeUnit.SECONDS.toMillis(1);\n    private static final double TICK_INTERVAL_DOUBLE = (double) TICK_INTERVAL;\n\n    private static final double ALPHA = 1 - exp(-1 / 60.0);\n\n    private long lastTick;\n    private long count = 0;\n    private boolean initialized = false;\n    private double rate = 0.0;\n\n    public InstanceLoadMeter() {\n        this.lastTick = System.currentTimeMillis();\n    }\n\n    /**\n     * Mark the occurrence of a given number of events.\n     */\n    public void mark() {\n        tickIfNecessary();\n        count++;\n    }\n\n    private void tickIfNecessary() {\n        final long newTick = System.currentTimeMillis();\n        final long age = newTick - lastTick;\n        if (age > TICK_INTERVAL) {\n            lastTick = newTick - age % TICK_INTERVAL;\n            final long requiredTicks = age / TICK_INTERVAL;\n            for (long i = 0; i < requiredTicks; i++) {\n                tick();\n            }\n        }\n    }\n\n    public double getOneMinuteRateNoTick() {\n        return rate * TICK_INTERVAL_DOUBLE;\n    }\n\n    public double getOneMinuteRate() {\n        tickIfNecessary();\n        return rate * TICK_INTERVAL_DOUBLE;\n    }\n\n    private void tick() {\n        final double instantRate = count / TICK_INTERVAL_DOUBLE;\n        count = 0;\n        if (initialized) {\n            rate += (ALPHA * (instantRate - rate));\n        } else {\n            rate = instantRate;\n            initialized = true;\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/stats/model/BlockingIOStat.java",
    "content": "package cc.blynk.server.core.stats.model;\n\nimport cc.blynk.server.core.BlockingIOProcessor;\nimport cc.blynk.server.core.model.widgets.ui.reporting.ReportScheduler;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 03.05.17.\n */\nclass BlockingIOStat {\n\n    private final int messagingActiveTasks;\n\n    private final long messagingExecutedTasks;\n\n    private final int historyActiveTasks;\n\n    private final long historyExecutedTasks;\n\n    private final int dbActiveTasks;\n\n    private final long dbExecutedTasks;\n\n    private final int reportingActiveTasks;\n\n    private final long reportingExecutedTasks;\n\n    private final int getServerActiveTasks;\n\n    private final long getServerExecutedTasks;\n\n    private final int reportsActive;\n\n    private final long reportsExecuted;\n\n    private final int reportsFutureMapSize;\n\n    BlockingIOStat(BlockingIOProcessor blockingIOProcessor, ReportScheduler reportScheduler) {\n        this(blockingIOProcessor.messagingExecutor.getQueue().size(),\n             blockingIOProcessor.messagingExecutor.getCompletedTaskCount(),\n\n             blockingIOProcessor.historyExecutor.getQueue().size(),\n             blockingIOProcessor.historyExecutor.getCompletedTaskCount(),\n\n             blockingIOProcessor.dbExecutor.getQueue().size(),\n             blockingIOProcessor.dbExecutor.getCompletedTaskCount(),\n\n             blockingIOProcessor.dbReportingExecutor.getQueue().size(),\n             blockingIOProcessor.dbReportingExecutor.getCompletedTaskCount(),\n\n             blockingIOProcessor.dbGetServerExecutor.getQueue().size(),\n             blockingIOProcessor.dbGetServerExecutor.getCompletedTaskCount(),\n\n             reportScheduler.getQueue().size(),\n             reportScheduler.getCompletedTaskCount(),\n             reportScheduler.map.size()\n        );\n    }\n\n    private BlockingIOStat(int messagingActiveTasks, long messagingExecutedTasks,\n                          int historyActiveTasks, long historyExecutedTasks,\n                          int dbActiveTasks, long dbExecutedTasks,\n                          int reportingActiveTasks, long reportingExecutedTasks,\n                          int getServerActiveTasks, long getServerExecutedTasks,\n                          int reportsActive, long reportsExecuted, int reportsFutureMapSize) {\n        this.messagingActiveTasks = messagingActiveTasks;\n        this.messagingExecutedTasks = messagingExecutedTasks;\n        this.historyActiveTasks = historyActiveTasks;\n        this.historyExecutedTasks = historyExecutedTasks;\n        this.dbActiveTasks = dbActiveTasks;\n        this.dbExecutedTasks = dbExecutedTasks;\n        this.reportingActiveTasks = reportingActiveTasks;\n        this.reportingExecutedTasks = reportingExecutedTasks;\n        this.getServerActiveTasks = getServerActiveTasks;\n        this.getServerExecutedTasks = getServerExecutedTasks;\n        this.reportsActive = reportsActive;\n        this.reportsExecuted = reportsExecuted;\n        this.reportsFutureMapSize = reportsFutureMapSize;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/stats/model/CommandStat.java",
    "content": "package cc.blynk.server.core.stats.model;\n\nimport static cc.blynk.server.core.protocol.enums.Command.ACTIVATE_DASHBOARD;\nimport static cc.blynk.server.core.protocol.enums.Command.ADD_ENERGY;\nimport static cc.blynk.server.core.protocol.enums.Command.ADD_PUSH_TOKEN;\nimport static cc.blynk.server.core.protocol.enums.Command.APP_SYNC;\nimport static cc.blynk.server.core.protocol.enums.Command.BLYNK_INTERNAL;\nimport static cc.blynk.server.core.protocol.enums.Command.BRIDGE;\nimport static cc.blynk.server.core.protocol.enums.Command.CONNECT_REDIRECT;\nimport static cc.blynk.server.core.protocol.enums.Command.CREATE_DASH;\nimport static cc.blynk.server.core.protocol.enums.Command.CREATE_DEVICE;\nimport static cc.blynk.server.core.protocol.enums.Command.CREATE_TAG;\nimport static cc.blynk.server.core.protocol.enums.Command.CREATE_WIDGET;\nimport static cc.blynk.server.core.protocol.enums.Command.DEACTIVATE_DASHBOARD;\nimport static cc.blynk.server.core.protocol.enums.Command.DELETE_DASH;\nimport static cc.blynk.server.core.protocol.enums.Command.DELETE_DEVICE;\nimport static cc.blynk.server.core.protocol.enums.Command.DELETE_TAG;\nimport static cc.blynk.server.core.protocol.enums.Command.DELETE_WIDGET;\nimport static cc.blynk.server.core.protocol.enums.Command.EMAIL;\nimport static cc.blynk.server.core.protocol.enums.Command.EVENTOR;\nimport static cc.blynk.server.core.protocol.enums.Command.EXPORT_GRAPH_DATA;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_DEVICES;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_ENERGY;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_SERVER;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_SHARE_TOKEN;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_TAGS;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE_CONNECTED;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE_LOGIN;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE_SYNC;\nimport static cc.blynk.server.core.protocol.enums.Command.LOAD_PROFILE_GZIPPED;\nimport static cc.blynk.server.core.protocol.enums.Command.LOGIN;\nimport static cc.blynk.server.core.protocol.enums.Command.PING;\nimport static cc.blynk.server.core.protocol.enums.Command.PUSH_NOTIFICATION;\nimport static cc.blynk.server.core.protocol.enums.Command.REDEEM;\nimport static cc.blynk.server.core.protocol.enums.Command.REFRESH_SHARE_TOKEN;\nimport static cc.blynk.server.core.protocol.enums.Command.REFRESH_TOKEN;\nimport static cc.blynk.server.core.protocol.enums.Command.REGISTER;\nimport static cc.blynk.server.core.protocol.enums.Command.RESPONSE;\nimport static cc.blynk.server.core.protocol.enums.Command.SET_WIDGET_PROPERTY;\nimport static cc.blynk.server.core.protocol.enums.Command.SHARE_LOGIN;\nimport static cc.blynk.server.core.protocol.enums.Command.SHARING;\nimport static cc.blynk.server.core.protocol.enums.Command.SMS;\nimport static cc.blynk.server.core.protocol.enums.Command.TWEET;\nimport static cc.blynk.server.core.protocol.enums.Command.UPDATE_DASH;\nimport static cc.blynk.server.core.protocol.enums.Command.UPDATE_DEVICE;\nimport static cc.blynk.server.core.protocol.enums.Command.UPDATE_TAG;\nimport static cc.blynk.server.core.protocol.enums.Command.UPDATE_WIDGET;\nimport static cc.blynk.server.core.protocol.enums.Command.WEB_HOOKS;\nimport static cc.blynk.server.core.protocol.enums.Command.WEB_SOCKETS;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 11.01.17.\n */\npublic class CommandStat {\n\n    public int response;\n    public int redeem;\n    public int hardwareConnected;\n    public int register;\n    public int login;\n    public int hardwareLogin;\n    public int loadProfile;\n    public int appSync;\n    public int sharing;\n    public int getToken;\n    public int ping;\n    public int activate;\n    public int deactivate;\n    public int refreshToken;\n    public int getGraphData;\n    public int exportGraphData;\n    public int setWidgetProperty;\n    public int bridge;\n    public int hardware;\n    public int getSharedDash;\n    public int getShareToken;\n    public int refreshShareToken;\n    public int shareLogin;\n    public int createProject;\n    public int updateProject;\n    public int deleteProject;\n    public int hardwareSync;\n    public int internal;\n\n    public int sms;\n    public int tweet;\n    public int email;\n    public int push;\n    public int addPushToken;\n\n    public int createWidget;\n    public int updateWidget;\n    public int deleteWidget;\n\n    public int createDevice;\n    public int updateDevice;\n    public int deleteDevice;\n    public int getDevices;\n\n    public int createTag;\n    public int updateTag;\n    public int deleteTag;\n    public int getTags;\n\n    public int addEnergy;\n    public int getEnergy;\n\n    public int getServer;\n    public int connectRedirect;\n\n    public int webSockets;\n\n    public int eventor;\n    public int webhooks;\n\n    public int appTotal;\n    public int mqttTotal;\n\n    void assign(short field, int val) {\n        switch (field) {\n            case RESPONSE :\n                this.response = val;\n                break;\n            case REDEEM :\n                this.redeem = val;\n                break;\n            case HARDWARE_CONNECTED :\n                this.hardwareConnected = val;\n                break;\n            case REGISTER :\n                this.register = val;\n                break;\n            case LOGIN :\n                this.login = val;\n                break;\n            case HARDWARE_LOGIN :\n                this.hardwareLogin = val;\n                break;\n            case LOAD_PROFILE_GZIPPED :\n                this.loadProfile = val;\n                break;\n            case APP_SYNC :\n                this.appSync = val;\n                break;\n            case SHARING :\n                this.sharing = val;\n                break;\n            case PING :\n                this.ping = val;\n                break;\n            case SMS :\n                this.sms = val;\n                break;\n            case ACTIVATE_DASHBOARD :\n                this.activate = val;\n                break;\n            case DEACTIVATE_DASHBOARD :\n                this.deactivate = val;\n                break;\n            case REFRESH_TOKEN :\n                this.refreshToken = val;\n                break;\n            case EXPORT_GRAPH_DATA :\n                this.exportGraphData = val;\n                break;\n            case SET_WIDGET_PROPERTY :\n                this.setWidgetProperty = val;\n                break;\n            case BRIDGE :\n                this.bridge = val;\n                break;\n            case HARDWARE :\n                this.hardware = val;\n                break;\n            case GET_SHARE_TOKEN :\n                this.getShareToken = val;\n                break;\n            case REFRESH_SHARE_TOKEN :\n                this.refreshShareToken = val;\n                break;\n            case SHARE_LOGIN :\n                this.shareLogin = val;\n                break;\n            case CREATE_DASH :\n                this.createProject = val;\n                break;\n            case UPDATE_DASH :\n                this.updateProject = val;\n                break;\n            case DELETE_DASH :\n                this.deleteProject = val;\n                break;\n            case HARDWARE_SYNC :\n                this.hardwareSync = val;\n                break;\n            case BLYNK_INTERNAL :\n                this.internal = val;\n                break;\n            case ADD_PUSH_TOKEN :\n                this.addPushToken = val;\n                break;\n            case TWEET :\n                this.tweet = val;\n                break;\n            case EMAIL :\n                this.email = val;\n                break;\n            case PUSH_NOTIFICATION :\n                this.push = val;\n                break;\n            case CREATE_WIDGET :\n                this.createWidget = val;\n                break;\n            case UPDATE_WIDGET :\n                this.updateWidget = val;\n                break;\n            case DELETE_WIDGET :\n                this.deleteWidget = val;\n                break;\n            case CREATE_DEVICE :\n                this.createDevice = val;\n                break;\n            case UPDATE_DEVICE :\n                this.updateDevice = val;\n                break;\n            case DELETE_DEVICE :\n                this.deleteDevice = val;\n                break;\n            case GET_DEVICES :\n                this.getDevices = val;\n                break;\n            case CREATE_TAG :\n                this.createTag = val;\n                break;\n            case UPDATE_TAG :\n                this.updateTag = val;\n                break;\n            case DELETE_TAG :\n                this.deleteTag = val;\n                break;\n            case GET_TAGS :\n                this.getTags = val;\n                break;\n            case ADD_ENERGY :\n                this.addEnergy = val;\n                break;\n            case GET_ENERGY :\n                this.getEnergy = val;\n                break;\n            case GET_SERVER :\n                this.getServer = val;\n                break;\n            case CONNECT_REDIRECT :\n                this.connectRedirect = val;\n                break;\n            case WEB_SOCKETS :\n                this.webSockets = val;\n                break;\n            case EVENTOR :\n                this.eventor = val;\n                break;\n            case WEB_HOOKS :\n                this.webhooks = val;\n                break;\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/stats/model/HttpStat.java",
    "content": "package cc.blynk.server.core.stats.model;\n\nimport static cc.blynk.server.core.protocol.enums.Command.HTTP_EMAIL;\nimport static cc.blynk.server.core.protocol.enums.Command.HTTP_GET_HISTORY_DATA;\nimport static cc.blynk.server.core.protocol.enums.Command.HTTP_GET_PIN_DATA;\nimport static cc.blynk.server.core.protocol.enums.Command.HTTP_GET_PROJECT;\nimport static cc.blynk.server.core.protocol.enums.Command.HTTP_IS_APP_CONNECTED;\nimport static cc.blynk.server.core.protocol.enums.Command.HTTP_IS_HARDWARE_CONNECTED;\nimport static cc.blynk.server.core.protocol.enums.Command.HTTP_NOTIFY;\nimport static cc.blynk.server.core.protocol.enums.Command.HTTP_QR;\nimport static cc.blynk.server.core.protocol.enums.Command.HTTP_TOTAL;\nimport static cc.blynk.server.core.protocol.enums.Command.HTTP_UPDATE_PIN_DATA;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 11.01.17.\n */\npublic class HttpStat {\n\n    public int isHardwareConnected;\n    public int isAppConnected;\n    public int getPinData;\n    public int updatePinData;\n    public int email;\n    public int notify;\n    public int getProject;\n    public int qr;\n    public int getHistoryPinData;\n    public int total;\n\n    void assign(short field, int val) {\n        switch (field) {\n            case HTTP_IS_HARDWARE_CONNECTED :\n                this.isHardwareConnected = val;\n                break;\n            case HTTP_IS_APP_CONNECTED :\n                this.isAppConnected = val;\n                break;\n            case HTTP_GET_PIN_DATA :\n                this.getPinData = val;\n                break;\n            case HTTP_UPDATE_PIN_DATA :\n                this.updatePinData = val;\n                break;\n            case HTTP_NOTIFY :\n                this.notify = val;\n                break;\n            case HTTP_EMAIL :\n                this.email = val;\n                break;\n            case HTTP_GET_PROJECT :\n                this.getProject = val;\n                break;\n            case HTTP_QR :\n                this.qr = val;\n                break;\n            case HTTP_GET_HISTORY_DATA:\n                this.getHistoryPinData = val;\n                break;\n            case HTTP_TOTAL :\n                this.total = val;\n                break;\n        }\n    }\n}\n\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/stats/model/MemoryStat.java",
    "content": "package cc.blynk.server.core.stats.model;\n\nimport io.netty.buffer.ByteBufAllocator;\nimport io.netty.buffer.ByteBufAllocatorMetric;\nimport io.netty.buffer.ByteBufAllocatorMetricProvider;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 5/18/17.\n */\npublic class MemoryStat {\n\n    public final long heapBytes;\n\n    public final long directBytes;\n\n    public MemoryStat(ByteBufAllocator byteBufAllocator) {\n        long directMemory = 0;\n        long heapMemory = 0;\n\n        if (byteBufAllocator instanceof ByteBufAllocatorMetricProvider) {\n            ByteBufAllocatorMetric metric = ((ByteBufAllocatorMetricProvider) byteBufAllocator).metric();\n            directMemory = metric.usedDirectMemory();\n            heapMemory = metric.usedHeapMemory();\n        }\n\n        this.directBytes = directMemory;\n        this.heapBytes = heapMemory;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/core/stats/model/Stat.java",
    "content": "package cc.blynk.server.core.stats.model;\n\nimport cc.blynk.server.core.BlockingIOProcessor;\nimport cc.blynk.server.core.dao.SessionDao;\nimport cc.blynk.server.core.dao.UserDao;\nimport cc.blynk.server.core.dao.UserKey;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.Session;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.model.widgets.ui.reporting.ReportScheduler;\nimport cc.blynk.server.core.protocol.enums.Command;\nimport cc.blynk.server.core.stats.GlobalStats;\nimport io.netty.buffer.ByteBufAllocator;\n\nimport java.util.Map;\nimport java.util.concurrent.atomic.LongAdder;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 19.07.15.\n */\npublic class Stat {\n\n    private final static long ONE_DAY = 24 * 60 * 60 * 1000;\n    private final static long ONE_WEEK = 7 * ONE_DAY;\n    private final static long ONE_MONTH = 30 * ONE_DAY;\n\n    public final CommandStat commands = new CommandStat();\n    public final HttpStat http = new HttpStat();\n    public final BlockingIOStat ioStat;\n    public final MemoryStat memoryStat;\n\n    public final int oneMinRate;\n    public final int registrations;\n    public final int active;\n    public final int activeWeek;\n    public final int activeMonth;\n    public final int connected;\n    public final int onlineApps;\n    public final int totalOnlineApps;\n    public final int onlineHards;\n    public final int totalOnlineHards;\n    public final transient long ts;\n\n    public Stat(SessionDao sessionDao, UserDao userDao, BlockingIOProcessor blockingIOProcessor,\n                GlobalStats globalStats, ReportScheduler reportScheduler, boolean reset) {\n        //yeap, some stats updates may be lost (because of sumThenReset()),\n        //but we don't care, cause this is just for general monitoring\n        for (Short command : Command.VALUES_NAME.keySet()) {\n            LongAdder longAdder = globalStats.specificCounters[command];\n            int val = (int) (reset ? longAdder.sumThenReset() : longAdder.sum());\n\n            this.http.assign(command, val);\n            this.commands.assign(command, val);\n        }\n\n        this.commands.appTotal = (int) globalStats.getTotalAppCounter(reset);\n        this.commands.mqttTotal = (int) globalStats.getTotalMqttCounter(reset);\n\n        this.oneMinRate = (int) globalStats.totalMessages.getOneMinuteRate();\n        int connectedSessions = 0;\n\n        int hardActive = 0;\n        int totalOnlineHards = 0;\n\n        int appActive = 0;\n        int totalOnlineApps = 0;\n\n        int active = 0;\n        int activeWeek = 0;\n        int activeMonth = 0;\n\n        this.ts = System.currentTimeMillis();\n        for (Map.Entry<UserKey, Session> entry: sessionDao.userSession.entrySet()) {\n            Session session = entry.getValue();\n\n            if (session.isHardwareConnected() && session.isAppConnected()) {\n                connectedSessions++;\n            }\n            if (session.isHardwareConnected()) {\n                hardActive++;\n                totalOnlineHards += session.hardwareChannels.size();\n            }\n            if (session.isAppConnected()) {\n                appActive++;\n                totalOnlineApps += session.appChannels.size();\n            }\n            UserKey userKey = entry.getKey();\n            User user = userDao.users.get(userKey);\n\n            if (user != null) {\n                if (this.ts - user.lastModifiedTs < ONE_DAY || dashUpdated(user, this.ts, ONE_DAY)) {\n                    active++;\n                    activeWeek++;\n                    activeMonth++;\n                    continue;\n                }\n                if (this.ts - user.lastModifiedTs < ONE_WEEK || dashUpdated(user, this.ts, ONE_WEEK)) {\n                    activeWeek++;\n                    activeMonth++;\n                    continue;\n                }\n                if (this.ts - user.lastModifiedTs < ONE_MONTH || dashUpdated(user, this.ts, ONE_MONTH)) {\n                    activeMonth++;\n                }\n            }\n        }\n\n        this.connected = connectedSessions;\n        this.onlineApps = appActive;\n        this.totalOnlineApps = totalOnlineApps;\n        this.onlineHards = hardActive;\n        this.totalOnlineHards = totalOnlineHards;\n\n        this.active = active;\n        this.activeWeek = activeWeek;\n        this.activeMonth = activeMonth;\n        this.registrations = userDao.users.size();\n\n        this.ioStat = new BlockingIOStat(blockingIOProcessor, reportScheduler);\n        this.memoryStat = new MemoryStat(ByteBufAllocator.DEFAULT);\n    }\n\n    private boolean dashUpdated(User user, long now, long period) {\n        for (DashBoard dash : user.profile.dashBoards) {\n            if (now - dash.updatedAt < period) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    @Override\n    public String toString() {\n        return JsonParser.toJson(this);\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/db/DBManager.java",
    "content": "package cc.blynk.server.db;\n\nimport cc.blynk.server.core.BlockingIOProcessor;\nimport cc.blynk.server.core.dao.UserKey;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.db.dao.CloneProjectDBDao;\nimport cc.blynk.server.db.dao.FlashedTokensDBDao;\nimport cc.blynk.server.db.dao.ForwardingTokenDBDao;\nimport cc.blynk.server.db.dao.PurchaseDBDao;\nimport cc.blynk.server.db.dao.RedeemDBDao;\nimport cc.blynk.server.db.dao.UserDBDao;\nimport cc.blynk.server.db.model.FlashedToken;\nimport cc.blynk.server.db.model.Purchase;\nimport cc.blynk.server.db.model.Redeem;\nimport cc.blynk.utils.properties.BaseProperties;\nimport cc.blynk.utils.properties.DBProperties;\nimport com.zaxxer.hikari.HikariConfig;\nimport com.zaxxer.hikari.HikariDataSource;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.io.Closeable;\nimport java.sql.Connection;\nimport java.sql.Statement;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport static cc.blynk.utils.properties.DBProperties.DB_PROPERTIES_FILENAME;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 19.02.16.\n */\npublic class DBManager implements Closeable {\n\n    private static final Logger log = LogManager.getLogger(DBManager.class);\n    private final HikariDataSource ds;\n\n    private final BlockingIOProcessor blockingIOProcessor;\n\n    public UserDBDao userDBDao;\n    private RedeemDBDao redeemDBDao;\n    private PurchaseDBDao purchaseDBDao;\n    private FlashedTokensDBDao flashedTokensDBDao;\n    CloneProjectDBDao cloneProjectDBDao;\n    public ForwardingTokenDBDao forwardingTokenDBDao;\n\n    public DBManager(BlockingIOProcessor blockingIOProcessor, boolean isEnabled) {\n        this(DB_PROPERTIES_FILENAME, blockingIOProcessor, isEnabled);\n    }\n\n    public DBManager(String propsFilename, BlockingIOProcessor blockingIOProcessor, boolean isEnabled) {\n        this.blockingIOProcessor = blockingIOProcessor;\n\n        DBProperties dbProperties = new DBProperties(propsFilename);\n        if (!isEnabled || dbProperties.size() == 0) {\n            log.info(\"Separate DB storage disabled.\");\n            this.ds = null;\n            return;\n        }\n\n        HikariConfig config = initConfig(dbProperties);\n\n        log.info(\"DB url : {}\", config.getJdbcUrl());\n        log.info(\"DB user : {}\", config.getUsername());\n        log.info(\"Connecting to DB...\");\n\n        HikariDataSource hikariDataSource;\n        try {\n            hikariDataSource = new HikariDataSource(config);\n        } catch (Exception e) {\n            log.error(\"Not able connect to DB. Skipping. Reason : {}\", e.getMessage());\n            this.ds = null;\n            return;\n        }\n\n        this.ds = hikariDataSource;\n        this.userDBDao = new UserDBDao(hikariDataSource);\n        this.redeemDBDao = new RedeemDBDao(hikariDataSource);\n        this.purchaseDBDao = new PurchaseDBDao(hikariDataSource);\n        this.flashedTokensDBDao = new FlashedTokensDBDao(hikariDataSource);\n        this.cloneProjectDBDao = new CloneProjectDBDao(hikariDataSource);\n        this.forwardingTokenDBDao = new ForwardingTokenDBDao(hikariDataSource);\n\n        checkDBVersion();\n\n        log.info(\"Connected to database successfully.\");\n    }\n\n    private void checkDBVersion() {\n        try {\n            int dbVersion = userDBDao.getDBVersion();\n            if (dbVersion < 90500) {\n                log.error(\"Current Postgres version is lower than minimum required 9.5.0 version. \"\n                        + \"PLEASE UPDATE YOUR DB.\");\n            }\n        } catch (Exception e) {\n            log.error(\"Error getting DB version.\", e.getMessage());\n        }\n    }\n\n    private HikariConfig initConfig(BaseProperties serverProperties) {\n        HikariConfig config = new HikariConfig();\n        config.setJdbcUrl(serverProperties.getProperty(\"jdbc.url\"));\n        config.setUsername(serverProperties.getProperty(\"user\"));\n        config.setPassword(serverProperties.getProperty(\"password\"));\n\n        config.setAutoCommit(false);\n        config.setConnectionTimeout(serverProperties.getLongProperty(\"connection.timeout.millis\"));\n        config.setMaximumPoolSize(5);\n        config.setMaxLifetime(0);\n        config.setConnectionTestQuery(\"SELECT 1\");\n        return config;\n    }\n\n    public void deleteUser(UserKey userKey) {\n        if (isDBEnabled() && userKey != null) {\n            blockingIOProcessor.executeDB(() -> userDBDao.deleteUser(userKey));\n        }\n    }\n\n    public void saveUsers(ArrayList<User> users) {\n        if (isDBEnabled() && users.size() > 0) {\n            blockingIOProcessor.executeDB(() -> userDBDao.save(users));\n        }\n    }\n\n    public Redeem selectRedeemByToken(String token) throws Exception {\n        if (isDBEnabled()) {\n            return redeemDBDao.selectRedeemByToken(token);\n        }\n        return null;\n    }\n\n    public boolean updateRedeem(String email, String token) throws Exception {\n        return redeemDBDao.updateRedeem(email, token);\n    }\n\n    public void insertRedeems(List<Redeem> redeemList) {\n        if (isDBEnabled() && redeemList.size() > 0) {\n            redeemDBDao.insertRedeems(redeemList);\n        }\n    }\n\n    public FlashedToken selectFlashedToken(String token) {\n        if (isDBEnabled()) {\n            return flashedTokensDBDao.selectFlashedToken(token);\n        }\n        return null;\n    }\n\n    public boolean activateFlashedToken(String token) {\n        return flashedTokensDBDao.activateFlashedToken(token);\n    }\n\n    public boolean insertFlashedTokens(FlashedToken... flashedTokenList) throws Exception {\n        if (isDBEnabled() && flashedTokenList.length > 0) {\n            flashedTokensDBDao.insertFlashedTokens(flashedTokenList);\n            return true;\n        }\n        return false;\n    }\n\n    public void insertPurchase(Purchase purchase) {\n        if (isDBEnabled()) {\n            purchaseDBDao.insertPurchase(purchase);\n        }\n    }\n\n    public boolean insertClonedProject(String token, String projectJson) throws Exception {\n        if (isDBEnabled()) {\n            cloneProjectDBDao.insertClonedProject(token, projectJson);\n            return true;\n        }\n        return false;\n    }\n\n    public String selectClonedProject(String token) throws Exception {\n        if (isDBEnabled()) {\n            return cloneProjectDBDao.selectClonedProjectByToken(token);\n        }\n        return null;\n    }\n\n    public boolean dbIsNotEnabled() {\n        return ds == null;\n    }\n\n    public boolean isDBEnabled() {\n        return !(ds == null || ds.isClosed());\n    }\n\n    public void executeSQL(String sql) throws Exception {\n        try (Connection connection = ds.getConnection();\n             Statement statement = connection.createStatement()) {\n            statement.execute(sql);\n            connection.commit();\n        }\n    }\n\n    public String getUserServerIp(String email, String appName) {\n        if (isDBEnabled()) {\n            return userDBDao.getUserServerIp(email, appName);\n        }\n        return null;\n    }\n\n    public String getServerByToken(String token) {\n        if (isDBEnabled()) {\n            return forwardingTokenDBDao.selectHostByToken(token);\n        }\n        return null;\n    }\n\n    public void assignServerToToken(String token, String serverIp, String email, int dashId, int deviceId) {\n        if (isDBEnabled()) {\n            blockingIOProcessor.executeDB(() ->\n                    forwardingTokenDBDao.insertTokenHost(token, serverIp, email, dashId, deviceId));\n        }\n    }\n\n    public void removeToken(String... tokens) {\n        if (isDBEnabled() && tokens.length > 0) {\n            blockingIOProcessor.executeDB(() -> forwardingTokenDBDao.deleteToken(tokens));\n        }\n    }\n\n    public Connection getConnection() throws Exception {\n        return ds.getConnection();\n    }\n\n    @Override\n    public void close() {\n        if (isDBEnabled()) {\n            System.out.println(\"Closing DB...\");\n            ds.close();\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/db/ReportingDBManager.java",
    "content": "package cc.blynk.server.db;\n\nimport cc.blynk.server.core.BlockingIOProcessor;\nimport cc.blynk.server.core.model.widgets.outputs.graph.GraphGranularityType;\nimport cc.blynk.server.core.reporting.average.AggregationKey;\nimport cc.blynk.server.core.reporting.average.AggregationValue;\nimport cc.blynk.server.core.stats.model.Stat;\nimport cc.blynk.server.db.dao.ReportingDBDao;\nimport cc.blynk.utils.properties.BaseProperties;\nimport cc.blynk.utils.properties.DBProperties;\nimport com.zaxxer.hikari.HikariConfig;\nimport com.zaxxer.hikari.HikariDataSource;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.io.Closeable;\nimport java.sql.Connection;\nimport java.sql.Statement;\nimport java.time.Instant;\nimport java.util.Map;\n\nimport static cc.blynk.utils.properties.DBProperties.DB_PROPERTIES_FILENAME;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 19.02.16.\n */\npublic class ReportingDBManager implements Closeable {\n\n    private static final Logger log = LogManager.getLogger(ReportingDBManager.class);\n    private final HikariDataSource ds;\n\n    private final BlockingIOProcessor blockingIOProcessor;\n    private final boolean cleanOldReporting;\n\n    public ReportingDBDao reportingDBDao;\n\n    public ReportingDBManager(BlockingIOProcessor blockingIOProcessor, boolean isEnabled) {\n        this(DB_PROPERTIES_FILENAME, blockingIOProcessor, isEnabled);\n    }\n\n    public ReportingDBManager(String propsFilename, BlockingIOProcessor blockingIOProcessor, boolean isEnabled) {\n        this.blockingIOProcessor = blockingIOProcessor;\n\n        DBProperties dbProperties = new DBProperties(propsFilename);\n        if (!isEnabled || dbProperties.size() == 0) {\n            log.info(\"Separate DB storage disabled.\");\n            this.ds = null;\n            this.cleanOldReporting = false;\n            return;\n        }\n\n        HikariConfig config = initConfig(dbProperties);\n\n        log.info(\"Reporting DB url : {}\", config.getJdbcUrl());\n        log.info(\"Reporting DB user : {}\", config.getUsername());\n        log.info(\"Connecting to reporting DB...\");\n\n        HikariDataSource hikariDataSource;\n        try {\n            hikariDataSource = new HikariDataSource(config);\n        } catch (Exception e) {\n            log.error(\"Not able connect to reporting DB. Skipping. Reason : {}\", e.getMessage());\n            this.ds = null;\n            this.cleanOldReporting = false;\n            return;\n        }\n\n        this.ds = hikariDataSource;\n        this.reportingDBDao = new ReportingDBDao(hikariDataSource);\n        this.cleanOldReporting = dbProperties.cleanReporting();\n\n        log.info(\"Connected to reporting database successfully.\");\n    }\n\n    private HikariConfig initConfig(BaseProperties serverProperties) {\n        HikariConfig config = new HikariConfig();\n        config.setJdbcUrl(serverProperties.getProperty(\"reporting.jdbc.url\"));\n        config.setUsername(serverProperties.getProperty(\"reporting.user\"));\n        config.setPassword(serverProperties.getProperty(\"reporting.password\"));\n\n        config.setAutoCommit(false);\n        config.setConnectionTimeout(serverProperties.getLongProperty(\"reporting.connection.timeout.millis\"));\n        config.setMaximumPoolSize(5);\n        config.setMaxLifetime(0);\n        config.setConnectionTestQuery(\"SELECT 1\");\n        return config;\n    }\n\n    public void insertStat(String region, Stat stat) {\n        if (isDBEnabled()) {\n            reportingDBDao.insertStat(region, stat);\n        }\n    }\n\n    public void insertReporting(Map<AggregationKey, AggregationValue> map, GraphGranularityType graphGranularityType) {\n        if (isDBEnabled() && map.size() > 0) {\n            blockingIOProcessor.executeReportingDB(() -> reportingDBDao.insert(map, graphGranularityType));\n        }\n    }\n\n    public void insertReportingRaw(Map<AggregationKey, Object> rawData) {\n        if (isDBEnabled() && rawData.size() > 0) {\n            blockingIOProcessor.executeReportingDB(() -> reportingDBDao.insertRawData(rawData));\n        }\n    }\n\n    public void cleanOldReportingRecords(Instant now) {\n        if (isDBEnabled() && cleanOldReporting) {\n            blockingIOProcessor.executeReportingDB(() -> reportingDBDao.cleanOldReportingRecords(now));\n        }\n    }\n\n    public boolean isDBEnabled() {\n        return !(ds == null || ds.isClosed());\n    }\n\n    public void executeSQL(String sql) throws Exception {\n        try (Connection connection = ds.getConnection();\n             Statement statement = connection.createStatement()) {\n            statement.execute(sql);\n            connection.commit();\n        }\n    }\n\n    public Connection getConnection() throws Exception {\n        return ds.getConnection();\n    }\n\n    @Override\n    public void close() {\n        if (isDBEnabled()) {\n            System.out.println(\"Closing Reporting DB...\");\n            ds.close();\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/db/dao/CloneProjectDBDao.java",
    "content": "package cc.blynk.server.db.dao;\n\nimport com.zaxxer.hikari.HikariDataSource;\n\nimport java.sql.Connection;\nimport java.sql.PreparedStatement;\nimport java.sql.ResultSet;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 09.03.16.\n */\npublic class CloneProjectDBDao {\n\n    private static final String selectClonedProjectByToken = \"SELECT * from cloned_projects where token = ?\";\n    private static final String insertClonedProject =\n            \"INSERT INTO cloned_projects (token, ts, json) values (?, NOW(), ?)\";\n\n    private final HikariDataSource ds;\n\n    public CloneProjectDBDao(HikariDataSource ds) {\n        this.ds = ds;\n    }\n\n    public void insertClonedProject(String token, String projectJson) throws Exception {\n        try (Connection connection = ds.getConnection();\n             PreparedStatement ps = connection.prepareStatement(insertClonedProject)) {\n\n            ps.setString(1, token);\n            ps.setString(2, projectJson);\n            ps.executeUpdate();\n\n            connection.commit();\n        }\n    }\n\n    public String selectClonedProjectByToken(String token) throws Exception {\n        try (Connection connection = ds.getConnection();\n             PreparedStatement statement = connection.prepareStatement(selectClonedProjectByToken)) {\n\n            statement.setString(1, token);\n            try (ResultSet rs = statement.executeQuery()) {\n                connection.commit();\n                if (rs.next()) {\n                    return rs.getString(\"json\");\n                }\n            }\n        }\n        return null;\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/db/dao/FlashedTokensDBDao.java",
    "content": "package cc.blynk.server.db.dao;\n\nimport cc.blynk.server.db.model.FlashedToken;\nimport com.zaxxer.hikari.HikariDataSource;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.sql.Connection;\nimport java.sql.PreparedStatement;\nimport java.sql.ResultSet;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 09.03.16.\n */\npublic class FlashedTokensDBDao {\n\n    private static final String selectToken = \"SELECT * from flashed_tokens where token = ?\";\n    private static final String activateToken =\n            \"UPDATE flashed_tokens SET is_activated = true, ts = NOW() WHERE token = ?\";\n    private static final String insertToken =\n            \"INSERT INTO flashed_tokens (token, app_name, email, project_id, device_id) values (?, ?, ?, ?, ?)\";\n\n    private static final Logger log = LogManager.getLogger(FlashedTokensDBDao.class);\n    private final HikariDataSource ds;\n\n    public FlashedTokensDBDao(HikariDataSource ds) {\n        this.ds = ds;\n    }\n\n    public FlashedToken selectFlashedToken(String token) {\n        log.info(\"Select flashed token {}.\", token);\n\n        try (Connection connection = ds.getConnection();\n             PreparedStatement statement = connection.prepareStatement(selectToken)) {\n\n            statement.setString(1, token);\n\n            try (ResultSet rs = statement.executeQuery()) {\n                connection.commit();\n\n                if (rs.next()) {\n                    return new FlashedToken(rs.getString(\"token\"), rs.getString(\"app_name\"),\n                            rs.getString(\"email\"), rs.getInt(\"project_id\"), rs.getInt(\"device_id\"),\n                            rs.getBoolean(\"is_activated\"), rs.getDate(\"ts\")\n                    );\n                }\n            }\n        } catch (Exception e) {\n            log.error(\"Error getting flashed token.\", e);\n        }\n\n        return null;\n    }\n\n    public boolean activateFlashedToken(String token) {\n        try (Connection connection = ds.getConnection();\n             PreparedStatement statement = connection.prepareStatement(activateToken)) {\n\n            statement.setString(1, token);\n            int updatedRows = statement.executeUpdate();\n            connection.commit();\n            return updatedRows == 1;\n        } catch (Exception e) {\n            return false;\n        }\n    }\n\n    public void insertFlashedTokens(FlashedToken[] flashedTokenList) throws Exception {\n        try (Connection connection = ds.getConnection();\n             PreparedStatement ps = connection.prepareStatement(insertToken)) {\n\n            for (FlashedToken flashedToken : flashedTokenList) {\n                insert(ps, flashedToken);\n                ps.addBatch();\n            }\n\n            ps.executeBatch();\n            connection.commit();\n        }\n    }\n\n    private static void insert(PreparedStatement ps, FlashedToken flashedToken) throws Exception {\n        ps.setString(1, flashedToken.token);\n        ps.setString(2, flashedToken.appId);\n        ps.setString(3, flashedToken.email);\n        ps.setInt(4, flashedToken.dashId);\n        ps.setInt(5, flashedToken.deviceId);\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/db/dao/ForwardingTokenDBDao.java",
    "content": "package cc.blynk.server.db.dao;\n\nimport com.zaxxer.hikari.HikariDataSource;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.sql.Connection;\nimport java.sql.PreparedStatement;\nimport java.sql.ResultSet;\nimport java.util.List;\nimport java.util.StringJoiner;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 09.03.16.\n */\npublic class ForwardingTokenDBDao {\n\n    private static final String selectHostByToken = \"SELECT host from forwarding_tokens where token = ?\";\n    private static final String insertTokenHostProject =\n            \"INSERT INTO forwarding_tokens (token, host, email, project_id, device_id) values (?, ?, ?, ?, ?)\";\n    private static final String deleteToken = \"delete from forwarding_tokens where token IN \";\n\n    private static final Logger log = LogManager.getLogger(ForwardingTokenDBDao.class);\n    private final HikariDataSource ds;\n\n    public ForwardingTokenDBDao(HikariDataSource ds) {\n        this.ds = ds;\n    }\n\n    public boolean insertTokenHostBatch(List<ForwardingTokenEntry> entries) {\n        try (Connection connection = ds.getConnection();\n             PreparedStatement ps = connection.prepareStatement(insertTokenHostProject)) {\n\n            for (ForwardingTokenEntry entry : entries) {\n                ps.setString(1, entry.token);\n                ps.setString(2, entry.host);\n                ps.setString(3, entry.email);\n                ps.setInt(4, entry.dashId);\n                ps.setInt(5, entry.deviceId);\n                ps.addBatch();\n            }\n\n            ps.executeBatch();\n            connection.commit();\n            return true;\n        } catch (Exception e) {\n            log.error(\"Error insert token host. Reason : {}\", e.getMessage());\n        }\n        return false;\n    }\n\n    public boolean insertTokenHost(String token, String host, String email, int dashId, int deviceId) {\n        try (Connection connection = ds.getConnection();\n             PreparedStatement ps = connection.prepareStatement(insertTokenHostProject)) {\n\n            ps.setString(1, token);\n            ps.setString(2, host);\n            ps.setString(3, email);\n            ps.setInt(4, dashId);\n            ps.setInt(5, deviceId);\n            ps.executeUpdate();\n\n            connection.commit();\n            return true;\n        } catch (Exception e) {\n            log.error(\"Error insert token host. Reason : {}\", e.getMessage());\n        }\n        return false;\n    }\n\n    public String selectHostByToken(String token) {\n        try (Connection connection = ds.getConnection();\n             PreparedStatement statement = connection.prepareStatement(selectHostByToken)) {\n\n            statement.setString(1, token);\n            try (ResultSet rs = statement.executeQuery()) {\n                connection.commit();\n                if (rs.next()) {\n                    return rs.getString(\"host\");\n                }\n            }\n        } catch (Exception e) {\n            log.error(\"Error getting token host. Reason : {}\", e.getMessage());\n        }\n        return null;\n    }\n\n    public boolean deleteToken(String... tokens) {\n        String query = deleteToken + makeQuestionMarks(tokens.length);\n\n        try (Connection connection = ds.getConnection();\n             PreparedStatement statement = connection.prepareStatement(query)) {\n\n            for (int i = 1; i <= tokens.length; i++) {\n                statement.setString(i, tokens[i - 1]);\n            }\n\n            statement.executeUpdate();\n            connection.commit();\n            return true;\n        } catch (Exception e) {\n            log.error(\"Error deleting token host. Reason : {}\", e.getMessage());\n        }\n        return false;\n    }\n\n    private static String makeQuestionMarks(int count) {\n        StringJoiner sj = new StringJoiner(\",\", \"(\", \")\");\n        for (int i = 0; i < count; i++) {\n            sj.add(\"?\");\n        }\n        return sj.toString();\n    }\n\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/db/dao/ForwardingTokenEntry.java",
    "content": "package cc.blynk.server.db.dao;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 17.11.17.\n */\npublic class ForwardingTokenEntry {\n\n    public final String token;\n    public final String host;\n    public final String email;\n    public final int dashId;\n    public final int deviceId;\n\n    public ForwardingTokenEntry(String token, String host, String email, int dashId, int deviceId) {\n        this.token = token;\n        this.host = host;\n        this.email = email;\n        this.dashId = dashId;\n        this.deviceId = deviceId;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/db/dao/PurchaseDBDao.java",
    "content": "package cc.blynk.server.db.dao;\n\nimport cc.blynk.server.db.model.Purchase;\nimport com.zaxxer.hikari.HikariDataSource;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.sql.Connection;\nimport java.sql.PreparedStatement;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 09.03.16.\n */\npublic class PurchaseDBDao {\n\n    private static final String insertPurchase =\n            \"INSERT INTO purchase (email, reward, transactionId, price) values (?, ?, ?, ?)\";\n\n    private static final Logger log = LogManager.getLogger(PurchaseDBDao.class);\n    private final HikariDataSource ds;\n\n    public PurchaseDBDao(HikariDataSource ds) {\n        this.ds = ds;\n    }\n\n    public void insertPurchase(Purchase purchase) {\n        try (Connection connection = ds.getConnection();\n             PreparedStatement ps = connection.prepareStatement(insertPurchase)) {\n\n            insert(ps, purchase);\n            ps.executeUpdate();\n\n            connection.commit();\n        } catch (Throwable e) {\n            log.error(\"Error inserting purchase data in DB. {}\", e.getMessage());\n        }\n    }\n\n    private static void insert(PreparedStatement ps, Purchase purchase) throws Exception {\n        ps.setString(1, purchase.email);\n        ps.setInt(2, purchase.reward);\n        ps.setString(3, purchase.transactionId);\n        ps.setDouble(4, purchase.price);\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/db/dao/RedeemDBDao.java",
    "content": "package cc.blynk.server.db.dao;\n\nimport cc.blynk.server.db.model.Redeem;\nimport com.zaxxer.hikari.HikariDataSource;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.sql.Connection;\nimport java.sql.PreparedStatement;\nimport java.sql.ResultSet;\nimport java.util.List;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 09.03.16.\n */\npublic class RedeemDBDao {\n\n    private static final String selectRedeemToken = \"SELECT * from redeem where token = ?\";\n    private static final String updateRedeemToken =\n            \"UPDATE redeem SET email = ?, version = 2, isRedeemed = true, ts = NOW() WHERE token = ? and version = 1\";\n    private static final String insertRedeemToken = \"INSERT INTO redeem (token, company, reward) values (?, ?, ?)\";\n\n    private static final Logger log = LogManager.getLogger(RedeemDBDao.class);\n    private final HikariDataSource ds;\n\n    public RedeemDBDao(HikariDataSource ds) {\n        this.ds = ds;\n    }\n\n    public Redeem selectRedeemByToken(String token) throws Exception {\n        log.info(\"Redeem select for {}\", token);\n\n        try (Connection connection = ds.getConnection();\n             PreparedStatement statement = connection.prepareStatement(selectRedeemToken)) {\n\n            statement.setString(1, token);\n\n            try (ResultSet rs = statement.executeQuery()) {\n                connection.commit();\n                if (rs.next()) {\n                    return new Redeem(rs.getString(\"token\"), rs.getString(\"company\"),\n                            rs.getBoolean(\"isRedeemed\"), rs.getString(\"email\"),\n                            rs.getInt(\"reward\"), rs.getInt(\"version\"),\n                            rs.getDate(\"ts\")\n                    );\n                }\n            }\n        }\n\n        return null;\n    }\n\n    public boolean updateRedeem(String email, String token) throws Exception {\n        try (Connection connection = ds.getConnection();\n             PreparedStatement statement = connection.prepareStatement(updateRedeemToken)) {\n\n            statement.setString(1, email);\n            statement.setString(2, token);\n            int updatedRows = statement.executeUpdate();\n            connection.commit();\n            return updatedRows == 1;\n        }\n    }\n\n    public void insertRedeems(List<Redeem> redeemList) {\n        try (Connection connection = ds.getConnection();\n             PreparedStatement ps = connection.prepareStatement(insertRedeemToken)) {\n\n            for (Redeem redeem : redeemList) {\n                insert(ps, redeem);\n                ps.addBatch();\n            }\n\n            ps.executeBatch();\n            connection.commit();\n        } catch (Exception e) {\n            log.error(\"Error inserting redeems data in DB.\", e);\n        }\n    }\n\n    private static void insert(PreparedStatement ps, Redeem redeem) throws Exception {\n        ps.setString(1, redeem.token);\n        ps.setString(2, redeem.company);\n        ps.setInt(3, redeem.reward);\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/db/dao/ReportingDBDao.java",
    "content": "package cc.blynk.server.db.dao;\n\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.widgets.outputs.graph.GraphGranularityType;\nimport cc.blynk.server.core.model.widgets.outputs.graph.GraphPeriod;\nimport cc.blynk.server.core.reporting.average.AggregationKey;\nimport cc.blynk.server.core.reporting.average.AggregationValue;\nimport cc.blynk.server.core.reporting.average.AverageAggregatorProcessor;\nimport cc.blynk.server.core.stats.model.CommandStat;\nimport cc.blynk.server.core.stats.model.HttpStat;\nimport cc.blynk.server.core.stats.model.Stat;\nimport cc.blynk.utils.DateTimeUtils;\nimport com.zaxxer.hikari.HikariDataSource;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.sql.Connection;\nimport java.sql.PreparedStatement;\nimport java.sql.SQLException;\nimport java.sql.Timestamp;\nimport java.sql.Types;\nimport java.time.Instant;\nimport java.time.temporal.ChronoUnit;\nimport java.util.Iterator;\nimport java.util.Map;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 09.03.16.\n */\npublic class ReportingDBDao {\n\n    public static final String insertMinute =\n            \"INSERT INTO reporting_average_minute (email, project_id, device_id, pin, pin_type, ts, value) \"\n                    + \"VALUES (?, ?, ?, ?, ?, ?, ?)\";\n    private static final String insertHourly =\n            \"INSERT INTO reporting_average_hourly (email, project_id, device_id, pin, pin_type, ts, value) \"\n                    + \"VALUES (?, ?, ?, ?, ?, ?, ?)\";\n    private static final String insertDaily =\n            \"INSERT INTO reporting_average_daily (email, project_id, device_id, pin, pin_type, ts, value) \"\n                    + \"VALUES (?, ?, ?, ?, ?, ?, ?)\";\n\n    private static final String insertRawData =\n            \"INSERT INTO reporting_raw_data (email, project_id, device_id, pin, pinType, ts, \"\n                    + \"stringValue, doubleValue) \"\n                    + \"VALUES (?, ?, ?, ?, ?, ?, ?, ?)\";\n\n    public static final String selectMinute =\n            \"SELECT ts, value FROM reporting_average_minute WHERE ts > ? ORDER BY ts DESC limit ?\";\n    public static final String selectHourly =\n            \"SELECT ts, value FROM reporting_average_hourly WHERE ts > ? ORDER BY ts DESC limit ?\";\n    public static final String selectDaily =\n            \"SELECT ts, value FROM reporting_average_daily WHERE ts > ? ORDER BY ts DESC limit ?\";\n\n    private static final String deleteMinute = \"DELETE FROM reporting_average_minute WHERE ts < ?\";\n    private static final String deleteHour = \"DELETE FROM reporting_average_hourly WHERE ts < ?\";\n    public static final String deleteDaily = \"DELETE FROM reporting_average_daily WHERE ts < ?\";\n\n    private static final String insertStatMinute =\n            \"INSERT INTO reporting_app_stat_minute (region, ts, active, active_week, active_month, \"\n                    + \"minute_rate, connected, online_apps, online_hards, \"\n                    + \"total_online_apps, total_online_hards, registrations) \"\n                    + \"VALUES (?,?,?,?,?,?,?,?,?,?,?,?)\";\n    private static final String insertStatCommandsMinute =\n            \"INSERT INTO reporting_app_command_stat_minute (region, ts, response, register, \"\n                    + \"login, load_profile, app_sync, sharing, get_token, ping, activate, \"\n                    + \"deactivate, refresh_token, get_graph_data, export_graph_data, \"\n                    + \"set_widget_property, bridge, hardware, get_share_dash, get_share_token, \"\n                    + \"refresh_share_token, share_login, create_project, update_project, \"\n                    + \"delete_project, hardware_sync, internal, sms, tweet, email, push, \"\n                    + \"add_push_token, create_widget, update_widget, delete_widget, create_device, \"\n                    + \"update_device, delete_device, get_devices, create_tag, update_tag, \"\n                    + \"delete_tag, get_tags, add_energy, get_energy, get_server, connect_redirect, \"\n                    + \"web_sockets, eventor, webhooks, appTotal, hardTotal) \"\n                    + \"VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?\"\n                    + \",?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)\";\n    //todo add one more column mqttTotal and replace hardTotal with mqtt total\n    private static final String insertStatHttpCommandMinute =\n            \"INSERT INTO reporting_http_command_stat_minute (region, ts, is_hardware_connected, \"\n                    + \"is_app_connected, get_pin_data, update_pin, email, push, get_project, qr,\"\n                    + \" get_history_pin_data, total) \"\n                    + \"VALUES (?,?,?,?,?,?,?,?,?,?,?,?)\";\n\n    private static final Logger log = LogManager.getLogger(ReportingDBDao.class);\n\n    private final HikariDataSource ds;\n\n    public ReportingDBDao(HikariDataSource ds) {\n        this.ds = ds;\n    }\n\n    public static void prepareReportingSelect(PreparedStatement ps, long ts, int limit) throws SQLException {\n        ps.setTimestamp(1, new Timestamp(ts), DateTimeUtils.UTC_CALENDAR);\n        ps.setInt(2, limit);\n    }\n\n    private static void prepareReportingInsert(PreparedStatement ps,\n                                               Map.Entry<AggregationKey, AggregationValue> entry,\n                                               GraphGranularityType type) throws SQLException {\n        AggregationKey key = entry.getKey();\n        AggregationValue value = entry.getValue();\n        prepareReportingInsert(ps, key.getEmail(), key.getDashId(), key.getDeviceId(),\n                key.getPin(), key.getPinType(), key.getTs(type), value.calcAverage());\n    }\n\n    public static void prepareReportingInsert(PreparedStatement ps,\n                                                 String email,\n                                                 int dashId,\n                                                 int deviceId,\n                                                 short pin,\n                                                 PinType pinType,\n                                                 long ts,\n                                                 double value) throws SQLException {\n        ps.setString(1, email);\n        ps.setInt(2, dashId);\n        ps.setInt(3, deviceId);\n        ps.setShort(4, pin);\n        ps.setInt(5, pinType.ordinal());\n        ps.setTimestamp(6, new Timestamp(ts), DateTimeUtils.UTC_CALENDAR);\n        ps.setDouble(7, value);\n    }\n\n    private static String getTableByGraphType(GraphGranularityType graphGranularityType) {\n        switch (graphGranularityType) {\n            case MINUTE :\n                return insertMinute;\n            case HOURLY :\n                return insertHourly;\n            default :\n                return insertDaily;\n        }\n    }\n\n    public void insertRawData(Map<AggregationKey, Object> rawData) {\n        long start = System.currentTimeMillis();\n\n        log.info(\"Storing raw reporting...\");\n        int counter = 0;\n\n        try (Connection connection = ds.getConnection();\n             PreparedStatement ps = connection.prepareStatement(insertRawData)) {\n\n            for (Iterator<Map.Entry<AggregationKey, Object>> iter = rawData.entrySet().iterator(); iter.hasNext();) {\n                Map.Entry<AggregationKey, Object> entry = iter.next();\n\n                final AggregationKey key = entry.getKey();\n                final Object value = entry.getValue();\n\n                ps.setString(1, key.getEmail());\n                ps.setInt(2, key.getDashId());\n                ps.setInt(3, key.getDeviceId());\n                ps.setShort(4, key.getPin());\n                ps.setString(5, key.getPinType().pinTypeString);\n                ps.setTimestamp(6, new Timestamp(key.ts), DateTimeUtils.UTC_CALENDAR);\n\n                if (value instanceof String) {\n                    ps.setString(7, (String) value);\n                    ps.setNull(8, Types.DOUBLE);\n                } else {\n                    ps.setNull(7, Types.VARCHAR);\n                    ps.setDouble(8, (Double) value);\n                }\n\n                ps.addBatch();\n                counter++;\n                iter.remove();\n            }\n\n            ps.executeBatch();\n            connection.commit();\n        } catch (Exception e) {\n            log.error(\"Error inserting raw reporting data in DB.\", e);\n        }\n\n        log.info(\"Storing raw reporting finished. Time {}. Records saved {}\",\n                System.currentTimeMillis() - start, counter);\n    }\n\n    public void insertStat(String region, Stat stat) {\n        final long ts = (stat.ts / AverageAggregatorProcessor.MINUTE) * AverageAggregatorProcessor.MINUTE;\n        final Timestamp timestamp = new Timestamp(ts);\n\n        try (Connection connection = ds.getConnection();\n             PreparedStatement appStatPS = connection.prepareStatement(insertStatMinute);\n             PreparedStatement commandStatPS = connection.prepareStatement(insertStatCommandsMinute);\n             PreparedStatement httpStatPS = connection.prepareStatement(insertStatHttpCommandMinute)) {\n\n            appStatPS.setString(1, region);\n            appStatPS.setTimestamp(2, timestamp, DateTimeUtils.UTC_CALENDAR);\n            appStatPS.setInt(3, stat.active);\n            appStatPS.setInt(4, stat.activeWeek);\n            appStatPS.setInt(5, stat.activeMonth);\n            appStatPS.setInt(6, stat.oneMinRate);\n            appStatPS.setInt(7, stat.connected);\n            appStatPS.setInt(8, stat.onlineApps);\n            appStatPS.setInt(9, stat.onlineHards);\n            appStatPS.setInt(10, stat.totalOnlineApps);\n            appStatPS.setInt(11, stat.totalOnlineHards);\n            appStatPS.setInt(12, stat.registrations);\n            appStatPS.executeUpdate();\n\n            final HttpStat hs = stat.http;\n            httpStatPS.setString(1, region);\n            httpStatPS.setTimestamp(2, timestamp, DateTimeUtils.UTC_CALENDAR);\n            httpStatPS.setInt(3, hs.isHardwareConnected);\n            httpStatPS.setInt(4, hs.isAppConnected);\n            httpStatPS.setInt(5, hs.getPinData);\n            httpStatPS.setInt(6, hs.updatePinData);\n            httpStatPS.setInt(7, hs.email);\n            httpStatPS.setInt(8, hs.notify);\n            httpStatPS.setInt(9, hs.getProject);\n            httpStatPS.setInt(10, hs.qr);\n            httpStatPS.setInt(11, hs.getHistoryPinData);\n            httpStatPS.setInt(12, hs.total);\n\n            httpStatPS.executeUpdate();\n\n            final CommandStat cs = stat.commands;\n            commandStatPS.setString(1, region);\n            commandStatPS.setTimestamp(2, timestamp, DateTimeUtils.UTC_CALENDAR);\n            commandStatPS.setInt(3, cs.response);\n            commandStatPS.setInt(4, cs.register);\n            commandStatPS.setInt(5, cs.login);\n            commandStatPS.setInt(6, cs.loadProfile);\n            commandStatPS.setInt(7, cs.appSync);\n            commandStatPS.setInt(8, cs.sharing);\n            commandStatPS.setInt(9, cs.getToken);\n            commandStatPS.setInt(10, cs.ping);\n            commandStatPS.setInt(11, cs.activate);\n            commandStatPS.setInt(12, cs.deactivate);\n            commandStatPS.setInt(13, cs.refreshToken);\n            commandStatPS.setInt(14, cs.getGraphData);\n            commandStatPS.setInt(15, cs.exportGraphData);\n            commandStatPS.setInt(16, cs.setWidgetProperty);\n            commandStatPS.setInt(17, cs.bridge);\n            commandStatPS.setInt(18, cs.hardware);\n            commandStatPS.setInt(19, cs.getSharedDash);\n            commandStatPS.setInt(20, cs.getShareToken);\n            commandStatPS.setInt(21, cs.refreshShareToken);\n            commandStatPS.setInt(22, cs.shareLogin);\n            commandStatPS.setInt(23, cs.createProject);\n            commandStatPS.setInt(24, cs.updateProject);\n            commandStatPS.setInt(25, cs.deleteProject);\n            commandStatPS.setInt(26, cs.hardwareSync);\n            commandStatPS.setInt(27, cs.internal);\n            commandStatPS.setInt(28, cs.sms);\n            commandStatPS.setInt(29, cs.tweet);\n            commandStatPS.setInt(30, cs.email);\n            commandStatPS.setInt(31, cs.push);\n            commandStatPS.setInt(32, cs.addPushToken);\n            commandStatPS.setInt(33, cs.createWidget);\n            commandStatPS.setInt(34, cs.updateWidget);\n            commandStatPS.setInt(35, cs.deleteWidget);\n            commandStatPS.setInt(36, cs.createDevice);\n            commandStatPS.setInt(37, cs.updateDevice);\n            commandStatPS.setInt(38, cs.deleteDevice);\n            commandStatPS.setInt(39, cs.getDevices);\n            commandStatPS.setInt(40, cs.createTag);\n            commandStatPS.setInt(41, cs.updateTag);\n            commandStatPS.setInt(42, cs.deleteTag);\n            commandStatPS.setInt(43, cs.getTags);\n            commandStatPS.setInt(44, cs.addEnergy);\n            commandStatPS.setInt(45, cs.getEnergy);\n            commandStatPS.setInt(46, cs.getServer);\n            commandStatPS.setInt(47, cs.connectRedirect);\n            commandStatPS.setInt(48, cs.webSockets);\n            commandStatPS.setInt(49, cs.eventor);\n            commandStatPS.setInt(50, cs.webhooks);\n            commandStatPS.setInt(51, cs.appTotal);\n            commandStatPS.setInt(52, cs.mqttTotal);\n            commandStatPS.executeUpdate();\n\n            connection.commit();\n        } catch (Exception e) {\n            log.error(\"Error inserting real time stat in DB.\", e);\n        }\n    }\n\n    public void insert(Map<AggregationKey, AggregationValue> map, GraphGranularityType graphGranularityType) {\n        long start = System.currentTimeMillis();\n\n        log.info(\"Storing {} reporting...\", graphGranularityType.name());\n\n        String insertSQL = getTableByGraphType(graphGranularityType);\n\n        try (Connection connection = ds.getConnection();\n             PreparedStatement ps = connection.prepareStatement(insertSQL)) {\n\n            for (Map.Entry<AggregationKey, AggregationValue> entry : map.entrySet()) {\n                prepareReportingInsert(ps, entry, graphGranularityType);\n                ps.addBatch();\n            }\n\n            ps.executeBatch();\n            connection.commit();\n        } catch (Exception e) {\n            log.error(\"Error inserting reporting data in DB.\", e);\n        }\n\n        log.info(\"Storing {} reporting finished. Time {}. Records saved {}\",\n                graphGranularityType.name(), System.currentTimeMillis() - start, map.size());\n    }\n\n    public void cleanOldReportingRecords(Instant now) {\n        log.info(\"Removing old reporting records...\");\n\n        int minuteRecordsRemoved = 0;\n        int hourRecordsRemoved = 0;\n\n        try (Connection connection = ds.getConnection();\n             PreparedStatement psMinute = connection.prepareStatement(deleteMinute);\n             PreparedStatement psHour = connection.prepareStatement(deleteHour)) {\n\n            //for minute table we store only data for last 24 hours\n            psMinute.setTimestamp(1, new Timestamp(now.minus(GraphPeriod.DAY.numberOfPoints + 1,\n                    ChronoUnit.MINUTES).toEpochMilli()), DateTimeUtils.UTC_CALENDAR);\n\n            //for hour table we store only data for last 3 months\n            psHour.setTimestamp(1, new Timestamp(now.minus(GraphPeriod.THREE_MONTHS.numberOfPoints + 1,\n                    ChronoUnit.HOURS).toEpochMilli()), DateTimeUtils.UTC_CALENDAR);\n\n            minuteRecordsRemoved = psMinute.executeUpdate();\n            hourRecordsRemoved = psHour.executeUpdate();\n\n            connection.commit();\n        } catch (Exception e) {\n            log.error(\"Error inserting reporting data in DB.\", e);\n        }\n        log.info(\"Removing finished. Minute records {}, hour records {}. Time {}\",\n                minuteRecordsRemoved, hourRecordsRemoved, System.currentTimeMillis() - now.toEpochMilli());\n    }\n\n}\n\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/db/dao/UserDBDao.java",
    "content": "package cc.blynk.server.db.dao;\n\nimport cc.blynk.server.core.dao.UserKey;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport com.zaxxer.hikari.HikariDataSource;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.sql.Connection;\nimport java.sql.PreparedStatement;\nimport java.sql.ResultSet;\nimport java.sql.SQLException;\nimport java.sql.Statement;\nimport java.sql.Timestamp;\nimport java.util.ArrayList;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ConcurrentMap;\n\nimport static cc.blynk.utils.DateTimeUtils.UTC_CALENDAR;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 09.03.16.\n */\npublic class UserDBDao {\n\n    private static final String upsertUser =\n            \"INSERT INTO users (email, appName, region, ip, name, pass, last_modified, last_logged,\"\n                    + \" last_logged_ip, is_facebook_user, is_super_admin, energy, json) \"\n                    + \"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (email, appName) DO UPDATE \"\n                    + \"SET ip = EXCLUDED.ip, pass = EXCLUDED.pass, name = EXCLUDED.name, \"\n                    + \"last_modified = EXCLUDED.last_modified, \"\n                    + \"last_logged = EXCLUDED.last_logged, last_logged_ip = EXCLUDED.last_logged_ip, \"\n                    + \"is_facebook_user = EXCLUDED.is_facebook_user, is_super_admin = EXCLUDED.is_super_admin, \"\n                    + \"energy = EXCLUDED.energy, json = EXCLUDED.json, region = EXCLUDED.region\";\n    private static final String selectAllUsers = \"SELECT * from users where region = ?\";\n    private static final String selectIpForUser = \"SELECT ip FROM users WHERE email = ? AND appName = ?\";\n    private static final String deleteUser = \"DELETE FROM users WHERE email = ? AND appName = ?\";\n\n    private static final Logger log = LogManager.getLogger(UserDBDao.class);\n    private final HikariDataSource ds;\n\n    public UserDBDao(HikariDataSource ds) {\n        this.ds = ds;\n    }\n\n    public int getDBVersion() throws Exception {\n        int dbVersion;\n        try (Connection connection = ds.getConnection();\n             Statement statement = connection.createStatement()) {\n\n            try (ResultSet rs = statement.executeQuery(\"SELECT current_setting('server_version_num')\")) {\n                rs.next();\n                dbVersion = rs.getInt(1);\n                connection.commit();\n            }\n        }\n        return dbVersion;\n    }\n\n    public String getUserServerIp(String email, String appName) {\n        String ip = null;\n\n        try (Connection connection = ds.getConnection();\n             PreparedStatement statement = connection.prepareStatement(selectIpForUser)) {\n\n            statement.setString(1, email);\n            statement.setString(2, appName);\n\n            try (ResultSet rs = statement.executeQuery()) {\n                while (rs.next()) {\n                    ip = rs.getString(\"ip\");\n                }\n                connection.commit();\n            }\n        } catch (Exception e) {\n            log.error(\"Error getting user server ip. {}-{}. Reason : {}\", email, appName, e.getMessage());\n        }\n\n        return ip;\n    }\n\n    public ConcurrentMap<UserKey, User> getAllUsers(String region) throws Exception {\n        ConcurrentMap<UserKey, User> users = new ConcurrentHashMap<>();\n\n        try (Connection connection = ds.getConnection();\n             PreparedStatement statement = connection.prepareStatement(selectAllUsers)) {\n\n            statement.setString(1, region);\n            try (ResultSet rs = statement.executeQuery()) {\n                while (rs.next()) {\n                    User user = new User(\n                            rs.getString(\"email\"),\n                            rs.getString(\"pass\"),\n                            rs.getString(\"appName\"),\n                            rs.getString(\"region\"),\n                            rs.getString(\"ip\"),\n                            rs.getBoolean(\"is_facebook_user\"),\n                            rs.getBoolean(\"is_super_admin\"),\n                            rs.getString(\"name\"),\n                            getTs(rs, \"last_modified\"),\n                            getTs(rs, \"last_logged\"),\n                            rs.getString(\"last_logged_ip\"),\n                            JsonParser.parseProfileFromString(rs.getString(\"json\")),\n                            rs.getInt(\"energy\")\n                            );\n\n                    users.put(new UserKey(user), user);\n                }\n                connection.commit();\n            }\n        }\n\n        log.info(\"Loaded {} users.\", users.size());\n\n        return users;\n    }\n\n    private static long getTs(ResultSet rs, String fieldName) throws SQLException {\n        Timestamp t = rs.getTimestamp(fieldName, UTC_CALENDAR);\n        return t == null ? 0 : t.getTime();\n    }\n\n    public void save(ArrayList<User> users) {\n        long start = System.currentTimeMillis();\n        log.info(\"Storing users...\");\n\n        try (Connection connection = ds.getConnection();\n             PreparedStatement ps = connection.prepareStatement(upsertUser)) {\n\n            for (User user : users) {\n                ps.setString(1, user.email);\n                ps.setString(2, user.appName);\n                ps.setString(3, user.region);\n                ps.setString(4, user.ip);\n                ps.setString(5, user.name);\n                ps.setString(6, user.pass);\n                ps.setTimestamp(7, new Timestamp(user.lastModifiedTs), UTC_CALENDAR);\n                ps.setTimestamp(8, new Timestamp(user.lastLoggedAt), UTC_CALENDAR);\n                ps.setString(9, user.lastLoggedIP); //finish\n                ps.setBoolean(10, user.isFacebookUser);\n                ps.setBoolean(11, user.isSuperAdmin);\n                ps.setInt(12, user.energy);\n                ps.setString(13, user.profile.toString());\n                ps.addBatch();\n            }\n\n            ps.executeBatch();\n            connection.commit();\n        } catch (Exception e) {\n            log.error(\"Error upserting users in DB.\", e);\n        }\n        log.info(\"Storing users finished. Time {}. Users saved {}\", System.currentTimeMillis() - start, users.size());\n    }\n\n    public boolean deleteUser(UserKey userKey) {\n        int removed = 0;\n\n        try (Connection connection = ds.getConnection();\n             PreparedStatement ps = connection.prepareStatement(deleteUser)) {\n\n            ps.setString(1, userKey.email);\n            ps.setString(2, userKey.appName);\n\n            removed = ps.executeUpdate();\n\n            connection.commit();\n        } catch (Exception e) {\n            log.error(\"Error removing user {} from DB.\", userKey, e);\n        }\n\n        return removed > 0;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/db/model/FlashedToken.java",
    "content": "package cc.blynk.server.db.model;\n\nimport java.util.Date;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 03.03.16.\n */\npublic class FlashedToken {\n\n    public final String token;\n\n    public final String appId;\n\n    public final String email;\n\n    public final int dashId;\n\n    public final int deviceId;\n\n    public boolean isActivated;\n\n    public Date ts;\n\n    public FlashedToken(String email, String token, String appId, int dashId, int deviceId) {\n        this.email = email;\n        this.token = token;\n        this.appId = appId;\n        this.dashId = dashId;\n        this.deviceId = deviceId;\n    }\n\n    public FlashedToken(String token, String appId, String email, int dashId,\n                        int deviceId, boolean isActivated, Date ts) {\n        this.token = token;\n        this.appId = appId;\n        this.email = email;\n        this.dashId = dashId;\n        this.deviceId = deviceId;\n        this.isActivated = isActivated;\n        this.ts = ts;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (!(o instanceof FlashedToken)) {\n            return false;\n        }\n\n        FlashedToken that = (FlashedToken) o;\n\n        if (deviceId != that.deviceId) {\n            return false;\n        }\n        if (token != null ? !token.equals(that.token) : that.token != null) {\n            return false;\n        }\n        return !(appId != null ? !appId.equals(that.appId) : that.appId != null);\n\n    }\n\n    @Override\n    public int hashCode() {\n        int result = token != null ? token.hashCode() : 0;\n        result = 31 * result + (appId != null ? appId.hashCode() : 0);\n        result = 31 * result + deviceId;\n        return result;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/db/model/Purchase.java",
    "content": "package cc.blynk.server.db.model;\n\nimport java.util.Date;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 24.03.16.\n */\npublic class Purchase {\n\n    public final String email;\n\n    public final int reward;\n\n    public final String transactionId;\n\n    public final double price;\n\n    public Date date;\n\n    public Purchase(String email, int reward, double price, String transactionId) {\n        this.email = email;\n        this.reward = reward;\n        this.transactionId = transactionId;\n        this.price = price;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/db/model/Redeem.java",
    "content": "package cc.blynk.server.db.model;\n\nimport java.util.Date;\nimport java.util.StringJoiner;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 03.03.16.\n */\npublic class Redeem {\n\n    public String token;\n\n    public String company;\n\n    public final boolean isRedeemed;\n\n    public String email;\n\n    public int reward;\n\n    public final int version;\n\n    public Date ts;\n\n    public String formatToken() {\n        return new StringJoiner(\"+\")\n                .add(company)\n                .add(String.valueOf(reward))\n                .add(token).toString();\n    }\n\n    public String formatToken(String title, String text) {\n        return new StringJoiner(\"+\")\n                .add(company)\n                .add(String.valueOf(reward))\n                .add(token)\n                .add(title)\n                .add(text)\n                .toString();\n    }\n\n    public Redeem() {\n        this.isRedeemed = false;\n        this.version = 1;\n    }\n\n    public Redeem(String token, String company, int reward) {\n        this();\n        this.token = token;\n        this.company = company;\n        this.reward = reward;\n    }\n\n    public Redeem(String token, String company, boolean isRedeemed, String email, int reward, int version, Date ts) {\n        this.token = token;\n        this.company = company;\n        this.isRedeemed = isRedeemed;\n        this.email = email;\n        this.reward = reward;\n        this.version = version;\n        this.ts = ts;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/internal/CommonByteBufUtil.java",
    "content": "package cc.blynk.server.internal;\n\nimport cc.blynk.server.core.protocol.model.messages.BinaryMessage;\nimport cc.blynk.server.core.protocol.model.messages.ResponseMessage;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\n\nimport java.nio.charset.StandardCharsets;\n\nimport static cc.blynk.server.core.protocol.enums.Command.DEVICE_OFFLINE;\nimport static cc.blynk.server.core.protocol.enums.Response.DEVICE_NOT_IN_NETWORK;\nimport static cc.blynk.server.core.protocol.enums.Response.ENERGY_LIMIT;\nimport static cc.blynk.server.core.protocol.enums.Response.FACEBOOK_USER_LOGIN_WITH_PASS;\nimport static cc.blynk.server.core.protocol.enums.Response.ILLEGAL_COMMAND;\nimport static cc.blynk.server.core.protocol.enums.Response.ILLEGAL_COMMAND_BODY;\nimport static cc.blynk.server.core.protocol.enums.Response.INVALID_TOKEN;\nimport static cc.blynk.server.core.protocol.enums.Response.NOTIFICATION_ERROR;\nimport static cc.blynk.server.core.protocol.enums.Response.NOTIFICATION_INVALID_BODY;\nimport static cc.blynk.server.core.protocol.enums.Response.NOTIFICATION_NOT_AUTHORIZED;\nimport static cc.blynk.server.core.protocol.enums.Response.NOT_ALLOWED;\nimport static cc.blynk.server.core.protocol.enums.Response.NO_ACTIVE_DASHBOARD;\nimport static cc.blynk.server.core.protocol.enums.Response.NO_DATA;\nimport static cc.blynk.server.core.protocol.enums.Response.OK;\nimport static cc.blynk.server.core.protocol.enums.Response.QUOTA_LIMIT;\nimport static cc.blynk.server.core.protocol.enums.Response.SERVER_ERROR;\nimport static cc.blynk.server.core.protocol.enums.Response.USER_ALREADY_REGISTERED;\nimport static cc.blynk.server.core.protocol.enums.Response.USER_NOT_AUTHENTICATED;\nimport static cc.blynk.server.core.protocol.enums.Response.USER_NOT_REGISTERED;\nimport static cc.blynk.utils.StringUtils.DEVICE_SEPARATOR;\n\n/**\n * Utility class that creates native netty buffers instead of java objects.\n * This is done in order to allocate less java objects and reduce GC pauses and load.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 10.03.16.\n */\npublic final class CommonByteBufUtil {\n\n    private CommonByteBufUtil() {\n    }\n\n    public static ResponseMessage energyLimit(int msgId) {\n        return makeResponse(msgId, ENERGY_LIMIT);\n    }\n\n    public static ResponseMessage notificationInvalidBody(int msgId) {\n        return makeResponse(msgId, NOTIFICATION_INVALID_BODY);\n    }\n\n    public static ResponseMessage notificationError(int msgId) {\n        return makeResponse(msgId, NOTIFICATION_ERROR);\n    }\n\n    public static ResponseMessage deviceNotInNetwork(int msgId) {\n        return makeResponse(msgId, DEVICE_NOT_IN_NETWORK);\n    }\n\n    public static ResponseMessage noActiveDash(int msgId) {\n        return makeResponse(msgId, NO_ACTIVE_DASHBOARD);\n    }\n\n    public static ResponseMessage notAllowed(int msgId) {\n        return makeResponse(msgId, NOT_ALLOWED);\n    }\n\n    public static ResponseMessage illegalCommandBody(int msgId) {\n        return makeResponse(msgId, ILLEGAL_COMMAND_BODY);\n    }\n\n    public static ResponseMessage illegalCommand(int msgId) {\n        return makeResponse(msgId, ILLEGAL_COMMAND);\n    }\n\n    public static ResponseMessage invalidToken(int msgId) {\n        return makeResponse(msgId, INVALID_TOKEN);\n    }\n\n    public static ResponseMessage alreadyRegistered(int msgId) {\n        return makeResponse(msgId, USER_ALREADY_REGISTERED);\n    }\n\n    public static ResponseMessage serverError(int msgId) {\n        return makeResponse(msgId, SERVER_ERROR);\n    }\n\n    public static ResponseMessage quotaLimit(int msgId) {\n        return makeResponse(msgId, QUOTA_LIMIT);\n    }\n\n    public static ResponseMessage noData(int msgId) {\n        return makeResponse(msgId, NO_DATA);\n    }\n\n    public static ResponseMessage ok(int msgId) {\n        return makeResponse(msgId, OK);\n    }\n\n    public static ResponseMessage notRegistered(int msgId) {\n        return makeResponse(msgId, USER_NOT_REGISTERED);\n    }\n\n    public static ResponseMessage facebookUserLoginWithPass(int msgId) {\n        return makeResponse(msgId, FACEBOOK_USER_LOGIN_WITH_PASS);\n    }\n\n    public static ResponseMessage notAuthenticated(int msgId) {\n        return makeResponse(msgId, USER_NOT_AUTHENTICATED);\n    }\n\n    public static ResponseMessage notificationNotAuthorized(int msgId) {\n        return makeResponse(msgId, NOTIFICATION_NOT_AUTHORIZED);\n    }\n\n    public static ResponseMessage makeResponse(int msgId, int responseCode) {\n        return new ResponseMessage(msgId, responseCode);\n    }\n\n    public static StringMessage deviceOffline(int dashId, int deviceId) {\n        return makeASCIIStringMessage(DEVICE_OFFLINE, 0,\n                String.valueOf(dashId) + DEVICE_SEPARATOR + deviceId);\n    }\n\n    public static StringMessage makeUTF8StringMessage(short cmd, int msgId, String data) {\n        return new StringMessage(msgId, cmd, data);\n    }\n\n    public static StringMessage makeASCIIStringMessage(short cmd, int msgId, String data) {\n        return new StringMessage(msgId, cmd, data, StandardCharsets.US_ASCII);\n    }\n\n    public static BinaryMessage makeBinaryMessage(short cmd, int msgId, byte[] byteData) {\n        return new BinaryMessage(msgId, cmd, byteData);\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/internal/EmptyArraysUtil.java",
    "content": "package cc.blynk.server.internal;\n\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.App;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.device.Tag;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.outputs.graph.GraphDataStream;\nimport cc.blynk.server.core.model.widgets.ui.reporting.Report;\nimport cc.blynk.server.core.model.widgets.ui.reporting.source.ReportDataStream;\nimport cc.blynk.server.core.model.widgets.ui.reporting.source.ReportSource;\nimport cc.blynk.server.core.model.widgets.ui.tiles.Tile;\nimport cc.blynk.server.core.model.widgets.ui.tiles.TileTemplate;\nimport io.netty.util.internal.EmptyArrays;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 30.09.17.\n */\npublic final class EmptyArraysUtil {\n\n    private EmptyArraysUtil() {\n    }\n\n    public static final int[] EMPTY_INTS = {};\n    public static final DashBoard[] EMPTY_DASHBOARDS = {};\n    public static final Tag[] EMPTY_TAGS = {};\n    public static final Device[] EMPTY_DEVICES = {};\n    public static final Widget[] EMPTY_WIDGETS = {};\n    public static final TileTemplate[] EMPTY_TEMPLATES = {};\n    public static final Tile[] EMPTY_DEVICE_TILES = {};\n    public static final byte[] EMPTY_BYTES = EmptyArrays.EMPTY_BYTES;\n    public static final App[] EMPTY_APPS = {};\n    public static final GraphDataStream[] EMPTY_GRAPH_DATA_STREAMS = {};\n    public static final ReportDataStream[] EMPTY_REPORT_DATA_STREAMS = {};\n    public static final ReportSource[] EMPTY_REPORT_SOURCES = {};\n    public static final Report[] EMPTY_REPORTS = {};\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/internal/QuotaLimitChecker.java",
    "content": "package cc.blynk.server.internal;\n\nimport cc.blynk.server.core.protocol.enums.Response;\nimport cc.blynk.server.core.stats.metrics.InstanceLoadMeter;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeResponse;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 10.03.18.\n */\npublic class QuotaLimitChecker {\n\n    private final static Logger log = LogManager.getLogger(QuotaLimitChecker.class);\n\n    /*\n    * in case of consistent quota limit exceed during long term, sending warning response back to exceeding channel\n    * for performance reason sending only 1 message within interval. In millis\n    *\n    * this property was never changed, so moving it to static field\n    */\n    private final static int USER_QUOTA_LIMIT_WARN_PERIOD = 60_000;\n\n    private final int userQuotaLimit;\n    private long lastQuotaExceededTime;\n    public final InstanceLoadMeter quotaMeter;\n\n    public QuotaLimitChecker(int userQuotaLimit) {\n        this.userQuotaLimit = userQuotaLimit;\n        this.quotaMeter = new InstanceLoadMeter();\n    }\n\n    public boolean quotaReached(ChannelHandlerContext ctx, int msgId) {\n        if (quotaMeter.getOneMinuteRate() > userQuotaLimit) {\n            sendErrorResponseIfTicked(ctx, msgId);\n            return true;\n        }\n        quotaMeter.mark();\n        return false;\n    }\n\n    private void sendErrorResponseIfTicked(ChannelHandlerContext ctx, int msgId) {\n        long now = System.currentTimeMillis();\n        //once a minute sending user response message in case limit is exceeded constantly\n        if (lastQuotaExceededTime + USER_QUOTA_LIMIT_WARN_PERIOD < now) {\n            lastQuotaExceededTime = now;\n            log.debug(\"User has exceeded message quota limit.\");\n            if (ctx.channel().isWritable()) {\n                ctx.channel().writeAndFlush(makeResponse(msgId, Response.QUOTA_LIMIT), ctx.voidPromise());\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/internal/ReregisterChannelUtil.java",
    "content": "package cc.blynk.server.internal;\n\nimport cc.blynk.server.core.model.auth.Session;\nimport io.netty.channel.ChannelFuture;\nimport io.netty.channel.ChannelFutureListener;\nimport io.netty.channel.ChannelHandlerContext;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 17.10.15.\n */\npublic final class ReregisterChannelUtil {\n\n    private ReregisterChannelUtil() {\n    }\n\n    public static void reRegisterChannel(ChannelHandlerContext ctx,\n                                         Session session, ChannelFutureListener completeHandler) {\n        ChannelFuture cf = ctx.deregister();\n        cf.addListener(new ChannelFutureListener() {\n            @Override\n            public void operationComplete(ChannelFuture channelFuture) {\n                session.initialEventLoop.register(channelFuture.channel()).addListener(completeHandler);\n            }\n        });\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/internal/SerializationUtil.java",
    "content": "package cc.blynk.server.internal;\n\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.ObjectInputStream;\nimport java.io.ObjectOutputStream;\nimport java.io.OutputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 19.09.18.\n */\npublic final class SerializationUtil {\n\n    private final static Logger log = LogManager.getLogger(SerializationUtil.class);\n\n    private SerializationUtil() {\n    }\n\n    public static Object deserialize(Path path) {\n        if (Files.exists(path)) {\n            try {\n                return deserializeObject(path);\n            } catch (Exception e) {\n                log.error(e);\n            }\n        }\n\n        return new ConcurrentHashMap<>();\n    }\n\n    public static void serialize(Path path, Map<?, ?> map) {\n        if (map.size() > 0) {\n            try {\n                serializeObject(path, map);\n            } catch (Exception e) {\n                log.error(e);\n            }\n        }\n    }\n\n    private static Object deserializeObject(Path path) throws IOException, ClassNotFoundException {\n        try (InputStream is = Files.newInputStream(path);\n             ObjectInputStream objectinputstream = new ObjectInputStream(is)) {\n            return objectinputstream.readObject();\n        }\n    }\n\n    private static void serializeObject(Path path, Object obj) throws IOException {\n        try (OutputStream os = Files.newOutputStream(path);\n             ObjectOutputStream oos = new ObjectOutputStream(os)) {\n            oos.writeObject(obj);\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/internal/StateHolderUtil.java",
    "content": "package cc.blynk.server.internal;\n\nimport cc.blynk.server.common.BaseSimpleChannelInboundHandler;\nimport cc.blynk.server.core.session.HardwareStateHolder;\nimport io.netty.channel.Channel;\n\n/**\n * Used instead of Netty's DefaultAttributeMap as it faster and\n * doesn't involves any synchronization at all.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 13.09.15.\n */\npublic final class StateHolderUtil {\n\n    private StateHolderUtil() {\n    }\n\n    public static HardwareStateHolder getHardState(Channel channel) {\n        BaseSimpleChannelInboundHandler handler = channel.pipeline().get(BaseSimpleChannelInboundHandler.class);\n        return handler == null ? null : (HardwareStateHolder) handler.getState();\n    }\n\n    public static boolean isSameDash(Channel channel, int dashId) {\n        BaseSimpleChannelInboundHandler handler = channel.pipeline().get(BaseSimpleChannelInboundHandler.class);\n        return handler != null && handler.getState().isSameDash(dashId);\n    }\n\n    public static boolean isSameDashAndDeviceId(Channel channel, int dashId, int deviceId) {\n        BaseSimpleChannelInboundHandler handler = channel.pipeline().get(BaseSimpleChannelInboundHandler.class);\n        return handler != null && handler.getState().isSameDashAndDeviceId(dashId, deviceId);\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/internal/token/BaseToken.java",
    "content": "package cc.blynk.server.internal.token;\n\nimport java.io.Serializable;\nimport java.util.concurrent.TimeUnit;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 12.10.18.\n */\npublic abstract class BaseToken implements Serializable {\n\n    public final String email;\n    private final long expireAt;\n    static final long DEFAULT_EXPIRE_TIME = TimeUnit.MINUTES.toMillis(45);\n\n    BaseToken(String email, long tokenExpirationPeriodMillis) {\n        this.email = email;\n        this.expireAt = System.currentTimeMillis() + tokenExpirationPeriodMillis;\n    }\n\n    boolean isExpired(long now) {\n        return expireAt < now;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/internal/token/ResetPassToken.java",
    "content": "package cc.blynk.server.internal.token;\n\nimport java.io.Serializable;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 12.10.18.\n */\npublic final class ResetPassToken extends BaseToken implements Serializable {\n\n    public final String appName;\n\n    public ResetPassToken(String email, String appName) {\n        super(email, DEFAULT_EXPIRE_TIME);\n        this.appName = appName;\n    }\n\n    @Override\n    public String toString() {\n        return \"ResetPassToken{\"\n                + \"email='\" + email + '\\''\n                + \", appName='\" + appName + '\\''\n                + '}';\n    }\n\n    public boolean isSame(String email, String appName) {\n        return this.email.equals(email) && this.appName.equals(appName);\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/internal/token/TokensPool.java",
    "content": "package cc.blynk.server.internal.token;\n\nimport cc.blynk.utils.FileUtils;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.io.Closeable;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\n\nimport static cc.blynk.server.internal.SerializationUtil.deserialize;\nimport static cc.blynk.server.internal.SerializationUtil.serialize;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 12.10.18.\n */\npublic final class TokensPool implements Closeable {\n\n    private static final Logger log = LogManager.getLogger(TokensPool.class);\n    private static final String TOKENS_TEMP_FILENAME = \"tokens_pool_temp.bin\";\n\n    private final String dataFolder;\n    private final ConcurrentHashMap<String, BaseToken> tokens;\n\n    @SuppressWarnings(\"unchecked\")\n    public TokensPool(String dataFolder) {\n        this.dataFolder = dataFolder;\n\n        Path path = Paths.get(dataFolder, TOKENS_TEMP_FILENAME);\n        this.tokens = (ConcurrentHashMap<String, BaseToken>) deserialize(path);\n        FileUtils.deleteQuietly(path);\n    }\n\n    public void addToken(String token, ResetPassToken user) {\n        log.info(\"Adding token for {} user to the pool\", user.email);\n        cleanupOldTokens();\n        tokens.put(token, user);\n    }\n\n    public ResetPassToken getResetPassToken(String token) {\n        BaseToken baseToken = getBaseToken(token);\n        if (baseToken instanceof ResetPassToken) {\n            return (ResetPassToken) baseToken;\n        }\n        return null;\n    }\n\n    public BaseToken getBaseToken(String token) {\n        cleanupOldTokens();\n        return tokens.get(token);\n    }\n\n    public boolean hasResetToken(String email, String appName) {\n        for (Map.Entry<String, BaseToken> entry : tokens.entrySet()) {\n            BaseToken tokenBase = entry.getValue();\n            if (tokenBase instanceof ResetPassToken) {\n                ResetPassToken resetPassToken = (ResetPassToken) tokenBase;\n                if (resetPassToken.isSame(email, appName)) {\n                    return true;\n                }\n            }\n        }\n        return false;\n    }\n\n    public void removeToken(String token) {\n        tokens.remove(token);\n    }\n\n    public int size() {\n        return tokens.size();\n    }\n\n    public void cleanupOldTokens() {\n        long now = System.currentTimeMillis();\n        tokens.entrySet().removeIf(entry -> entry.getValue().isExpired(now));\n    }\n\n    //just for tests\n    public ConcurrentHashMap<String, BaseToken> getTokens() {\n        return tokens;\n    }\n\n    @Override\n    public void close() {\n        serialize(Paths.get(dataFolder, TOKENS_TEMP_FILENAME), tokens);\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/transport/TransportTypeHolder.java",
    "content": "package cc.blynk.server.transport;\n\nimport cc.blynk.utils.properties.ServerProperties;\nimport io.netty.channel.EventLoopGroup;\nimport io.netty.channel.ServerChannel;\nimport io.netty.channel.epoll.Epoll;\nimport io.netty.channel.epoll.EpollEventLoopGroup;\nimport io.netty.channel.epoll.EpollServerSocketChannel;\nimport io.netty.channel.nio.NioEventLoopGroup;\nimport io.netty.channel.socket.nio.NioServerSocketChannel;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.io.Closeable;\n\n/**\n * Used in order to re-use EventLoopGroups, this is done for performance reasons.\n * To create less threads and minimize memory footprint (recommended way by netty devs)\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 25.04.15.\n */\npublic class TransportTypeHolder implements Closeable {\n\n    private static final Logger log = LogManager.getLogger(TransportTypeHolder.class);\n\n    public final EventLoopGroup bossGroup;\n    public final EventLoopGroup workerGroup;\n    public final Class<? extends ServerChannel> channelClass;\n\n    public TransportTypeHolder(ServerProperties serverProperties) {\n        this(serverProperties.getIntProperty(\"server.worker.threads\", Runtime.getRuntime().availableProcessors() * 2));\n    }\n\n    private TransportTypeHolder(int workerThreads) {\n        if (Epoll.isAvailable()) {\n            log.info(\"Using native epoll transport.\");\n            bossGroup = new EpollEventLoopGroup(1);\n            workerGroup = new EpollEventLoopGroup(workerThreads);\n            channelClass = EpollServerSocketChannel.class;\n        } else {\n            bossGroup = new NioEventLoopGroup(1);\n            workerGroup = new NioEventLoopGroup(workerThreads);\n            channelClass = NioServerSocketChannel.class;\n        }\n    }\n\n    @Override\n    public void close() {\n        System.out.println(\"Stopping Transport Holder...\");\n        if (bossGroup != null) {\n            bossGroup.shutdownGracefully();\n        }\n        if (workerGroup != null) {\n            workerGroup.shutdownGracefully();\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/workers/ReadingWidgetsWorker.java",
    "content": "package cc.blynk.server.workers;\n\nimport cc.blynk.server.core.dao.SessionDao;\nimport cc.blynk.server.core.dao.UserDao;\nimport cc.blynk.server.core.dao.UserKey;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.Profile;\nimport cc.blynk.server.core.model.auth.Session;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Tag;\nimport cc.blynk.server.core.model.widgets.FrequencyWidget;\nimport cc.blynk.server.core.model.widgets.Target;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.ui.DeviceSelector;\nimport cc.blynk.server.core.model.widgets.ui.tiles.DeviceTiles;\nimport cc.blynk.server.core.model.widgets.ui.tiles.Tile;\nimport cc.blynk.server.core.model.widgets.ui.tiles.TileTemplate;\nimport cc.blynk.server.core.session.HardwareStateHolder;\nimport cc.blynk.server.internal.StateHolderUtil;\nimport io.netty.channel.Channel;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.util.Map;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 02.02.17.\n */\npublic class ReadingWidgetsWorker implements Runnable {\n\n    private static final Logger log = LogManager.getLogger(ReadingWidgetsWorker.class);\n\n    private final SessionDao sessionDao;\n    private final UserDao userDao;\n    private final boolean allowRunWithoutApp;\n\n    private int tickedWidgets = 0;\n    private int counter = 0;\n    private long totalTime = 0;\n\n    public ReadingWidgetsWorker(SessionDao sessionDao, UserDao userDao, boolean allowRunWithoutApp) {\n        this.sessionDao = sessionDao;\n        this.userDao = userDao;\n        this.allowRunWithoutApp = allowRunWithoutApp;\n    }\n\n    @Override\n    public void run() {\n        long now = System.currentTimeMillis();\n        try {\n            process(now);\n            totalTime += System.currentTimeMillis() - now;\n        } catch (Exception e) {\n            log.error(\"Error processing reading widgets. \", e);\n        }\n\n        counter++;\n        if (counter == 60) {\n            log.info(\"Ticked widgets for 1 minute : {}. Per second : {}, total time : {} ms\",\n                    tickedWidgets, tickedWidgets / 60, totalTime);\n            tickedWidgets = 0;\n            counter = 0;\n            totalTime = 0;\n        }\n    }\n\n    private void process(long now) {\n        for (Map.Entry<UserKey, Session> entry : sessionDao.userSession.entrySet()) {\n            Session session = entry.getValue();\n            //for now checking widgets for active app only\n            if ((allowRunWithoutApp || session.isAppConnected()) && session.isHardwareConnected()) {\n                UserKey userKey = entry.getKey();\n                User user = userDao.users.get(userKey);\n                if (user != null) {\n                    Profile profile = user.profile;\n                    for (DashBoard dashBoard : profile.dashBoards) {\n                        if (dashBoard.isActive) {\n                            for (Channel channel : session.hardwareChannels) {\n                                HardwareStateHolder stateHolder = StateHolderUtil.getHardState(channel);\n                                if (stateHolder != null && stateHolder.dash.id == dashBoard.id) {\n                                    int deviceId = stateHolder.device.id;\n                                    for (Widget widget : dashBoard.widgets) {\n                                        if (widget instanceof FrequencyWidget) {\n                                            process(channel, (FrequencyWidget) widget,\n                                                    profile, dashBoard, deviceId, now);\n                                        } else if (widget instanceof DeviceTiles) {\n                                            processDeviceTile(channel, (DeviceTiles) widget, deviceId, now);\n                                        }\n                                    }\n                                    channel.flush();\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    private void processDeviceTile(Channel channel,  DeviceTiles deviceTiles, int deviceId, long now) {\n        for (Tile tile : deviceTiles.tiles) {\n            if (tile.deviceId == deviceId && tile.isTicked(now)) {\n                TileTemplate tileTemplate = deviceTiles.getTileTemplateById(tile.templateId);\n                if (tileTemplate != null) {\n                    for (Widget tileWidget : tileTemplate.widgets) {\n                        if (tileWidget instanceof FrequencyWidget) {\n                            FrequencyWidget frequencyWidget = (FrequencyWidget) tileWidget;\n                            if (frequencyWidget.hasReadingInterval() && channel.isWritable()) {\n                                frequencyWidget.writeReadingCommand(channel);\n                                tickedWidgets++;\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    private void process(Channel channel, FrequencyWidget frequencyWidget,\n                         Profile profile, DashBoard dashBoard, int deviceId, long now) {\n        if (channel.isWritable()\n                && sameDeviceId(profile, dashBoard, frequencyWidget.getDeviceId(), deviceId)\n                && frequencyWidget.isTicked(now)) {\n            frequencyWidget.writeReadingCommand(channel);\n            tickedWidgets++;\n        }\n    }\n\n    private boolean sameDeviceId(Profile profile, DashBoard dash, int targetId, int channelDeviceId) {\n        Target target;\n        if (targetId < Tag.START_TAG_ID) {\n            target = profile.getDeviceById(dash, targetId);\n        } else if (targetId < DeviceSelector.DEVICE_SELECTOR_STARTING_ID) {\n            target = profile.getTagById(dash, targetId);\n        } else {\n            //means widget assigned to device selector widget.\n            target = dash.getDeviceSelector(targetId);\n        }\n        return target != null && target.isSelected(channelDeviceId);\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/workers/timer/TimerKey.java",
    "content": "package cc.blynk.server.workers.timer;\n\nimport cc.blynk.server.core.dao.UserKey;\nimport cc.blynk.server.core.model.widgets.others.eventor.TimerTime;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 27.12.16.\n */\npublic class TimerKey {\n\n    public final UserKey userKey;\n\n    public final int dashId;\n\n    public final int deviceId;\n\n    public final long widgetId;\n\n    public final int additionalId;\n\n    public final long deviceTilesId;\n\n    public final long templateId;\n\n    public final TimerTime time;\n\n    public TimerKey(UserKey userKey, int dashId, int deviceId, long widgetId,\n                    int additionalId, long deviceTilesId, long templateId, TimerTime time) {\n        this.userKey = userKey;\n        this.dashId = dashId;\n        this.deviceId = deviceId;\n        this.widgetId = widgetId;\n        this.additionalId = additionalId;\n        this.deviceTilesId = deviceTilesId;\n        this.templateId = templateId;\n        this.time = time;\n    }\n\n    public boolean isTilesTimer() {\n        return deviceTilesId != -1 && templateId != -1;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (!(o instanceof TimerKey)) {\n            return false;\n        }\n\n        TimerKey timerKey = (TimerKey) o;\n\n        if (dashId != timerKey.dashId) {\n            return false;\n        }\n        if (deviceId != timerKey.deviceId) {\n            return false;\n        }\n        if (widgetId != timerKey.widgetId) {\n            return false;\n        }\n        if (additionalId != timerKey.additionalId) {\n            return false;\n        }\n        if (deviceTilesId != timerKey.deviceTilesId) {\n            return false;\n        }\n        if (templateId != timerKey.templateId) {\n            return false;\n        }\n        if (userKey != null ? !userKey.equals(timerKey.userKey) : timerKey.userKey != null) {\n            return false;\n        }\n        return time != null ? time.equals(timerKey.time) : timerKey.time == null;\n    }\n\n    @Override\n    public int hashCode() {\n        int result = userKey != null ? userKey.hashCode() : 0;\n        result = 31 * result + dashId;\n        result = 31 * result + deviceId;\n        result = 31 * result + (int) (widgetId ^ (widgetId >>> 32));\n        result = 31 * result + additionalId;\n        result = 31 * result + (int) (deviceTilesId ^ (deviceTilesId >>> 32));\n        result = 31 * result + (int) (templateId ^ (templateId >>> 32));\n        result = 31 * result + (time != null ? time.hashCode() : 0);\n        return result;\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/cc/blynk/server/workers/timer/TimerWorker.java",
    "content": "package cc.blynk.server.workers.timer;\n\nimport cc.blynk.server.core.dao.SessionDao;\nimport cc.blynk.server.core.dao.UserDao;\nimport cc.blynk.server.core.dao.UserKey;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.Profile;\nimport cc.blynk.server.core.model.auth.Session;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Tag;\nimport cc.blynk.server.core.model.widgets.Target;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.controls.Timer;\nimport cc.blynk.server.core.model.widgets.others.eventor.Eventor;\nimport cc.blynk.server.core.model.widgets.others.eventor.Rule;\nimport cc.blynk.server.core.model.widgets.others.eventor.TimerTime;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.action.BaseAction;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.action.SetPinAction;\nimport cc.blynk.server.core.model.widgets.others.eventor.model.action.notification.NotifyAction;\nimport cc.blynk.server.core.model.widgets.ui.DeviceSelector;\nimport cc.blynk.server.core.model.widgets.ui.tiles.DeviceTiles;\nimport cc.blynk.server.core.model.widgets.ui.tiles.Tile;\nimport cc.blynk.server.core.model.widgets.ui.tiles.TileTemplate;\nimport cc.blynk.server.core.processors.EventorProcessor;\nimport cc.blynk.server.notifications.push.GCMWrapper;\nimport cc.blynk.utils.DateTimeUtils;\nimport cc.blynk.utils.IntArray;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.time.ZonedDateTime;\nimport java.util.ArrayList;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ConcurrentMap;\nimport java.util.concurrent.atomic.AtomicReferenceArray;\n\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE;\nimport static cc.blynk.server.internal.EmptyArraysUtil.EMPTY_INTS;\n\n/**\n * Timer worker class responsible for triggering all timers at specified time.\n * Current implementation is some kind of Hashed Wheel Timer.\n * In general idea is very simple :\n *\n * Select timers at specified cell timer[secondsOfDayNow]\n * and run it one by one, instead of naive implementation\n * with iteration over all profiles every second\n *\n * + Concurrency around it as timerWorker may be accessed from different threads.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/6/2015.\n *\n */\npublic class TimerWorker implements Runnable {\n\n    private static final Logger log = LogManager.getLogger(TimerWorker.class);\n    public static final int TIMER_MSG_ID = 7777;\n\n    private final UserDao userDao;\n    private final SessionDao sessionDao;\n    private final GCMWrapper gcmWrapper;\n    private final AtomicReferenceArray<ConcurrentHashMap<TimerKey, BaseAction[]>> timerExecutors;\n    private final static int size = 86400;\n\n    @SuppressWarnings(\"unchecked\")\n    public TimerWorker(UserDao userDao, SessionDao sessionDao, GCMWrapper gcmWrapper) {\n        this.userDao = userDao;\n        this.sessionDao = sessionDao;\n        this.gcmWrapper = gcmWrapper;\n        //array cell for every second in a day,\n        //yes, it costs a bit of memory, but still cheap :)\n        this.timerExecutors = new AtomicReferenceArray<>(size);\n        init(userDao.users);\n    }\n\n    private void init(ConcurrentMap<UserKey, User> users) {\n        int counter = 0;\n        for (Map.Entry<UserKey, User> entry : users.entrySet()) {\n            for (DashBoard dash : entry.getValue().profile.dashBoards) {\n                int dashId = dash.id;\n                for (Widget widget : dash.widgets) {\n                    if (widget instanceof DeviceTiles) {\n                        DeviceTiles deviceTiles = (DeviceTiles) widget;\n                        counter += add(entry.getKey(), deviceTiles, dashId);\n                    } else if (widget instanceof Timer) {\n                        Timer timer = (Timer) widget;\n                        add(entry.getKey(), timer, dashId, -1, -1);\n                        counter++;\n                    } else if (widget instanceof Eventor) {\n                        Eventor eventor = (Eventor) widget;\n                        add(entry.getKey(), eventor, dashId);\n                        counter++;\n                    }\n                }\n            }\n        }\n        log.info(\"Timers : {}\", counter);\n    }\n\n    public int add(UserKey userKey, DeviceTiles deviceTiles, int dashId) {\n        int counter = 0;\n        for (TileTemplate template : deviceTiles.templates) {\n            for (Widget widgetInTemplate : template.widgets) {\n                if (widgetInTemplate instanceof Timer) {\n                    add(userKey, (Timer) widgetInTemplate, dashId, deviceTiles.id, template.id);\n                    counter++;\n                }\n            }\n        }\n        return counter;\n    }\n\n    public void add(UserKey userKey, Eventor eventor, int dashId) {\n        if (eventor.rules != null) {\n            for (Rule rule : eventor.rules) {\n                if (rule.isValidTimerRule()) {\n                    add(userKey, dashId, eventor.deviceId, eventor.id,\n                            rule.triggerTime.id, rule.triggerTime, rule.actions);\n                }\n            }\n        }\n    }\n\n    public void add(UserKey userKey, Timer timer, int dashId, long deviceTilesId, long templateId) {\n        if (timer.isValid()) {\n            if (timer.isValidStart()) {\n                TimerTime timerTime = new TimerTime(timer.startTime);\n                SetPinAction action = new SetPinAction(timer.pin, timer.pinType, timer.startValue);\n                TimerKey timerKey = new TimerKey(userKey, dashId, timer.deviceId, timer.id, 0,\n                        deviceTilesId, templateId, timerTime);\n                getExecutorOrCreate(timerTime.time).put(timerKey, new BaseAction[]{action});\n            }\n            if (timer.isValidStop()) {\n                TimerTime timerTime = new TimerTime(timer.stopTime);\n                SetPinAction action = new SetPinAction(timer.pin, timer.pinType, timer.stopValue);\n                TimerKey timerKey = new TimerKey(userKey, dashId, timer.deviceId, timer.id, 1,\n                        deviceTilesId, templateId, timerTime);\n                getExecutorOrCreate(timerTime.time).put(timerKey, new BaseAction[]{action});\n            }\n        }\n    }\n\n    private void add(UserKey userKey, int dashId, int deviceId, long widgetId,\n                     int additionalId, TimerTime time, BaseAction[] actions) {\n        ArrayList<BaseAction> validActions = new ArrayList<>(actions.length);\n        for (BaseAction action : actions) {\n            if (action.isValid()) {\n                validActions.add(action);\n            }\n        }\n        if (!validActions.isEmpty()) {\n            getExecutorOrCreate(time.time).put(\n                    new TimerKey(userKey, dashId, deviceId, widgetId, additionalId,\n                            -1L, -1L, time),\n                    validActions.toArray(new BaseAction[0]));\n        }\n    }\n\n    public void delete(UserKey userKey, Eventor eventor, int dashId) {\n        if (eventor.rules != null) {\n            for (Rule rule : eventor.rules) {\n                if (rule.isValidTimerRule()) {\n                    delete(userKey, dashId, eventor.deviceId,\n                            eventor.id, rule.triggerTime.id, -1L, -1L, rule.triggerTime);\n                }\n            }\n        }\n    }\n\n    public void delete(UserKey userKey, Timer timer, int dashId, long deviceTilesId, long templateId) {\n        if (Timer.isValidTime(timer.startTime)) {\n            delete(userKey, dashId, timer.deviceId, timer.id, 0,\n                    deviceTilesId, templateId, new TimerTime(timer.startTime));\n        }\n        if (Timer.isValidTime(timer.stopTime)) {\n            delete(userKey, dashId, timer.deviceId, timer.id, 1,\n                    deviceTilesId, templateId, new TimerTime(timer.stopTime));\n        }\n    }\n\n    private void delete(UserKey userKey, int dashId, int deviceId, long widgetId, int additionalId,\n                        long deviceTilesId, long templateId, TimerTime time) {\n        ConcurrentHashMap<TimerKey, BaseAction[]> secondExecutor = timerExecutors.get(time.time);\n        if (secondExecutor != null) {\n            secondExecutor.remove(new TimerKey(userKey, dashId, deviceId,\n                    widgetId, additionalId,\n                    deviceTilesId, templateId, time));\n        }\n    }\n\n    //may be improved in Java9 with compareAndExchange\n    private ConcurrentHashMap<TimerKey, BaseAction[]> getExecutorOrCreate(int seconds) {\n        ConcurrentHashMap<TimerKey, BaseAction[]> secondExecutor = timerExecutors.get(seconds);\n        if (secondExecutor != null) {\n            return secondExecutor;\n        }\n        ConcurrentHashMap<TimerKey, BaseAction[]> newSecondExecutorMap = new ConcurrentHashMap<>();\n        if (timerExecutors.compareAndSet(seconds, null, newSecondExecutorMap)) {\n            return newSecondExecutorMap;\n        }\n        return timerExecutors.get(seconds);\n    }\n\n    private int actuallySendTimers;\n    private int activeTimers;\n\n    @Override\n    public void run() {\n        log.trace(\"Starting timer...\");\n\n        long now = System.currentTimeMillis();\n        ConcurrentMap<TimerKey, BaseAction[]> tickedExecutors = timerExecutors.get((int) ((now / 1000) % 86400));\n\n        if (tickedExecutors == null) {\n            return;\n        }\n\n        try {\n            this.activeTimers = 0;\n            this.actuallySendTimers = 0;\n            send(tickedExecutors, now);\n        } catch (Exception e) {\n            log.error(\"Error running timers. \", e);\n        }\n\n        if (activeTimers > 0) {\n            log.info(\"Timer finished. Ready {}, Active {}, Actual {}. Processing time : {} ms\",\n                    tickedExecutors.size(), activeTimers, actuallySendTimers, System.currentTimeMillis() - now);\n        }\n    }\n\n    private void send(ConcurrentMap<TimerKey, BaseAction[]> tickedExecutors, long now) {\n        ZonedDateTime currentDateTime = ZonedDateTime.now(DateTimeUtils.UTC);\n\n        for (Map.Entry<TimerKey, BaseAction[]> entry : tickedExecutors.entrySet()) {\n            TimerKey key = entry.getKey();\n            BaseAction[] actions = entry.getValue();\n            if (key.time.isTickTime(currentDateTime)) {\n                User user = userDao.users.get(key.userKey);\n                if (user != null) {\n                    DashBoard dash = user.profile.getDashById(key.dashId);\n                    if (dash != null && dash.isActive) {\n                        activeTimers++;\n                        process(user.profile, dash, key, actions, now);\n                    }\n                }\n            }\n        }\n    }\n\n    private void process(Profile profile, DashBoard dash, TimerKey key, BaseAction[] actions, long now) {\n        for (BaseAction action : actions) {\n            if (action instanceof SetPinAction) {\n                SetPinAction setPinAction = (SetPinAction) action;\n\n                int[] deviceIds = EMPTY_INTS;\n                if (key.isTilesTimer()) {\n                    Widget widget = dash.getWidgetById(key.deviceTilesId);\n                    if (widget instanceof DeviceTiles) {\n                        IntArray intArray = new IntArray();\n                        DeviceTiles deviceTiles = (DeviceTiles) widget;\n                        for (Tile tile : deviceTiles.tiles) {\n                            if (tile.templateId == key.templateId) {\n                                intArray.add(tile.deviceId);\n                            }\n                        }\n                        deviceIds = intArray.toArray();\n                    }\n                } else {\n                    Target target;\n                    int targetId = key.deviceId;\n                    if (targetId < Tag.START_TAG_ID) {\n                        target = profile.getDeviceById(dash, targetId);\n                    } else if (targetId < DeviceSelector.DEVICE_SELECTOR_STARTING_ID) {\n                        target = profile.getTagById(dash, targetId);\n                    } else {\n                        //means widget assigned to device selector widget.\n                        target = dash.getDeviceSelector(targetId);\n                    }\n                    if (target == null) {\n                        return;\n                    }\n\n                    deviceIds = target.getDeviceIds();\n                }\n\n                if (deviceIds.length == 0) {\n                    return;\n                }\n\n                for (int deviceId : deviceIds) {\n                    profile.update(dash, deviceId, setPinAction.dataStream.pin,\n                            setPinAction.dataStream.pinType, setPinAction.value, now);\n                }\n\n                triggerTimer(sessionDao, key.userKey, setPinAction.makeHardwareBody(), key.dashId, deviceIds);\n            } else if (action instanceof NotifyAction) {\n                NotifyAction notifyAction = (NotifyAction) action;\n                EventorProcessor.push(gcmWrapper, dash, notifyAction.message);\n            }\n        }\n    }\n\n    private void triggerTimer(SessionDao sessionDao, UserKey userKey, String value, int dashId, int[] deviceIds) {\n        Session session = sessionDao.get(userKey);\n        if (session != null) {\n            if (!session.sendMessageToHardware(dashId, HARDWARE, TIMER_MSG_ID, value, deviceIds)) {\n                actuallySendTimers++;\n            }\n            for (int deviceId : deviceIds) {\n                session.sendToApps(HARDWARE, TIMER_MSG_ID, dashId, deviceId, value);\n            }\n        }\n    }\n\n    public void deleteTimers(UserKey userKey, DashBoard dash) {\n        for (Widget widget : dash.widgets) {\n            if (widget instanceof DeviceTiles) {\n                DeviceTiles deviceTiles = (DeviceTiles) widget;\n                deleteTimers(userKey, dash.id, deviceTiles);\n            } else if (widget instanceof Timer) {\n                delete(userKey, (Timer) widget, dash.id, -1L, -1L);\n            } else if (widget instanceof Eventor) {\n                delete(userKey, (Eventor) widget, dash.id);\n            }\n        }\n    }\n\n    private void deleteTimers(UserKey userKey, int dashId, DeviceTiles deviceTiles) {\n        for (TileTemplate template : deviceTiles.templates) {\n            for (Widget widgetInTemplate : template.widgets) {\n                if (widgetInTemplate instanceof Timer) {\n                    delete(userKey, (Timer) widgetInTemplate, dashId, deviceTiles.id, template.id);\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "server/core/src/main/java/module-info.java",
    "content": "/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 17.05.18.\n */\nmodule cc.blynk.core {\n    exports cc.blynk.server.core.dao;\n    exports cc.blynk.server;\n    exports cc.blynk.server.core.dao.ota;\n    exports cc.blynk.server.core.model.auth;\n    exports cc.blynk.server.core.protocol.enums;\n    exports cc.blynk.server.core.protocol.handlers;\n    exports cc.blynk.server.core.model.serialization;\n    exports cc.blynk.server.core.stats;\n    exports cc.blynk.server.core.model;\n    exports cc.blynk.server.core.stats.model;\n    exports cc.blynk.server.core;\n    exports cc.blynk.server.core.model.enums;\n    exports cc.blynk.server.core.model.storage;\n    exports cc.blynk.server.core.model.widgets;\n    exports cc.blynk.server.core.model.widgets.notifications;\n    exports cc.blynk.server.core.model.widgets.others.rtc;\n    exports cc.blynk.server.core.model.widgets.outputs.graph;\n    exports cc.blynk.server.core.model.widgets.ui.tiles;\n    exports cc.blynk.server.core.model.widgets.ui.reporting;\n    exports cc.blynk.server.core.processors;\n    exports cc.blynk.server.core.protocol.exceptions;\n    exports cc.blynk.server.db;\n    exports cc.blynk.server.internal;\n    exports cc.blynk.server.common;\n    exports cc.blynk.server.core.model.device;\n    exports cc.blynk.server.workers.timer;\n    exports cc.blynk.server.core.model.widgets.others.eventor;\n    exports cc.blynk.server.core.model.widgets.others.webhook;\n    exports cc.blynk.server.core.reporting.average;\n    exports cc.blynk.server.core.reporting.raw;\n    exports cc.blynk.server.core.reporting;\n    exports cc.blynk.server.db.model;\n    exports cc.blynk.server.db.dao;\n    exports cc.blynk.server.core.protocol.model.messages;\n    exports cc.blynk.server.core.session;\n    exports cc.blynk.server.core.model.widgets.controls;\n    exports cc.blynk.server.transport;\n    exports cc.blynk.server.workers;\n    exports cc.blynk.server.core.model.storage.key;\n    exports cc.blynk.server.core.model.storage.value;\n    exports cc.blynk.server.core.model.widgets.ui;\n    requires cc.blynk.server.notifications.mail;\n    requires cc.blynk.server.notifications.push;\n    requires cc.blynk.server.notifications.sms;\n    requires cc.blynk.server.notifications.twitter;\n    requires cc.blynk.server.acme;\n    requires cc.blynk.utils;\n    requires io.netty.transport.epoll;\n    requires io.netty.common;\n    requires io.netty.transport;\n    requires io.netty.handler;\n    requires io.netty.buffer;\n    requires io.netty.codec;\n    requires io.netty.codec.http;\n    requires async.http.client;\n    requires com.zaxxer.hikari;\n    requires java.sql;\n    requires com.fasterxml.jackson.databind;\n    requires com.fasterxml.jackson.annotation;\n    requires org.apache.logging.log4j;\n    requires com.fasterxml.jackson.core;\n}"
  },
  {
    "path": "server/core/src/main/resources/create_schema.sql",
    "content": "CREATE DATABASE blynk;\n\n\\connect blynk\n\nCREATE TABLE users (\n  email text NOT NULL,\n  appName text NOT NULL,\n  region text,\n  ip text,\n  name text,\n  pass text,\n  last_modified timestamp,\n  last_logged timestamp,\n  last_logged_ip text,\n  is_facebook_user bool,\n  is_super_admin bool DEFAULT FALSE,\n  energy int,\n  json text,\n  PRIMARY KEY(email, appName)\n);\n\nCREATE TABLE redeem (\n  token character(32) PRIMARY KEY,\n  company text,\n  isRedeemed boolean DEFAULT FALSE,\n  reward integer NOT NULL DEFAULT 0,\n  email text,\n  version integer NOT NULL DEFAULT 1,\n  ts timestamp\n);\n\nCREATE TABLE flashed_tokens (\n  token character(32),\n  app_name text,\n  email text,\n  project_id int4 NOT NULL,\n  device_id int4 NOT NULL,\n  is_activated boolean DEFAULT FALSE,\n  ts timestamp,\n  PRIMARY KEY(token, app_name)\n);\n\nCREATE TABLE cloned_projects (\n  token character(32),\n  ts timestamp,\n  json text,\n  PRIMARY KEY(token)\n);\n\nCREATE TABLE purchase (\n  email text,\n  reward integer NOT NULL,\n  transactionId text,\n  price float8,\n  ts timestamp NOT NULL DEFAULT NOW(),\n  PRIMARY KEY (email, transactionId)\n);\n\nCREATE TABLE forwarding_tokens (\n  token character(32),\n  host text,\n  email text,\n  project_id int4,\n  device_id int4,\n  ts timestamp DEFAULT NOW(),\n  PRIMARY KEY(token, host)\n);\n\ncreate user test with password 'test';\nGRANT CONNECT ON DATABASE blynk TO test;\nGRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO test;\n\n-- Create a group\nCREATE ROLE readaccess;\n-- Grant access to existing tables\nGRANT USAGE ON SCHEMA public TO readaccess;\nGRANT SELECT ON ALL TABLES IN SCHEMA public TO readaccess;\n-- Grant access to future tables\nALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO readaccess;"
  },
  {
    "path": "server/core/src/main/resources/db.properties",
    "content": "jdbc.url=jdbc:postgresql://localhost:5432/blynk?tcpKeepAlive=true&socketTimeout=150\nuser=test\npassword=test\nconnection.timeout.millis=30000\nclean.reporting=true\n\nreporting.jdbc.url=jdbc:postgresql://localhost:5432/blynk_reporting?tcpKeepAlive=true&socketTimeout=150\nreporting.user=test\nreporting.password=test\nreporting.connection.timeout.millis=30000"
  },
  {
    "path": "server/core/src/main/resources/dynamic_provisioning_mail.html",
    "content": "Hi there,<br>\n<br>\nNice app you made with Blynk for {project_name}!<br>\n<br>\nHere is what's next:<br>\n\n<ul>\n    <li>Prepare your firmware to work with Dynamic Provisioning workflow. Check the <a href=\"http://help.blynk.cc/hardware-and-libraries/esp8266/myplant-demo\">tutorial</a>.</li>\n\n    <li>For Dynamic Provisioning you don't need to upload Auth Tokens to your devices. During the provisioning process, device will be connected to your WiFi network and will obtain a new Auth Token automatically. Learn <a href=\"http://help.blynk.cc/publishing-apps-made-with-blynk/1240196-provisioning-products-with-auth-tokens/dynamic-auth-token-provisioning\">how Device Provisioning works</a>.</li>\n</ul>\n\n<b>If you would like to publish your app to App Store and Google Play, check out our <a href=\"https://www.blynk.io/plans/\">plans</a> and send a request.</b><br>\n<br>\nLet's build a connected world together!<br>\n<br>\n--<br>\n<br>\nBlynk Team<br>\n<br>\n<a href=\"https://www.blynk.io\">blynk.io</a>\n<br>\n<a href=\"https://www.blynk.cc\">blynk.cc</a>"
  },
  {
    "path": "server/core/src/main/resources/migrataion.temp",
    "content": "drop table reporting_raw_data;\ndrop table reporting_average_minute;\ndrop table reporting_average_hourly;\ndrop table reporting_average_daily;\ndrop table reporting_app_stat_minute;\ndrop table reporting_app_command_stat_minute;\ndrop table reporting_http_command_stat_minute;"
  },
  {
    "path": "server/core/src/main/resources/reporting_schema.sql",
    "content": "CREATE DATABASE blynk_reporting;\n\n\\connect blynk_reporting\n\nCREATE TABLE reporting_raw_data (\n  email text,\n  project_id int4,\n  device_id int4,\n  pin int2,\n  pinType char,\n  ts timestamp,\n  stringValue text,\n  doubleValue float8,\n\n  PRIMARY KEY (email, project_id, device_id, pin, pinType, ts)\n);\n\nCREATE TABLE reporting_average_minute (\n  email text,\n  project_id int4,\n  device_id int8,\n  pin int2,\n  pin_type int2,\n  ts timestamp with time zone,\n  value float8,\n  PRIMARY KEY (email, project_id, device_id, pin, pin_type, ts)\n);\n\nCREATE TABLE reporting_average_hourly (\n  email text,\n  project_id int4,\n  device_id int8,\n  pin int2,\n  pin_type int2,\n  ts timestamp with time zone,\n  value float8,\n  PRIMARY KEY (email, project_id, device_id, pin, pin_type, ts)\n);\n\nCREATE TABLE reporting_average_daily (\n  email text,\n  project_id int4,\n  device_id int8,\n  pin int2,\n  pin_type int2,\n  ts timestamp with time zone,\n  value float8,\n  PRIMARY KEY (email, project_id, device_id, pin, pin_type, ts)\n);\n\nCREATE TABLE reporting_app_stat_minute (\n  region text,\n  ts timestamp,\n  active int4,\n  active_week int4,\n  active_month int4,\n  minute_rate int4,\n  connected int4,\n  online_apps int4,\n  online_hards int4,\n  total_online_apps int4,\n  total_online_hards int4,\n  registrations int4,\n  PRIMARY KEY (region, ts)\n);\n\nCREATE TABLE reporting_app_command_stat_minute (\n  region text,\n  ts timestamp,\n  response int4,\n  register int4,\n  login int4,\n  load_profile int4,\n  app_sync int4,\n  sharing int4,\n  get_token int4,\n  ping int4,\n  activate int4,\n  deactivate int4,\n  refresh_token int4,\n  get_graph_data int4,\n  export_graph_data int4,\n  set_widget_property int4,\n  bridge int4,\n  hardware int4,\n  get_share_dash int4,\n  get_share_token int4,\n  refresh_share_token int4,\n  share_login int4,\n  create_project int4,\n  update_project int4,\n  delete_project int4,\n  hardware_sync int4,\n  internal int4,\n  sms int4,\n  tweet int4,\n  email int4,\n  push int4,\n  add_push_token int4,\n  create_widget int4,\n  update_widget int4,\n  delete_widget int4,\n  create_device int4,\n  update_device int4,\n  delete_device int4,\n  get_devices int4,\n  create_tag int4,\n  update_tag int4,\n  delete_tag int4,\n  get_tags int4,\n  add_energy int4,\n  get_energy int4,\n  get_server int4,\n  connect_redirect int4,\n  web_sockets int4,\n  eventor int4,\n  webhooks int4,\n  appTotal int4,\n  hardTotal int4,\n\n  PRIMARY KEY (region, ts)\n);\n\nCREATE TABLE reporting_http_command_stat_minute (\n  region text,\n  ts timestamp,\n  is_hardware_connected int4,\n  is_app_connected int4,\n  get_pin_data int4,\n  update_pin int4,\n  email int4,\n  push int4,\n  get_project int4,\n  qr int4,\n  get_history_pin_data int4,\n  total int4,\n  PRIMARY KEY (region, ts)\n);\n\ncreate user test with password 'test';\nGRANT CONNECT ON DATABASE blynk_reporting TO test;\nGRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO test;\n\n-- Create a group\nCREATE ROLE readaccess;\n-- Grant access to existing tables\nGRANT USAGE ON SCHEMA public TO readaccess;\nGRANT SELECT ON ALL TABLES IN SCHEMA public TO readaccess;\n-- Grant access to future tables\nALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO readaccess;"
  },
  {
    "path": "server/core/src/main/resources/server.properties",
    "content": "#hardware mqtt port\nhardware.mqtt.port=8440\n\n#http, plain web sockets and plain hardware port\nhttp.port=8080\n\n#if this property is true csv download url will use port 80 and will ignore http.port\nforce.port.80.for.csv=false\n\n#if this property is true redirect_command will use 80 port and will ignore http.port\nforce.port.80.for.redirect=true\n\n#secured https, web sockets and app port\nhttps.port=9443\n\n#address to bind to. by default bounded to all interfaces\nlisten.address=\n\n#by default server uses embedded in jar cert to simplify local server installation.\n#WARNNING DO NOT USE THIS CERTIFICATES ON PRODUCTION OR IN WHERE ENVIRNOMENTS REAL SECURITY REQUIRED.\n#provide either full path to files either use '.' for specifying current directory. For instance \"./myfile.crt\"\nserver.ssl.cert=\nserver.ssl.key=\nserver.ssl.key.pass=\n\n#by default System.getProperty(\"java.io.tmpdir\")/blynk used\ndata.folder=\n\n#folder for logs.\nlogs.folder=./logs\n\n#log debug level. trace|debug|info|error. Defines how precise logging will be.\nlog.level=info\n\n#maximum number of devices allowed per account\nuser.devices.limit=50\n\n#maximum number of tags allowed per account\nuser.tags.limit=100\n\n#defines maximum allowed number of user dashboards. Needed to limit possible number of tokens.\nuser.dashboard.max.limit=100\n\n#defines maximum allowed widget size in KBs as json string.\nuser.widget.max.size.limit=20\n\n#user is limited with 100 messages per second.\nuser.message.quota.limit=100\n\n#maximum allowed number of notification queue. Queue responsible for processing email, pushes, twits sending.\n#Because of performance issue - those queue is processed in separate thread, this is required due\n#to blocking nature of all above operations. Usually limit shouldn't be reached.\nnotifications.queue.limit=2000\n\n#Number of threads for performing blocking operations - push, twits, emails, db queries.\n#Recommended to hold this value low unless you have to perform a lot of blocking operations.\nblocking.processor.thread.pool.limit=6\n\n#this setting defines how often we can send mail/tweet/push or any other notification. Specified in seconds\nnotifications.frequency.user.quota.limit=5\n\n#this setting defines how often we can send webhooks. Specified in miliseconds\nwebhooks.frequency.user.quota.limit=1000\n\n#this setting defines how big could be response for webhook GET request. Specified in kbs\nwebhooks.response.size.limit=96\n\n#maximum size of user profile in kb's\nuser.profile.max.size=256\n\n#number of strings to store in terminal widget\nterminal.strings.pool.size=25\n\n#number of strings to store in map widget\nmap.strings.pool.size=25\n\n#number of strings to store in lcd widget\nlcd.strings.pool.size=6\n\n#maximum number of rows allowed\ntable.rows.pool.size=100\n\n#period in millis for saving all user DB to disk.\nprofile.save.worker.period=60000\n\n#period in millis for saving stats to disk.\nstats.print.worker.period=60000\n\n#max size of web request in bytes, 256 kb (256x1024) is default\nweb.request.max.size=524288\n\n#maximum number of points that are fetched during CSV export\n#43200 == 60 * 24 * 30 - minutes points for 1 month\ncsv.export.data.points.max=43200\n\n#specifies maximum period of time when hardware socket could be idle. After which\n#socket will be closed due to non activity. In seconds. Default value 10 if not provided.\n#leave it empty for infinity timeout\nhard.socket.idle.timeout=10\n\n#enable DB\nenable.db=false\n\n#enable raw data storage to DB\nenable.raw.db.data.store=false\n\n#size of async logger ring buffer. should be increased for loads >2-3k req/sec\nasync.logger.ring.buffer.size=2048\n\n#when true - allows reading worker to trigger hardware even app is offline\nallow.reading.widget.without.active.app=false\n\n#when enabled server will also store hardware and app IP\nallow.store.ip=true\n\n#initial amount of energy\ninitial.energy=100000\n\n#ADMINISTRATION SECTION\n\nadmin.rootPath=/admin\n\n#used for reset password page and certificate generation.\n#by default current server IP is taken. could be replaced with more friendly hostname.\n#it is recommended to override this property with your server IP to avoid possible problems of host resolving\n#server.host=test.blynk.cc\n\n#used for fallback page for reset user password, in most cases it should be the same as server.host\n#IP is not allowed here, it should be blynk-cloud.com for Blynk app\n#or *.blynk.cc for private servers with own apps\nrestore.host=blynk-cloud.com\n\nproduct.name=Blynk\n\n#email used for certificate registration, could be omitted in case you already specified it in mail.properties\n#contact.email=\n\n#network interface to determine server's current IP.\n#only the first characters of the interface's name are needed.\n#the default setting eth will use the first ethX interface found (i.e. eth0)\nnet.interface=eth\n\n#comma separated list of administrator IPs. allow access to admin UI only for those IPs.\n#you may set it for 0.0.0.0/0 to allow access for all.\n#you may use CIDR notation. For instance, 192.168.0.53/24\nallowed.administrator.ips=0.0.0.0/0,::/0\n\n# default admin name and password. that will be created on initial server start\nadmin.email=admin@blynk.cc\nadmin.pass=\n"
  },
  {
    "path": "server/core/src/main/resources/single_token_mail_body.txt",
    "content": "\nHappy Blynking!\n-\nGetting Started Guide -> https://www.blynk.cc/getting-started\nDocumentation -> http://docs.blynk.cc/\nSketch generator -> https://examples.blynk.cc/\n\nLatest Blynk library -> https://github.com/blynkkk/blynk-library/releases/download/v0.6.1/Blynk_Release_v0.6.1.zip\nLatest Blynk server -> https://github.com/blynkkk/blynk-server/releases/download/v0.41.16/server-0.41.16.jar\n-\nhttps://www.blynk.cc\ntwitter.com/blynk_app\nwww.facebook.com/blynkapp"
  },
  {
    "path": "server/core/src/main/resources/static_provisioning_mail.html",
    "content": "Hi there,<br>\n<br>\nNice app you made with Blynk for {project_name}!<br>\n<br>\nHere is what's next:<br>\n\n<ul>\n    <li>Find the QR code attached. If your app can connect to more than one device, there will be QR codes for every device. Learn <a href=\"http://help.blynk.cc/publishing-apps-made-with-blynk/1240196-provisioning-products-with-auth-tokens/static-auth-token-provisioning\">how Static Device Provisioning works</a>.</li>\n\n    <li>Since you chose Static Device Provisioning, you need to pre-flash every device with Auth Token: {DYNAMIC_SECTION}</li>\n</ul>\n\n<b>If you would like to publish your app to App Store and Google Play, check out our <a href=\"https://www.blynk.io/plans/\">plans</a> and send a request.</b><br>\n<br>\nLet's build a connected world together!<br>\n<br>\n--<br>\n<br>\nBlynk Team<br>\n<br>\n<a href=\"https://www.blynk.io\">blynk.io</a>\n<br>\n<a href=\"https://www.blynk.cc\">blynk.cc</a>"
  },
  {
    "path": "server/core/src/main/resources/template_id_mail.html",
    "content": "Template ID for {template_name} is: {template_id}.<br>\n<br>\nThis ID should be added in <a href=\"https://github.com/blynkkk/blynk-library/blob/master/examples/Export_Demo/Template_ESP32/Settings.h\">Settings.h</a>. Simply change this line\n<br>\n<p>\n    <i>\n#define BOARD_TEMPLATE_ID             \"{template_id}\" // ID of the Tile Template. Can be found in Tile Template Settings\n    </i>\n</p>\nTemplate ID is used during device provisioning process and defines which template will be assigned to the device of this particular type.\n<br>\n<br>\n--<br>\n<br>\nBlynk Team<br>\n<br>\n<a href=\"https://www.blynk.io\">blynk.io</a>\n<br>\n<a href=\"https://www.blynk.cc\">blynk.cc</a>"
  },
  {
    "path": "server/core/src/test/java/cc/blynk/server/core/dao/CSVGeneratorTest.java",
    "content": "package cc.blynk.server.core.dao;\n\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.utils.AppNameUtil;\nimport cc.blynk.utils.FileUtils;\nimport org.junit.Test;\n\nimport java.nio.ByteBuffer;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\n\nimport static org.junit.Assert.assertEquals;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 29.06.17.\n */\npublic class CSVGeneratorTest {\n\n    private CSVGenerator csvGenerator = new CSVGenerator(new ReportingDiskDao(\"/tmp\", true));\n\n    @Test\n    public void generateCSV() throws Exception {\n        User user  = new User();\n        user.email = \"test@blynk.cc\";\n        user.appName = AppNameUtil.BLYNK;\n\n        Path path = Paths.get(\"/home/doom369/hourly_data.csv.gz\");\n\n        final ByteBuffer buf = ByteBuffer.allocate(2 * 16);\n        buf.putDouble(1).putLong(1);\n        buf.putDouble(2).putLong(2);\n        buf.flip();\n\n        //CSVGenerator.makeGzippedCSVFile(buf, path);\n    }\n\n    @Test\n    public void testForcePort80Property() {\n        assertEquals(\"http://myhost/\", FileUtils.downloadUrl(\"myhost\", \"8080\", true));\n        assertEquals(\"http://myhost:8080/\", FileUtils.downloadUrl(\"myhost\", \"8080\", false));\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/test/java/cc/blynk/server/core/dao/ReportingDaoTest.java",
    "content": "package cc.blynk.server.core.dao;\n\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.widgets.outputs.graph.GraphGranularityType;\nimport org.junit.Test;\n\nimport static org.junit.Assert.assertEquals;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 07.09.16.\n */\npublic class ReportingDaoTest {\n\n    final String REPORTING_MINUTE_FILE_NAME = \"history_%s-0_%c%d_minute.bin\";\n    final String REPORTING_HOURLY_FILE_NAME = \"history_%s-0_%c%d_hourly.bin\";\n    final String REPORTING_DAILY_FILE_NAME = \"history_%s-0_%c%d_daily.bin\";\n\n    @Test\n    public void testFileName() {\n        int dashId = 1;\n        PinType pinType = PinType.VIRTUAL;\n        short pin = 2;\n\n        assertEquals(String.format(REPORTING_MINUTE_FILE_NAME, dashId, pinType.pintTypeChar, pin),\n                ReportingDiskDao.generateFilename(dashId, 0, pinType, pin, GraphGranularityType.MINUTE));\n\n        assertEquals(String.format(REPORTING_HOURLY_FILE_NAME, dashId, pinType.pintTypeChar, pin),\n                ReportingDiskDao.generateFilename(dashId, 0, pinType, pin, GraphGranularityType.HOURLY));\n\n        assertEquals(String.format(REPORTING_DAILY_FILE_NAME, dashId, pinType.pintTypeChar, pin),\n                ReportingDiskDao.generateFilename(dashId, 0, pinType, pin, GraphGranularityType.DAILY));\n\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/test/java/cc/blynk/server/core/dao/functions/MedianGraphFunctionTest.java",
    "content": "package cc.blynk.server.core.dao.functions;\n\nimport org.junit.Test;\n\nimport static org.junit.Assert.assertEquals;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 22.07.17.\n */\npublic class MedianGraphFunctionTest {\n\n    @Test\n    public void testMedianFunction() {\n        MedianGraphFunction medianFunction = new MedianGraphFunction();\n        medianFunction.apply(0);\n        assertEquals(0, medianFunction.getResult(), 0.0001);\n\n        medianFunction.apply(1);\n        assertEquals(0.5, medianFunction.getResult(), 0.0001);\n\n        medianFunction.apply(2);\n        assertEquals(1, medianFunction.getResult(), 0.0001);\n\n        medianFunction.apply(3);\n        assertEquals(1.5, medianFunction.getResult(), 0.0001);\n    }\n\n    @Test\n    public void testMedianFunction2() {\n        MedianGraphFunction medianFunction = new MedianGraphFunction();\n        medianFunction.apply(0);\n        medianFunction.apply(0);\n        medianFunction.apply(0);\n        medianFunction.apply(0);\n        medianFunction.apply(0);\n        medianFunction.apply(0);\n        assertEquals(0, medianFunction.getResult(), 0.0001);\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/test/java/cc/blynk/server/core/device/SerializationForBoardTypeTest.java",
    "content": "package cc.blynk.server.core.device;\n\nimport cc.blynk.server.core.model.device.BoardType;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport org.junit.Test;\n\nimport static org.junit.Assert.assertEquals;\n\npublic class SerializationForBoardTypeTest {\n\n    @Test\n    public void someTEst() throws Exception {\n        assertEquals(\"\\\"Arduino UNO\\\"\", JsonParser.MAPPER.writeValueAsString(BoardType.Arduino_UNO));\n        assertEquals(BoardType.Arduino_UNO, JsonParser.MAPPER.readValue(\"\\\"Arduino UNO\\\"\", BoardType.class));\n    }\n\n    @Test\n    public void testUnknownProperty() throws Exception {\n        assertEquals(BoardType.Generic_Board, JsonParser.MAPPER.readValue(\"\\\"\\\"\", BoardType.class));\n        assertEquals(BoardType.Generic_Board, JsonParser.MAPPER.readValue(\"\\\"aaaa\\\"\", BoardType.class));\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/test/java/cc/blynk/server/core/model/CopyObjectTest.java",
    "content": "package cc.blynk.server.core.model;\n\nimport cc.blynk.server.core.model.serialization.CopyUtil;\nimport org.junit.Test;\n\nimport static org.junit.Assert.assertNotNull;\nimport static org.junit.Assert.assertNotSame;\n\npublic class CopyObjectTest {\n\n    @Test\n    public void testDeepCopy() {\n        Profile profile = new Profile();\n\n        Profile copy = CopyUtil.deepCopy(profile);\n        assertNotNull(copy);\n        assertNotSame(copy, profile);\n        assertNotSame(copy.pinsStorage, profile.pinsStorage);\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/test/java/cc/blynk/server/core/model/DataStreamStorageSerializationTest.java",
    "content": "package cc.blynk.server.core.model;\n\nimport cc.blynk.server.core.dao.FileManager;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.enums.WidgetProperty;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.model.storage.key.DashPinPropertyStorageKey;\nimport cc.blynk.server.core.model.storage.key.DashPinStorageKey;\nimport cc.blynk.server.core.model.storage.value.MultiPinStorageValue;\nimport cc.blynk.server.core.model.storage.value.MultiPinStorageValueType;\nimport cc.blynk.server.core.model.storage.value.PinStorageValue;\nimport cc.blynk.server.core.model.storage.value.SinglePinStorageValue;\nimport org.junit.Test;\n\nimport java.io.InputStream;\n\nimport static cc.blynk.server.core.model.DataStreamValuesUpdateCorrectTest.parseProfile;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\nimport static org.junit.Assert.assertTrue;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 19.11.16.\n */\npublic class DataStreamStorageSerializationTest {\n\n    @Test\n    public void testMigrationOfOldDataIsCorrect() {\n        InputStream is = this.getClass().getResourceAsStream(\"/json_test/user_profile_json_old_pinstorage.txt\");\n\n        FileManager fileManager = new FileManager(\"123\", \"host\");\n        User user = new User();\n        user.profile = parseProfile(is);\n        fileManager.makeProfileChanges(user);\n        assertEquals(1, user.profile.dashBoards.length);\n        assertNotNull(user.profile.dashBoards[0].pinsStorage);\n        assertEquals(0, user.profile.dashBoards[0].pinsStorage.size());\n        assertEquals(3, user.profile.pinsStorage.size());\n\n        DashPinStorageKey pinStorageKey = new DashPinStorageKey(1, 0, PinType.VIRTUAL, (short) 0);\n        DashPinStorageKey pinStorageKey2 = new DashPinStorageKey(1, 0, PinType.DIGITAL, (short) 111);\n        DashPinPropertyStorageKey pinStorageKey3 = new DashPinPropertyStorageKey(1, 0, PinType.VIRTUAL, (short) 0, WidgetProperty.LABEL);\n\n        assertEquals(\"1\", ((SinglePinStorageValue) user.profile.pinsStorage.get(pinStorageKey)).value);\n        assertEquals(\"2\", ((SinglePinStorageValue) user.profile.pinsStorage.get(pinStorageKey2)).value);\n        assertEquals(\"3\", ((SinglePinStorageValue) user.profile.pinsStorage.get(pinStorageKey3)).value);\n    }\n\n    @Test\n    public void testSerializeSingleEmptyValue() {\n        User user = new User();\n        user.email = \"123\";\n        user.profile = new Profile();\n        user.profile.dashBoards = new DashBoard[] {\n                new DashBoard()\n        };\n        user.lastModifiedTs = 0;\n        DashPinStorageKey pinStorageKey = new DashPinStorageKey(1, 0, PinType.VIRTUAL, (short) 0);\n        user.profile.pinsStorage.put(pinStorageKey, new SinglePinStorageValue());\n\n        String result = user.toString();\n        assertTrue(result.contains(\"\\\"1-0-v0\\\":\\\"\\\"\"));\n    }\n\n    @Test\n    public void testSerializeSingleValue() {\n        User user = new User();\n        user.email = \"123\";\n        user.profile = new Profile();\n        user.profile.dashBoards = new DashBoard[] {\n                new DashBoard()\n        };\n        user.lastModifiedTs = 0;\n        DashPinStorageKey pinStorageKey = new DashPinStorageKey(1, 0, PinType.VIRTUAL, (short) 0);\n        DashPinStorageKey pinStorageKey2 = new DashPinStorageKey(1, 0, PinType.DIGITAL, (short) 1);\n        DashPinPropertyStorageKey pinStorageKey3 = new DashPinPropertyStorageKey(1, 0, PinType.VIRTUAL, (short) 0, WidgetProperty.LABEL);\n        user.profile.pinsStorage.put(pinStorageKey, new SinglePinStorageValue(\"1\"));\n        user.profile.pinsStorage.put(pinStorageKey2,new SinglePinStorageValue(\"2\"));\n        user.profile.pinsStorage.put(pinStorageKey3, new SinglePinStorageValue(\"3\"));\n\n        String result = user.toString();\n        assertTrue(result.contains(\"\\\"1-0-v0\\\":\\\"1\\\"\"));\n        assertTrue(result.contains(\"\\\"1-0-d1\\\":\\\"2\\\"\"));\n        assertTrue(result.contains(\"\\\"1-0-v0-label\\\":\\\"3\\\"\"));\n    }\n\n    @Test\n    public void testSerializeMultiValueEmpty() {\n        User user = new User();\n        user.email = \"123\";\n        user.profile = new Profile();\n        user.profile.dashBoards = new DashBoard[] {\n                new DashBoard()\n        };\n        user.lastModifiedTs = 0;\n        DashPinStorageKey pinStorageKey = new DashPinStorageKey(1, 0, PinType.VIRTUAL, (short) 0);\n        PinStorageValue pinStorageValue = new MultiPinStorageValue(MultiPinStorageValueType.LCD);\n        user.profile.pinsStorage.put(pinStorageKey, pinStorageValue);\n\n        String result = user.toString();\n        assertTrue(result.contains(\"\\\"1-0-v0\\\":{\\\"type\\\":\\\"LCD\\\"}\"));\n    }\n\n    @Test\n    public void testSerializeMultiValueWithSingleValue() {\n        User user = new User();\n        user.email = \"123\";\n        user.profile = new Profile();\n        user.profile.dashBoards = new DashBoard[] {\n                new DashBoard()\n        };\n        user.lastModifiedTs = 0;\n        DashPinStorageKey pinStorageKey = new DashPinStorageKey(1, 0, PinType.VIRTUAL, (short) 0);\n        PinStorageValue pinStorageValue = new MultiPinStorageValue(MultiPinStorageValueType.LCD);\n        pinStorageValue.update(\"1\");\n        user.profile.pinsStorage.put(pinStorageKey, pinStorageValue);\n\n        String result = user.toString();\n        assertTrue(result.contains(\"\\\"1-0-v0\\\":{\\\"type\\\":\\\"LCD\\\",\\\"values\\\":[\\\"1\\\"]}\"));\n    }\n\n    @Test\n    public void testSerializeMultiValueWithMultipleValues() {\n        User user = new User();\n        user.email = \"123\";\n        user.profile = new Profile();\n        user.profile.dashBoards = new DashBoard[] {\n                new DashBoard()\n        };\n        user.lastModifiedTs = 0;\n        DashPinStorageKey pinStorageKey = new DashPinStorageKey(1, 0, PinType.VIRTUAL, (short) 0);\n        PinStorageValue pinStorageValue = new MultiPinStorageValue(MultiPinStorageValueType.LCD);\n        pinStorageValue.update(\"1\");\n        pinStorageValue.update(\"2\");\n        user.profile.pinsStorage.put(pinStorageKey, pinStorageValue);\n\n        String result = user.toString();\n        assertTrue(result.contains(\"\\\"1-0-v0\\\":{\\\"type\\\":\\\"LCD\\\",\\\"values\\\":[\\\"1\\\",\\\"2\\\"]}\"));\n    }\n\n    @Test\n    public void testSerializeMultiValueWithMultipleValuesAndLimit() {\n        User user = new User();\n        user.email = \"123\";\n        user.profile = new Profile();\n        user.profile.dashBoards = new DashBoard[] {\n                new DashBoard()\n        };\n        user.lastModifiedTs = 0;\n        DashPinStorageKey pinStorageKey = new DashPinStorageKey(1, 0, PinType.VIRTUAL, (short) 0);\n        PinStorageValue pinStorageValue = new MultiPinStorageValue(MultiPinStorageValueType.LCD);\n        pinStorageValue.update(\"1\");\n        pinStorageValue.update(\"2\");\n        pinStorageValue.update(\"3\");\n        pinStorageValue.update(\"4\");\n        pinStorageValue.update(\"5\");\n        pinStorageValue.update(\"6\");\n        pinStorageValue.update(\"7\");\n        user.profile.pinsStorage.put(pinStorageKey, pinStorageValue);\n\n        String result = user.toString();\n        assertTrue(result.contains(\"\\\"1-0-v0\\\":{\\\"type\\\":\\\"LCD\\\",\\\"values\\\":[\\\"2\\\",\\\"3\\\",\\\"4\\\",\\\"5\\\",\\\"6\\\",\\\"7\\\"]}\"));\n    }\n\n    @Test\n    public void testSerializeMultiValueWithNilValue() {\n        User user = new User();\n        user.email = \"123\";\n        user.profile = new Profile();\n        user.profile.dashBoards = new DashBoard[] {\n                new DashBoard()\n        };\n        user.lastModifiedTs = 0;\n        DashPinStorageKey pinStorageKey = new DashPinStorageKey(1, 0, PinType.VIRTUAL, (short) 0);\n        PinStorageValue pinStorageValue = new MultiPinStorageValue(MultiPinStorageValueType.LCD);\n        pinStorageValue.update(\"\\0\");\n        user.profile.pinsStorage.put(pinStorageKey, pinStorageValue);\n\n        String result = user.toString();\n        assertTrue(result.contains(\"\\\"1-0-v0\\\":{\\\"type\\\":\\\"LCD\\\",\\\"values\\\":[\\\"\\\\u0000\\\"]}\"));\n    }\n\n    @Test\n    public void testDeserializeSingleValue() throws Exception{\n        String expectedString = \"{\\\"email\\\":\\\"123\\\",\\\"appName\\\":\\\"Blynk\\\",\\\"lastModifiedTs\\\":0,\\\"lastLoggedAt\\\":0,\" +\n                \"\\\"profile\\\":{\\\"dashBoards\\\":[{\\\"id\\\":0,\\\"createdAt\\\":0,\\\"updatedAt\\\":0,\\\"theme\\\":\\\"Blynk\\\",\\\"keepScreenOn\\\":false,\\\"isShared\\\":false,\\\"isActive\\\":false}],\" +\n                \"\\\"pinsStorage\\\":{\\\"1-0-v0\\\":\\\"1\\\",\\\"1-0-d111\\\":\\\"2\\\", \\\"1-0-v0-label\\\":\\\"3\\\"}\" +\n                \"},\\\"isFacebookUser\\\":false,\\\"energy\\\":2000,\\\"id\\\":\\\"123-Blynk\\\"}\";\n\n        User user = JsonParser.parseUserFromString(expectedString);\n        assertNotNull(user);\n        assertEquals(3, user.profile.pinsStorage.size());\n\n        DashPinStorageKey pinStorageKey = new DashPinStorageKey(1, 0, PinType.VIRTUAL, (short) 0);\n        DashPinStorageKey pinStorageKey2 = new DashPinStorageKey(1, 0, PinType.DIGITAL, (short) 111);\n        DashPinPropertyStorageKey pinStorageKey3 = new DashPinPropertyStorageKey(1, 0, PinType.VIRTUAL, (short) 0, WidgetProperty.LABEL);\n\n        assertEquals(\"1\", ((SinglePinStorageValue) user.profile.pinsStorage.get(pinStorageKey)).value);\n        assertEquals(\"2\", ((SinglePinStorageValue) user.profile.pinsStorage.get(pinStorageKey2)).value);\n        assertEquals(\"3\", ((SinglePinStorageValue) user.profile.pinsStorage.get(pinStorageKey3)).value);\n    }\n\n    @Test\n    public void testDeserializeMultiValue() throws Exception {\n        String expectedString = \"{\\\"email\\\":\\\"123\\\",\\\"appName\\\":\\\"Blynk\\\",\\\"lastModifiedTs\\\":0,\\\"lastLoggedAt\\\":0,\" +\n                \"\\\"profile\\\":{\\\"dashBoards\\\":[{\\\"id\\\":0,\\\"createdAt\\\":0,\\\"updatedAt\\\":0,\\\"theme\\\":\\\"Blynk\\\",\\\"keepScreenOn\\\":false,\\\"isShared\\\":false,\\\"isActive\\\":false}],\" +\n                \"\\\"pinsStorage\\\":{\\\"1-0-v0\\\":{\\\"type\\\":\\\"LCD\\\",\\\"values\\\":[\\\"1\\\",\\\"2\\\"]}}\" +\n                \"},\\\"isFacebookUser\\\":false,\\\"energy\\\":2000,\\\"id\\\":\\\"123-Blynk\\\"}\";\n\n        User user = JsonParser.parseUserFromString(expectedString);\n        assertNotNull(user);\n        assertEquals(1, user.profile.pinsStorage.size());\n\n        DashPinStorageKey pinStorageKey = new DashPinStorageKey(1, 0, PinType.VIRTUAL, (short) 0);\n\n        assertEquals(\"1\", ((MultiPinStorageValue) user.profile.pinsStorage.get(pinStorageKey)).values.poll());\n        assertEquals(\"2\", ((MultiPinStorageValue) user.profile.pinsStorage.get(pinStorageKey)).values.poll());\n    }\n\n    @Test\n    public void testDeserializeEmptyMultiValue() throws Exception {\n        String expectedString = \"{\\\"email\\\":\\\"123\\\",\\\"appName\\\":\\\"Blynk\\\",\\\"lastModifiedTs\\\":0,\\\"lastLoggedAt\\\":0,\" +\n                \"\\\"profile\\\":{\" +\n                \"\\\"dashBoards\\\":[{\\\"id\\\":0,\\\"createdAt\\\":0,\\\"updatedAt\\\":0,\\\"theme\\\":\\\"Blynk\\\",\\\"keepScreenOn\\\":false,\\\"isShared\\\":false,\\\"isActive\\\":false}],\" +\n                \"\\\"pinsStorage\\\":{\\\"0-0-v0\\\":{\\\"type\\\":\\\"LCD\\\"}}\" +\n                \"},\\\"isFacebookUser\\\":false,\\\"energy\\\":2000,\\\"id\\\":\\\"123-Blynk\\\"}\";\n\n        User user = JsonParser.parseUserFromString(expectedString);\n        assertNotNull(user);\n        assertEquals(1, user.profile.pinsStorage.size());\n\n        DashPinStorageKey pinStorageKey = new DashPinStorageKey(0, 0, PinType.VIRTUAL, (short) 0);\n\n        assertNotNull(user.profile.pinsStorage.get(pinStorageKey));\n        assertEquals(0, ((MultiPinStorageValue) user.profile.pinsStorage.get(pinStorageKey)).values.size());\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/test/java/cc/blynk/server/core/model/DataStreamValuesUpdateCorrectTest.java",
    "content": "package cc.blynk.server.core.model;\n\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.model.widgets.controls.Button;\nimport cc.blynk.server.core.model.widgets.controls.RGB;\nimport cc.blynk.utils.NumberUtil;\nimport cc.blynk.utils.StringUtils;\nimport com.fasterxml.jackson.databind.ObjectReader;\nimport org.apache.commons.lang3.ArrayUtils;\nimport org.junit.Test;\n\nimport java.io.InputStream;\n\nimport static cc.blynk.utils.StringUtils.split3;\nimport static org.junit.Assert.assertEquals;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 02.11.15.\n */\npublic class DataStreamValuesUpdateCorrectTest {\n\n    private static final ObjectReader profileReader = JsonParser.init().readerFor(Profile.class);\n\n    public static Profile parseProfile(InputStream reader) {\n        try {\n            return profileReader.readValue(reader);\n        } catch (Exception e) {\n            throw new RuntimeException(\"Error parsing profile\");\n        }\n    }\n\n    @Test\n    public void testHas1Pin() {\n        InputStream is = this.getClass().getResourceAsStream(\"/json_test/user_profile_json.txt\");\n\n        Profile profile = parseProfile(is);\n        DashBoard dash = profile.dashBoards[0];\n        dash.isActive = true;\n\n        Button button = dash.getWidgetByType(Button.class);\n        assertEquals(1, button.pin);\n        assertEquals(PinType.DIGITAL, button.pinType);\n        assertEquals(\"1\", button.value);\n\n        update(profile, 0, \"dw 1 0\".replaceAll(\" \", StringUtils.BODY_SEPARATOR_STRING));\n        assertEquals(\"0\", button.value);\n\n        update(profile, 0, \"aw 1 1\".replaceAll(\" \", StringUtils.BODY_SEPARATOR_STRING));\n        assertEquals(\"0\", button.value);\n\n        update(profile, 0, \"dw 1 1\".replaceAll(\" \", StringUtils.BODY_SEPARATOR_STRING));\n        assertEquals(\"1\", button.value);\n\n        RGB rgb = new RGB();\n        rgb.dataStreams = new DataStream[3];\n        rgb.dataStreams[0] = new DataStream((short) 0, false, false, PinType.VIRTUAL, null, 0, 255, null);\n        rgb.dataStreams[1] = new DataStream((short) 1, false, false, PinType.VIRTUAL, null, 0, 255, null);\n        rgb.dataStreams[2] = new DataStream((short) 2, false, false, PinType.VIRTUAL, null, 0, 255, null);\n\n\n        dash.widgets = ArrayUtils.add(dash.widgets, rgb);\n\n        update(profile, 0, \"vw 0 100\".replaceAll(\" \", StringUtils.BODY_SEPARATOR_STRING));\n        update(profile, 0, \"vw 1 101\".replaceAll(\" \", StringUtils.BODY_SEPARATOR_STRING));\n        update(profile, 0, \"vw 2 102\".replaceAll(\" \", StringUtils.BODY_SEPARATOR_STRING));\n\n        for (int i = 0; i < rgb.dataStreams.length; i++) {\n            assertEquals(\"10\" + i, rgb.dataStreams[i].value);\n        }\n\n        rgb = new RGB();\n        rgb.dataStreams = new DataStream[3];\n        rgb.dataStreams[0] = new DataStream((short) 4, false, false, PinType.VIRTUAL, null, 0, 255, null);\n        rgb.dataStreams[1] = new DataStream((short) 4, false, false, PinType.VIRTUAL, null, 0, 255, null);\n        rgb.dataStreams[2] = new DataStream((short) 4, false, false, PinType.VIRTUAL, null, 0, 255, null);\n\n        dash.widgets = ArrayUtils.add(dash.widgets, rgb);\n\n        update(profile, 0, \"vw 4 100 101 102\".replaceAll(\" \", StringUtils.BODY_SEPARATOR_STRING));\n\n        assertEquals(\"100 101 102\".replaceAll(\" \", StringUtils.BODY_SEPARATOR_STRING), rgb.dataStreams[0].value);\n    }\n\n    public static void update(Profile profile, int deviceId, String body) {\n        update(profile, deviceId, split3(body));\n    }\n\n    public static void update(Profile profile, int deviceId, String[] splitted) {\n        final PinType type = PinType.getPinType(splitted[0].charAt(0));\n        final short pin = NumberUtil.parsePin(splitted[1]);\n        profile.update(profile.dashBoards[0], deviceId, pin, type, splitted[2], System.currentTimeMillis());\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/test/java/cc/blynk/server/core/model/widgets/DataStreamGetJsonValueTest.java",
    "content": "package cc.blynk.server.core.model.widgets;\n\nimport cc.blynk.server.core.model.DataStream;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.widgets.controls.RGB;\nimport cc.blynk.server.core.model.widgets.outputs.ValueDisplay;\nimport cc.blynk.utils.StringUtils;\nimport org.junit.Test;\n\nimport static org.junit.Assert.assertEquals;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 25.12.15.\n */\npublic class DataStreamGetJsonValueTest {\n\n    private static DataStream createPinWithValue(String val) {\n        return new DataStream((short) 1, false, false, PinType.VIRTUAL, val, 0, 255, null);\n    }\n\n    @Test\n    public void testSinglePin() {\n        OnePinWidget onePinWidget = new ValueDisplay();\n        onePinWidget.value = null;\n\n        assertEquals(\"[]\", onePinWidget.getJsonValue());\n\n        onePinWidget.value = \"1.0\";\n        assertEquals(\"[\\\"1.0\\\"]\", onePinWidget.getJsonValue());\n    }\n\n    @Test\n    public void testMultiPinSplit() {\n        RGB multiPinWidget = new RGB();\n        multiPinWidget.dataStreams = null;\n        multiPinWidget.splitMode = true;\n\n        assertEquals(\"[]\", multiPinWidget.getJsonValue());\n\n        multiPinWidget.dataStreams = new DataStream[3];\n        multiPinWidget.dataStreams[0] = createPinWithValue(\"1\");\n        multiPinWidget.dataStreams[1] = createPinWithValue(\"2\");\n        multiPinWidget.dataStreams[2] = createPinWithValue(\"3\");\n\n        assertEquals(\"[\\\"1\\\",\\\"2\\\",\\\"3\\\"]\", multiPinWidget.getJsonValue());\n    }\n\n    @Test\n    public void testMultiPinMerge() {\n        RGB multiPinWidget = new RGB();\n        multiPinWidget.dataStreams = null;\n        multiPinWidget.splitMode = false;\n\n        assertEquals(\"[]\", multiPinWidget.getJsonValue());\n\n        multiPinWidget.dataStreams = new DataStream[3];\n        multiPinWidget.dataStreams[0] = createPinWithValue(\"1 2 3\".replaceAll(\" \", StringUtils.BODY_SEPARATOR_STRING));\n\n        assertEquals(\"[\\\"1\\\",\\\"2\\\",\\\"3\\\"]\", multiPinWidget.getJsonValue());\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/test/java/cc/blynk/server/core/model/widgets/MultiPinWidgetsToJsonTest.java",
    "content": "package cc.blynk.server.core.model.widgets;\n\nimport cc.blynk.server.core.model.DataStream;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.widgets.controls.RGB;\nimport cc.blynk.server.core.model.widgets.controls.TwoAxisJoystick;\nimport cc.blynk.server.core.model.widgets.outputs.LCD;\nimport org.junit.Test;\n\nimport static org.junit.Assert.assertEquals;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 20.11.17.\n */\npublic class MultiPinWidgetsToJsonTest {\n\n    @Test\n    public void testJoystick() {\n        TwoAxisJoystick joystick = new TwoAxisJoystick();\n        joystick.dataStreams = new DataStream[] {\n                new DataStream((short) 1, false, false, PinType.VIRTUAL, \"value\", 0, 250, \"label\"),\n                new DataStream((short) 2, false, false, PinType.VIRTUAL, \"value2\", 0, 250, \"label\")\n        };\n        assertEquals(\"[\\\"value\\\"]\", joystick.getJsonValue());\n\n        joystick.split = true;\n        assertEquals(\"[\\\"value\\\",\\\"value2\\\"]\", joystick.getJsonValue());\n    }\n\n    @Test\n    public void testRGB() {\n        RGB rgb = new RGB();\n        rgb.dataStreams = new DataStream[] {\n                new DataStream((short) 1, false, false, PinType.VIRTUAL, \"value\", 0, 250, \"label\"),\n                new DataStream((short) 2, false, false, PinType.VIRTUAL, \"value2\", 0, 250, \"label\")\n        };\n        assertEquals(\"[\\\"value\\\"]\", rgb.getJsonValue());\n\n        rgb.splitMode = true;\n        assertEquals(\"[\\\"value\\\",\\\"value2\\\"]\", rgb.getJsonValue());\n    }\n\n    @Test\n    public void testLCD() {\n        LCD lcd = new LCD();\n        lcd.advancedMode = true;\n        lcd.dataStreams = new DataStream[] {\n                new DataStream((short) 1, false, false, PinType.VIRTUAL, \"value\", 0, 250, \"label\"),\n                new DataStream((short) 2, false, false, PinType.VIRTUAL, \"value2\", 0, 250, \"label\")\n        };\n        assertEquals(\"[\\\"value\\\"]\", lcd.getJsonValue());\n\n        lcd.advancedMode = false;\n        assertEquals(\"[\\\"value\\\",\\\"value2\\\"]\", lcd.getJsonValue());\n    }\n\n    @Test\n    public void testJoystickMultiValue() {\n        TwoAxisJoystick joystick = new TwoAxisJoystick();\n        joystick.dataStreams = new DataStream[] {\n                new DataStream((short) 1, false, false, PinType.VIRTUAL, \"value\\0value2\", 0, 250, \"label\")\n        };\n        assertEquals(\"[\\\"value\\\",\\\"value2\\\"]\", joystick.getJsonValue());\n    }\n}\n"
  },
  {
    "path": "server/core/src/test/java/cc/blynk/server/core/model/widgets/others/RTCSerializationTest.java",
    "content": "package cc.blynk.server.core.model.widgets.others;\n\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.others.rtc.RTC;\nimport cc.blynk.utils.DateTimeUtils;\nimport org.junit.Ignore;\nimport org.junit.Test;\n\nimport java.time.ZoneId;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\nimport static org.junit.Assert.assertNull;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 30.03.16.\n */\npublic class RTCSerializationTest {\n\n    @Test\n    public void testDeSerializationIsCorrect() {\n        String widgetString = \"{\\\"id\\\":1, \\\"x\\\":1, \\\"y\\\":1, \\\"type\\\":\\\"RTC\\\", \\\"tzName\\\":\\\"Australia/Sydney\\\"}\";\n        Widget widget = JsonParser.parseWidget(widgetString, 0);\n\n        assertNotNull(widget);\n\n        RTC rtc = (RTC) widget;\n        assertNotNull(rtc.tzName);\n        assertEquals(ZoneId.of(\"Australia/Sydney\"), rtc.tzName);\n    }\n\n    @Test\n    @Ignore(\"travis uses old java and fails here\")\n    public void unsupportedTimeZoneForKnownLocationCanadaTest() {\n        String widgetString = \"{\\\"id\\\":1, \\\"x\\\":1, \\\"y\\\":1, \\\"type\\\":\\\"RTC\\\", \\\"tzName\\\":\\\"Canada/East-Saskatchewan\\\"}\";\n        Widget widget = JsonParser.parseWidget(widgetString, 0);\n\n        assertNotNull(widget);\n\n        RTC rtc = (RTC) widget;\n        assertNotNull(rtc.tzName);\n        assertEquals(ZoneId.of(\"America/Regina\"), rtc.tzName);\n    }\n\n    @Test\n    public void unsupportedTimeZoneForKnownLocationHanoiTest() {\n        String widgetString = \"{\\\"id\\\":1, \\\"x\\\":1, \\\"y\\\":1, \\\"type\\\":\\\"RTC\\\", \\\"tzName\\\":\\\"Asia/Hanoi\\\"}\";\n        Widget widget = JsonParser.parseWidget(widgetString, 0);\n\n        assertNotNull(widget);\n\n        RTC rtc = (RTC) widget;\n        assertNotNull(rtc.tzName);\n        assertEquals(ZoneId.of(\"Asia/Ho_Chi_Minh\"), rtc.tzName);\n    }\n\n    @Test\n    public void unsupportedTimeZoneTest() {\n        String widgetString = \"{\\\"id\\\":1, \\\"x\\\":1, \\\"y\\\":1, \\\"type\\\":\\\"RTC\\\", \\\"tzName\\\":\\\"Canada/East-xxx\\\"}\";\n        RTC rtc = (RTC) JsonParser.parseWidget(widgetString, 0);\n        assertNotNull(rtc);\n        assertEquals(ZoneId.of(\"UTC\"), rtc.tzName);\n    }\n\n    @Test\n    public void testDeSerializationIsCorrectForNull() {\n        String widgetString = \"{\\\"id\\\":1, \\\"x\\\":1, \\\"y\\\":1, \\\"type\\\":\\\"RTC\\\"}\";\n        Widget widget = JsonParser.parseWidget(widgetString, 0);\n\n        assertNotNull(widget);\n\n        RTC rtc = (RTC) widget;\n        assertNull(rtc.tzName);\n    }\n\n    @Test\n    public void testSerializationIsCorrect() throws Exception {\n        RTC rtc = new RTC();\n        rtc.tzName = ZoneId.of(\"Australia/Sydney\");\n\n        String widgetString = JsonParser.MAPPER.writeValueAsString(rtc);\n\n        assertNotNull(widgetString);\n        assertEquals(\"{\\\"type\\\":\\\"RTC\\\",\\\"id\\\":0,\\\"x\\\":0,\\\"y\\\":0,\\\"color\\\":0,\\\"width\\\":0,\\\"height\\\":0,\\\"tabId\\\":0,\\\"isDefaultColor\\\":false,\\\"tzName\\\":\\\"Australia/Sydney\\\"}\", widgetString);\n    }\n\n    @Test\n    public void testSerializationIsCorrectUTC() throws Exception {\n        RTC rtc = new RTC();\n        rtc.tzName = DateTimeUtils.UTC;\n\n        String widgetString = JsonParser.MAPPER.writeValueAsString(rtc);\n\n        assertNotNull(widgetString);\n        assertEquals(\"{\\\"type\\\":\\\"RTC\\\",\\\"id\\\":0,\\\"x\\\":0,\\\"y\\\":0,\\\"color\\\":0,\\\"width\\\":0,\\\"height\\\":0,\\\"tabId\\\":0,\\\"isDefaultColor\\\":false,\\\"tzName\\\":\\\"UTC\\\"}\", widgetString);\n    }\n\n    @Test\n    public void testSerializationIsCorrectForNull() throws Exception {\n        RTC rtc = new RTC();\n        rtc.tzName = null;\n\n        String widgetString = JsonParser.MAPPER.writeValueAsString(rtc);\n\n        assertNotNull(widgetString);\n        assertEquals(\"{\\\"type\\\":\\\"RTC\\\",\\\"id\\\":0,\\\"x\\\":0,\\\"y\\\":0,\\\"color\\\":0,\\\"width\\\":0,\\\"height\\\":0,\\\"tabId\\\":0,\\\"isDefaultColor\\\":false}\", widgetString);\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/test/java/cc/blynk/server/core/model/widgets/ui/TableSerializationTest.java",
    "content": "package cc.blynk.server.core.model.widgets.ui;\n\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.model.widgets.ui.table.Column;\nimport cc.blynk.server.core.model.widgets.ui.table.Row;\nimport cc.blynk.server.core.model.widgets.ui.table.Table;\nimport org.junit.Test;\n\nimport static org.junit.Assert.assertEquals;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 8/20/16.\n */\npublic class TableSerializationTest {\n\n    @Test\n    public void testTableNoRowsJson() throws Exception {\n        Table table = new Table();\n        table.columns = new Column[3];\n        table.columns[0] = new Column(\"indicator\");\n        table.columns[1] = new Column(\"name\");\n        table.columns[2] = new Column(\"value\");\n\n        String json = JsonParser.MAPPER.writeValueAsString(table);\n        assertEquals(\"{\\\"type\\\":\\\"TABLE\\\",\\\"id\\\":0,\\\"x\\\":0,\\\"y\\\":0,\\\"color\\\":0,\\\"width\\\":0,\\\"height\\\":0,\\\"tabId\\\":0,\\\"isDefaultColor\\\":false,\\\"deviceId\\\":0,\\\"pin\\\":-1,\\\"pwmMode\\\":false,\\\"rangeMappingOn\\\":false,\\\"min\\\":0.0,\\\"max\\\":0.0,\\\"columns\\\":[{\\\"name\\\":\\\"indicator\\\"},{\\\"name\\\":\\\"name\\\"},{\\\"name\\\":\\\"value\\\"}],\\\"currentRowIndex\\\":0,\\\"isReoderingAllowed\\\":false,\\\"isClickableRows\\\":false}\", json);\n    }\n\n    @Test\n    public void testTableSingleRowJson() throws Exception {\n        Table table = new Table();\n        table.columns = new Column[3];\n        table.columns[0] = new Column(\"indicator\");\n        table.columns[1] = new Column(\"name\");\n        table.columns[2] = new Column(\"value\");\n\n        Row row = new Row(1, \"Adskiy trash\", \"6:33\", false);\n        table.rows.add(row);\n\n        String json = JsonParser.MAPPER.writeValueAsString(table);\n        assertEquals(\"{\\\"type\\\":\\\"TABLE\\\",\\\"id\\\":0,\\\"x\\\":0,\\\"y\\\":0,\\\"color\\\":0,\\\"width\\\":0,\\\"height\\\":0,\\\"tabId\\\":0,\\\"isDefaultColor\\\":false,\\\"deviceId\\\":0,\\\"pin\\\":-1,\\\"pwmMode\\\":false,\\\"rangeMappingOn\\\":false,\\\"min\\\":0.0,\\\"max\\\":0.0,\\\"columns\\\":[{\\\"name\\\":\\\"indicator\\\"},{\\\"name\\\":\\\"name\\\"},{\\\"name\\\":\\\"value\\\"}],\\\"rows\\\":[{\\\"id\\\":1,\\\"name\\\":\\\"Adskiy trash\\\",\\\"value\\\":\\\"6:33\\\",\\\"isSelected\\\":false}],\\\"currentRowIndex\\\":0,\\\"isReoderingAllowed\\\":false,\\\"isClickableRows\\\":false}\", json);\n    }\n\n    @Test\n    public void testDeserializeTable() throws Exception {\n        Table deserializedTable = (Table) JsonParser.parseWidget(\"{\\\"type\\\":\\\"TABLE\\\",\\\"id\\\":0,\\\"x\\\":0,\\\"y\\\":0,\\\"color\\\":0,\\\"width\\\":0,\\\"height\\\":0,\\\"tabId\\\":0,\\\"isDefaultColor\\\":false,\\\"deviceId\\\":0,\\\"pin\\\":-1,\\\"pwmMode\\\":false,\\\"rangeMappingOn\\\":false,\\\"min\\\":0.0,\\\"max\\\":0.0,\\\"columns\\\":[{\\\"name\\\":\\\"indicator\\\"},{\\\"name\\\":\\\"name\\\"},{\\\"name\\\":\\\"value\\\"}],\\\"rows\\\":[{\\\"id\\\":1,\\\"name\\\":\\\"Adskiy trash\\\",\\\"value\\\":\\\"6:33\\\",\\\"isSelected\\\":false}],\\\"currentRowIndex\\\":0,\\\"isReoderingAllowed\\\":false,\\\"isClickableRows\\\":false}\");\n\n        Table table = new Table();\n        table.columns = new Column[3];\n        table.columns[0] = new Column(\"indicator\");\n        table.columns[1] = new Column(\"name\");\n        table.columns[2] = new Column(\"value\");\n\n        Row row = new Row(1, \"Adskiy trash\", \"6:33\", false);\n        table.rows.add(row);\n\n        assertEquals(deserializedTable.rows.peek().id, table.rows.peek().id);\n        assertEquals(deserializedTable.rows.peek().name, table.rows.peek().name);\n        assertEquals(deserializedTable.rows.peek().isSelected, table.rows.peek().isSelected);\n        assertEquals(deserializedTable.rows.peek().value, table.rows.peek().value);\n    }\n\n    @Test\n    public void testTableMultiRowJson() throws Exception {\n        Table table = new Table();\n        table.columns = new Column[3];\n        table.columns[0] = new Column(\"indicator\");\n        table.columns[1] = new Column(\"name\");\n        table.columns[2] = new Column(\"value\");\n\n        Row row = new Row(1, \"Adskiy trash\", \"6:33\", false);\n        table.rows.add(row);\n\n        Row row2 = new Row(2, \"Adskiy trash2\", \"6:332\", false);\n        table.rows.add(row2);\n\n        String json = JsonParser.MAPPER.writeValueAsString(table);\n        assertEquals(\"{\\\"type\\\":\\\"TABLE\\\",\\\"id\\\":0,\\\"x\\\":0,\\\"y\\\":0,\\\"color\\\":0,\\\"width\\\":0,\\\"height\\\":0,\\\"tabId\\\":0,\\\"isDefaultColor\\\":false,\\\"deviceId\\\":0,\\\"pin\\\":-1,\\\"pwmMode\\\":false,\\\"rangeMappingOn\\\":false,\\\"min\\\":0.0,\\\"max\\\":0.0,\\\"columns\\\":[{\\\"name\\\":\\\"indicator\\\"},{\\\"name\\\":\\\"name\\\"},{\\\"name\\\":\\\"value\\\"}],\\\"rows\\\":[{\\\"id\\\":1,\\\"name\\\":\\\"Adskiy trash\\\",\\\"value\\\":\\\"6:33\\\",\\\"isSelected\\\":false},{\\\"id\\\":2,\\\"name\\\":\\\"Adskiy trash2\\\",\\\"value\\\":\\\"6:332\\\",\\\"isSelected\\\":false}],\\\"currentRowIndex\\\":0,\\\"isReoderingAllowed\\\":false,\\\"isClickableRows\\\":false}\", json);\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/test/java/cc/blynk/server/core/model/widgets/ui/reporting/ReportingModelTest.java",
    "content": "package cc.blynk.server.core.model.widgets.ui.reporting;\n\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.model.widgets.outputs.graph.GraphGranularityType;\nimport cc.blynk.server.core.model.widgets.ui.reporting.source.ReportDataStream;\nimport cc.blynk.server.core.model.widgets.ui.reporting.source.ReportSource;\nimport cc.blynk.server.core.model.widgets.ui.reporting.source.TileTemplateReportSource;\nimport cc.blynk.server.core.model.widgets.ui.reporting.type.DailyReport;\nimport cc.blynk.server.core.model.widgets.ui.reporting.type.DayOfMonth;\nimport cc.blynk.server.core.model.widgets.ui.reporting.type.MonthlyReport;\nimport cc.blynk.server.core.model.widgets.ui.reporting.type.OneTimeReport;\nimport cc.blynk.server.core.model.widgets.ui.reporting.type.ReportDurationType;\nimport cc.blynk.server.core.model.widgets.ui.reporting.type.WeeklyReport;\nimport com.fasterxml.jackson.databind.ObjectWriter;\nimport org.junit.Test;\n\nimport java.time.LocalDateTime;\nimport java.time.ZoneId;\nimport java.time.ZoneOffset;\n\nimport static cc.blynk.server.core.model.widgets.ui.reporting.ReportOutput.CSV_FILE_PER_DEVICE_PER_PIN;\nimport static org.junit.Assert.assertEquals;\n\npublic class ReportingModelTest {\n\n    private ObjectWriter ow = JsonParser.init().writerWithDefaultPrettyPrinter().forType(ReportingWidget.class);\n\n    @Test\n    public void printModel() throws Exception {\n        ReportDataStream reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, \"Temperature\", true);\n        ReportSource reportSource = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream},\n                1,\n                null\n                );\n\n        ReportSource reportSource2 = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream},\n                1,\n                new int[] {0, 1}\n        );\n\n        Report report = new Report(1, \"My One Time Report\",\n                new ReportSource[] {reportSource2},\n                new OneTimeReport(86400), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE_PER_PIN, null, ZoneId.of(\"UTC\"), 0, 0, null);\n\n        Report report2 = new Report(2, \"My Daily Report\",\n                new ReportSource[] {reportSource2},\n                new DailyReport(60, ReportDurationType.CUSTOM, 100, 200), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE_PER_PIN, null, ZoneId.of(\"UTC\"), 0, 0, null);\n\n        Report report3 = new Report(3, \"My Daily Report\",\n                new ReportSource[] {reportSource2},\n                new WeeklyReport(60, ReportDurationType.CUSTOM, 100, 200, 1), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE_PER_PIN, null, ZoneId.of(\"UTC\"), 0, 0, null);\n\n        Report report4 = new Report(4, \"My Daily Report\",\n                new ReportSource[] {reportSource2},\n                new MonthlyReport(60, ReportDurationType.CUSTOM, 100, 200, DayOfMonth.FIRST), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE_PER_PIN, null, ZoneId.of(\"UTC\"), 0, 0, null);\n\n        ReportingWidget reportingWidget = new ReportingWidget();\n        reportingWidget.reportSources = new ReportSource[] {\n                reportSource\n        };\n        reportingWidget.reports = new Report[] {\n                report,\n                report2,\n                report3,\n                report4\n        };\n\n\n        System.out.println(ow.writeValueAsString(reportingWidget));\n    }\n\n    @Test\n    public void testEmailDynamicPart() {\n        ReportDataStream reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, \"Temperature\", true);\n\n        ReportSource reportSource2 = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream},\n                1,\n                new int[] {0, 1}\n        );\n\n        Report report = new Report(1, \"My One Time Report\",\n                new ReportSource[] {reportSource2},\n                new OneTimeReport(86400), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE_PER_PIN, null, ZoneId.of(\"UTC\"), 0, 0, null);\n\n        LocalDateTime localDateTime = LocalDateTime.of(2018, 2, 20, 10, 10);\n        long millis = localDateTime.toEpochSecond(ZoneOffset.UTC) * 1000;\n\n        Report report2 = new Report(2, \"My Daily Report\",\n                new ReportSource[] {reportSource2},\n                new DailyReport(millis, ReportDurationType.CUSTOM, millis, millis), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE_PER_PIN, null, ZoneId.of(\"UTC\"), 0, 0, null);\n\n        LocalDateTime start = LocalDateTime.of(2018, 3, 21, 0, 0, 0);\n        LocalDateTime end = LocalDateTime.of(2019, 3, 21, 0, 0, 0);\n        Report report3 = new Report(3, \"My Weekly Report\",\n                new ReportSource[] {reportSource2},\n                new WeeklyReport(millis, ReportDurationType.CUSTOM, start.toEpochSecond(ZoneOffset.UTC) * 1000, end.toEpochSecond(ZoneOffset.UTC) * 1000, 1), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE_PER_PIN, null, ZoneId.of(\"UTC\"), 0, 0, null);\n\n        Report report4 = new Report(4, \"My Monthly Report\",\n                new ReportSource[] {reportSource2},\n                new MonthlyReport(millis, ReportDurationType.CUSTOM, start.toEpochSecond(ZoneOffset.UTC) * 1000, end.toEpochSecond(ZoneOffset.UTC) * 1000, DayOfMonth.FIRST), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE_PER_PIN, null, ZoneId.of(\"UTC\"), 0, 0, null);\n\n        Report report5 = new Report(4, \"My Monthly Report 2\",\n                new ReportSource[] {reportSource2},\n                new MonthlyReport(millis, ReportDurationType.CUSTOM, start.toEpochSecond(ZoneOffset.UTC) * 1000, end.toEpochSecond(ZoneOffset.UTC) * 1000, DayOfMonth.LAST), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE_PER_PIN, null, ZoneId.of(\"UTC\"), 0, 0, null);\n\n        Report report6 = new Report(2, \"My Daily Report\",\n                new ReportSource[] {reportSource2},\n                new DailyReport(millis, ReportDurationType.INFINITE, millis, millis), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE_PER_PIN, null, ZoneId.of(\"UTC\"), 0, 0, null);\n\n        assertEquals(\"Report name: My One Time Report<br>Period: One time\", report.buildDynamicSection());\n        assertEquals(\"Report name: My Daily Report<br>Period: Daily, at \" + localDateTime.toLocalTime() + \"<br>Start date: 2018-02-20<br>End date: 2018-02-20<br>\", report2.buildDynamicSection());\n        assertEquals(\"Report name: My Weekly Report<br>Period: Weekly, at \" + localDateTime.toLocalTime() + \" every Monday<br>Start date: 2018-03-21<br>End date: 2019-03-21<br>\", report3.buildDynamicSection());\n        assertEquals(\"Report name: My Monthly Report<br>Period: Monthly, at \" + localDateTime.toLocalTime() + \" at the first day of every month<br>Start date: 2018-03-21<br>End date: 2019-03-21<br>\", report4.buildDynamicSection());\n        assertEquals(\"Report name: My Monthly Report 2<br>Period: Monthly, at \" + localDateTime.toLocalTime() + \" at the last day of every month<br>Start date: 2018-03-21<br>End date: 2019-03-21<br>\", report5.buildDynamicSection());\n        assertEquals(\"Report name: My Daily Report<br>Period: Daily, at \" + localDateTime.toLocalTime() + \"\", report6.buildDynamicSection());\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/test/java/cc/blynk/server/core/reporting/average/AverageAggregatorTest.java",
    "content": "package cc.blynk.server.core.reporting.average;\n\nimport cc.blynk.server.core.dao.ReportingDiskDao;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.reporting.raw.BaseReportingKey;\nimport cc.blynk.utils.AppNameUtil;\nimport org.junit.Test;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.time.LocalDateTime;\nimport java.time.ZoneOffset;\n\nimport static cc.blynk.server.core.reporting.average.AverageAggregatorProcessor.DAY;\nimport static cc.blynk.server.core.reporting.average.AverageAggregatorProcessor.HOUR;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertTrue;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 29.08.15.\n */\npublic class AverageAggregatorTest {\n\n    private final String reportingFolder = Paths.get(System.getProperty(\"java.io.tmpdir\"), \"data\").toString();\n\n    private static long getMillis(int year, int month, int dayOfMonth, int hour, int minute) {\n        LocalDateTime dateTime = LocalDateTime.of(year, month, dayOfMonth, hour, minute);\n        return dateTime.toInstant(ZoneOffset.ofTotalSeconds(0)).toEpochMilli();\n    }\n\n    @Test\n    public void testAverageWorksOkForOnePin() {\n        AverageAggregatorProcessor averageAggregator = new AverageAggregatorProcessor(\"\");\n        User user = new User();\n        user.email = \"test@test.com\";\n        user.appName = AppNameUtil.BLYNK;\n\n        PinType pinType = PinType.VIRTUAL;\n        int dashId = 1;\n        short pin = 1;\n\n        long ts = getMillis(2015, 8, 1, 0, 0);\n\n        int COUNT = 100;\n\n        double expectedAverage = 0;\n        for (int i = 0; i < COUNT; i++) {\n            expectedAverage += i;\n            averageAggregator.collect(new BaseReportingKey(user.email, user.appName, dashId, 0, pinType, pin), ts, i);\n        }\n        expectedAverage /= COUNT;\n\n        assertEquals(1, averageAggregator.getHourly().size());\n        assertEquals(1, averageAggregator.getDaily().size());\n\n        assertEquals(expectedAverage, averageAggregator.getHourly().get(new AggregationKey(user.email, user.appName, dashId, 0, pinType, pin, ts / HOUR)).calcAverage(), 0);\n        assertEquals(expectedAverage, averageAggregator.getDaily().get(new AggregationKey(user.email, user.appName, dashId, 0, pinType, pin, ts / DAY)).calcAverage(), 0);\n    }\n\n    @Test\n    public void testAverageWorksForOneDay() {\n        AverageAggregatorProcessor averageAggregator = new AverageAggregatorProcessor(\"\");\n        User user = new User();\n        user.email = \"test@test.com\";\n        user.appName = AppNameUtil.BLYNK;\n        PinType pinType = PinType.VIRTUAL;\n        int dashId = 1;\n        short pin = 1;\n\n        double expectedDailyAverage = 0;\n\n        int COUNT = 100;\n\n        for (int hour = 0; hour < 24; hour++) {\n            long ts = getMillis(2015, 8, 1, hour, 0);\n\n            double expectedAverage = 0;\n            for (int i = 0; i < COUNT; i++) {\n                expectedAverage += i;\n                averageAggregator.collect(new BaseReportingKey(user.email, user.appName, dashId, 0, pinType, pin), ts, i);\n            }\n            expectedDailyAverage += expectedAverage;\n            expectedAverage /= COUNT;\n\n            assertEquals(hour + 1, averageAggregator.getHourly().size());\n\n            assertEquals(expectedAverage, averageAggregator.getHourly().get(new AggregationKey(user.email, user.appName, dashId, 0, pinType, pin, ts / HOUR)).calcAverage(), 0);\n        }\n        expectedDailyAverage /= COUNT * 24;\n\n        assertEquals(24, averageAggregator.getHourly().size());\n        assertEquals(1, averageAggregator.getDaily().size());\n        assertEquals(expectedDailyAverage, averageAggregator.getDaily().get(new AggregationKey(user.email, user.appName, dashId, 0, pinType, pin, getMillis(2015, 8, 1, 0, 0) / DAY)).calcAverage(), 0);\n    }\n\n    @Test\n    public void testTempFilesCreated() throws IOException {\n        Path dir = Paths.get(reportingFolder, \"\");\n        if (Files.notExists(dir)) {\n            Files.createDirectories(dir);\n        }\n\n        AverageAggregatorProcessor averageAggregator = new AverageAggregatorProcessor(reportingFolder);\n\n        User user = new User();\n        user.email = \"test@test.com\";\n        user.appName = AppNameUtil.BLYNK;\n        PinType pinType = PinType.VIRTUAL;\n        int dashId = 1;\n        short pin = 1;\n\n        double expectedDailyAverage = 0;\n\n        int COUNT = 100;\n\n        for (int hour = 0; hour < 24; hour++) {\n            long ts = getMillis(2015, 8, 1, hour, 0);\n\n            double expectedAverage = 0;\n            for (int i = 0; i < COUNT; i++) {\n                expectedAverage += i;\n                averageAggregator.collect(new BaseReportingKey(user.email, user.appName, dashId, 0, pinType, pin), ts, i);\n            }\n            expectedDailyAverage += expectedAverage;\n            expectedAverage /= COUNT;\n\n            assertEquals(hour + 1, averageAggregator.getHourly().size());\n\n            assertEquals(expectedAverage, averageAggregator.getHourly().get(new AggregationKey(user.email, user.appName, dashId, 0, pinType, pin, ts / HOUR)).calcAverage(), 0);\n        }\n        expectedDailyAverage /= COUNT * 24;\n\n        assertEquals(24, averageAggregator.getHourly().size());\n        assertEquals(1, averageAggregator.getDaily().size());\n        assertEquals(expectedDailyAverage, averageAggregator.getDaily().get(new AggregationKey(new BaseReportingKey(user.email, user.appName, dashId, 0, pinType, pin), getMillis(2015, 8, 1, 0, 0) / DAY)).calcAverage(), 0);\n\n\n        averageAggregator.close();\n\n        assertTrue(Files.exists(Paths.get(reportingFolder, AverageAggregatorProcessor.HOURLY_TEMP_FILENAME)));\n        assertTrue(Files.exists(Paths.get(reportingFolder, AverageAggregatorProcessor.DAILY_TEMP_FILENAME)));\n\n        averageAggregator = new AverageAggregatorProcessor(reportingFolder);\n\n        assertEquals(24, averageAggregator.getHourly().size());\n        assertEquals(1, averageAggregator.getDaily().size());\n        assertEquals(expectedDailyAverage, averageAggregator.getDaily().get(new AggregationKey(new BaseReportingKey(user.email, user.appName, dashId, 0, pinType, pin), getMillis(2015, 8, 1, 0, 0) / DAY)).calcAverage(), 0);\n\n        assertTrue(Files.notExists(Paths.get(reportingFolder, AverageAggregatorProcessor.HOURLY_TEMP_FILENAME)));\n        assertTrue(Files.notExists(Paths.get(reportingFolder, AverageAggregatorProcessor.DAILY_TEMP_FILENAME)));\n\n        ReportingDiskDao reportingDao = new ReportingDiskDao(reportingFolder, true);\n\n        reportingDao.delete(user, dashId, 0, PinType.VIRTUAL, pin);\n        assertTrue(Files.notExists(Paths.get(reportingFolder, AverageAggregatorProcessor.HOURLY_TEMP_FILENAME)));\n        assertTrue(Files.notExists(Paths.get(reportingFolder, AverageAggregatorProcessor.DAILY_TEMP_FILENAME)));\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/test/java/cc/blynk/server/db/CloneProjectTest.java",
    "content": "package cc.blynk.server.db;\n\nimport cc.blynk.server.core.BlockingIOProcessor;\nimport org.junit.AfterClass;\nimport org.junit.Before;\nimport org.junit.BeforeClass;\nimport org.junit.Test;\n\nimport static org.junit.Assert.*;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 19.02.16.\n */\npublic class CloneProjectTest {\n\n    private static DBManager dbManager;\n    private static BlockingIOProcessor blockingIOProcessor;\n\n    @BeforeClass\n    public static void init() throws Exception {\n        blockingIOProcessor = new BlockingIOProcessor(4, 5000);\n        dbManager = new DBManager(\"db-test.properties\", blockingIOProcessor, true);\n        assertNotNull(dbManager.getConnection());\n    }\n\n    @AfterClass\n    public static void close() {\n        dbManager.close();\n    }\n\n    @Before\n    public void cleanAll() throws Exception {\n        //clean everything just in case\n        dbManager.executeSQL(\"DELETE FROM cloned_projects\");\n    }\n\n    @Test\n    public void testNoToken() throws Exception {\n        assertNull(dbManager.cloneProjectDBDao.selectClonedProjectByToken(\"123\"));\n    }\n\n    @Test\n    public void testInsertAndSelect() throws Exception {\n        assertTrue(dbManager.insertClonedProject(\"token\", \"json\"));\n        assertEquals(\"json\", dbManager.selectClonedProject(\"token\"));\n    }\n\n    @Test\n    public void testInsertAndSelectWrong() throws Exception {\n        assertTrue(dbManager.insertClonedProject(\"token\", \"json\"));\n        assertNull(dbManager.selectClonedProject(\"token2\"));\n    }\n\n}\n\n"
  },
  {
    "path": "server/core/src/test/java/cc/blynk/server/db/DBManagerTest.java",
    "content": "package cc.blynk.server.db;\n\nimport cc.blynk.server.core.BlockingIOProcessor;\nimport cc.blynk.server.core.dao.UserKey;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.Profile;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.reporting.average.AverageAggregatorProcessor;\nimport cc.blynk.server.db.dao.ReportingDBDao;\nimport cc.blynk.server.db.model.Purchase;\nimport cc.blynk.server.db.model.Redeem;\nimport cc.blynk.utils.AppNameUtil;\nimport cc.blynk.utils.DateTimeUtils;\nimport org.junit.AfterClass;\nimport org.junit.Before;\nimport org.junit.BeforeClass;\nimport org.junit.Ignore;\nimport org.junit.Test;\nimport org.postgresql.copy.CopyManager;\nimport org.postgresql.core.BaseConnection;\n\nimport java.io.File;\nimport java.io.FileOutputStream;\nimport java.io.OutputStreamWriter;\nimport java.io.Writer;\nimport java.sql.Connection;\nimport java.sql.PreparedStatement;\nimport java.sql.ResultSet;\nimport java.sql.Statement;\nimport java.util.ArrayList;\nimport java.util.Calendar;\nimport java.util.Map;\nimport java.util.TimeZone;\nimport java.util.UUID;\nimport java.util.concurrent.ConcurrentMap;\nimport java.util.zip.GZIPOutputStream;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertFalse;\nimport static org.junit.Assert.assertNotNull;\nimport static org.junit.Assert.assertNull;\nimport static org.junit.Assert.assertTrue;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 19.02.16.\n */\npublic class DBManagerTest {\n\n    private static DBManager dbManager;\n    private static BlockingIOProcessor blockingIOProcessor;\n    private static final Calendar UTC = Calendar.getInstance(TimeZone.getTimeZone(\"UTC\"));\n\n    @BeforeClass\n    public static void init() throws Exception {\n        blockingIOProcessor = new BlockingIOProcessor(4, 10000);\n        dbManager = new DBManager(\"db-test.properties\", blockingIOProcessor, true);\n        assertNotNull(dbManager.getConnection());\n    }\n\n    @AfterClass\n    public static void close() {\n        dbManager.close();\n    }\n\n    @Before\n    public void cleanAll() throws Exception {\n        //clean everything just in case\n        dbManager.executeSQL(\"DELETE FROM users\");\n        dbManager.executeSQL(\"DELETE FROM purchase\");\n        dbManager.executeSQL(\"DELETE FROM redeem\");\n    }\n\n    @Test\n    public void test() throws Exception {\n        assertNotNull(dbManager.getConnection());\n    }\n\n    @Test\n    public void testDbVersion() throws Exception {\n        int dbVersion = dbManager.userDBDao.getDBVersion();\n        assertTrue(dbVersion >= 90500);\n    }\n\n    @Test\n    @Ignore(\"not used right now in read code\")\n    public void testCopy100RecordsIntoFile() throws Exception {\n        System.out.println(\"Starting\");\n\n        int a = 0;\n\n        long start = System.currentTimeMillis();\n        try (Connection connection = dbManager.getConnection();\n             PreparedStatement ps = connection.prepareStatement(ReportingDBDao.insertMinute)) {\n\n            String userName = \"test@gmail.com\";\n            long minute = (System.currentTimeMillis() / AverageAggregatorProcessor.MINUTE) * AverageAggregatorProcessor.MINUTE;\n\n            for (int i = 0; i < 100; i++) {\n                ReportingDBDao.prepareReportingInsert(ps, userName, 1, 0, (short) 0, PinType.VIRTUAL, minute, (double) i);\n                ps.addBatch();\n                minute += AverageAggregatorProcessor.MINUTE;\n                a++;\n            }\n\n            ps.executeBatch();\n            connection.commit();\n        }\n\n        System.out.println(\"Finished : \" + (System.currentTimeMillis() - start)  + \" millis. Executed : \" + a);\n\n\n        try (Connection connection = dbManager.getConnection();\n             Writer gzipWriter = new OutputStreamWriter(new GZIPOutputStream(new FileOutputStream(new File(\"/home/doom369/output.csv.gz\"))), \"UTF-8\")) {\n\n            CopyManager copyManager = new CopyManager(connection.unwrap(BaseConnection.class));\n\n\n            String selectQuery = \"select pintype || pin, ts, value from reporting_average_minute where project_id = 1 and email = 'test@gmail.com'\";\n            long res = copyManager.copyOut(\"COPY (\" + selectQuery + \" ) TO STDOUT WITH (FORMAT CSV)\", gzipWriter);\n            System.out.println(res);\n        }\n\n\n    }\n\n    @Test\n    public void testUpsertForDifferentApps() throws Exception {\n        ArrayList<User> users = new ArrayList<>();\n        users.add(new User(\"test1@gmail.com\", \"pass\", \"testapp2\", \"local\", \"127.0.0.1\", false, false));\n        users.add(new User(\"test1@gmail.com\", \"pass\", \"testapp1\", \"local\", \"127.0.0.1\", false, false));\n        dbManager.userDBDao.save(users);\n        ConcurrentMap<UserKey, User> dbUsers = dbManager.userDBDao.getAllUsers(\"local\");\n        assertEquals(2, dbUsers.size());\n    }\n\n    @Test\n    public void testUpsertAndSelect() throws Exception {\n        ArrayList<User> users = new ArrayList<>();\n        for (int i = 0; i < 10000; i++) {\n            users.add(new User(\"test\" + i + \"@gmail.com\", \"pass\", AppNameUtil.BLYNK, \"local\", \"127.0.0.1\", false, false));\n        }\n        //dbManager.saveUsers(users);\n        dbManager.userDBDao.save(users);\n\n        ConcurrentMap<UserKey, User> dbUsers = dbManager.userDBDao.getAllUsers(\"local\");\n        System.out.println(\"Records : \" + dbUsers.size());\n    }\n\n    @Test\n    public void testUpsertUser() throws Exception {\n        ArrayList<User> users = new ArrayList<>();\n        User user = new User(\"test@gmail.com\", \"pass\", AppNameUtil.BLYNK, \"local\", \"127.0.0.1\", false, false);\n        user.name = \"123\";\n        user.lastModifiedTs = 0;\n        user.lastLoggedAt = 1;\n        user.lastLoggedIP = \"127.0.0.1\";\n        users.add(user);\n        user = new User(\"test@gmail.com\", \"pass\", AppNameUtil.BLYNK, \"local\", \"127.0.0.1\", false, false);\n        user.lastModifiedTs = 0;\n        user.lastLoggedAt = 1;\n        user.lastLoggedIP = \"127.0.0.1\";\n        user.name = \"123\";\n        users.add(user);\n        user = new User(\"test2@gmail.com\", \"pass\", AppNameUtil.BLYNK, \"local\", \"127.0.0.1\", false, false);\n        user.lastModifiedTs = 0;\n        user.lastLoggedAt = 1;\n        user.lastLoggedIP = \"127.0.0.1\";\n        user.name = \"123\";\n        users.add(user);\n\n        dbManager.userDBDao.save(users);\n\n        try (Connection connection = dbManager.getConnection();\n             Statement statement = connection.createStatement();\n             ResultSet rs = statement.executeQuery(\"select * from users where email = 'test@gmail.com'\")) {\n            while (rs.next()) {\n                assertEquals(\"test@gmail.com\", rs.getString(\"email\"));\n                assertEquals(AppNameUtil.BLYNK, rs.getString(\"appName\"));\n                assertEquals(\"local\", rs.getString(\"region\"));\n                assertEquals(\"123\", rs.getString(\"name\"));\n                assertEquals(\"pass\", rs.getString(\"pass\"));\n                assertEquals(0, rs.getTimestamp(\"last_modified\", DateTimeUtils.UTC_CALENDAR).getTime());\n                assertEquals(1, rs.getTimestamp(\"last_logged\", DateTimeUtils.UTC_CALENDAR).getTime());\n                assertEquals(\"127.0.0.1\", rs.getString(\"last_logged_ip\"));\n                assertFalse(rs.getBoolean(\"is_facebook_user\"));\n                assertFalse(rs.getBoolean(\"is_super_admin\"));\n                assertEquals(2000, rs.getInt(\"energy\"));\n\n                assertEquals(\"{}\", rs.getString(\"json\"));\n            }\n            connection.commit();\n        }\n    }\n\n    @Test\n    public void testUpsertUserFieldUpdated() throws Exception {\n        ArrayList<User> users = new ArrayList<>();\n        User user = new User(\"test@gmail.com\", \"pass\", AppNameUtil.BLYNK, \"local\", \"127.0.0.1\", false, false);\n        user.lastModifiedTs = 0;\n        user.lastLoggedAt = 1;\n        user.lastLoggedIP = \"127.0.0.1\";\n        users.add(user);\n\n        dbManager.userDBDao.save(users);\n\n        users = new ArrayList<>();\n        user = new User(\"test@gmail.com\", \"pass2\", AppNameUtil.BLYNK, \"local2\", \"127.0.0.1\", true, true);\n        user.name = \"1234\";\n        user.lastModifiedTs = 1;\n        user.lastLoggedAt = 2;\n        user.lastLoggedIP = \"127.0.0.2\";\n        user.energy = 1000;\n        user.profile = new Profile();\n        DashBoard dash = new DashBoard();\n        dash.id = 1;\n        dash.name = \"123\";\n        user.profile.dashBoards = new DashBoard[]{dash};\n\n        users.add(user);\n\n        dbManager.userDBDao.save(users);\n\n        ConcurrentMap<UserKey, User>  persistent = dbManager.userDBDao.getAllUsers(\"local2\");\n\n        user = persistent.get(new UserKey(\"test@gmail.com\", AppNameUtil.BLYNK));\n\n        assertEquals(\"test@gmail.com\", user.email);\n        assertEquals(AppNameUtil.BLYNK, user.appName);\n        assertEquals(\"local2\", user.region);\n        assertEquals(\"pass2\", user.pass);\n        assertEquals(\"1234\", user.name);\n        assertEquals(\"127.0.0.1\", user.ip);\n        assertEquals(1, user.lastModifiedTs);\n        assertEquals(2, user.lastLoggedAt);\n        assertEquals(\"127.0.0.2\", user.lastLoggedIP);\n        assertTrue(user.isFacebookUser);\n        assertTrue(user.isSuperAdmin);\n        assertEquals(1000, user.energy);\n\n        assertEquals(\"{\\\"dashBoards\\\":[{\\\"id\\\":1,\\\"parentId\\\":-1,\\\"isPreview\\\":false,\\\"name\\\":\\\"123\\\",\\\"createdAt\\\":0,\\\"updatedAt\\\":0,\\\"theme\\\":\\\"Blynk\\\",\\\"keepScreenOn\\\":false,\\\"isAppConnectedOn\\\":false,\\\"isNotificationsOff\\\":false,\\\"isShared\\\":false,\\\"isActive\\\":false,\\\"widgetBackgroundOn\\\":false,\\\"color\\\":-1,\\\"isDefaultColor\\\":true}]}\", user.profile.toString());\n    }\n\n    @Test\n    public void testInsertAndGetUser() throws Exception {\n        ArrayList<User> users = new ArrayList<>();\n        User user = new User(\"test@gmail.com\", \"pass\", AppNameUtil.BLYNK, \"local\", \"127.0.0.1\", true, true);\n        user.lastModifiedTs = 0;\n        user.lastLoggedAt = 1;\n        user.lastLoggedIP = \"127.0.0.1\";\n        user.profile = new Profile();\n        DashBoard dash = new DashBoard();\n        dash.id = 1;\n        dash.name = \"123\";\n        user.profile.dashBoards = new DashBoard[]{dash};\n        users.add(user);\n\n        dbManager.userDBDao.save(users);\n\n        ConcurrentMap<UserKey, User> dbUsers = dbManager.userDBDao.getAllUsers(\"local\");\n\n        assertNotNull(dbUsers);\n        assertEquals(1, dbUsers.size());\n        User dbUser = dbUsers.get(new UserKey(user.email, user.appName));\n\n        assertEquals(\"test@gmail.com\", dbUser.email);\n        assertEquals(AppNameUtil.BLYNK, dbUser.appName);\n        assertEquals(\"local\", dbUser.region);\n        assertEquals(\"pass\", dbUser.pass);\n        assertEquals(0, dbUser.lastModifiedTs);\n        assertEquals(1, dbUser.lastLoggedAt);\n        assertEquals(\"127.0.0.1\", dbUser.lastLoggedIP);\n        assertEquals(\"{\\\"dashBoards\\\":[{\\\"id\\\":1,\\\"parentId\\\":-1,\\\"isPreview\\\":false,\\\"name\\\":\\\"123\\\",\\\"createdAt\\\":0,\\\"updatedAt\\\":0,\\\"theme\\\":\\\"Blynk\\\",\\\"keepScreenOn\\\":false,\\\"isAppConnectedOn\\\":false,\\\"isNotificationsOff\\\":false,\\\"isShared\\\":false,\\\"isActive\\\":false,\\\"widgetBackgroundOn\\\":false,\\\"color\\\":-1,\\\"isDefaultColor\\\":true}]}\", dbUser.profile.toString());\n        assertTrue(dbUser.isFacebookUser);\n        assertTrue(dbUser.isSuperAdmin);\n        assertEquals(2000, dbUser.energy);\n\n        assertEquals(\"{\\\"dashBoards\\\":[{\\\"id\\\":1,\\\"parentId\\\":-1,\\\"isPreview\\\":false,\\\"name\\\":\\\"123\\\",\\\"createdAt\\\":0,\\\"updatedAt\\\":0,\\\"theme\\\":\\\"Blynk\\\",\\\"keepScreenOn\\\":false,\\\"isAppConnectedOn\\\":false,\\\"isNotificationsOff\\\":false,\\\"isShared\\\":false,\\\"isActive\\\":false,\\\"widgetBackgroundOn\\\":false,\\\"color\\\":-1,\\\"isDefaultColor\\\":true}]}\", dbUser.profile.toString());\n    }\n\n    @Test\n    public void testInsertGetDeleteUser() throws Exception {\n        ArrayList<User> users = new ArrayList<>();\n        User user = new User(\"test@gmail.com\", \"pass\", AppNameUtil.BLYNK, \"local\", \"127.0.0.1\", true, true);\n        user.lastModifiedTs = 0;\n        user.lastLoggedAt = 1;\n        user.lastLoggedIP = \"127.0.0.1\";\n        user.profile = new Profile();\n        DashBoard dash = new DashBoard();\n        dash.id = 1;\n        dash.name = \"123\";\n        user.profile.dashBoards = new DashBoard[]{dash};\n        users.add(user);\n\n        dbManager.userDBDao.save(users);\n\n        Map<UserKey, User> dbUsers = dbManager.userDBDao.getAllUsers(\"local\");\n\n        assertNotNull(dbUsers);\n        assertEquals(1, dbUsers.size());\n        User dbUser = dbUsers.get(new UserKey(user.email, user.appName));\n\n        assertEquals(\"test@gmail.com\", dbUser.email);\n        assertEquals(AppNameUtil.BLYNK, dbUser.appName);\n        assertEquals(\"local\", dbUser.region);\n        assertEquals(\"pass\", dbUser.pass);\n        assertEquals(0, dbUser.lastModifiedTs);\n        assertEquals(1, dbUser.lastLoggedAt);\n        assertEquals(\"127.0.0.1\", dbUser.lastLoggedIP);\n        assertEquals(\"{\\\"dashBoards\\\":[{\\\"id\\\":1,\\\"parentId\\\":-1,\\\"isPreview\\\":false,\\\"name\\\":\\\"123\\\",\\\"createdAt\\\":0,\\\"updatedAt\\\":0,\\\"theme\\\":\\\"Blynk\\\",\\\"keepScreenOn\\\":false,\\\"isAppConnectedOn\\\":false,\\\"isNotificationsOff\\\":false,\\\"isShared\\\":false,\\\"isActive\\\":false,\\\"widgetBackgroundOn\\\":false,\\\"color\\\":-1,\\\"isDefaultColor\\\":true}]}\", dbUser.profile.toString());\n        assertTrue(dbUser.isFacebookUser);\n        assertTrue(dbUser.isSuperAdmin);\n        assertEquals(2000, dbUser.energy);\n\n        assertEquals(\"{\\\"dashBoards\\\":[{\\\"id\\\":1,\\\"parentId\\\":-1,\\\"isPreview\\\":false,\\\"name\\\":\\\"123\\\",\\\"createdAt\\\":0,\\\"updatedAt\\\":0,\\\"theme\\\":\\\"Blynk\\\",\\\"keepScreenOn\\\":false,\\\"isAppConnectedOn\\\":false,\\\"isNotificationsOff\\\":false,\\\"isShared\\\":false,\\\"isActive\\\":false,\\\"widgetBackgroundOn\\\":false,\\\"color\\\":-1,\\\"isDefaultColor\\\":true}]}\", dbUser.profile.toString());\n\n        assertTrue(dbManager.userDBDao.deleteUser(new UserKey(user.email, user.appName)));\n        dbUsers = dbManager.userDBDao.getAllUsers(\"local\");\n        assertNotNull(dbUsers);\n        assertEquals(0, dbUsers.size());\n    }\n\n    @Test\n    public void testRedeem() throws Exception {\n        assertNull(dbManager.selectRedeemByToken(\"123\"));\n        String token = UUID.randomUUID().toString().replace(\"-\", \"\");\n        dbManager.executeSQL(\"insert into redeem (token) values('\" + token + \"')\");\n        assertNotNull(dbManager.selectRedeemByToken(token));\n        assertNull(dbManager.selectRedeemByToken(\"123\"));\n    }\n\n    @Test\n    public void testPurchase() throws Exception {\n        dbManager.insertPurchase(new Purchase(\"test@gmail.com\", 1000, 1.00D, \"123456\"));\n\n\n        try (Connection connection = dbManager.getConnection();\n             Statement statement = connection.createStatement();\n             ResultSet rs = statement.executeQuery(\"select * from purchase\")) {\n\n            while (rs.next()) {\n                assertEquals(\"test@gmail.com\", rs.getString(\"email\"));\n                assertEquals(1000, rs.getInt(\"reward\"));\n                assertEquals(\"123456\", rs.getString(\"transactionId\"));\n                assertEquals(0.99D, rs.getDouble(\"price\"), 0.1D);\n                assertNotNull(rs.getDate(\"ts\"));\n            }\n\n            connection.commit();\n        }\n    }\n\n    @Test\n    public void testOptimisticLockingRedeem() throws Exception {\n        String token = UUID.randomUUID().toString().replace(\"-\", \"\");\n        dbManager.executeSQL(\"insert into redeem (token) values('\" + token + \"')\");\n\n        Redeem redeem = dbManager.selectRedeemByToken(token);\n        assertNotNull(redeem);\n        assertEquals(redeem.token, token);\n        assertFalse(redeem.isRedeemed);\n        assertEquals(1, redeem.version);\n        assertNull(redeem.ts);\n\n        assertTrue(dbManager.updateRedeem(\"user@user.com\", token));\n        assertFalse(dbManager.updateRedeem(\"user@user.com\", token));\n\n        redeem = dbManager.selectRedeemByToken(token);\n        assertNotNull(redeem);\n        assertEquals(redeem.token, token);\n        assertTrue(redeem.isRedeemed);\n        assertEquals(2, redeem.version);\n        assertEquals(\"user@user.com\", redeem.email);\n        assertNotNull(redeem.ts);\n    }\n\n    @Test\n    public void getUserIpNotExists() {\n        String userIp = dbManager.userDBDao.getUserServerIp(\"test@gmail.com\", AppNameUtil.BLYNK);\n        assertNull(userIp);\n    }\n\n    @Test\n    public void getUserIp() {\n        ArrayList<User> users = new ArrayList<>();\n        User user = new User(\"test@gmail.com\", \"pass\", AppNameUtil.BLYNK, \"local\", \"127.0.0.1\", false, false);\n        user.lastModifiedTs = 0;\n        user.lastLoggedAt = 1;\n        user.lastLoggedIP = \"127.0.0.1\";\n        users.add(user);\n\n        dbManager.userDBDao.save(users);\n\n        String userIp = dbManager.userDBDao.getUserServerIp(\"test@gmail.com\", AppNameUtil.BLYNK);\n        assertEquals(\"127.0.0.1\", userIp);\n    }\n}\n"
  },
  {
    "path": "server/core/src/test/java/cc/blynk/server/db/FlashedTokensManagerTest.java",
    "content": "package cc.blynk.server.db;\n\nimport cc.blynk.server.core.BlockingIOProcessor;\nimport cc.blynk.server.db.model.FlashedToken;\nimport org.junit.AfterClass;\nimport org.junit.Before;\nimport org.junit.BeforeClass;\nimport org.junit.Test;\n\nimport java.sql.Connection;\nimport java.sql.ResultSet;\nimport java.sql.Statement;\nimport java.util.UUID;\n\nimport static org.junit.Assert.*;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 19.02.16.\n */\npublic class FlashedTokensManagerTest {\n\n    private static DBManager dbManager;\n    private static BlockingIOProcessor blockingIOProcessor;\n\n    @BeforeClass\n    public static void init() throws Exception {\n        blockingIOProcessor = new BlockingIOProcessor(4, 5000);\n        dbManager = new DBManager(\"db-test.properties\", blockingIOProcessor, true);\n        assertNotNull(dbManager.getConnection());\n    }\n\n    @AfterClass\n    public static void close() {\n        dbManager.close();\n    }\n\n    @Before\n    public void cleanAll() throws Exception {\n        //clean everything just in case\n        dbManager.executeSQL(\"DELETE FROM flashed_tokens\");\n    }\n\n    @Test\n    public void test() throws Exception {\n        assertNotNull(dbManager.getConnection());\n    }\n\n    @Test\n    public void testNoToken() throws Exception {\n        assertNull(dbManager.selectFlashedToken(\"123\"));\n    }\n\n    @Test\n    public void testInsertAndSelect() throws Exception {\n        FlashedToken[] list = new FlashedToken[1];\n        String token = UUID.randomUUID().toString().replace(\"-\", \"\");\n        FlashedToken flashedToken = new FlashedToken(\"test@blynk.cc\", token, \"appname\", 1, 0);\n        list[0] = flashedToken;\n        dbManager.insertFlashedTokens(list);\n\n\n        FlashedToken selected = dbManager.selectFlashedToken(token);\n\n        assertEquals(flashedToken, selected);\n    }\n\n    @Test\n    public void testInsertToken() throws Exception {\n        FlashedToken[] list = new FlashedToken[1];\n        String token = UUID.randomUUID().toString().replace(\"-\", \"\");\n        FlashedToken flashedToken = new FlashedToken(\"test@blynk.cc\", token, \"appname\", 1, 0);\n        list[0] = flashedToken;\n        dbManager.insertFlashedTokens(list);\n\n        try (Connection connection = dbManager.getConnection();\n             Statement statement = connection.createStatement();\n             ResultSet rs = statement.executeQuery(\"select * from flashed_tokens\")) {\n\n            while (rs.next()) {\n                assertEquals(flashedToken.token, rs.getString(\"token\"));\n                assertEquals(flashedToken.appId, rs.getString(\"app_name\"));\n                assertEquals(flashedToken.deviceId, rs.getInt(\"device_id\"));\n                assertFalse(rs.getBoolean(\"is_activated\"));\n                assertNull(rs.getDate(\"ts\"));\n            }\n\n            connection.commit();\n        }\n    }\n\n    @Test\n    public void testInsertAndActivate() throws Exception {\n        FlashedToken[] list = new FlashedToken[1];\n        String token = UUID.randomUUID().toString().replace(\"-\", \"\");\n        FlashedToken flashedToken = new FlashedToken(\"test@blynk.cc\", token, \"appname\", 1, 0);\n        list[0] = flashedToken;\n        dbManager.insertFlashedTokens(list);\n        dbManager.activateFlashedToken(flashedToken.token);\n\n        try (Connection connection = dbManager.getConnection();\n             Statement statement = connection.createStatement();\n             ResultSet rs = statement.executeQuery(\"select * from flashed_tokens\")) {\n\n            while (rs.next()) {\n                assertEquals(flashedToken.token, rs.getString(\"token\"));\n                assertEquals(flashedToken.appId, rs.getString(\"app_name\"));\n                assertEquals(flashedToken.deviceId, rs.getInt(\"device_id\"));\n                assertTrue(rs.getBoolean(\"is_activated\"));\n                assertNotNull(rs.getDate(\"ts\"));\n            }\n\n            connection.commit();\n        }\n    }\n\n}\n\n"
  },
  {
    "path": "server/core/src/test/java/cc/blynk/server/db/ForwardingTokenTest.java",
    "content": "package cc.blynk.server.db;\n\nimport cc.blynk.server.core.BlockingIOProcessor;\nimport org.junit.AfterClass;\nimport org.junit.Before;\nimport org.junit.BeforeClass;\nimport org.junit.Test;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\nimport static org.junit.Assert.assertNull;\nimport static org.junit.Assert.assertTrue;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 19.02.16.\n */\npublic class ForwardingTokenTest {\n\n    private static DBManager dbManager;\n    private static BlockingIOProcessor blockingIOProcessor;\n\n    @BeforeClass\n    public static void init() throws Exception {\n        blockingIOProcessor = new BlockingIOProcessor(4, 5000);\n        dbManager = new DBManager(\"db-test.properties\", blockingIOProcessor, true);\n        assertNotNull(dbManager.getConnection());\n    }\n\n    @AfterClass\n    public static void close() {\n        dbManager.close();\n    }\n\n    @Before\n    public void cleanAll() throws Exception {\n        //clean everything just in case\n        dbManager.executeSQL(\"DELETE FROM forwarding_tokens\");\n    }\n\n    @Test\n    public void testNoToken() throws Exception {\n        assertNull(dbManager.forwardingTokenDBDao.selectHostByToken(\"123\"));\n    }\n\n    @Test\n    public void testInsertAndSelect() throws Exception {\n        assertTrue(dbManager.forwardingTokenDBDao.insertTokenHost(\"token\", \"host\", \"email\", 0, 0));\n        assertEquals(\"host\", dbManager.forwardingTokenDBDao.selectHostByToken(\"token\"));\n    }\n\n    @Test\n    public void testInsertAndSelectWrong() throws Exception {\n        assertTrue(dbManager.forwardingTokenDBDao.insertTokenHost(\"token\", \"host\", \"email\", 0, 0));\n        assertNull(dbManager.forwardingTokenDBDao.selectHostByToken(\"token2\"));\n    }\n\n    @Test\n    public void deleteToken() throws Exception {\n        assertTrue(dbManager.forwardingTokenDBDao.insertTokenHost(\"token\", \"host\", \"email\", 0, 0));\n        assertTrue(dbManager.forwardingTokenDBDao.deleteToken(\"token\"));\n        assertNull(dbManager.forwardingTokenDBDao.selectHostByToken(\"token\"));\n    }\n\n    @Test\n    public void invalidToken() throws Exception {\n        assertNull(dbManager.forwardingTokenDBDao.selectHostByToken(\"\\0\"));\n    }\n\n    @Test\n    public void deleteTokens() throws Exception {\n        assertTrue(dbManager.forwardingTokenDBDao.insertTokenHost(\"token1\", \"host1\", \"email\", 0, 0));\n        assertTrue(dbManager.forwardingTokenDBDao.insertTokenHost(\"token2\", \"host2\", \"email\", 0, 0));\n        assertTrue(dbManager.forwardingTokenDBDao.insertTokenHost(\"token3\", \"host3\", \"email\", 0, 0));\n        assertTrue(dbManager.forwardingTokenDBDao.deleteToken(\"token1\", \"token2\"));\n        assertNull(dbManager.forwardingTokenDBDao.selectHostByToken(\"token1\"));\n        assertNull(dbManager.forwardingTokenDBDao.selectHostByToken(\"token2\"));\n        assertEquals(\"host3\", dbManager.forwardingTokenDBDao.selectHostByToken(\"token3\"));\n    }\n}\n\n"
  },
  {
    "path": "server/core/src/test/java/cc/blynk/server/db/RawDataDBTest.java",
    "content": "package cc.blynk.server.db;\n\nimport cc.blynk.server.core.BlockingIOProcessor;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.reporting.raw.BaseReportingKey;\nimport cc.blynk.server.core.reporting.raw.RawDataProcessor;\nimport cc.blynk.utils.AppNameUtil;\nimport cc.blynk.utils.NumberUtil;\nimport org.junit.AfterClass;\nimport org.junit.Before;\nimport org.junit.BeforeClass;\nimport org.junit.Test;\n\nimport java.sql.Connection;\nimport java.sql.ResultSet;\nimport java.sql.Statement;\nimport java.util.Calendar;\nimport java.util.TimeZone;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\nimport static org.junit.Assert.assertNull;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 19.02.16.\n */\npublic class RawDataDBTest {\n\n    private static ReportingDBManager reportingDBManager;\n    private static BlockingIOProcessor blockingIOProcessor;\n    private static final Calendar UTC = Calendar.getInstance(TimeZone.getTimeZone(\"UTC\"));\n    private static User user;\n\n    @BeforeClass\n    public static void init() throws Exception {\n        blockingIOProcessor = new BlockingIOProcessor(4, 10000);\n        reportingDBManager = new ReportingDBManager(\"db-test.properties\", blockingIOProcessor, true);\n        assertNotNull(reportingDBManager.getConnection());\n        user = new User();\n        user.email = \"test@test.com\";\n        user.appName = AppNameUtil.BLYNK;\n    }\n\n    @AfterClass\n    public static void close() {\n        reportingDBManager.close();\n    }\n\n    @Before\n    public void cleanAll() throws Exception {\n        //clean everything just in case\n        reportingDBManager.executeSQL(\"DELETE FROM reporting_raw_data\");\n    }\n\n    @Test\n    public void testInsertStringAsRawData() throws Exception {\n        RawDataProcessor rawDataProcessor = new RawDataProcessor(true);\n        rawDataProcessor.collect(new BaseReportingKey(user.email, user.appName, 1, 2, PinType.VIRTUAL, (short) 3), 1111111111, \"Lamp is ON\", NumberUtil.NO_RESULT);\n\n        //invoking directly dao to avoid separate thread execution\n        reportingDBManager.reportingDBDao.insertRawData(rawDataProcessor.rawStorage);\n\n        try (Connection connection = reportingDBManager.getConnection();\n             Statement statement = connection.createStatement();\n             ResultSet rs = statement.executeQuery(\"select * from reporting_raw_data\")) {\n\n\n            while (rs.next()) {\n                assertEquals(\"test@test.com\", rs.getString(\"email\"));\n                assertEquals(1, rs.getInt(\"project_id\"));\n                assertEquals(2, rs.getInt(\"device_id\"));\n                assertEquals(3, rs.getByte(\"pin\"));\n                assertEquals(\"v\", rs.getString(\"pinType\"));\n                assertEquals(1111111111, rs.getTimestamp(\"ts\", UTC).getTime());\n                assertEquals(\"Lamp is ON\", rs.getString(\"stringValue\"));\n                assertNull(rs.getString(\"doubleValue\"));\n\n            }\n\n            connection.commit();\n        }\n    }\n\n    @Test\n    public void testInsertDoubleAsRawData() throws Exception {\n        RawDataProcessor rawDataProcessor = new RawDataProcessor(true);\n        rawDataProcessor.collect(new BaseReportingKey(user.email, user.appName, 1, 2, PinType.VIRTUAL, (short) 3), 1111111111, \"Lamp is ON\", 1.33D);\n\n        //invoking directly dao to avoid separate thread execution\n        reportingDBManager.reportingDBDao.insertRawData(rawDataProcessor.rawStorage);\n\n        try (Connection connection = reportingDBManager.getConnection();\n             Statement statement = connection.createStatement();\n             ResultSet rs = statement.executeQuery(\"select * from reporting_raw_data\")) {\n\n\n            while (rs.next()) {\n                assertEquals(\"test@test.com\", rs.getString(\"email\"));\n                assertEquals(1, rs.getInt(\"project_id\"));\n                assertEquals(2, rs.getInt(\"device_id\"));\n                assertEquals(3, rs.getByte(\"pin\"));\n                assertEquals(\"v\", rs.getString(\"pinType\"));\n                assertEquals(1111111111, rs.getTimestamp(\"ts\", UTC).getTime());\n                assertNull(rs.getString(\"stringValue\"));\n                assertEquals(1.33D, rs.getDouble(\"doubleValue\"), 0.0000001);\n\n            }\n\n            connection.commit();\n        }\n\n    }\n\n    //todo tests for large batches.\n\n\n}\n"
  },
  {
    "path": "server/core/src/test/java/cc/blynk/server/db/RealtimeStatsDBTest.java",
    "content": "package cc.blynk.server.db;\n\nimport cc.blynk.server.core.BlockingIOProcessor;\nimport cc.blynk.server.core.dao.SessionDao;\nimport cc.blynk.server.core.dao.UserDao;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.widgets.outputs.graph.GraphGranularityType;\nimport cc.blynk.server.core.model.widgets.ui.reporting.ReportScheduler;\nimport cc.blynk.server.core.reporting.average.AggregationKey;\nimport cc.blynk.server.core.reporting.average.AggregationValue;\nimport cc.blynk.server.core.reporting.average.AverageAggregatorProcessor;\nimport cc.blynk.server.core.stats.GlobalStats;\nimport cc.blynk.server.core.stats.model.CommandStat;\nimport cc.blynk.server.core.stats.model.HttpStat;\nimport cc.blynk.server.core.stats.model.Stat;\nimport cc.blynk.server.db.dao.ReportingDBDao;\nimport cc.blynk.utils.AppNameUtil;\nimport org.junit.AfterClass;\nimport org.junit.Before;\nimport org.junit.BeforeClass;\nimport org.junit.Test;\n\nimport java.sql.Connection;\nimport java.sql.PreparedStatement;\nimport java.sql.ResultSet;\nimport java.sql.Statement;\nimport java.time.Instant;\nimport java.util.Calendar;\nimport java.util.Collections;\nimport java.util.Map;\nimport java.util.TimeZone;\nimport java.util.concurrent.ConcurrentHashMap;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 19.02.16.\n */\npublic class RealtimeStatsDBTest {\n\n    private static ReportingDBManager reportingDBManager;\n    private static BlockingIOProcessor blockingIOProcessor;\n    private static final Calendar UTC = Calendar.getInstance(TimeZone.getTimeZone(\"UTC\"));\n\n    @BeforeClass\n    public static void init() throws Exception {\n        blockingIOProcessor = new BlockingIOProcessor(4, 10000);\n        reportingDBManager = new ReportingDBManager(\"db-test.properties\", blockingIOProcessor, true);\n        assertNotNull(reportingDBManager.getConnection());\n    }\n\n    @AfterClass\n    public static void close() {\n        reportingDBManager.close();\n    }\n\n    @Before\n    public void cleanAll() throws Exception {\n        //clean everything just in case\n        reportingDBManager.executeSQL(\"DELETE FROM reporting_app_stat_minute\");\n        reportingDBManager.executeSQL(\"DELETE FROM reporting_app_command_stat_minute\");\n        reportingDBManager.executeSQL(\"DELETE FROM reporting_http_command_stat_minute\");\n        reportingDBManager.executeSQL(\"DELETE FROM reporting_average_minute\");\n        reportingDBManager.executeSQL(\"DELETE FROM reporting_average_hourly\");\n        reportingDBManager.executeSQL(\"DELETE FROM reporting_average_daily\");\n    }\n\n    @Test\n    public void testRealTimeStatsInsertWroks() throws Exception {\n        String region = \"ua\";\n        long now = System.currentTimeMillis();\n\n        SessionDao sessionDao = new SessionDao();\n        UserDao userDao = new UserDao(new ConcurrentHashMap<>(), \"test\", \"127.0.0.1\");\n        BlockingIOProcessor blockingIOProcessor = new BlockingIOProcessor(6, 1000);\n\n        Stat stat = new Stat(sessionDao, userDao, blockingIOProcessor, new GlobalStats(), new ReportScheduler(1, \"http://localhost/\", null, null, Collections.emptyMap()), false);\n        int i;\n\n        final HttpStat hs = stat.http;\n        i = 0;\n        hs.isHardwareConnected = i++;\n        hs.isAppConnected = i++;\n        hs.getPinData = i++;\n        hs.updatePinData = i++;\n        hs.email = i++;\n        hs.notify = i++;\n        hs.getProject = i++;\n        hs.qr = i++;\n        hs.getHistoryPinData = i++;\n        hs.total = i;\n\n        final CommandStat cs = stat.commands;\n        i = 0;\n        cs.response = i++;\n        cs.register = i++;\n        cs.login = i++;\n        cs.loadProfile = i++;\n        cs.appSync = i++;\n        cs.sharing = i++;\n        cs.getToken = i++;\n        cs.ping = i++;\n        cs.activate = i++;\n        cs.deactivate = i++;\n        cs.refreshToken = i++;\n        cs.getGraphData = i++;\n        cs.exportGraphData = i++;\n        cs.setWidgetProperty = i++;\n        cs.bridge = i++;\n        cs.hardware = i++;\n        cs.getSharedDash = i++;\n        cs.getShareToken = i++;\n        cs.refreshShareToken = i++;\n        cs.shareLogin = i++;\n        cs.createProject = i++;\n        cs.updateProject = i++;\n        cs.deleteProject = i++;\n        cs.hardwareSync = i++;\n        cs.internal = i++;\n        cs.sms = i++;\n        cs.tweet = i++;\n        cs.email = i++;\n        cs.push = i++;\n        cs.addPushToken = i++;\n        cs.createWidget = i++;\n        cs.updateWidget = i++;\n        cs.deleteWidget = i++;\n        cs.createDevice = i++;\n        cs.updateDevice = i++;\n        cs.deleteDevice = i++;\n        cs.getDevices = i++;\n        cs.createTag = i++;\n        cs.updateTag = i++;\n        cs.deleteTag = i++;\n        cs.getTags = i++;\n        cs.addEnergy = i++;\n        cs.getEnergy = i++;\n        cs.getServer = i++;\n        cs.connectRedirect = i++;\n        cs.webSockets = i++;\n        cs.eventor = i++;\n        cs.webhooks = i++;\n        cs.appTotal = i++;\n        cs.mqttTotal = i;\n\n        reportingDBManager.reportingDBDao.insertStat(region, stat);\n\n        try (Connection connection = reportingDBManager.getConnection();\n             Statement statement = connection.createStatement();\n             ResultSet rs = statement.executeQuery(\"select * from reporting_app_stat_minute\")) {\n\n\n            while (rs.next()) {\n                assertEquals(region, rs.getString(\"region\"));\n                assertEquals((now / AverageAggregatorProcessor.MINUTE) * AverageAggregatorProcessor.MINUTE, rs.getTimestamp(\"ts\", UTC).getTime());\n\n                assertEquals(0, rs.getInt(\"minute_rate\"));\n                assertEquals(0, rs.getInt(\"registrations\"));\n                assertEquals(0, rs.getInt(\"active\"));\n                assertEquals(0, rs.getInt(\"active_week\"));\n                assertEquals(0, rs.getInt(\"active_month\"));\n                assertEquals(0, rs.getInt(\"connected\"));\n                assertEquals(0, rs.getInt(\"online_apps\"));\n                assertEquals(0, rs.getInt(\"total_online_apps\"));\n                assertEquals(0, rs.getInt(\"online_hards\"));\n                assertEquals(0, rs.getInt(\"total_online_hards\"));\n            }\n\n            connection.commit();\n        }\n\n        try (Connection connection = reportingDBManager.getConnection();\n             Statement statement = connection.createStatement();\n             ResultSet rs = statement.executeQuery(\"select * from reporting_http_command_stat_minute\")) {\n            i = 0;\n            while (rs.next()) {\n                assertEquals(region, rs.getString(\"region\"));\n                assertEquals((now / AverageAggregatorProcessor.MINUTE) * AverageAggregatorProcessor.MINUTE, rs.getTimestamp(\"ts\", UTC).getTime());\n\n                assertEquals(i++, rs.getInt(\"is_hardware_connected\"));\n                assertEquals(i++, rs.getInt(\"is_app_connected\"));\n                assertEquals(i++, rs.getInt(\"get_pin_data\"));\n                assertEquals(i++, rs.getInt(\"update_pin\"));\n                assertEquals(i++, rs.getInt(\"email\"));\n                assertEquals(i++, rs.getInt(\"push\"));\n                assertEquals(i++, rs.getInt(\"get_project\"));\n                assertEquals(i++, rs.getInt(\"qr\"));\n                assertEquals(i++, rs.getInt(\"get_history_pin_data\"));\n                assertEquals(i++, rs.getInt(\"total\"));\n            }\n\n            connection.commit();\n        }\n\n        try (Connection connection = reportingDBManager.getConnection();\n             Statement statement = connection.createStatement();\n             ResultSet rs = statement.executeQuery(\"select * from reporting_app_command_stat_minute\")) {\n            i = 0;\n            while (rs.next()) {\n                assertEquals(region, rs.getString(\"region\"));\n                assertEquals((now / AverageAggregatorProcessor.MINUTE) * AverageAggregatorProcessor.MINUTE, rs.getTimestamp(\"ts\", UTC).getTime());\n\n                assertEquals(i++, rs.getInt(\"response\"));\n                assertEquals(i++, rs.getInt(\"register\"));\n                assertEquals(i++, rs.getInt(\"login\"));\n                assertEquals(i++, rs.getInt(\"load_profile\"));\n                assertEquals(i++, rs.getInt(\"app_sync\"));\n                assertEquals(i++, rs.getInt(\"sharing\"));\n                assertEquals(i++, rs.getInt(\"get_token\"));\n                assertEquals(i++, rs.getInt(\"ping\"));\n                assertEquals(i++, rs.getInt(\"activate\"));\n                assertEquals(i++, rs.getInt(\"deactivate\"));\n                assertEquals(i++, rs.getInt(\"refresh_token\"));\n                assertEquals(i++, rs.getInt(\"get_graph_data\"));\n                assertEquals(i++, rs.getInt(\"export_graph_data\"));\n                assertEquals(i++, rs.getInt(\"set_widget_property\"));\n                assertEquals(i++, rs.getInt(\"bridge\"));\n                assertEquals(i++, rs.getInt(\"hardware\"));\n                assertEquals(i++, rs.getInt(\"get_share_dash\"));\n                assertEquals(i++, rs.getInt(\"get_share_token\"));\n                assertEquals(i++, rs.getInt(\"refresh_share_token\"));\n                assertEquals(i++, rs.getInt(\"share_login\"));\n                assertEquals(i++, rs.getInt(\"create_project\"));\n                assertEquals(i++, rs.getInt(\"update_project\"));\n                assertEquals(i++, rs.getInt(\"delete_project\"));\n                assertEquals(i++, rs.getInt(\"hardware_sync\"));\n                assertEquals(i++, rs.getInt(\"internal\"));\n                assertEquals(i++, rs.getInt(\"sms\"));\n                assertEquals(i++, rs.getInt(\"tweet\"));\n                assertEquals(i++, rs.getInt(\"email\"));\n                assertEquals(i++, rs.getInt(\"push\"));\n                assertEquals(i++, rs.getInt(\"add_push_token\"));\n                assertEquals(i++, rs.getInt(\"create_widget\"));\n                assertEquals(i++, rs.getInt(\"update_widget\"));\n                assertEquals(i++, rs.getInt(\"delete_widget\"));\n                assertEquals(i++, rs.getInt(\"create_device\"));\n                assertEquals(i++, rs.getInt(\"update_device\"));\n                assertEquals(i++, rs.getInt(\"delete_device\"));\n                assertEquals(i++, rs.getInt(\"get_devices\"));\n                assertEquals(i++, rs.getInt(\"create_tag\"));\n                assertEquals(i++, rs.getInt(\"update_tag\"));\n                assertEquals(i++, rs.getInt(\"delete_tag\"));\n                assertEquals(i++, rs.getInt(\"get_tags\"));\n                assertEquals(i++, rs.getInt(\"add_energy\"));\n                assertEquals(i++, rs.getInt(\"get_energy\"));\n                assertEquals(i++, rs.getInt(\"get_server\"));\n                assertEquals(i++, rs.getInt(\"connect_redirect\"));\n                assertEquals(i++, rs.getInt(\"web_sockets\"));\n                assertEquals(i++, rs.getInt(\"eventor\"));\n                assertEquals(i++, rs.getInt(\"webhooks\"));\n                assertEquals(i++, rs.getInt(\"appTotal\"));\n                assertEquals(i, rs.getInt(\"hardTotal\"));\n            }\n\n            connection.commit();\n        }\n\n\n    }\n\n    @Test\n    public void testManyConnections() throws Exception {\n        User user = new User();\n        user.email = \"test@test.com\";\n        user.appName = AppNameUtil.BLYNK;\n        Map<AggregationKey, AggregationValue> map = new ConcurrentHashMap<>();\n        AggregationValue value = new AggregationValue();\n        value.update(1);\n        long ts = System.currentTimeMillis();\n        for (int i = 0; i < 60; i++) {\n            map.put(new AggregationKey(user.email, user.appName, i, 0, PinType.ANALOG, (short) i, ts), value);\n            reportingDBManager.insertReporting(map, GraphGranularityType.MINUTE);\n            reportingDBManager.insertReporting(map, GraphGranularityType.HOURLY);\n            reportingDBManager.insertReporting(map, GraphGranularityType.DAILY);\n\n            map.clear();\n        }\n\n        while (blockingIOProcessor.messagingExecutor.getActiveCount() > 0) {\n            Thread.sleep(100);\n        }\n\n    }\n\n    @Test\n    public void cleanOutdatedRecords() {\n        reportingDBManager.reportingDBDao.cleanOldReportingRecords(Instant.now());\n    }\n\n    @Test\n    public void testDeleteWorksAsExpected() throws Exception {\n        long minute;\n        try (Connection connection = reportingDBManager.getConnection();\n             PreparedStatement ps = connection.prepareStatement(ReportingDBDao.insertMinute)) {\n\n            minute = (System.currentTimeMillis() / AverageAggregatorProcessor.MINUTE) * AverageAggregatorProcessor.MINUTE;\n\n            for (int i = 0; i < 370; i++) {\n                ReportingDBDao.prepareReportingInsert(ps, \"test1111@gmail.com\", 1, 0, (short) 0, PinType.VIRTUAL, minute, (double) i);\n                ps.addBatch();\n                minute += AverageAggregatorProcessor.MINUTE;\n            }\n\n            ps.executeBatch();\n            connection.commit();\n        }\n    }\n\n    @Test\n    public void testInsert1000RecordsAndSelect() throws Exception {\n        int a = 0;\n\n        String userName = \"test@gmail.com\";\n\n        long start = System.currentTimeMillis();\n        long minute = (start / AverageAggregatorProcessor.MINUTE) * AverageAggregatorProcessor.MINUTE;\n        long startMinute = minute;\n\n        try (Connection connection = reportingDBManager.getConnection();\n             PreparedStatement ps = connection.prepareStatement(ReportingDBDao.insertMinute)) {\n\n            for (int i = 0; i < 1000; i++) {\n                ReportingDBDao.prepareReportingInsert(ps, userName, 1, 2, (short) 0, PinType.VIRTUAL, minute, (double) i);\n                ps.addBatch();\n                minute += AverageAggregatorProcessor.MINUTE;\n                a++;\n            }\n\n            ps.executeBatch();\n            connection.commit();\n        }\n\n        System.out.println(\"Finished : \" + (System.currentTimeMillis() - start)  + \" millis. Executed : \" + a);\n\n\n        try (Connection connection = reportingDBManager.getConnection();\n             Statement statement = connection.createStatement();\n             ResultSet rs = statement.executeQuery(\"select * from reporting_average_minute order by ts ASC\")) {\n\n            int i = 0;\n            while (rs.next()) {\n                assertEquals(userName, rs.getString(\"email\"));\n                assertEquals(1, rs.getInt(\"project_id\"));\n                assertEquals(2, rs.getInt(\"device_id\"));\n                assertEquals(0, rs.getByte(\"pin\"));\n                assertEquals(PinType.VIRTUAL, PinType.values()[rs.getInt(\"pin_type\")]);\n                assertEquals(startMinute, rs.getTimestamp(\"ts\", UTC).getTime());\n                assertEquals((double) i, rs.getDouble(\"value\"), 0.0001);\n                startMinute += AverageAggregatorProcessor.MINUTE;\n                i++;\n            }\n            connection.commit();\n        }\n    }\n\n    @Test\n    public void testSelect() throws Exception {\n        long ts = 1455924480000L;\n        try (Connection connection = reportingDBManager.getConnection();\n             PreparedStatement ps = connection.prepareStatement(ReportingDBDao.selectMinute)) {\n\n            ReportingDBDao.prepareReportingSelect(ps, ts, 2);\n            ResultSet rs = ps.executeQuery();\n\n\n            while(rs.next()) {\n                System.out.println(rs.getLong(\"ts\") + \" \" + rs.getDouble(\"value\"));\n            }\n\n            rs.close();\n        }\n    }\n}\n"
  },
  {
    "path": "server/core/src/test/java/cc/blynk/server/internal/SerializationTokenPoolTest.java",
    "content": "package cc.blynk.server.internal;\n\nimport cc.blynk.server.internal.token.BaseToken;\nimport cc.blynk.server.internal.token.ResetPassToken;\nimport cc.blynk.server.internal.token.TokensPool;\nimport cc.blynk.utils.TokenGeneratorUtil;\nimport org.junit.Test;\n\nimport java.util.concurrent.ConcurrentHashMap;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\n\npublic class SerializationTokenPoolTest {\n\n    @Test\n    public void someTEst() {\n        String path = System.getProperty(\"java.io.tmpdir\");\n        TokensPool tokensPool = new TokensPool(path);\n\n        String token = TokenGeneratorUtil.generateNewToken();\n        ResetPassToken resetPassToken = new ResetPassToken(\"dima@mail.us\", \"Blynk\");\n        tokensPool.addToken(token, resetPassToken);\n        tokensPool.close();\n\n        TokensPool tokensPool2 = new TokensPool(path);\n        ConcurrentHashMap<String, BaseToken> tokens = tokensPool2.getTokens();\n        assertNotNull(tokens);\n        assertEquals(1, tokens.size());\n        ResetPassToken resetPassToken2 = (ResetPassToken) tokens.get(token);\n        assertNotNull(resetPassToken2);\n        assertEquals(\"dima@mail.us\", resetPassToken2.email);\n        assertEquals(\"Blynk\", resetPassToken2.appName);\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/test/java/cc/blynk/test/utils/FileManagerIntegrationTest.java",
    "content": "package cc.blynk.test.utils;\n\nimport cc.blynk.server.core.dao.FileManager;\nimport cc.blynk.server.core.dao.UserKey;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.utils.AppNameUtil;\nimport org.junit.Before;\nimport org.junit.Test;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.Map;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\n\n/**\n * User: ddumanskiy\n * Date: 09.12.13\n * Time: 8:07\n */\npublic class FileManagerIntegrationTest {\n\n    private final User user1 = new User(\"name1\", \"pass1\", AppNameUtil.BLYNK, \"local\", \"127.0.0.1\", false, false);\n    private final User user2 = new User(\"name2\", \"pass2\", AppNameUtil.BLYNK, \"local\", \"127.0.0.1\", false, false);\n\n    private FileManager fileManager;\n\n    @Before\n    public void cleanup() throws IOException {\n        String dataFolder = Paths.get(System.getProperty(\"java.io.tmpdir\"), \"blynk\").toString();\n        org.apache.commons.io.FileUtils.deleteDirectory(Paths.get(dataFolder).toFile());\n        fileManager = new FileManager(dataFolder, null);\n    }\n\n    @Test\n    public void testGenerateFileName() {\n        Path file = fileManager.generateFileName(user1.email, user1.appName);\n        assertEquals(\"name1.Blynk.user\", file.getFileName().toString());\n    }\n\n    @Test\n    public void testNotNullTokenManager() throws IOException {\n        fileManager.overrideUserFile(user1);\n\n        Map<UserKey, User> users = fileManager.deserializeUsers();\n        assertNotNull(users);\n        assertNotNull(users.get(new UserKey(user1.email, AppNameUtil.BLYNK)));\n    }\n\n    @Test\n    public void testCreationTempFile() throws IOException {\n        fileManager.overrideUserFile(user1);\n        //file existence ignored\n        fileManager.overrideUserFile(user1);\n    }\n\n    @Test\n    public void testReadListOfFiles() throws IOException {\n        fileManager.overrideUserFile(user1);\n        fileManager.overrideUserFile(user2);\n        Path fakeFile = Paths.get(fileManager.getDataDir().toString(), \"123.txt\");\n        Files.deleteIfExists(fakeFile);\n        Files.createFile(fakeFile);\n\n        Map<UserKey, User> users = fileManager.deserializeUsers();\n        assertNotNull(users);\n        assertEquals(2, users.size());\n        assertNotNull(users.get(new UserKey(user1.email, AppNameUtil.BLYNK)));\n        assertNotNull(users.get(new UserKey(user2.email, AppNameUtil.BLYNK)));\n    }\n\n    @Test\n    public void testOverrideFiles() throws IOException {\n        fileManager.overrideUserFile(user1);\n        fileManager.overrideUserFile(user1);\n\n        Map<UserKey, User> users = fileManager.deserializeUsers();\n        assertNotNull(users);\n        assertNotNull(users.get(new UserKey(user1.email, AppNameUtil.BLYNK)));\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/test/java/cc/blynk/test/utils/JsonParsingTest.java",
    "content": "package cc.blynk.test.utils;\n\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.DataStream;\nimport cc.blynk.server.core.model.Profile;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.controls.Button;\nimport cc.blynk.server.core.model.widgets.controls.RGB;\nimport cc.blynk.server.core.model.widgets.controls.Timer;\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.ObjectReader;\nimport org.junit.Test;\n\nimport java.io.InputStream;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertFalse;\nimport static org.junit.Assert.assertNotNull;\nimport static org.junit.Assert.assertTrue;\n\n/**\n * User: ddumanskiy\n * Date: 21.11.13\n * Time: 13:27\n */\npublic class JsonParsingTest {\n\n    private static final ObjectReader profileReader = JsonParser.init().readerFor(Profile.class);\n\n    public static Profile parseProfile(InputStream reader) {\n        try {\n            return profileReader.readValue(reader);\n        } catch (Exception e) {\n            throw new RuntimeException(\"Error parsing profile\");\n        }\n    }\n\n    @Test\n    public void testParseUserProfile() {\n        InputStream is = this.getClass().getResourceAsStream(\"/json_test/user_profile_json.txt\");\n\n        Profile profile = parseProfile(is);\n        assertNotNull(profile);\n        assertNotNull(profile.dashBoards);\n        assertEquals(profile.dashBoards.length, 1);\n\n        DashBoard dashBoard = profile.dashBoards[0];\n\n        assertNotNull(dashBoard);\n\n        assertEquals(1, dashBoard.id);\n        assertEquals(\"My Dashboard\", dashBoard.name);\n        assertNotNull(dashBoard.widgets);\n        assertEquals(dashBoard.widgets.length, 8);\n\n        for (Widget widget : dashBoard.widgets) {\n            assertNotNull(widget);\n            assertEquals(1, widget.x);\n            assertEquals(1, widget.y);\n            assertEquals(1, widget.id);\n            assertEquals(\"Some Text\", widget.label);\n        }\n    }\n\n    @Test\n    public void testUserProfileToJson() {\n        InputStream is = this.getClass().getResourceAsStream(\"/json_test/user_profile_json.txt\");\n\n        Profile profile = parseProfile(is);\n        String userProfileString = profile.toString();\n\n        assertNotNull(userProfileString);\n        assertTrue(userProfileString.contains(\"dashBoards\"));\n    }\n\n    @Test\n    public void testParseIOSProfile() {\n        InputStream is = this.getClass().getResourceAsStream(\"/json_test/user_ios_profile_json.txt\");\n\n        Profile profile = parseProfile(is);\n\n        assertNotNull(profile);\n        assertNotNull(profile.dashBoards);\n        assertEquals(1, profile.dashBoards.length);\n        assertNotNull(profile.dashBoards[0].widgets);\n        assertNotNull(profile.dashBoards[0].widgets[0]);\n        assertNotNull(profile.dashBoards[0].widgets[1]);\n        assertTrue(((Button) profile.dashBoards[0].widgets[0]).pushMode);\n        assertFalse(((Button) profile.dashBoards[0].widgets[1]).pushMode);\n    }\n\n    @Test\n    public void testJoystickAndFieldAreParsed() {\n        InputStream is = this.getClass().getResourceAsStream(\"/json_test/user_profile_json_with_outdated_widget.txt\");\n\n        Profile profile = parseProfile(is);\n\n        assertNotNull(profile);\n        assertNotNull(profile.dashBoards);\n        assertEquals(1, profile.dashBoards.length);\n        assertNotNull(profile.dashBoards[0].widgets);\n        assertNotNull(profile.dashBoards[0].widgets[0]);\n        assertNotNull(profile.dashBoards[0].widgets[1]);\n        assertTrue(profile.dashBoards[0].widgets[0] instanceof Button);\n        assertTrue(profile.dashBoards[0].widgets[1] instanceof Button);\n    }\n\n    @Test\n    public void testJSONToRGB() {\n        InputStream is = this.getClass().getResourceAsStream(\"/json_test/user_profile_json_RGB.txt\");\n\n        Profile profile = parseProfile(is);\n\n        assertNotNull(profile);\n        assertNotNull(profile.dashBoards);\n        assertEquals(1, profile.dashBoards.length);\n        assertNotNull(profile.dashBoards[0]);\n        assertNotNull(profile.dashBoards[0].widgets);\n        assertEquals(1, profile.dashBoards[0].widgets.length);\n\n        RGB rgb = (RGB) profile.dashBoards[0].widgets[0];\n\n        assertNotNull(rgb.dataStreams);\n        assertEquals(2, rgb.dataStreams.length);\n        DataStream dataStream1 = rgb.dataStreams[0];\n        DataStream dataStream2 = rgb.dataStreams[1];\n\n        assertNotNull(dataStream1);\n        assertNotNull(dataStream2);\n\n        assertEquals(1, dataStream1.pin);\n        assertEquals(2, dataStream2.pin);\n\n        assertEquals(\"1\", dataStream1.value);\n        assertEquals(\"2\", dataStream2.value);\n\n        assertEquals(PinType.DIGITAL, dataStream1.pinType);\n        assertEquals(PinType.DIGITAL, dataStream2.pinType);\n\n        assertFalse(dataStream1.pwmMode);\n        assertTrue(dataStream2.pwmMode);\n\n    }\n\n    @Test\n    public void testUserProfileToJson2() {\n        InputStream is = this.getClass().getResourceAsStream(\"/json_test/user_profile_json_2.txt\");\n\n        Profile profile = parseProfile(is);\n        String userProfileString = profile.toString();\n\n        assertNotNull(userProfileString);\n        assertTrue(userProfileString.contains(\"dashBoards\"));\n    }\n\n    @Test\n    public void testUserProfileToJson3() {\n        InputStream is = this.getClass().getResourceAsStream(\"/json_test/user_profile_json_3.txt\");\n\n        Profile profile = parseProfile(is);\n        String userProfileString = profile.toString();\n\n        assertNotNull(userProfileString);\n        assertTrue(userProfileString.contains(\"dashBoards\"));\n    }\n\n    @Test\n    public void testUserProfileToJsonWithTimer() {\n        InputStream is = this.getClass().getResourceAsStream(\"/json_test/user_profile_with_timer.txt\");\n\n        Profile profile = parseProfile(is);\n        String userProfileString = profile.toString();\n\n        assertNotNull(userProfileString);\n        assertTrue(userProfileString.contains(\"dashBoards\"));\n        List<Timer> timers = getActiveTimerWidgets(profile);\n        assertNotNull(timers);\n        assertEquals(1, timers.size());\n    }\n\n    private List<Timer> getActiveTimerWidgets(Profile profile) {\n        if (profile.dashBoards.length == 0) {\n            return Collections.emptyList();\n        }\n\n        List<Timer> activeTimers = new ArrayList<>();\n        for (DashBoard dashBoard : profile.dashBoards) {\n            if (dashBoard.isActive) {\n                activeTimers.addAll(getTimerWidgets(dashBoard));\n            }\n        }\n        return activeTimers;\n    }\n\n    private List<Timer> getTimerWidgets(DashBoard dashBoard) {\n        if (dashBoard.widgets.length == 0) {\n            return Collections.emptyList();\n        }\n\n        List<Timer> timerWidgets = new ArrayList<>();\n        for (Widget widget : dashBoard.widgets) {\n            if (widget instanceof Timer) {\n                Timer timer = (Timer) widget;\n                if ((timer.startTime != -1 && timer.startValue != null && !timer.startValue.isEmpty()) ||\n                        (timer.stopTime != -1 && timer.stopValue != null && !timer.stopValue.isEmpty())) {\n                    timerWidgets.add(timer);\n                }\n            }\n        }\n\n        return timerWidgets;\n    }\n\n\n    @Test\n    public void correctSerializedObject() throws JsonProcessingException {\n        Button button = new Button();\n        button.id = 1;\n        button.label = \"MyButton\";\n        button.x = 2;\n        button.y = 2;\n        button.color = 0;\n        button.width = 2;\n        button.height = 2;\n        button.pushMode = false;\n\n        String result = JsonParser.MAPPER.writeValueAsString(button);\n\n        assertEquals(\"{\\\"type\\\":\\\"BUTTON\\\",\\\"id\\\":1,\\\"x\\\":2,\\\"y\\\":2,\\\"color\\\":0,\\\"width\\\":2,\\\"height\\\":2,\\\"tabId\\\":0,\\\"label\\\":\\\"MyButton\\\",\\\"isDefaultColor\\\":false,\\\"deviceId\\\":0,\\\"pin\\\":-1,\\\"pwmMode\\\":false,\\\"rangeMappingOn\\\":false,\\\"min\\\":0.0,\\\"max\\\":0.0,\\\"pushMode\\\":false}\", result);\n    }\n}\n"
  },
  {
    "path": "server/core/src/test/java/cc/blynk/test/utils/NumberUtilTest.java",
    "content": "package cc.blynk.test.utils;\n\nimport org.junit.Test;\n\nimport java.util.concurrent.ThreadLocalRandom;\n\nimport static cc.blynk.utils.NumberUtil.NO_RESULT;\nimport static cc.blynk.utils.NumberUtil.parseDouble;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertTrue;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 23.01.17.\n */\npublic class NumberUtilTest {\n\n    @Test\n    public void testCorrectResultForInt() {\n        for (int i = 0; i < 10_000; i++) {\n            int random = ThreadLocalRandom.current().nextInt(Integer.MIN_VALUE, Integer.MAX_VALUE);\n            double parsed = parseDouble(String.valueOf(random));\n            //System.out.println(random);\n            assertEquals(random, parsed, 0.0000000001);\n        }\n    }\n\n    @Test\n    public void testCorrectResultForDouble() {\n        for (int i = 0; i < 10_000; i++) {\n            double random = ThreadLocalRandom.current().nextDouble(-100000, 100000);\n            double parsed = parseDouble(String.valueOf(random));\n            //System.out.println(random);\n            assertEquals(random, parsed, 0.0000000001);\n        }\n    }\n\n    @Test\n    public void testCorrectResultForDouble2() {\n        for (int i = 0; i < 10_000; i++) {\n            double random = ThreadLocalRandom.current().nextDouble();\n            double parsed = parseDouble(String.valueOf(random));\n            //System.out.println(random);\n            assertEquals(random, parsed, 0.0000000001);\n        }\n    }\n\n    @Test(expected = NullPointerException.class)\n    public void testExpectError() {\n        parseDouble(null);\n    }\n\n    @Test\n    public void testExpectError2() {\n        assertTrue(parseDouble(\"\") == NO_RESULT);\n    }\n\n    @Test\n    public void testExpectError3() {\n        assertTrue(parseDouble(\"123.123F\") == NO_RESULT);\n    }\n\n    @Test\n    public void testExpectError4() {\n        assertTrue(parseDouble(\"p 123.123\") == NO_RESULT);\n    }\n\n    @Test\n    public void testExpectError5() {\n        assertTrue(parseDouble(\"p 123.123\") == NO_RESULT);\n    }\n\n    @Test\n    public void testCustomValue() {\n        double d;\n        d = parseDouble(\"0\");\n        assertEquals(d, 0, 0.0000000001);\n\n        d = parseDouble(\"0.0\");\n        assertEquals(d, 0, 0.0000000001);\n\n        d = parseDouble(\"1.0\");\n        assertEquals(d, 1.0, 0.0000000001);\n\n        d = parseDouble(\"+1.0\");\n        assertEquals(d, 1.0, 0.0000000001);\n\n        d = parseDouble(\"-1.0\");\n        assertEquals(d, -1.0, 0.0000000001);\n    }\n}\n"
  },
  {
    "path": "server/core/src/test/java/cc/blynk/test/utils/StringUtilPerfTest.java",
    "content": "package cc.blynk.test.utils;\n\nimport cc.blynk.utils.StringUtils;\nimport org.openjdk.jmh.annotations.Benchmark;\nimport org.openjdk.jmh.annotations.BenchmarkMode;\nimport org.openjdk.jmh.annotations.Fork;\nimport org.openjdk.jmh.annotations.Measurement;\nimport org.openjdk.jmh.annotations.Mode;\nimport org.openjdk.jmh.annotations.OutputTimeUnit;\nimport org.openjdk.jmh.annotations.Scope;\nimport org.openjdk.jmh.annotations.Setup;\nimport org.openjdk.jmh.annotations.State;\nimport org.openjdk.jmh.annotations.Warmup;\nimport org.openjdk.jmh.infra.BenchmarkParams;\n\nimport java.util.concurrent.TimeUnit;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 16.01.16.\n */\n@BenchmarkMode(Mode.AverageTime)\n@OutputTimeUnit(TimeUnit.NANOSECONDS)\n@State(Scope.Thread)\n@Fork(1)\n@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)\n@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)\npublic class StringUtilPerfTest {\n\n     /*\n     * Should your benchmark require returning multiple results, you have to\n     * consider two options (detailed below).\n     *\n     * NOTE: If you are only producing a single result, it is more readable to\n     * use the implicit return, as in JMHSample_08_DeadCode. Do not make your benchmark\n     * code less readable with explicit Blackholes!\n     */\n\n    public String vw_1;\n    public String aw_1_2;\n    public String vw_99_900;\n    public String vw_99_22222;\n    public String aw_100_900;\n    public String aw_10_long_text;\n\n    @Setup\n    public void setup(BenchmarkParams params) {\n        vw_1 = \"vw 1\".replaceAll(\" \", \"\\0\");\n        aw_1_2 = \"aw 1 2\".replaceAll(\" \", \"\\0\");\n        vw_99_900 = \"vw 99 900\".replaceAll(\" \", \"\\0\");\n        vw_99_22222 = \"vw 99 22222.32\".replaceAll(\" \", \"\\0\");\n        aw_100_900 = \"aw 100 200\".replaceAll(\" \", \"\\0\");\n        aw_10_long_text = \"aw 10  dsfdsfdsfdsfdsfdsfdsfdsfd gfdsgdfg dfg dfg dfsgdf gdfs gdfsg dfsg dfsg dfs\".replaceAll(\" \", \"\\0\");\n    }\n\n    @Benchmark\n    public String[] split3_vw_1() {\n        return vw_1.split(\"\\0\", 3);\n    }\n\n    @Benchmark\n    public String[] customSplit3_vw_1() {\n        return StringUtils.split3(vw_1);\n    }\n\n    @Benchmark\n    public String[] split3_aw_1_2() {\n        return aw_1_2.split(\"\\0\", 3);\n    }\n\n    @Benchmark\n    public String[] customSplit3_aw_1_2() {\n        return StringUtils.split3(aw_1_2);\n    }\n\n    @Benchmark\n    public String[] split3_vw_99_900() {\n        return vw_99_900.split(\"\\0\", 3);\n    }\n\n    @Benchmark\n    public String[] customSplit3_vw_99_900() {\n        return StringUtils.split3(vw_99_900);\n    }\n\n    @Benchmark\n    public String[] split3_vw_99_22222() {\n        return vw_99_22222.split(\"\\0\", 3);\n    }\n\n    @Benchmark\n    public String[] customSplit3_vw_99_22222() {\n        return StringUtils.split3(vw_99_22222);\n    }\n\n    @Benchmark\n    public String[] split3_aw_100_900() {\n        return aw_100_900.split(\"\\0\", 3);\n    }\n\n    @Benchmark\n    public String[] customSplit3_aw_100_900() {\n        return StringUtils.split3(aw_100_900);\n    }\n\n    @Benchmark\n    public String[] split3_aw_10_long_text() {\n        return aw_10_long_text.split(\"\\0\", 3);\n    }\n\n    @Benchmark\n    public String[] customSplit3_aw_10_long_text() {\n        return StringUtils.split3(aw_10_long_text);\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/test/java/cc/blynk/test/utils/StringUtilsTest.java",
    "content": "package cc.blynk.test.utils;\n\nimport cc.blynk.utils.StringUtils;\nimport org.junit.Test;\n\nimport static cc.blynk.utils.StringUtils.BODY_SEPARATOR_STRING;\nimport static cc.blynk.utils.StringUtils.split3;\nimport static org.junit.Assert.assertArrayEquals;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/18/2015.\n */\npublic class StringUtilsTest {\n\n    @Test\n    public void testCorrectFastNewSplit() {\n        String in = \"ar 1 2 3 4 5 6\".replaceAll(\" \", \"\\0\");\n\n        String res = StringUtils.fetchPin(in);\n        assertNotNull(res);\n        assertEquals(\"1\", res);\n\n\n        in = \"ar 22222\".replaceAll(\" \", \"\\0\");\n        res = StringUtils.fetchPin(in);\n        assertNotNull(res);\n        assertEquals(\"22222\", res);\n\n        in = \"1 1\".replaceAll(\" \", \"\\0\");\n        res = StringUtils.fetchPin(in);\n        assertNotNull(res);\n        assertEquals(\"\", res);\n    }\n\n    @Test\n    public void testCorrectSplit3() {\n        String[] res = StringUtils.split3(\"aw 1 2\".replaceAll(\" \", StringUtils.BODY_SEPARATOR_STRING));\n        assertEquals(\"aw\", res[0]);\n        assertEquals(\"1\", res[1]);\n        assertEquals(\"2\", res[2]);\n\n        res = StringUtils.split3(\"dw 11 22\".replaceAll(\" \", StringUtils.BODY_SEPARATOR_STRING));\n        assertEquals(\"dw\", res[0]);\n        assertEquals(\"11\", res[1]);\n        assertEquals(\"22\", res[2]);\n\n        String s = \"sdfsafdfdgfjdasfjds;lfjd;lsf dsfld;las fd;slaj fd;lsfj das\";\n        res = StringUtils.split3(\"vw 255 \".replaceAll(\" \", StringUtils.BODY_SEPARATOR_STRING) + s);\n        assertEquals(\"vw\", res[0]);\n        assertEquals(\"255\", res[1]);\n        assertEquals(s, res[2]);\n\n        s = \"sdfsafdfdgfjdasfjds;lfjd;lsf\\0dsfld;las\\0fd;slaj\\0fd;lsfj\\0das\";\n        res = StringUtils.split3(\"vw 255 \".replaceAll(\" \", StringUtils.BODY_SEPARATOR_STRING) + s);\n        assertEquals(\"vw\", res[0]);\n        assertEquals(\"255\", res[1]);\n        assertEquals(s, res[2]);\n    }\n\n    @Test\n    public void splitOk() {\n        String body = \"vw 1 123\".replaceAll(\" \", \"\\0\");\n        String[] stringSplitResult = body.split(BODY_SEPARATOR_STRING, 3);\n        String[] customSplitResult = split3(body);\n\n        assertArrayEquals(stringSplitResult, customSplitResult);\n\n        body = \"vw 1 123 124\".replaceAll(\" \", \"\\0\");\n        stringSplitResult = body.split(BODY_SEPARATOR_STRING, 3);\n        customSplitResult = split3(body);\n\n        assertArrayEquals(stringSplitResult, customSplitResult);\n\n        body = \"vw\".replaceAll(\" \", \"\\0\");\n        stringSplitResult = body.split(BODY_SEPARATOR_STRING, 3);\n        customSplitResult = split3(body);\n\n        assertArrayEquals(stringSplitResult, customSplitResult);\n\n        body = \"1 vw\".replaceAll(\" \", \"\\0\");\n        stringSplitResult = body.split(BODY_SEPARATOR_STRING, 3);\n        customSplitResult = split3(body);\n\n        assertArrayEquals(stringSplitResult, customSplitResult);\n\n        body = \"vw 2\".replaceAll(\" \", \"\\0\");\n        stringSplitResult = body.split(BODY_SEPARATOR_STRING, 3);\n        customSplitResult = split3(body);\n\n        assertArrayEquals(stringSplitResult, customSplitResult);\n\n        body = \"2 vw 2 222\".replaceAll(\" \", \"\\0\");\n        stringSplitResult = body.split(BODY_SEPARATOR_STRING, 3);\n        customSplitResult = split3(body);\n\n        assertArrayEquals(stringSplitResult, customSplitResult);\n    }\n\n\n}\n"
  },
  {
    "path": "server/core/src/test/java/cc/blynk/test/utils/UserStatisticsTest.java",
    "content": "package cc.blynk.test.utils;\n\nimport cc.blynk.server.core.dao.FileManager;\nimport cc.blynk.server.core.dao.UserKey;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport org.junit.BeforeClass;\nimport org.junit.Ignore;\nimport org.junit.Test;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.OutputStream;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.zip.Deflater;\nimport java.util.zip.DeflaterOutputStream;\n\n/**\n * User: ddumanskiy\n * Date: 09.12.13\n * Time: 8:07\n */\n@Ignore\npublic class UserStatisticsTest {\n\n    static FileManager fileManager;\n    static Map<UserKey, User> users;\n\n    @BeforeClass\n    public static void init() {\n        fileManager = new FileManager(\"/home/doom369/test/root/data\", null);\n        users = fileManager.deserializeUsers();\n    }\n\n    public static byte[] compress(byte[] data) throws Exception {\n        ByteArrayOutputStream baos = new ByteArrayOutputStream(8192);\n\n        try (OutputStream out = new DeflaterOutputStream(baos, new Deflater(Deflater.BEST_COMPRESSION))) {\n            out.write(data);\n        }\n        return baos.toByteArray();\n    }\n\n    @Test\n    public void read() {\n        System.out.println(\"Registered users : \" + users.size());\n    }\n\n    @Test\n    public void printWidgetUsage() {\n        System.out.println();\n        System.out.println(\"Widget Usage :\");\n        Map<String, Integer> boards = new HashMap<>();\n        for (User user : users.values()) {\n                for (DashBoard dashBoard : user.profile.dashBoards) {\n                    if (dashBoard.widgets != null) {\n                        for (Widget widget : dashBoard.widgets) {\n                            Integer i = boards.get(widget.getClass().getSimpleName());\n                            if (i == null) {\n                                i = 0;\n                            }\n                            boards.put(widget.getClass().getSimpleName(), ++i);\n                        }\n                    }\n                }\n        }\n\n        for (Map.Entry<String, Integer> entry : boards.entrySet()) {\n            System.out.println(entry.getKey() + \" : \" + entry.getValue());\n        }\n    }\n\n    @Test\n    public void printDashFilling() {\n        System.out.println();\n        System.out.println(\"Dashboard Space Usage :\");\n\n        List<Integer> all = new ArrayList<>();\n        for (User user : users.values()) {\n                for (DashBoard dashBoard : user.profile.dashBoards) {\n                    if (dashBoard.widgets.length > 3) {\n                        int sum = 0;\n                        for (Widget widget : dashBoard.widgets) {\n                           sum += widget.height * widget.width;\n                        }\n                        all.add(sum);\n                    }\n            }\n        }\n\n        Collections.sort(all);\n\n        System.out.println(\"Mediana of cells used : \" + all.get(all.size() / 2));\n        System.out.println(\"Avg. percentage of space filling : \" + all.get(all.size() / 2) * 100 / 72 + \"%\");\n        //System.out.println(\"Average filled square per dash : \" + (sum / dashes));\n        //System.out.println(\"Percentage : \" + (int)((sum / dashes) * 100 / 72));\n    }\n\n    @Test\n    public void printOutdatedProfiles() {\n        long now = System.currentTimeMillis();\n        int counter = 0;\n        long PERIOD = 1000L * 60 * 60 * 24 * 90;\n        for (User user : users.values()) {\n            if (user.lastModifiedTs < (now - PERIOD)) {\n                counter++;\n            }\n        }\n        System.out.println(\"Profiles not updated more then 3 months : \" + counter);\n    }\n\n    @Test\n    public void dashesPerUser() {\n        int usersCounter = 0;\n        int dashesCounter = 0;\n        int maxDashes = 0;\n        int widgetCount = 0;\n        for (User user : users.values()) {\n            if (user.profile.dashBoards.length == 0) {\n                continue;\n            }\n            usersCounter++;\n            dashesCounter += user.profile.dashBoards.length;\n            maxDashes = Math.max(user.profile.dashBoards.length, maxDashes);\n            for (DashBoard dash : user.profile.dashBoards) {\n                widgetCount += dash.widgets.length;\n            }\n        }\n\n        System.out.println(\"Dashboards per user : \" + (double) dashesCounter / usersCounter);\n        System.out.println(\"Widgets per user : \" + (double) widgetCount / usersCounter);\n        System.out.println(\"Max dashes : \" + maxDashes);\n    }\n\n}\n"
  },
  {
    "path": "server/core/src/test/resources/db-test.properties",
    "content": "jdbc.url=jdbc:postgresql://localhost:5432/blynk?tcpKeepAlive=true&socketTimeout=150\nuser=test\npassword=test\nconnection.timeout.millis=30000\nclean.reporting=true\n\nreporting.jdbc.url=jdbc:postgresql://localhost:5432/blynk_reporting?tcpKeepAlive=true&socketTimeout=150\nreporting.user=test\nreporting.password=test\nreporting.connection.timeout.millis=30000"
  },
  {
    "path": "server/core/src/test/resources/json_test/user_ios_profile_json.txt",
    "content": "{\n    \"dashBoards\" :\n        [\n            {\n             \"id\":1,\n             \"name\":\"My Dashboard\",\n             \"widgets\"  : [\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"BUTTON\", \"pushMode\":1, \"pinType\":\"DIGITAL\", \"pin\":1, \"value\":\"1\"},\n                {\"id\":2, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"BUTTON\", \"pushMode\":0, \"pinType\":\"DIGITAL\", \"pin\":1, \"value\":\"1\"}\n             ],\n             \"boardType\":\"UNO\"\n            }\n        ]\n}"
  },
  {
    "path": "server/core/src/test/resources/json_test/user_profile_json.txt",
    "content": "{\n    \"dashBoards\" :\n        [\n            {\n             \"id\":1,\n             \"name\":\"My Dashboard\",\n             \"widgets\"  : [\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"BUTTON\",         \"pinType\":\"DIGITAL\", \"pin\":1, \"value\":\"1\"},\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"SLIDER\",  \"pinType\":\"DIGITAL\", \"pin\":2, \"value\":\"1\", \"state\":\"ON\"},\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"SLIDER\",  \"pinType\":\"ANALOG\", \"pin\":3, \"value\":\"0\", \"state\":\"OFF\"},\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"SLIDER\",         \"pinType\":\"VIRTUAL\", \"pin\":4, \"value\":\"244\" },\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"TIMER\",          \"pinType\":\"DIGITAL\", \"pin\":5, \"value\":\"1\"},\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"LED\",            \"pinType\":\"DIGITAL\", \"pin\":6},\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"DIGIT4_DISPLAY\", \"pinType\":\"DIGITAL\", \"pin\":7},\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"GRAPH\",          \"pinType\":\"DIGITAL\", \"pin\":8}\n             ],\n             \"boardType\":\"UNO\"\n            }\n        ]\n}"
  },
  {
    "path": "server/core/src/test/resources/json_test/user_profile_json_2.txt",
    "content": "{\n   \"dashBoards\":[\n      {\n         \"id\":1,\n         \"boardType\":\"Arduino UNO\",\n         \"widgets\":[\n            {\n               \"x\":0,\n               \"type\":\"SLIDER\",\n               \"y\":4,\n               \"id\":9013884621491657959,\n               \"label\":\"SLIDER\",\n               \"width\":8,\n               \"height\":1,\n               \"pin\":-1\n            },\n            {\n               \"x\":0,\n               \"type\":\"BUTTON\",\n               \"y\":0,\n               \"id\":8101118353106571483,\n               \"label\":\"BUTTON\",\n               \"width\":2,\n               \"height\":2,\n               \"pin\":-1\n            }\n         ]\n      }\n   ]\n}"
  },
  {
    "path": "server/core/src/test/resources/json_test/user_profile_json_3.txt",
    "content": "{\"dashBoards\":[{\"id\":1,\"boardType\":\"Arduino UNO\",\"widgets\":[{\"y\":0,\"id\":2948750487994149329,\"type\":\"BUTTON\",\"x\":0,\"label\":\"BUTTON\"},{\"y\":6,\"id\":4396013099992412521,\"type\":\"SLIDER\",\"x\":2,\"label\":\"SLIDER\"}]}]}"
  },
  {
    "path": "server/core/src/test/resources/json_test/user_profile_json_4.txt",
    "content": "{\n    \"dashBoards\" :\n        [\n            {\n             \"id\":1,\n             \"name\":\"My Dashboard\",\n             \"widgets\"  : [\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"BUTTON\",         \"pinType\":\"DIGITAL\", \"pin\":1, \"value\":\"1\"},\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"SLIDER\",  \"pinType\":\"DIGITAL\", \"pin\":2, \"value\":\"1\", \"state\":\"ON\"},\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"SLIDER\",  \"pinType\":\"ANALOG\", \"pin\":3, \"value\":\"0\", \"state\":\"OFF\"},\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"SLIDER\",         \"pinType\":\"VIRTUAL\", \"pin\":4, \"value\":\"244\" },\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"TIMER\",          \"pinType\":\"DIGITAL\", \"pin\":5, \"value\":\"1\"},\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"LED\",            \"pinType\":\"DIGITAL\", \"pin\":6},\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"DIGIT4_DISPLAY\", \"pinType\":\"DIGITAL\", \"pin\":7}\n             ],\n             \"boardType\":\"UNO\"\n            }\n        ]\n}"
  },
  {
    "path": "server/core/src/test/resources/json_test/user_profile_json_5.txt",
    "content": "{\n    \"dashBoards\" :\n        [\n            {\n             \"id\":1,\n             \"name\":\"My Dashboard\",\n             \"widgets\"  : [\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"BUTTON\",         \"pinType\":\"DIGITAL\", \"pin\":1, \"value\":\"1\"},\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"SLIDER\",  \"pinType\":\"DIGITAL\", \"pin\":2, \"value\":\"1\", \"state\":\"ON\"},\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"SLIDER\",  \"pinType\":\"ANALOG\", \"pin\":3, \"value\":\"0\", \"state\":\"OFF\"},\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"SLIDER\",         \"pinType\":\"VIRTUAL\", \"pin\":4, \"value\":\"244\" },\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"TIMER\",          \"pinType\":\"DIGITAL\", \"pin\":5, \"value\":\"1\"},\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"LED\",            \"pinType\":\"DIGITAL\", \"pin\":6},\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"DIGIT4_DISPLAY\", \"pinType\":\"DIGITAL\", \"pin\":7},\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"GRAPH\",          \"pinType\":\"DIGITAL\", \"pin\":8},\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"GRAPH\",          \"pinType\":\"DIGITAL\", \"pin\":9}\n             ],\n             \"boardType\":\"UNO\"\n            },\n\n            {\n                         \"id\":2,\n                         \"name\":\"My Dashboard\",\n                         \"widgets\"  : [\n                            {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"BUTTON\",         \"pinType\":\"DIGITAL\", \"pin\":1, \"value\":\"1\"},\n                            {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"SLIDER\",  \"pinType\":\"DIGITAL\", \"pin\":2, \"value\":\"1\", \"state\":\"ON\"},\n                            {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"SLIDER\",  \"pinType\":\"ANALOG\", \"pin\":3, \"value\":\"0\", \"state\":\"OFF\"},\n                            {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"SLIDER\",         \"pinType\":\"VIRTUAL\", \"pin\":4, \"value\":\"244\" },\n                            {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"TIMER\",          \"pinType\":\"DIGITAL\", \"pin\":5, \"value\":\"1\"},\n                            {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"LED\",            \"pinType\":\"DIGITAL\", \"pin\":6},\n                            {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"DIGIT4_DISPLAY\", \"pinType\":\"DIGITAL\", \"pin\":7},\n                            {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"GRAPH\",          \"pinType\":\"DIGITAL\", \"pin\":8},\n                            {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"GRAPH\",          \"pinType\":\"DIGITAL\", \"pin\":2}\n                         ],\n                         \"boardType\":\"UNO\"\n            }\n        ]\n}"
  },
  {
    "path": "server/core/src/test/resources/json_test/user_profile_json_RGB.txt",
    "content": "{\n    \"dashBoards\" :\n        [\n            {\n             \"id\":1,\n             \"name\":\"My Dashboard\",\n             \"widgets\"  : [\n                {\"id\":1, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"RGB\", \"pins\": [{\"pinType\":\"DIGITAL\", \"pin\":1, \"value\":\"1\"}, {\"pinType\":\"DIGITAL\", \"pin\":2, \"value\":\"2\", \"pwmMode\":true}]}\n                ],\n             \"boardType\":\"UNO\"\n            }\n        ]\n}"
  },
  {
    "path": "server/core/src/test/resources/json_test/user_profile_json_old_pinstorage.txt",
    "content": "{\n    \"dashBoards\" :\n        [\n            {\n             \"id\":1,\n             \"name\":\"My Dashboard\",\n             \"boardType\":\"UNO\"\n            }\n        ],\n    \"pinsStorage\": {\"1-0-v0\":\"1\",\"1-0-d111\":\"2\", \"1-0-v0-label\":\"3\"}\n}"
  },
  {
    "path": "server/core/src/test/resources/json_test/user_profile_json_with_outdated_widget.txt",
    "content": "{\n    \"dashBoards\" :\n        [\n            {\n             \"id\":1,\n             \"name\":\"My Dashboard\",\n             \"widgets\"  : [\n                {\"id\":1, \"x\":1, \"horizontal\":true, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text\", \"type\":\"ONE_AXIS_JOYSTICK\", \"pushMode\":1, \"pinType\":\"DIGITAL\", \"pin\":1, \"value\":\"1\"},\n                {\"id\":2, \"x\":1, \"y\":1, \"dashBoardId\":1, \"label\":\"Some Text2\", \"type\":\"FIELD_INPUT\", \"pushMode\":0, \"pinType\":\"DIGITAL\", \"pin\":1, \"value\":\"1\"}\n             ],\n             \"boardType\":\"UNO\"\n            }\n        ]\n}"
  },
  {
    "path": "server/core/src/test/resources/json_test/user_profile_with_timer.txt",
    "content": "{\"dashBoards\":[{\"boardType\":\"Arduino Mega\", \"isActive\":true, \"widgets\":[{\"pushMode\":false,\"type\":\"BUTTON\",\"pinType\":\"DIGITAL\",\"pin\":13,\"index\":0,\"id\":2,\"height\":2,\"width\":2,\"x\":0,\"y\":0},{\"stopValue\":\"dw\\u00009\\u00000\",\"startValue\":\"dw\\u00009\\u00001\",\"stopTime\":-1,\"startTime\":65640,\"type\":\"TIMER\",\"label\":\"\",\"pinType\":\"DIGITAL\",\"pin\":9,\"index\":43,\"id\":3,\"height\":1,\"width\":3,\"x\":3,\"y\":5}],\"name\":\"NEW PROJECT\",\"id\":1}]}\n"
  },
  {
    "path": "server/core/src/test/resources/log4j2-test.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n    <appenders>\n\n        <Console name=\"Console\" target=\"SYSTEM_OUT\">\n            <PatternLayout pattern=\"%d{HH:mm:ss.SSS} %-5level - %msg%n\"/>\n        </Console>\n\n        <RollingFile name=\"LogFile\" fileName=\"./blynk.log\"\n                     filePattern=\"./blynk.log.%d{yyyy-MM-dd}\">\n            <PatternLayout>\n                <pattern>%d{HH:mm:ss.SSS} %-5level - %msg%n</pattern>\n            </PatternLayout>\n            <Policies>\n                <TimeBasedTriggeringPolicy/>\n            </Policies>\n        </RollingFile>\n\n        <Console name=\"timerLog\" target=\"SYSTEM_OUT\">\n            <PatternLayout pattern=\"%d{HH:mm:ss.SSS} %-5level - %msg%n\"/>\n        </Console>\n\n    </appenders>\n    <loggers>\n\n        <Logger name=\"cc.blynk.server.workers.timer.TimerWorker\" level=\"debug\" additivity=\"false\">\n            <appender-ref ref=\"timerLog\"/>\n        </Logger>\n\n        <root level=\"DEBUG\">\n            <appender-ref ref=\"Console\"/>\n            <appender-ref ref=\"LogFile\"/>\n        </root>\n    </loggers>\n</configuration>"
  },
  {
    "path": "server/http-admin/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <artifactId>server</artifactId>\n        <groupId>cc.blynk.server</groupId>\n        <version>0.41.17-SNAPSHOT</version>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n\n    <groupId>cc.blynk.server.admin</groupId>\n    <artifactId>http-admin</artifactId>\n\n    <dependencies>\n        <dependency>\n            <groupId>cc.blynk.server</groupId>\n            <artifactId>core</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>cc.blynk.server.api.core</groupId>\n            <artifactId>http-core</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n    </dependencies>\n\n</project>"
  },
  {
    "path": "server/http-admin/src/main/java/cc/blynk/server/admin/http/handlers/IpFilterHandler.java",
    "content": "package cc.blynk.server.admin.http.handlers;\n\nimport io.netty.channel.ChannelHandler;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.handler.ipfilter.AbstractRemoteAddressFilter;\nimport io.netty.handler.ipfilter.IpFilterRuleType;\nimport io.netty.handler.ipfilter.IpSubnetFilterRule;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.net.InetSocketAddress;\nimport java.util.HashSet;\nimport java.util.Set;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 16.12.15.\n */\n@ChannelHandler.Sharable\npublic class IpFilterHandler extends AbstractRemoteAddressFilter<InetSocketAddress> {\n\n    private static final Logger log = LogManager.getLogger(IpFilterHandler.class);\n\n    private final Set<String> allowedIPs = new HashSet<>();\n    private final Set<IpSubnetFilterRule> rules = new HashSet<>();\n\n    public IpFilterHandler(String[] allowedIPs) {\n        if (allowedIPs == null) {\n            return;\n        }\n        for (String allowedIP : allowedIPs) {\n            if (allowedIP.contains(\"/\")) {\n                String[] split = allowedIP.split(\"/\");\n                String ip = split[0];\n                int cidr = Integer.parseInt(split[1]);\n                this.rules.add(new IpSubnetFilterRule(ip, cidr, IpFilterRuleType.ACCEPT));\n            } else {\n                this.allowedIPs.add(allowedIP);\n            }\n        }\n    }\n\n    public boolean accept(ChannelHandlerContext ctx) {\n        return accept(ctx, (InetSocketAddress) ctx.channel().remoteAddress());\n    }\n\n    @Override\n    public boolean accept(ChannelHandlerContext ctx, InetSocketAddress remoteAddress) {\n        if (allowedIPs.size() == 0 && rules.size() == 0) {\n            log.error(\"allowed.administrator.ips property is empty. Access restricted.\");\n            return false;\n        }\n\n        String remoteHost = remoteAddress.getAddress().getHostAddress();\n        if (allowedIPs.contains(remoteHost)) {\n            return true;\n        }\n\n        for (IpSubnetFilterRule rule : rules) {\n            if (rule.matches(remoteAddress)) {\n                return true;\n            }\n        }\n\n        log.error(\"Access restricted for {}.\", remoteHost);\n        return false;\n    }\n}\n"
  },
  {
    "path": "server/http-admin/src/main/java/cc/blynk/server/admin/http/logic/Config.java",
    "content": "package cc.blynk.server.admin.http.logic;\n\nimport cc.blynk.server.core.model.serialization.JsonParser;\n\nimport java.io.PrintWriter;\nimport java.io.StringWriter;\nimport java.util.Properties;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 04.04.16.\n */\npublic class Config {\n\n    public String name;\n    public String body;\n\n    Config() {\n    }\n\n    Config(String name) {\n        this.name = name;\n    }\n\n    Config(String name, String body) {\n        this.name = name;\n        this.body = body;\n    }\n\n    Config(String name, Properties properties) {\n        this.name = name;\n        //return only editable options\n        this.body = getPropertyAsString(properties);\n    }\n\n    private static String getPropertyAsString(Properties prop) {\n        StringWriter writer = new StringWriter();\n        prop.list(new PrintWriter(writer));\n        return writer.getBuffer().replace(0, \"-- listing properties --\\n\".length(), \"\").toString();\n    }\n\n    @Override\n    public String toString() {\n        try {\n            return JsonParser.toJson(this);\n        } catch (Exception e) {\n            return \"{}\";\n        }\n    }\n}\n"
  },
  {
    "path": "server/http-admin/src/main/java/cc/blynk/server/admin/http/logic/ConfigsLogic.java",
    "content": "package cc.blynk.server.admin.http.logic;\n\nimport cc.blynk.core.http.CookiesBaseHttpHandler;\nimport cc.blynk.core.http.Response;\nimport cc.blynk.core.http.annotation.Consumes;\nimport cc.blynk.core.http.annotation.GET;\nimport cc.blynk.core.http.annotation.PUT;\nimport cc.blynk.core.http.annotation.Path;\nimport cc.blynk.core.http.annotation.PathParam;\nimport cc.blynk.core.http.annotation.QueryParam;\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.TextHolder;\nimport cc.blynk.utils.http.MediaType;\nimport cc.blynk.utils.properties.GCMProperties;\nimport cc.blynk.utils.properties.MailProperties;\nimport cc.blynk.utils.properties.ServerProperties;\nimport cc.blynk.utils.properties.TwitterProperties;\nimport io.netty.channel.ChannelHandler;\n\nimport java.io.IOException;\nimport java.io.StringReader;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Properties;\n\nimport static cc.blynk.core.http.Response.appendTotalCountHeader;\nimport static cc.blynk.core.http.Response.badRequest;\nimport static cc.blynk.core.http.Response.ok;\nimport static cc.blynk.core.http.utils.AdminHttpUtil.sort;\nimport static cc.blynk.utils.FileLoaderUtil.TOKEN_MAIL_BODY;\nimport static cc.blynk.utils.properties.DBProperties.DB_PROPERTIES_FILENAME;\nimport static cc.blynk.utils.properties.GCMProperties.GCM_PROPERTIES_FILENAME;\nimport static cc.blynk.utils.properties.MailProperties.MAIL_PROPERTIES_FILENAME;\nimport static cc.blynk.utils.properties.ServerProperties.SERVER_PROPERTIES_FILENAME;\nimport static cc.blynk.utils.properties.TwitterProperties.TWITTER_PROPERTIES_FILENAME;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 09.12.15.\n */\n@Path(\"/config\")\n@ChannelHandler.Sharable\npublic class ConfigsLogic extends CookiesBaseHttpHandler {\n\n    private final TextHolder textHolder;\n    private final ServerProperties serverProperties;\n\n    public ConfigsLogic(Holder holder, String rootPath) {\n        super(holder, rootPath);\n        this.textHolder = holder.textHolder;\n        this.serverProperties = holder.props;\n    }\n\n    @GET\n    @Path(\"\")\n    public Response getConfigs(@QueryParam(\"_filters\") String filterParam,\n                             @QueryParam(\"_page\") int page,\n                             @QueryParam(\"_perPage\") int size,\n                             @QueryParam(\"_sortField\") String sortField,\n                             @QueryParam(\"_sortDir\") String sortOrder) {\n\n        List<Config> configs = new ArrayList<>();\n        configs.add(new Config(SERVER_PROPERTIES_FILENAME));\n        configs.add(new Config(MAIL_PROPERTIES_FILENAME));\n        configs.add(new Config(GCM_PROPERTIES_FILENAME));\n        configs.add(new Config(DB_PROPERTIES_FILENAME));\n        configs.add(new Config(TWITTER_PROPERTIES_FILENAME));\n        configs.add(new Config(TOKEN_MAIL_BODY));\n\n        return appendTotalCountHeader(\n                                ok(sort(configs, sortField, sortOrder), page, size), configs.size()\n        );\n    }\n\n    @GET\n    @Path(\"/{name}\")\n    public Response getConfigByName(@PathParam(\"name\") String name) {\n        switch (name) {\n            case TOKEN_MAIL_BODY :\n                return ok(new Config(name, textHolder.tokenBody).toString());\n            case SERVER_PROPERTIES_FILENAME :\n                return ok(new Config(name, serverProperties).toString());\n            case MAIL_PROPERTIES_FILENAME :\n                return ok(new Config(name, new MailProperties(Collections.emptyMap())).toString());\n            case GCM_PROPERTIES_FILENAME :\n                return ok(new Config(name, new GCMProperties(Collections.emptyMap())).toString());\n            case TWITTER_PROPERTIES_FILENAME :\n                return ok(new Config(name, new TwitterProperties(Collections.emptyMap())).toString());\n            default :\n                return badRequest();\n        }\n    }\n\n\n    @PUT\n    @Consumes(value = MediaType.APPLICATION_JSON)\n    @Path(\"/{name}\")\n    public Response updateConfig(@PathParam(\"name\") String name,\n                               Config updatedConfig) {\n\n        log.info(\"Updating config {}. New body : \", name);\n        log.info(\"{}\", updatedConfig.body);\n\n        switch (name) {\n            case TOKEN_MAIL_BODY:\n                textHolder.tokenBody = updatedConfig.body;\n                break;\n            case SERVER_PROPERTIES_FILENAME :\n                Properties properties = readPropertiesFromString(updatedConfig.body);\n                serverProperties.putAll(properties);\n                break;\n        }\n\n        return ok(updatedConfig.toString());\n    }\n\n    private static Properties readPropertiesFromString(String propertiesAsString) {\n        Properties properties = new Properties();\n        try {\n            properties.load(new StringReader(propertiesAsString));\n        } catch (IOException e) {\n            log.error(\"Error reading properties as string. {}\", e.getMessage());\n        }\n        return properties;\n    }\n\n}\n"
  },
  {
    "path": "server/http-admin/src/main/java/cc/blynk/server/admin/http/logic/HardwareStatsLogic.java",
    "content": "package cc.blynk.server.admin.http.logic;\n\nimport cc.blynk.core.http.CookiesBaseHttpHandler;\nimport cc.blynk.core.http.Response;\nimport cc.blynk.core.http.annotation.GET;\nimport cc.blynk.core.http.annotation.Path;\nimport cc.blynk.core.http.annotation.QueryParam;\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.core.dao.UserDao;\nimport io.netty.channel.ChannelHandler;\n\nimport static cc.blynk.core.http.Response.ok;\nimport static cc.blynk.core.http.utils.AdminHttpUtil.convertMapToPair;\nimport static cc.blynk.core.http.utils.AdminHttpUtil.sort;\nimport static cc.blynk.core.http.utils.AdminHttpUtil.sortStringAsInt;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 09.12.15.\n */\n@Path(\"/hardwareInfo\")\n@ChannelHandler.Sharable\npublic class HardwareStatsLogic extends CookiesBaseHttpHandler {\n\n    private final UserDao userDao;\n\n    public HardwareStatsLogic(Holder holder, String rootPath) {\n        super(holder, rootPath);\n        this.userDao = holder.userDao;\n    }\n\n    @GET\n    @Path(\"/blynkVersion\")\n    public Response getLibraryVersion(@QueryParam(\"_sortField\") String sortField,\n                                      @QueryParam(\"_sortDir\") String sortOrder) {\n        return ok(sortStringAsInt(convertMapToPair(userDao.getLibraryVersion()), sortField, sortOrder));\n    }\n\n    @GET\n    @Path(\"/cpuType\")\n    public Response getBoards(@QueryParam(\"_sortField\") String sortField,\n                              @QueryParam(\"_sortDir\") String sortOrder) {\n        return ok(sort(convertMapToPair(userDao.getCpuType()), sortField, sortOrder));\n    }\n\n    @GET\n    @Path(\"/connectionType\")\n    public Response getFacebookLogins(@QueryParam(\"_sortField\") String sortField,\n                                      @QueryParam(\"_sortDir\") String sortOrder) {\n        return ok(sort(convertMapToPair(userDao.getConnectionType()), sortField, sortOrder));\n    }\n\n    @GET\n    @Path(\"/boards\")\n    public Response getHardwareBoards(@QueryParam(\"_sortField\") String sortField,\n                                      @QueryParam(\"_sortDir\") String sortOrder) {\n        return ok(sort(convertMapToPair(userDao.getHardwareBoards()), sortField, sortOrder));\n    }\n\n}\n"
  },
  {
    "path": "server/http-admin/src/main/java/cc/blynk/server/admin/http/logic/OTALogic.java",
    "content": "package cc.blynk.server.admin.http.logic;\n\nimport cc.blynk.core.http.AuthHeadersBaseHttpHandler;\nimport cc.blynk.core.http.Response;\nimport cc.blynk.core.http.annotation.Context;\nimport cc.blynk.core.http.annotation.GET;\nimport cc.blynk.core.http.annotation.Metric;\nimport cc.blynk.core.http.annotation.Path;\nimport cc.blynk.core.http.annotation.QueryParam;\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.core.dao.TokenValue;\nimport cc.blynk.server.core.dao.UserKey;\nimport cc.blynk.server.core.dao.ota.OTAInfo;\nimport cc.blynk.server.core.dao.ota.OTAManager;\nimport cc.blynk.server.core.model.auth.Session;\nimport cc.blynk.server.core.model.auth.User;\nimport io.netty.channel.ChannelHandler;\nimport io.netty.channel.ChannelHandlerContext;\n\nimport static cc.blynk.core.http.Response.badRequest;\nimport static cc.blynk.core.http.Response.ok;\nimport static cc.blynk.server.core.protocol.enums.Command.BLYNK_INTERNAL;\nimport static cc.blynk.server.core.protocol.enums.Command.HTTP_START_OTA;\nimport static cc.blynk.server.core.protocol.enums.Command.HTTP_STOP_OTA;\n\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 03.12.15.\n */\n@Path(\"/ota\")\n@ChannelHandler.Sharable\npublic class OTALogic extends AuthHeadersBaseHttpHandler {\n\n    private final OTAManager otaManager;\n\n    private static final String OTA_DIR = \"/static/ota/\";\n\n    public OTALogic(Holder holder, String rootPath) {\n        super(holder, rootPath);\n        this.otaManager = holder.otaManager;\n    }\n\n    @GET\n    @Path(\"/start\")\n    @Metric(HTTP_STOP_OTA)\n    public Response startOTA(@QueryParam(\"fileName\") String filename,\n                             @QueryParam(\"token\") String token) {\n        TokenValue tokenValue = tokenManager.getTokenValueByToken(token);\n\n        if (tokenValue == null) {\n            log.debug(\"Requested token {} not found.\", token);\n            return badRequest(\"Invalid token.\");\n        }\n\n        User user = tokenValue.user;\n\n        if (user == null) {\n            return badRequest(\"Invalid auth credentials.\");\n        }\n\n        Session session = sessionDao.get(new UserKey(user));\n        if (session == null) {\n            log.debug(\"No session for user {}.\", user.email);\n            return badRequest(\"Device wasn't connected yet.\");\n        }\n\n        String otaFile = OTA_DIR + (filename == null ? \"firmware_ota.bin\" : filename);\n        String body = OTAInfo.makeHardwareBody(otaManager.serverHostUrl, otaFile);\n        if (session.sendMessageToHardware(BLYNK_INTERNAL, 7777, body)) {\n            log.debug(\"No device in session.\");\n            return badRequest(\"No device in session.\");\n        }\n\n        tokenValue.device.updateOTAInfo(user.email);\n\n        return ok();\n    }\n\n    @GET\n    @Path(\"/stop\")\n    @Metric(HTTP_START_OTA)\n    public Response stopOTA(@Context ChannelHandlerContext ctx) {\n        User initiator = ctx.channel().attr(AuthHeadersBaseHttpHandler.USER).get();\n        otaManager.stop(initiator);\n\n        return ok();\n    }\n\n}\n"
  },
  {
    "path": "server/http-admin/src/main/java/cc/blynk/server/admin/http/logic/StatsLogic.java",
    "content": "package cc.blynk.server.admin.http.logic;\n\nimport cc.blynk.core.http.CookiesBaseHttpHandler;\nimport cc.blynk.core.http.Response;\nimport cc.blynk.core.http.annotation.GET;\nimport cc.blynk.core.http.annotation.Path;\nimport cc.blynk.core.http.annotation.QueryParam;\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.admin.http.response.IpNameResponse;\nimport cc.blynk.server.admin.http.response.RequestPerSecondResponse;\nimport cc.blynk.server.core.BlockingIOProcessor;\nimport cc.blynk.server.core.dao.FileManager;\nimport cc.blynk.server.core.dao.UserDao;\nimport cc.blynk.server.core.dao.UserKey;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.Session;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.model.widgets.ui.reporting.ReportScheduler;\nimport cc.blynk.server.core.stats.GlobalStats;\nimport cc.blynk.server.core.stats.model.Stat;\nimport io.netty.channel.ChannelHandler;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\nimport static cc.blynk.core.http.Response.ok;\nimport static cc.blynk.core.http.utils.AdminHttpUtil.convertMapToPair;\nimport static cc.blynk.core.http.utils.AdminHttpUtil.convertObjectToMap;\nimport static cc.blynk.core.http.utils.AdminHttpUtil.sort;\nimport static cc.blynk.core.http.utils.AdminHttpUtil.sortStringAsInt;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 09.12.15.\n */\n@Path(\"/stats\")\n@ChannelHandler.Sharable\npublic class StatsLogic extends CookiesBaseHttpHandler {\n\n    private final UserDao userDao;\n    private final FileManager fileManager;\n    private final BlockingIOProcessor blockingIOProcessor;\n    private final GlobalStats globalStats;\n    private final ReportScheduler reportScheduler;\n\n    public StatsLogic(Holder holder, String rootPath) {\n        super(holder, rootPath);\n        this.userDao = holder.userDao;\n        this.fileManager = holder.fileManager;\n        this.blockingIOProcessor = holder.blockingIOProcessor;\n        this.globalStats = holder.stats;\n        this.reportScheduler = holder.reportScheduler;\n    }\n\n    @GET\n    @Path(\"/realtime\")\n    public Response getReatime() {\n       return ok(Collections.singletonList(\n               new Stat(sessionDao, userDao, blockingIOProcessor, globalStats, reportScheduler, false)));\n    }\n\n    @GET\n    @Path(\"/requestsPerUser\")\n    public Response getRequestPerUser(@QueryParam(\"_sortField\") String sortField,\n                                          @QueryParam(\"_sortDir\") String sortOrder) {\n        List<RequestPerSecondResponse> res = new ArrayList<>();\n        for (Map.Entry<UserKey, Session> entry : sessionDao.userSession.entrySet()) {\n            Session session = entry.getValue();\n\n            int appReqRate = session.getAppRequestRate();\n            int hardReqRate = session.getHardRequestRate();\n\n            if (appReqRate > 0 || hardReqRate > 0) {\n                res.add(new RequestPerSecondResponse(entry.getKey().email, appReqRate, hardReqRate));\n            }\n        }\n        return ok(sort(res, sortField, sortOrder));\n    }\n\n    @GET\n    @Path(\"/messages\")\n    public Response getMessages(@QueryParam(\"_sortField\") String sortField,\n                                    @QueryParam(\"_sortDir\") String sortOrder) {\n        return ok(sort(convertObjectToMap(\n                new Stat(sessionDao, userDao, blockingIOProcessor, globalStats, reportScheduler, false).commands),\n                sortField, sortOrder));\n    }\n\n    @GET\n    @Path(\"/widgets\")\n    public Response getWidgets(@QueryParam(\"_sortField\") String sortField,\n                                   @QueryParam(\"_sortDir\") String sortOrder) {\n        return ok(sort(convertMapToPair(userDao.getWidgetsUsage()), sortField, sortOrder));\n    }\n\n    @GET\n    @Path(\"/projectsPerUser\")\n    public Response getProjectsPerUser(@QueryParam(\"_sortField\") String sortField,\n                                       @QueryParam(\"_sortDir\") String sortOrder) {\n        return ok(sortStringAsInt(convertMapToPair(userDao.getProjectsPerUser()), sortField, sortOrder));\n    }\n\n    @GET\n    @Path(\"/boards\")\n    public Response getBoards(@QueryParam(\"_sortField\") String sortField,\n                              @QueryParam(\"_sortDir\") String sortOrder) {\n        return ok(sort(convertMapToPair(userDao.getBoardsUsage()), sortField, sortOrder));\n    }\n\n    @GET\n    @Path(\"/facebookLogins\")\n    public Response getFacebookLogins(@QueryParam(\"_sortField\") String sortField,\n                                      @QueryParam(\"_sortDir\") String sortOrder) {\n        return ok(sort(convertMapToPair(userDao.getFacebookLogin()), sortField, sortOrder));\n    }\n\n    @GET\n    @Path(\"/filledSpace\")\n    public Response getFilledSpace(@QueryParam(\"_sortField\") String sortField,\n                                   @QueryParam(\"_sortDir\") String sortOrder) {\n        return ok(sortStringAsInt(convertMapToPair(userDao.getFilledSpace()), sortField, sortOrder));\n    }\n\n    @GET\n    @Path(\"/userProfileSize\")\n    public Response getUserProfileSize(@QueryParam(\"_sortField\") String sortField,\n                                       @QueryParam(\"_sortDir\") String sortOrder) {\n        return ok(sortStringAsInt(convertMapToPair(fileManager.getUserProfilesSize()), sortField, sortOrder));\n    }\n\n\n    @GET\n    @Path(\"/webHookHosts\")\n    public Response getWebHookHosts(@QueryParam(\"_sortField\") String sortField,\n                                    @QueryParam(\"_sortDir\") String sortOrder) {\n        return ok(sortStringAsInt(convertMapToPair(userDao.getWebHookHosts()), sortField, sortOrder));\n    }\n\n    @GET\n    @Path(\"/ips\")\n    public Response getIps(@QueryParam(\"_filters\") String filterParam,\n                           @QueryParam(\"_page\") int page,\n                           @QueryParam(\"_perPage\") int size,\n                           @QueryParam(\"_sortField\") String sortField,\n                           @QueryParam(\"_sortDir\") String sortOrder) {\n\n        if (filterParam != null) {\n            IpFilter filter = JsonParser.readAny(filterParam, IpFilter.class);\n            filterParam = filter == null ? null : filter.ip;\n        }\n\n        return ok(sort(searchByIP(filterParam), sortField, sortOrder));\n    }\n\n    private static class IpFilter {\n        public String ip;\n    }\n\n    private List<IpNameResponse> searchByIP(String ip) {\n        Set<IpNameResponse> res = new HashSet<>();\n        int counter = 0;\n\n        for (User user : userDao.users.values()) {\n            if (user.lastLoggedIP != null) {\n                String name = user.email + \"-\" + user.appName;\n                if (ip == null) {\n                    res.add(new IpNameResponse(counter++, name, user.lastLoggedIP, \"app\"));\n                    for (DashBoard dashBoard : user.profile.dashBoards) {\n                        for (Device device : dashBoard.devices) {\n                            if (device.lastLoggedIP != null) {\n                                res.add(new IpNameResponse(counter++, name, device.lastLoggedIP, \"hard\"));\n                            }\n                        }\n                    }\n                } else {\n                    if (user.lastLoggedIP.contains(ip) || deviceContains(user, ip)) {\n                        res.add(new IpNameResponse(counter++, name, user.lastLoggedIP, \"hard\"));\n                    }\n                }\n            }\n        }\n\n        return new ArrayList<>(res);\n    }\n\n    private boolean deviceContains(User user, String ip) {\n        for (DashBoard dash : user.profile.dashBoards) {\n            for (Device device : dash.devices) {\n                if (device.lastLoggedIP != null && device.lastLoggedIP.contains(ip)) {\n                    return true;\n                }\n            }\n        }\n        return false;\n    }\n\n\n}\n"
  },
  {
    "path": "server/http-admin/src/main/java/cc/blynk/server/admin/http/logic/UsersLogic.java",
    "content": "package cc.blynk.server.admin.http.logic;\n\nimport cc.blynk.core.http.CookiesBaseHttpHandler;\nimport cc.blynk.core.http.Response;\nimport cc.blynk.core.http.annotation.Consumes;\nimport cc.blynk.core.http.annotation.DELETE;\nimport cc.blynk.core.http.annotation.GET;\nimport cc.blynk.core.http.annotation.PUT;\nimport cc.blynk.core.http.annotation.Path;\nimport cc.blynk.core.http.annotation.PathParam;\nimport cc.blynk.core.http.annotation.QueryParam;\nimport cc.blynk.core.http.model.Filter;\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.core.dao.FileManager;\nimport cc.blynk.server.core.dao.ReportingDiskDao;\nimport cc.blynk.server.core.dao.SessionDao;\nimport cc.blynk.server.core.dao.TokenManager;\nimport cc.blynk.server.core.dao.TokenValue;\nimport cc.blynk.server.core.dao.UserDao;\nimport cc.blynk.server.core.dao.UserKey;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.Session;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.db.DBManager;\nimport cc.blynk.utils.SHA256Util;\nimport cc.blynk.utils.TokenGeneratorUtil;\nimport cc.blynk.utils.http.MediaType;\nimport cc.blynk.utils.validators.BlynkEmailValidator;\nimport io.netty.channel.ChannelHandler;\n\nimport java.util.List;\n\nimport static cc.blynk.core.http.Response.appendTotalCountHeader;\nimport static cc.blynk.core.http.Response.badRequest;\nimport static cc.blynk.core.http.Response.notFound;\nimport static cc.blynk.core.http.Response.ok;\nimport static cc.blynk.core.http.utils.AdminHttpUtil.sort;\n\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 03.12.15.\n */\n@Path(\"/users\")\n@ChannelHandler.Sharable\npublic class UsersLogic extends CookiesBaseHttpHandler {\n\n    private final UserDao userDao;\n    private final FileManager fileManager;\n    private final DBManager dbManager;\n    private final ReportingDiskDao reportingDao;\n\n    public UsersLogic(Holder holder, String rootPath) {\n        super(holder, rootPath);\n        this.userDao = holder.userDao;\n        this.fileManager = holder.fileManager;\n        this.dbManager = holder.dbManager;\n        this.reportingDao = holder.reportingDiskDao;\n    }\n\n    //for tests only\n    public UsersLogic(UserDao userDao, SessionDao sessionDao, DBManager dbManager,\n                      FileManager fileManager, TokenManager tokenManager,\n                      ReportingDiskDao reportingDao, String rootPath) {\n        super(tokenManager, sessionDao, null, rootPath);\n        this.userDao = userDao;\n        this.fileManager = fileManager;\n        this.dbManager = dbManager;\n        this.reportingDao = reportingDao;\n    }\n\n    @GET\n    @Path(\"\")\n    public Response getUsers(@QueryParam(\"_filters\") String filterParam,\n                             @QueryParam(\"_page\") int page,\n                             @QueryParam(\"_perPage\") int size,\n                             @QueryParam(\"_sortField\") String sortField,\n                             @QueryParam(\"_sortDir\") String sortOrder) {\n        if (filterParam != null) {\n            Filter filter = JsonParser.readAny(filterParam, Filter.class);\n            filterParam = filter == null ? null : filter.name;\n        }\n\n        List<User> users = userDao.searchByUsername(filterParam, null);\n        return appendTotalCountHeader(\n                ok(sort(users, sortField, sortOrder), page, size), users.size()\n        );\n    }\n\n    @GET\n    @Path(\"/{id}\")\n    public Response getUserByName(@PathParam(\"id\") String id) {\n        String[] parts =  slitByLast(id);\n        String email = parts[0];\n        String appName = parts[1];\n        User user = userDao.getByName(email, appName);\n        if (user == null) {\n            return notFound();\n        }\n        return ok(user);\n    }\n\n    @GET\n    @Path(\"/names/getAll\")\n    public Response getAllUserNames() {\n        return ok(userDao.users.keySet());\n    }\n\n    @GET\n    @Path(\"/token/assign\")\n    public Response assignToken(@QueryParam(\"old\") String oldToken, @QueryParam(\"new\") String newToken) {\n        TokenValue tokenValue = tokenManager.getTokenValueByToken(oldToken);\n\n        if (tokenValue == null) {\n            return badRequest(\"This token not exists.\");\n        }\n\n        tokenManager.assignToken(tokenValue.user, tokenValue.dash, tokenValue.device, newToken);\n        return ok();\n    }\n\n    @GET\n    @Path(\"/token/force\")\n    public Response forceToken(@QueryParam(\"email\") String email,\n                               @QueryParam(\"app\") String app,\n                               @QueryParam(\"dashId\") int dashId,\n                               @QueryParam(\"deviceId\") int deviceId,\n                               @QueryParam(\"new\") String newToken) {\n\n        User user = userDao.getUsers().get(new UserKey(email, app));\n\n        if (user == null) {\n            return badRequest(\"No user with such email.\");\n        }\n\n        DashBoard dash = user.profile.getDashByIdOrThrow(dashId);\n        Device device = user.profile.getDeviceById(dash, deviceId);\n\n        tokenManager.assignToken(user, dash, device, newToken);\n        return ok();\n    }\n\n    @PUT\n    @Consumes(value = MediaType.APPLICATION_JSON)\n    @Path(\"/{id}\")\n    public Response updateUser(@PathParam(\"id\") String id,\n                               User updatedUser) {\n\n        log.debug(\"Updating user {}\", id);\n\n        String[] parts =  slitByLast(id);\n        String name = parts[0];\n        String appName = parts[1];\n\n        User oldUser = userDao.getByName(name, appName);\n\n        //name was changed, but not password - do not allow this.\n        //as name is used as salt for pass generation\n        if (!updatedUser.email.equals(oldUser.email) && updatedUser.pass.equals(oldUser.pass)) {\n            return badRequest(\"You need also change password when changing email.\");\n        }\n\n        if (BlynkEmailValidator.isNotValidEmail(updatedUser.email)) {\n            return badRequest(\"Wring email address.\");\n        }\n\n        //user name was changed\n        if (!updatedUser.email.equals(oldUser.email)) {\n            deleteUserByName(id);\n            for (DashBoard dashBoard : oldUser.profile.dashBoards) {\n                for (Device device : dashBoard.devices) {\n                    String token;\n                    if (device.token == null) {\n                        token = TokenGeneratorUtil.generateNewToken();\n                    } else {\n                        token = device.token;\n                    }\n                    tokenManager.assignToken(updatedUser, dashBoard, device, token);\n                }\n            }\n        }\n\n        //if pass was changed, call hash function.\n        if (!updatedUser.pass.equals(oldUser.pass)) {\n            log.debug(\"Updating pass for {}.\", updatedUser.email);\n            updatedUser.pass = SHA256Util.makeHash(updatedUser.pass, updatedUser.email);\n        }\n\n        userDao.add(updatedUser);\n\n        for (DashBoard dash : updatedUser.profile.dashBoards) {\n            for (Device device : dash.devices) {\n                if (device.token != null) {\n                    tokenManager.updateRegularCache(device.token, updatedUser, dash, device);\n                }\n            }\n            if (dash.sharedToken != null) {\n                tokenManager.updateSharedCache(dash.sharedToken, updatedUser, dash.id);\n            }\n        }\n\n        updatedUser.lastModifiedTs = System.currentTimeMillis();\n        log.debug(\"Adding new user {}\", updatedUser.email);\n\n        return ok(updatedUser);\n    }\n\n    @DELETE\n    @Path(\"/{id}\")\n    public Response deleteUserByName(@PathParam(\"id\") String id) {\n        String[] parts =  slitByLast(id);\n        String email = parts[0];\n        String appName = parts[1];\n\n        UserKey userKey = new UserKey(email, appName);\n        User user = userDao.delete(userKey);\n        if (user == null) {\n            return notFound();\n        }\n\n        if (!fileManager.delete(email, appName)) {\n            return notFound();\n        }\n\n        reportingDao.delete(user);\n\n        dbManager.deleteUser(userKey);\n\n        Session session = sessionDao.userSession.remove(userKey);\n        if (session != null) {\n            session.closeAll();\n        }\n\n        log.info(\"User {} successfully removed.\", email);\n\n        return ok();\n    }\n\n    private String[] slitByLast(String id) {\n        int i = id.lastIndexOf(\"-\");\n        return new String[] {\n                id.substring(0, i),\n                id.substring(i + 1)\n        };\n    }\n\n}\n"
  },
  {
    "path": "server/http-admin/src/main/java/cc/blynk/server/admin/http/response/IpNameResponse.java",
    "content": "package cc.blynk.server.admin.http.response;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 10.12.15.\n */\npublic final class IpNameResponse {\n\n    public final int id;\n\n    public final String name;\n\n    public final String ip;\n\n    public final String type;\n\n    public IpNameResponse(int id, String name, String ip, String type) {\n        this.id = id;\n        this.name = name;\n        this.ip = ip;\n        this.type = type;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n\n        IpNameResponse that = (IpNameResponse) o;\n\n        if (name != null ? !name.equals(that.name) : that.name != null) {\n            return false;\n        }\n        if (ip != null ? !ip.equals(that.ip) : that.ip != null) {\n            return false;\n        }\n        return type != null ? type.equals(that.type) : that.type == null;\n    }\n\n    @Override\n    public int hashCode() {\n        int result = name != null ? name.hashCode() : 0;\n        result = 31 * result + (ip != null ? ip.hashCode() : 0);\n        result = 31 * result + (type != null ? type.hashCode() : 0);\n        return result;\n    }\n}\n"
  },
  {
    "path": "server/http-admin/src/main/java/cc/blynk/server/admin/http/response/RequestPerSecondResponse.java",
    "content": "package cc.blynk.server.admin.http.response;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 10.12.15.\n */\npublic class RequestPerSecondResponse {\n\n    public final String name;\n\n    public final int appRate;\n\n    public final int hardRate;\n\n    public RequestPerSecondResponse(String name, int appRate, int hardRate) {\n        this.name = name;\n        this.appRate = appRate;\n        this.hardRate = hardRate;\n    }\n\n\n}\n"
  },
  {
    "path": "server/http-admin/src/main/java/module-info.java",
    "content": "/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 17.05.18.\n */\nmodule http.admin {\n    requires cc.blynk.http.core;\n    requires cc.blynk.core;\n    requires cc.blynk.utils;\n    requires io.netty.transport;\n    requires org.apache.logging.log4j;\n}"
  },
  {
    "path": "server/http-admin/src/main/resources/static/admin.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n    <meta charset=\"utf-8\">\n    <title>Blynk Administration</title>\n    <link rel=\"stylesheet\" href=\"/static/css/ng-admin.min.css\">\n</head>\n<body ng-app=\"app\">\n<div ui-view></div>\n<script src=\"/static/js/ng-admin.min.js\" type=\"text/javascript\"></script>\n<script src=\"/static/js/admin.js\" type=\"text/javascript\"></script>\n</body>\n</html>"
  },
  {
    "path": "server/http-admin/src/main/resources/static/css/blynk.css",
    "content": "@charset \"UTF-8\";/*!\n * Bootstrap v3.3.7 (http://getbootstrap.com)\n * Copyright 2011-2016 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */.label,sub,sup{vertical-align:baseline}hr,img{border:0}pre,textarea{overflow:auto}body,figure{margin:0}.img-responsive,.img-thumbnail,.table,label{max-width:100%}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.pre-scrollable{max-height:340px}html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}b,optgroup,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0}mark{background:#ff0;color:#000}sub,sup{font-size:75%;line-height:0;position:relative}sup{top:-.5em}sub{bottom:-.25em}img{vertical-align:middle}svg:not(:root){overflow:hidden}hr{box-sizing:content-box;height:0}code,kbd,pre,samp{font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}textarea{resize:none}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{blockquote,img,pre,tr{page-break-inside:avoid}*,:after,:before{background:0 0!important;color:#000!important;box-shadow:none!important;text-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:\" (\" attr(href) \")\"}abbr[title]:after{content:\" (\" attr(title) \")\"}a[href^=\"#\"]:after,a[href^=\"javascript:\"]:after{content:\"\"}blockquote,pre{border:1px solid #999}thead{display:table-header-group}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}.btn,.btn-danger.active,.btn-danger:active,.btn-default.active,.btn-default:active,.btn-info.active,.btn-info:active,.btn-primary.active,.btn-primary:active,.btn-warning.active,.btn-warning:active,.btn.active,.btn:active,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover,.form-control,.modal .active.btn-default,.modal .btn-default:active,.modal .open>.dropdown-toggle.btn-default,.navbar-toggle,.open>.btn-danger.dropdown-toggle,.open>.btn-default.dropdown-toggle,.open>.btn-info.dropdown-toggle,.open>.btn-primary.dropdown-toggle,.open>.btn-warning.dropdown-toggle{background-image:none}.Select-control,.btn-group-justified,.input-group{border-collapse:separate}.img-thumbnail,body{background-color:#fff}@font-face{font-family:'Glyphicons Halflings';src:url(/static/fonts/bootstrap/glyphicons-halflings-regular.eot);src:url(/static/fonts/bootstrap/glyphicons-halflings-regular.eot?#iefix) format(\"embedded-opentype\"),url(../fonts/bootstrap/glyphicons-halflings-regular.woff2) format(\"woff2\"),url(/static/fonts/bootstrap/glyphicons-halflings-regular.woff) format(\"woff\"),url(/static/fonts/bootstrap/glyphicons-halflings-regular.ttf) format(\"truetype\"),url(/static/fonts/bootstrap/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format(\"svg\")}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:\"\\002a\"}.glyphicon-plus:before{content:\"\\002b\"}.glyphicon-eur:before,.glyphicon-euro:before{content:\"\\20ac\"}.glyphicon-minus:before{content:\"\\2212\"}.glyphicon-cloud:before{content:\"\\2601\"}.glyphicon-envelope:before{content:\"\\2709\"}.glyphicon-pencil:before{content:\"\\270f\"}.glyphicon-glass:before{content:\"\\e001\"}.glyphicon-music:before{content:\"\\e002\"}.glyphicon-search:before{content:\"\\e003\"}.glyphicon-heart:before{content:\"\\e005\"}.glyphicon-star:before{content:\"\\e006\"}.glyphicon-star-empty:before{content:\"\\e007\"}.glyphicon-user:before{content:\"\\e008\"}.glyphicon-film:before{content:\"\\e009\"}.glyphicon-th-large:before{content:\"\\e010\"}.glyphicon-th:before{content:\"\\e011\"}.glyphicon-th-list:before{content:\"\\e012\"}.glyphicon-ok:before{content:\"\\e013\"}.glyphicon-remove:before{content:\"\\e014\"}.glyphicon-zoom-in:before{content:\"\\e015\"}.glyphicon-zoom-out:before{content:\"\\e016\"}.glyphicon-off:before{content:\"\\e017\"}.glyphicon-signal:before{content:\"\\e018\"}.glyphicon-cog:before{content:\"\\e019\"}.glyphicon-trash:before{content:\"\\e020\"}.glyphicon-home:before{content:\"\\e021\"}.glyphicon-file:before{content:\"\\e022\"}.glyphicon-time:before{content:\"\\e023\"}.glyphicon-road:before{content:\"\\e024\"}.glyphicon-download-alt:before{content:\"\\e025\"}.glyphicon-download:before{content:\"\\e026\"}.glyphicon-upload:before{content:\"\\e027\"}.glyphicon-inbox:before{content:\"\\e028\"}.glyphicon-play-circle:before{content:\"\\e029\"}.glyphicon-repeat:before{content:\"\\e030\"}.glyphicon-refresh:before{content:\"\\e031\"}.glyphicon-list-alt:before{content:\"\\e032\"}.glyphicon-lock:before{content:\"\\e033\"}.glyphicon-flag:before{content:\"\\e034\"}.glyphicon-headphones:before{content:\"\\e035\"}.glyphicon-volume-off:before{content:\"\\e036\"}.glyphicon-volume-down:before{content:\"\\e037\"}.glyphicon-volume-up:before{content:\"\\e038\"}.glyphicon-qrcode:before{content:\"\\e039\"}.glyphicon-barcode:before{content:\"\\e040\"}.glyphicon-tag:before{content:\"\\e041\"}.glyphicon-tags:before{content:\"\\e042\"}.glyphicon-book:before{content:\"\\e043\"}.glyphicon-bookmark:before{content:\"\\e044\"}.glyphicon-print:before{content:\"\\e045\"}.glyphicon-camera:before{content:\"\\e046\"}.glyphicon-font:before{content:\"\\e047\"}.glyphicon-bold:before{content:\"\\e048\"}.glyphicon-italic:before{content:\"\\e049\"}.glyphicon-text-height:before{content:\"\\e050\"}.glyphicon-text-width:before{content:\"\\e051\"}.glyphicon-align-left:before{content:\"\\e052\"}.glyphicon-align-center:before{content:\"\\e053\"}.glyphicon-align-right:before{content:\"\\e054\"}.glyphicon-align-justify:before{content:\"\\e055\"}.glyphicon-list:before{content:\"\\e056\"}.glyphicon-indent-left:before{content:\"\\e057\"}.glyphicon-indent-right:before{content:\"\\e058\"}.glyphicon-facetime-video:before{content:\"\\e059\"}.glyphicon-picture:before{content:\"\\e060\"}.glyphicon-map-marker:before{content:\"\\e062\"}.glyphicon-adjust:before{content:\"\\e063\"}.glyphicon-tint:before{content:\"\\e064\"}.glyphicon-edit:before{content:\"\\e065\"}.glyphicon-share:before{content:\"\\e066\"}.glyphicon-check:before{content:\"\\e067\"}.glyphicon-move:before{content:\"\\e068\"}.glyphicon-step-backward:before{content:\"\\e069\"}.glyphicon-fast-backward:before{content:\"\\e070\"}.glyphicon-backward:before{content:\"\\e071\"}.glyphicon-play:before{content:\"\\e072\"}.glyphicon-pause:before{content:\"\\e073\"}.glyphicon-stop:before{content:\"\\e074\"}.glyphicon-forward:before{content:\"\\e075\"}.glyphicon-fast-forward:before{content:\"\\e076\"}.glyphicon-step-forward:before{content:\"\\e077\"}.glyphicon-eject:before{content:\"\\e078\"}.glyphicon-chevron-left:before{content:\"\\e079\"}.glyphicon-chevron-right:before{content:\"\\e080\"}.glyphicon-plus-sign:before{content:\"\\e081\"}.glyphicon-minus-sign:before{content:\"\\e082\"}.glyphicon-remove-sign:before{content:\"\\e083\"}.glyphicon-ok-sign:before{content:\"\\e084\"}.glyphicon-question-sign:before{content:\"\\e085\"}.glyphicon-info-sign:before{content:\"\\e086\"}.glyphicon-screenshot:before{content:\"\\e087\"}.glyphicon-remove-circle:before{content:\"\\e088\"}.glyphicon-ok-circle:before{content:\"\\e089\"}.glyphicon-ban-circle:before{content:\"\\e090\"}.glyphicon-arrow-left:before{content:\"\\e091\"}.glyphicon-arrow-right:before{content:\"\\e092\"}.glyphicon-arrow-up:before{content:\"\\e093\"}.glyphicon-arrow-down:before{content:\"\\e094\"}.glyphicon-share-alt:before{content:\"\\e095\"}.glyphicon-resize-full:before{content:\"\\e096\"}.glyphicon-resize-small:before{content:\"\\e097\"}.glyphicon-exclamation-sign:before{content:\"\\e101\"}.glyphicon-gift:before{content:\"\\e102\"}.glyphicon-leaf:before{content:\"\\e103\"}.glyphicon-fire:before{content:\"\\e104\"}.glyphicon-eye-open:before{content:\"\\e105\"}.glyphicon-eye-close:before{content:\"\\e106\"}.glyphicon-warning-sign:before{content:\"\\e107\"}.glyphicon-plane:before{content:\"\\e108\"}.glyphicon-calendar:before{content:\"\\e109\"}.glyphicon-random:before{content:\"\\e110\"}.glyphicon-comment:before{content:\"\\e111\"}.glyphicon-magnet:before{content:\"\\e112\"}.glyphicon-chevron-up:before{content:\"\\e113\"}.glyphicon-chevron-down:before{content:\"\\e114\"}.glyphicon-retweet:before{content:\"\\e115\"}.glyphicon-shopping-cart:before{content:\"\\e116\"}.glyphicon-folder-close:before{content:\"\\e117\"}.glyphicon-folder-open:before{content:\"\\e118\"}.glyphicon-resize-vertical:before{content:\"\\e119\"}.glyphicon-resize-horizontal:before{content:\"\\e120\"}.glyphicon-hdd:before{content:\"\\e121\"}.glyphicon-bullhorn:before{content:\"\\e122\"}.glyphicon-bell:before{content:\"\\e123\"}.glyphicon-certificate:before{content:\"\\e124\"}.glyphicon-thumbs-up:before{content:\"\\e125\"}.glyphicon-thumbs-down:before{content:\"\\e126\"}.glyphicon-hand-right:before{content:\"\\e127\"}.glyphicon-hand-left:before{content:\"\\e128\"}.glyphicon-hand-up:before{content:\"\\e129\"}.glyphicon-hand-down:before{content:\"\\e130\"}.glyphicon-circle-arrow-right:before{content:\"\\e131\"}.glyphicon-circle-arrow-left:before{content:\"\\e132\"}.glyphicon-circle-arrow-up:before{content:\"\\e133\"}.glyphicon-circle-arrow-down:before{content:\"\\e134\"}.glyphicon-globe:before{content:\"\\e135\"}.glyphicon-wrench:before{content:\"\\e136\"}.glyphicon-tasks:before{content:\"\\e137\"}.glyphicon-filter:before{content:\"\\e138\"}.glyphicon-briefcase:before{content:\"\\e139\"}.glyphicon-fullscreen:before{content:\"\\e140\"}.glyphicon-dashboard:before{content:\"\\e141\"}.glyphicon-paperclip:before{content:\"\\e142\"}.glyphicon-heart-empty:before{content:\"\\e143\"}.glyphicon-link:before{content:\"\\e144\"}.glyphicon-phone:before{content:\"\\e145\"}.glyphicon-pushpin:before{content:\"\\e146\"}.glyphicon-usd:before{content:\"\\e148\"}.glyphicon-gbp:before{content:\"\\e149\"}.glyphicon-sort:before{content:\"\\e150\"}.glyphicon-sort-by-alphabet:before{content:\"\\e151\"}.glyphicon-sort-by-alphabet-alt:before{content:\"\\e152\"}.glyphicon-sort-by-order:before{content:\"\\e153\"}.glyphicon-sort-by-order-alt:before{content:\"\\e154\"}.glyphicon-sort-by-attributes:before{content:\"\\e155\"}.glyphicon-sort-by-attributes-alt:before{content:\"\\e156\"}.glyphicon-unchecked:before{content:\"\\e157\"}.glyphicon-expand:before{content:\"\\e158\"}.glyphicon-collapse-down:before{content:\"\\e159\"}.glyphicon-collapse-up:before{content:\"\\e160\"}.glyphicon-log-in:before{content:\"\\e161\"}.glyphicon-flash:before{content:\"\\e162\"}.glyphicon-log-out:before{content:\"\\e163\"}.glyphicon-new-window:before{content:\"\\e164\"}.glyphicon-record:before{content:\"\\e165\"}.glyphicon-save:before{content:\"\\e166\"}.glyphicon-open:before{content:\"\\e167\"}.glyphicon-saved:before{content:\"\\e168\"}.glyphicon-import:before{content:\"\\e169\"}.glyphicon-export:before{content:\"\\e170\"}.glyphicon-send:before{content:\"\\e171\"}.glyphicon-floppy-disk:before{content:\"\\e172\"}.glyphicon-floppy-saved:before{content:\"\\e173\"}.glyphicon-floppy-remove:before{content:\"\\e174\"}.glyphicon-floppy-save:before{content:\"\\e175\"}.glyphicon-floppy-open:before{content:\"\\e176\"}.glyphicon-credit-card:before{content:\"\\e177\"}.glyphicon-transfer:before{content:\"\\e178\"}.glyphicon-cutlery:before{content:\"\\e179\"}.glyphicon-header:before{content:\"\\e180\"}.glyphicon-compressed:before{content:\"\\e181\"}.glyphicon-earphone:before{content:\"\\e182\"}.glyphicon-phone-alt:before{content:\"\\e183\"}.glyphicon-tower:before{content:\"\\e184\"}.glyphicon-stats:before{content:\"\\e185\"}.glyphicon-sd-video:before{content:\"\\e186\"}.glyphicon-hd-video:before{content:\"\\e187\"}.glyphicon-subtitles:before{content:\"\\e188\"}.glyphicon-sound-stereo:before{content:\"\\e189\"}.glyphicon-sound-dolby:before{content:\"\\e190\"}.glyphicon-sound-5-1:before{content:\"\\e191\"}.glyphicon-sound-6-1:before{content:\"\\e192\"}.glyphicon-sound-7-1:before{content:\"\\e193\"}.glyphicon-copyright-mark:before{content:\"\\e194\"}.glyphicon-registration-mark:before{content:\"\\e195\"}.glyphicon-cloud-download:before{content:\"\\e197\"}.glyphicon-cloud-upload:before{content:\"\\e198\"}.glyphicon-tree-conifer:before{content:\"\\e199\"}.glyphicon-tree-deciduous:before{content:\"\\e200\"}.glyphicon-cd:before{content:\"\\e201\"}.glyphicon-save-file:before{content:\"\\e202\"}.glyphicon-open-file:before{content:\"\\e203\"}.glyphicon-level-up:before{content:\"\\e204\"}.glyphicon-copy:before{content:\"\\e205\"}.glyphicon-paste:before{content:\"\\e206\"}.glyphicon-alert:before{content:\"\\e209\"}.glyphicon-equalizer:before{content:\"\\e210\"}.glyphicon-king:before{content:\"\\e211\"}.glyphicon-queen:before{content:\"\\e212\"}.glyphicon-pawn:before{content:\"\\e213\"}.glyphicon-bishop:before{content:\"\\e214\"}.glyphicon-knight:before{content:\"\\e215\"}.glyphicon-baby-formula:before{content:\"\\e216\"}.glyphicon-tent:before{content:\"\\26fa\"}.glyphicon-blackboard:before{content:\"\\e218\"}.glyphicon-bed:before{content:\"\\e219\"}.glyphicon-apple:before{content:\"\\f8ff\"}.glyphicon-erase:before{content:\"\\e221\"}.glyphicon-hourglass:before{content:\"\\231b\"}.glyphicon-lamp:before{content:\"\\e223\"}.glyphicon-duplicate:before{content:\"\\e224\"}.glyphicon-piggy-bank:before{content:\"\\e225\"}.glyphicon-scissors:before{content:\"\\e226\"}.glyphicon-bitcoin:before,.glyphicon-btc:before,.glyphicon-xbt:before{content:\"\\e227\"}.glyphicon-jpy:before,.glyphicon-yen:before{content:\"\\00a5\"}.glyphicon-rub:before,.glyphicon-ruble:before{content:\"\\20bd\"}.glyphicon-scale:before{content:\"\\e230\"}.glyphicon-ice-lolly:before{content:\"\\e231\"}.glyphicon-ice-lolly-tasted:before{content:\"\\e232\"}.glyphicon-education:before{content:\"\\e233\"}.glyphicon-option-horizontal:before{content:\"\\e234\"}.glyphicon-option-vertical:before{content:\"\\e235\"}.glyphicon-menu-hamburger:before{content:\"\\e236\"}.glyphicon-modal-window:before{content:\"\\e237\"}.glyphicon-oil:before{content:\"\\e238\"}.glyphicon-grain:before{content:\"\\e239\"}.glyphicon-sunglasses:before{content:\"\\e240\"}.glyphicon-text-size:before{content:\"\\e241\"}.glyphicon-text-color:before{content:\"\\e242\"}.glyphicon-text-background:before{content:\"\\e243\"}.glyphicon-object-align-top:before{content:\"\\e244\"}.glyphicon-object-align-bottom:before{content:\"\\e245\"}.glyphicon-object-align-horizontal:before{content:\"\\e246\"}.glyphicon-object-align-left:before{content:\"\\e247\"}.glyphicon-object-align-vertical:before{content:\"\\e248\"}.glyphicon-object-align-right:before{content:\"\\e249\"}.glyphicon-triangle-right:before{content:\"\\e250\"}.glyphicon-triangle-left:before{content:\"\\e251\"}.glyphicon-triangle-bottom:before{content:\"\\e252\"}.glyphicon-triangle-top:before{content:\"\\e253\"}.glyphicon-console:before{content:\"\\e254\"}.glyphicon-superscript:before{content:\"\\e255\"}.glyphicon-subscript:before{content:\"\\e256\"}.glyphicon-menu-left:before{content:\"\\e257\"}.glyphicon-menu-right:before{content:\"\\e258\"}.glyphicon-menu-down:before{content:\"\\e259\"}.glyphicon-menu-up:before{content:\"\\e260\"}*,:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:transparent}body{font-size:14px;line-height:1.42857;color:#27292d}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#23c890;text-decoration:none}a:focus,a:hover{color:#188761;text-decoration:underline}a:focus{outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px}.img-responsive{display:block;height:auto}.img-rounded{border-radius:2pt}.img-thumbnail{padding:4px;line-height:1.42857;border:1px solid #ddd;border-radius:2pt;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}dt,kbd kbd{font-weight:700}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{background-color:#fcf8e3;padding:.2em}.list-inline,.list-unstyled{padding-left:0;list-style:none}.text-left{text-align:left}.text-right{text-align:right}.text-center,.v-breadcrumb-label{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.initialism,.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#23c890}a.text-primary:focus,a.text-primary:hover{color:#1b9d71}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#23c890}a.bg-primary:focus,a.bg-primary:hover{background-color:#1b9d71}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,a.bg-danger:hover{background-color:#e4b9b9}pre code,table{background-color:transparent}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}dl,ol,ul{margin-top:0}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child,ol ol,ol ul,ul ol,ul ul{margin-bottom:0}address,dl{margin-bottom:20px}ol,ul{margin-bottom:10px}.list-inline{margin-left:-5px}.list-inline>li{display:inline-block;padding-left:5px;padding-right:5px}dd,dt{line-height:1.42857}dd{margin-left:0}.dl-horizontal dd:after,.dl-horizontal dd:before{content:\" \";display:table}.dl-horizontal dd:after{clear:both}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}.container{width:750px}}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dropdown-menu>li>a,.form-horizontal .form-group:after,.modal-footer:after,.modal-header:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}abbr[data-original-title],abbr[title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;line-height:1.42857;color:#777}legend,pre{color:#333}blockquote .small:before,blockquote footer:before,blockquote small:before{content:'\\2014 \\00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0;text-align:right}code,kbd{padding:2px 4px;font-size:90%;border-radius:2pt}caption,th{text-align:left}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:''}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:'\\00A0 \\2014'}address{font-style:normal;line-height:1.42857}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,\"Courier New\",monospace}code{color:#c7254e;background-color:#f9f2f4}kbd{color:#fff;background-color:#333;box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:2pt}.container-fluid:after,.container-fluid:before,.container:after,.container:before,.row:after,.row:before{display:table;content:\" \"}.container,.container-fluid{margin-right:auto;margin-left:auto}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;border-radius:0}.container,.container-fluid{padding-left:15px;padding-right:15px}.pre-scrollable{overflow-y:scroll}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:970px}}.row{margin-left:-15px;margin-right:-15px}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-left:15px;padding-right:15px}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{float:left}.col-xs-1{width:8.33333%}.col-xs-2{width:16.66667%}.col-xs-3{width:25%}.col-xs-4{width:33.33333%}.col-xs-5{width:41.66667%}.col-xs-6{width:50%}.col-xs-7{width:58.33333%}.col-xs-8{width:66.66667%}.col-xs-9{width:75%}.col-xs-10{width:83.33333%}.col-xs-11{width:91.66667%}.col-xs-12{width:100%}.col-xs-pull-0{right:auto}.col-xs-pull-1{right:8.33333%}.col-xs-pull-2{right:16.66667%}.col-xs-pull-3{right:25%}.col-xs-pull-4{right:33.33333%}.col-xs-pull-5{right:41.66667%}.col-xs-pull-6{right:50%}.col-xs-pull-7{right:58.33333%}.col-xs-pull-8{right:66.66667%}.col-xs-pull-9{right:75%}.col-xs-pull-10{right:83.33333%}.col-xs-pull-11{right:91.66667%}.col-xs-pull-12{right:100%}.col-xs-push-0{left:auto}.col-xs-push-1{left:8.33333%}.col-xs-push-2{left:16.66667%}.col-xs-push-3{left:25%}.col-xs-push-4{left:33.33333%}.col-xs-push-5{left:41.66667%}.col-xs-push-6{left:50%}.col-xs-push-7{left:58.33333%}.col-xs-push-8{left:66.66667%}.col-xs-push-9{left:75%}.col-xs-push-10{left:83.33333%}.col-xs-push-11{left:91.66667%}.col-xs-push-12{left:100%}.col-xs-offset-0{margin-left:0}.col-xs-offset-1{margin-left:8.33333%}.col-xs-offset-2{margin-left:16.66667%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-4{margin-left:33.33333%}.col-xs-offset-5{margin-left:41.66667%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-7{margin-left:58.33333%}.col-xs-offset-8{margin-left:66.66667%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-10{margin-left:83.33333%}.col-xs-offset-11{margin-left:91.66667%}.col-xs-offset-12{margin-left:100%}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-1{width:8.33333%}.col-sm-2{width:16.66667%}.col-sm-3{width:25%}.col-sm-4{width:33.33333%}.col-sm-5{width:41.66667%}.col-sm-6{width:50%}.col-sm-7{width:58.33333%}.col-sm-8{width:66.66667%}.col-sm-9{width:75%}.col-sm-10{width:83.33333%}.col-sm-11{width:91.66667%}.col-sm-12{width:100%}.col-sm-pull-0{right:auto}.col-sm-pull-1{right:8.33333%}.col-sm-pull-2{right:16.66667%}.col-sm-pull-3{right:25%}.col-sm-pull-4{right:33.33333%}.col-sm-pull-5{right:41.66667%}.col-sm-pull-6{right:50%}.col-sm-pull-7{right:58.33333%}.col-sm-pull-8{right:66.66667%}.col-sm-pull-9{right:75%}.col-sm-pull-10{right:83.33333%}.col-sm-pull-11{right:91.66667%}.col-sm-pull-12{right:100%}.col-sm-push-0{left:auto}.col-sm-push-1{left:8.33333%}.col-sm-push-2{left:16.66667%}.col-sm-push-3{left:25%}.col-sm-push-4{left:33.33333%}.col-sm-push-5{left:41.66667%}.col-sm-push-6{left:50%}.col-sm-push-7{left:58.33333%}.col-sm-push-8{left:66.66667%}.col-sm-push-9{left:75%}.col-sm-push-10{left:83.33333%}.col-sm-push-11{left:91.66667%}.col-sm-push-12{left:100%}.col-sm-offset-0{margin-left:0}.col-sm-offset-1{margin-left:8.33333%}.col-sm-offset-2{margin-left:16.66667%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-4{margin-left:33.33333%}.col-sm-offset-5{margin-left:41.66667%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-7{margin-left:58.33333%}.col-sm-offset-8{margin-left:66.66667%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-10{margin-left:83.33333%}.col-sm-offset-11{margin-left:91.66667%}.col-sm-offset-12{margin-left:100%}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-1{width:8.33333%}.col-md-2{width:16.66667%}.col-md-3{width:25%}.col-md-4{width:33.33333%}.col-md-5{width:41.66667%}.col-md-6{width:50%}.col-md-7{width:58.33333%}.col-md-8{width:66.66667%}.col-md-9{width:75%}.col-md-10{width:83.33333%}.col-md-11{width:91.66667%}.col-md-12{width:100%}.col-md-pull-0{right:auto}.col-md-pull-1{right:8.33333%}.col-md-pull-2{right:16.66667%}.col-md-pull-3{right:25%}.col-md-pull-4{right:33.33333%}.col-md-pull-5{right:41.66667%}.col-md-pull-6{right:50%}.col-md-pull-7{right:58.33333%}.col-md-pull-8{right:66.66667%}.col-md-pull-9{right:75%}.col-md-pull-10{right:83.33333%}.col-md-pull-11{right:91.66667%}.col-md-pull-12{right:100%}.col-md-push-0{left:auto}.col-md-push-1{left:8.33333%}.col-md-push-2{left:16.66667%}.col-md-push-3{left:25%}.col-md-push-4{left:33.33333%}.col-md-push-5{left:41.66667%}.col-md-push-6{left:50%}.col-md-push-7{left:58.33333%}.col-md-push-8{left:66.66667%}.col-md-push-9{left:75%}.col-md-push-10{left:83.33333%}.col-md-push-11{left:91.66667%}.col-md-push-12{left:100%}.col-md-offset-0{margin-left:0}.col-md-offset-1{margin-left:8.33333%}.col-md-offset-2{margin-left:16.66667%}.col-md-offset-3{margin-left:25%}.col-md-offset-4{margin-left:33.33333%}.col-md-offset-5{margin-left:41.66667%}.col-md-offset-6{margin-left:50%}.col-md-offset-7{margin-left:58.33333%}.col-md-offset-8{margin-left:66.66667%}.col-md-offset-9{margin-left:75%}.col-md-offset-10{margin-left:83.33333%}.col-md-offset-11{margin-left:91.66667%}.col-md-offset-12{margin-left:100%}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-1{width:8.33333%}.col-lg-2{width:16.66667%}.col-lg-3{width:25%}.col-lg-4{width:33.33333%}.col-lg-5{width:41.66667%}.col-lg-6{width:50%}.col-lg-7{width:58.33333%}.col-lg-8{width:66.66667%}.col-lg-9{width:75%}.col-lg-10{width:83.33333%}.col-lg-11{width:91.66667%}.col-lg-12{width:100%}.col-lg-pull-0{right:auto}.col-lg-pull-1{right:8.33333%}.col-lg-pull-2{right:16.66667%}.col-lg-pull-3{right:25%}.col-lg-pull-4{right:33.33333%}.col-lg-pull-5{right:41.66667%}.col-lg-pull-6{right:50%}.col-lg-pull-7{right:58.33333%}.col-lg-pull-8{right:66.66667%}.col-lg-pull-9{right:75%}.col-lg-pull-10{right:83.33333%}.col-lg-pull-11{right:91.66667%}.col-lg-pull-12{right:100%}.col-lg-push-0{left:auto}.col-lg-push-1{left:8.33333%}.col-lg-push-2{left:16.66667%}.col-lg-push-3{left:25%}.col-lg-push-4{left:33.33333%}.col-lg-push-5{left:41.66667%}.col-lg-push-6{left:50%}.col-lg-push-7{left:58.33333%}.col-lg-push-8{left:66.66667%}.col-lg-push-9{left:75%}.col-lg-push-10{left:83.33333%}.col-lg-push-11{left:91.66667%}.col-lg-push-12{left:100%}.col-lg-offset-0{margin-left:0}.col-lg-offset-1{margin-left:8.33333%}.col-lg-offset-2{margin-left:16.66667%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-4{margin-left:33.33333%}.col-lg-offset-5{margin-left:41.66667%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-7{margin-left:58.33333%}.col-lg-offset-8{margin-left:66.66667%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-10{margin-left:83.33333%}.col-lg-offset-11{margin-left:91.66667%}.col-lg-offset-12{margin-left:100%}}caption{padding-top:8px;padding-bottom:8px;color:#777}.table{width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered,.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover,.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}table col[class*=col-]{position:static;float:none;display:table-column}table td[class*=col-],table th[class*=col-]{position:static;float:none;display:table-cell}.btn-group>.btn-group,.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group,.dropdown-menu{float:left}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{overflow-x:auto;min-height:.01%}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset,legend{padding:0;border:0}fieldset{margin:0;min-width:0}legend{display:block;width:100%;margin-bottom:20px;font-size:21px;line-height:inherit;border-bottom:1px solid #e5e5e5}label{display:inline-block;margin-bottom:5px}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-appearance:none}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\\9;line-height:normal}.form-control,output{font-size:14px;line-height:1.42857;color:#555;display:block}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=checkbox]:focus,input[type=radio]:focus{outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px}output{padding-top:7px}.form-control{width:100%;height:34px;padding:6px 12px;background-color:#fff;border:1px solid #9ea3a6;border-radius:2pt;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.has-success .b-radio,.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .form-control-feedback,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.b-radio label,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.form-control::-ms-expand{border:0;background-color:transparent}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=time].form-control,input[type=datetime-local].form-control,input[type=month].form-control{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=time],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],.input-group-sm>.input-group-btn>input[type=date].btn,.input-group-sm>.input-group-btn>input[type=time].btn,.input-group-sm>.input-group-btn>input[type=datetime-local].btn,.input-group-sm>.input-group-btn>input[type=month].btn,.input-group-sm>input[type=date].form-control,.input-group-sm>input[type=date].input-group-addon,.input-group-sm>input[type=time].form-control,.input-group-sm>input[type=time].input-group-addon,.input-group-sm>input[type=datetime-local].form-control,.input-group-sm>input[type=datetime-local].input-group-addon,.input-group-sm>input[type=month].form-control,.input-group-sm>input[type=month].input-group-addon,input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=time],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],.input-group-lg>.input-group-btn>input[type=date].btn,.input-group-lg>.input-group-btn>input[type=time].btn,.input-group-lg>.input-group-btn>input[type=datetime-local].btn,.input-group-lg>.input-group-btn>input[type=month].btn,.input-group-lg>input[type=date].form-control,.input-group-lg>input[type=date].input-group-addon,.input-group-lg>input[type=time].form-control,.input-group-lg>input[type=time].input-group-addon,.input-group-lg>input[type=datetime-local].form-control,.input-group-lg>input[type=datetime-local].input-group-addon,.input-group-lg>input[type=month].form-control,.input-group-lg>input[type=month].input-group-addon,input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.b-radio,.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.b-radio label,.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.b-radio input[type=radio],.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-left:-20px;margin-top:4px\\9}.b-radio+.b-radio,.b-radio+.radio,.checkbox+.checkbox,.radio+.b-radio,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;vertical-align:middle;font-weight:400;cursor:pointer}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}.checkbox-inline.disabled,.checkbox.disabled label,.disabled.b-radio label,.radio-inline.disabled,.radio.disabled label,fieldset[disabled] .b-radio label,fieldset[disabled] .checkbox label,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio label,fieldset[disabled] .radio-inline,fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}.form-control-static{padding-top:7px;padding-bottom:7px;margin-bottom:0;min-height:34px}.form-control-static.input-lg,.form-control-static.input-sm,.input-group-lg>.form-control-static.form-control,.input-group-lg>.form-control-static.input-group-addon,.input-group-lg>.input-group-btn>.form-control-static.btn,.input-group-sm>.form-control-static.form-control,.input-group-sm>.form-control-static.input-group-addon,.input-group-sm>.input-group-btn>.form-control-static.btn{padding-left:0;padding-right:0}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn,.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:2pt}.input-group-sm>.input-group-btn>select.btn,.input-group-sm>select.form-control,.input-group-sm>select.input-group-addon,select.input-sm{height:30px;line-height:30px}.input-group-sm>.input-group-btn>select[multiple].btn,.input-group-sm>.input-group-btn>textarea.btn,.input-group-sm>select[multiple].form-control,.input-group-sm>select[multiple].input-group-addon,.input-group-sm>textarea.form-control,.input-group-sm>textarea.input-group-addon,select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:2pt}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn,.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.33333;border-radius:2pt}.input-group-lg>.input-group-btn>select.btn,.input-group-lg>select.form-control,.input-group-lg>select.input-group-addon,select.input-lg{height:46px;line-height:46px}.input-group-lg>.input-group-btn>select[multiple].btn,.input-group-lg>.input-group-btn>textarea.btn,.input-group-lg>select[multiple].form-control,.input-group-lg>select[multiple].input-group-addon,.input-group-lg>textarea.form-control,.input-group-lg>textarea.input-group-addon,select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.33333;border-radius:2pt}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.33333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.collapsing,.dropdown,.dropup{position:relative}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-group-lg>.form-control+.form-control-feedback,.input-group-lg>.input-group-addon+.form-control-feedback,.input-group-lg>.input-group-btn>.btn+.form-control-feedback,.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-group-sm>.form-control+.form-control-feedback,.input-group-sm>.input-group-addon+.form-control-feedback,.input-group-sm>.input-group-btn>.btn+.form-control-feedback,.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;border-color:#3c763d;background-color:#dff0d8}.has-warning .b-radio,.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .form-control-feedback,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.b-radio label,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;border-color:#8a6d3b;background-color:#fcf8e3}.has-error .b-radio,.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .form-control-feedback,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.b-radio label,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;border-color:#a94442;background-color:#f2dede}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-bottom:10px;color:#626771}@media (min-width:768px){.form-inline .form-control-static,.form-inline .form-group{display:inline-block}.form-inline .control-label,.form-inline .form-group{margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .b-radio,.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .b-radio label,.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .b-radio input[type=radio],.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}.form-horizontal .control-label{text-align:right;margin-bottom:0;padding-top:7px}}.form-horizontal .b-radio,.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{margin-top:0;margin-bottom:0;padding-top:7px}.form-horizontal .b-radio,.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-left:-15px;margin-right:-15px}.form-horizontal .form-group:after,.form-horizontal .form-group:before{content:\" \";display:table}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;margin-bottom:0;font-weight:400;text-align:center;vertical-align:middle;touch-action:manipulation;cursor:pointer;border:1px solid transparent;white-space:nowrap;padding:6px 12px;font-size:14px;line-height:1.42857;border-radius:2pt;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{color:#333;text-decoration:none}.btn.active,.btn:active{outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#c5c5c5;border-color:#ccc}.btn-default.focus,.btn-default:focus{color:#333;background-color:#acacac;border-color:#8c8c8c}.btn-default.active,.btn-default:active,.btn-default:hover,.open>.btn-default.dropdown-toggle{color:#333;background-color:#acacac;border-color:#adadad}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.btn-default.dropdown-toggle.focus,.open>.btn-default.dropdown-toggle:focus,.open>.btn-default.dropdown-toggle:hover{color:#333;background-color:#9a9a9a;border-color:#8c8c8c}.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#c5c5c5;border-color:#ccc}.btn-default .badge{color:#c5c5c5;background-color:#333}.btn-primary,.modal .btn-default{color:#27292d;background-color:#23c890;border-color:#1fb280}.btn-primary.focus,.btn-primary:focus,.modal .btn-default:focus,.modal .focus.btn-default{color:#27292d;background-color:#1b9d71;border-color:#0c4632}.btn-primary.active,.btn-primary:active,.btn-primary:hover,.modal .active.btn-default,.modal .btn-default:active,.modal .btn-default:hover,.modal .open>.dropdown-toggle.btn-default,.open>.btn-primary.dropdown-toggle{color:#27292d;background-color:#1b9d71;border-color:#167e5b}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.modal .active.btn-default:focus,.modal .active.btn-default:hover,.modal .active.focus.btn-default,.modal .btn-default:active.focus,.modal .btn-default:active:focus,.modal .btn-default:active:hover,.modal .open>.dropdown-toggle.btn-default:focus,.modal .open>.dropdown-toggle.btn-default:hover,.modal .open>.dropdown-toggle.focus.btn-default,.open>.btn-primary.dropdown-toggle.focus,.open>.btn-primary.dropdown-toggle:focus,.open>.btn-primary.dropdown-toggle:hover{color:#27292d;background-color:#167e5b;border-color:#0c4632}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,.modal .disabled.btn-default:focus,.modal .disabled.btn-default:hover,.modal .disabled.focus.btn-default,.modal [disabled].btn-default:focus,.modal [disabled].btn-default:hover,.modal [disabled].focus.btn-default,.modal fieldset[disabled] .btn-default:focus,.modal fieldset[disabled] .btn-default:hover,.modal fieldset[disabled] .focus.btn-default,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover,fieldset[disabled] .modal .btn-default:focus,fieldset[disabled] .modal .btn-default:hover,fieldset[disabled] .modal .focus.btn-default{background-color:#23c890;border-color:#1fb280}.btn-primary .badge,.modal .btn-default .badge{color:#23c890;background-color:#27292d}.btn-success{color:#fff;background-color:#23c890;border-color:#1fb280}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#1b9d71;border-color:#0c4632}.btn-success.active,.btn-success:active,.btn-success:hover,.open>.btn-success.dropdown-toggle{color:#fff;background-color:#1b9d71;border-color:#167e5b}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.btn-success.dropdown-toggle.focus,.open>.btn-success.dropdown-toggle:focus,.open>.btn-success.dropdown-toggle:hover{color:#fff;background-color:#167e5b;border-color:#0c4632}.btn-success.active,.btn-success:active,.open>.btn-success.dropdown-toggle{background-image:none}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#23c890;border-color:#1fb280}.btn-success .badge{color:#23c890;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info.active,.btn-info:active,.btn-info:hover,.open>.btn-info.dropdown-toggle{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.btn-info.dropdown-toggle.focus,.open>.btn-info.dropdown-toggle:focus,.open>.btn-info.dropdown-toggle:hover{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning.active,.btn-warning:active,.btn-warning:hover,.open>.btn-warning.dropdown-toggle{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.btn-warning.dropdown-toggle.focus,.open>.btn-warning.dropdown-toggle:focus,.open>.btn-warning.dropdown-toggle:hover{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger.active,.btn-danger:active,.btn-danger:hover,.open>.btn-danger.dropdown-toggle{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.btn-danger.dropdown-toggle.focus,.open>.btn-danger.dropdown-toggle:focus,.open>.btn-danger.dropdown-toggle:hover{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{color:#23c890;font-weight:400;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#188761;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.33333;border-radius:2pt}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:2pt}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:2pt}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{height:0;overflow:hidden;-webkit-transition-property:height,visibility;transition-property:height,visibility;-webkit-transition-duration:.35s;transition-duration:.35s;-webkit-transition-timing-function:ease;transition-timing-function:ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\\9;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;padding:5px 0;margin:2px 0 0;list-style:none;font-size:14px;text-align:left;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:2pt;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175);background-clip:padding-box}.dropdown-menu-right,.dropdown-menu.pull-right{left:auto;right:0}.dropdown-header,.dropdown-menu>li>a{display:block;padding:3px 20px;line-height:1.42857;white-space:nowrap}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle,.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child,.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child),.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn,.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{font-weight:400;color:#333}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{text-decoration:none;color:#262626;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;outline:0;background-color:#23c890}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;background-color:transparent;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);cursor:not-allowed}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-left{left:0;right:auto}.dropdown-header{font-size:12px;color:#777}.dropdown-backdrop{position:fixed;left:0;right:0;bottom:0;top:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px dashed;border-bottom:4px solid\\9;content:\"\"}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{left:0;right:auto}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar:after,.btn-toolbar:before{content:\" \";display:table}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn .caret,.btn-group>.btn:first-child{margin-left:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-left:8px;padding-right:8px}.btn-group-lg.btn-group>.btn+.dropdown-toggle,.btn-group>.btn-lg+.dropdown-toggle{padding-left:12px;padding-right:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn-group-lg>.btn .caret,.btn-lg .caret{border-width:5px 5px 0}.dropup .btn-group-lg>.btn .caret,.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before{content:\" \";display:table}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-radius:2pt 2pt 0 0}.btn-group-vertical>.btn:last-child:not(:first-child){border-radius:0 0 2pt 2pt}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn,.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed}.btn-group-justified>.btn,.btn-group-justified>.btn-group{float:none;display:table-cell;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group,.input-group-btn,.input-group-btn>.btn{position:relative}.input-group{display:table}.input-group[class*=col-]{float:none;padding-left:0;padding-right:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #9ea3a6;border-radius:2pt}.input-group-addon.input-sm,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.input-group-addon.btn{padding:5px 10px;font-size:12px;border-radius:2pt}.input-group-addon.input-lg,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.input-group-addon.btn{padding:10px 16px;font-size:18px;border-radius:2pt}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{font-size:0;white-space:nowrap}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{margin-bottom:0;padding-left:0;list-style:none}.nav:after,.nav:before{content:\" \";display:table}.nav>li,.nav>li>a{display:block;position:relative}.nav:after{clear:both}.nav>li>a{padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;background-color:transparent;cursor:not-allowed}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#23c890}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857;border:1px solid transparent;border-radius:2pt 2pt 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent;cursor:default}.nav-pills>li{float:left}.nav-justified>li,.nav-stacked>li,.nav-tabs.nav-justified>li{float:none}.nav-pills>li>a{border-radius:2pt}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#23c890}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified,.nav-tabs.nav-justified{width:100%}.nav-justified>li>a,.nav-tabs.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}.nav-tabs-justified,.nav-tabs.nav-justified{border-bottom:0}.nav-tabs-justified>li>a,.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:2pt}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-justified>li,.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a,.nav-tabs.nav-justified>li>a{margin-bottom:0}.nav-tabs-justified>li>a,.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:2pt 2pt 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before{display:table;content:\" \"}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{position:relative;min-height:40pt;margin-bottom:20px;border:1px solid transparent}.navbar-collapse{overflow-x:visible;padding-right:15px;padding-left:15px;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar{border-radius:0}.navbar-header{float:left}.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-left:0;padding-right:0}}.embed-responsive,.modal,.modal-open,.progress{overflow:hidden}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}.navbar-static-top{z-index:1000;border-width:0 0 1px}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;padding:12.5pt 15px;line-height:20px;height:40pt}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}.navbar-fixed-bottom,.navbar-fixed-top,.navbar-static-top{border-radius:0}.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;margin-right:15px;padding:9px 10px;margin-top:7.25pt;margin-bottom:7.25pt;background-color:transparent;border:1px solid transparent;border-radius:2pt}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}.navbar-nav{margin:6.25pt -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}.progress-bar-striped,.progress-striped .progress-bar,.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}@media (min-width:768px){.navbar-toggle{display:none}.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:12.5pt;padding-bottom:12.5pt}}.navbar-form{padding:10px 15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);margin:7.25pt -15px}@media (min-width:768px){.navbar-form .form-control-static,.navbar-form .form-group{display:inline-block}.navbar-form .control-label,.navbar-form .form-group{margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .b-radio,.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .b-radio label,.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .b-radio input[type=radio],.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}.navbar-form{width:auto;border:0;margin-left:0;margin-right:0;padding-top:0;padding-bottom:0;-webkit-box-shadow:none;box-shadow:none}}.breadcrumb>li,.pagination{display:inline-block}.btn .badge,.btn .label{top:-1px;position:relative}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-radius:0}.navbar-btn{margin-top:7.25pt;margin-bottom:7.25pt}.btn-group-sm>.navbar-btn.btn,.navbar-btn.btn-sm{margin-top:8.75pt;margin-bottom:8.75pt}.btn-group-xs>.navbar-btn.btn,.navbar-btn.btn-xs{margin-top:9pt;margin-bottom:9pt}.navbar-text{margin-top:12.5pt;margin-bottom:12.5pt}@media (min-width:768px){.navbar-text{float:left;margin-left:15px;margin-right:15px}.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:0}.navbar-default .navbar-brand{color:#fff}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#e6e6e6;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#fff}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:0}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{background-color:#e7e7e7;color:#555}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#fff}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#fff}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#fff}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#090909}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>li>a,.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#090909}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{background-color:#090909;color:#fff}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#090909}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#090909}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#090909}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:2pt}.breadcrumb>li+li:before{content:\"/ \";padding:0 5px;color:#ccc}.breadcrumb>.active{color:#777}.pagination{padding-left:0;margin:20px 0;border-radius:2pt}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;line-height:1.42857;text-decoration:none;color:#23c890;background-color:#fff;border:1px solid #ddd;margin-left:-1px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span,.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:2pt;border-top-left-radius:2pt}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span,.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span,.pagination>li:last-child>a,.pagination>li:last-child>span{border-bottom-right-radius:2pt;border-top-right-radius:2pt}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:2pt;border-top-left-radius:2pt}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:2;color:#188761;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:3;color:#fff;background-color:#23c890;border-color:#23c890;cursor:default}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;background-color:#fff;border-color:#ddd;cursor:not-allowed}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.33333}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.badge,.label{font-weight:700;line-height:1;white-space:nowrap;text-align:center}.pager{padding-left:0;margin:20px 0;list-style:none;text-align:center}.pager:after,.pager:before{content:\" \";display:table}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;background-color:#fff;cursor:not-allowed}.label{display:inline;padding:.2em .6em .3em;font-size:75%;color:#fff;border-radius:.25em}.label:empty{display:none}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#23c890}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#1b9d71}.label-success{background-color:#23c890}.label-success[href]:focus,.label-success[href]:hover{background-color:#1b9d71}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;color:#fff;vertical-align:middle;background-color:#777;border-radius:10px}.badge:empty{display:none}.media-object,.thumbnail{display:block}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#23c890;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.jumbotron,.jumbotron .h1,.jumbotron h1{color:inherit}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;background-color:#eee}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.alert .alert-link,.close{font-weight:700}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{border-radius:2pt;padding-left:15px;padding-right:15px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-left:60px;padding-right:60px}.jumbotron .h1,.jumbotron h1{font-size:63px}}.thumbnail{padding:4px;margin-bottom:20px;line-height:1.42857;background-color:#fff;border:1px solid #ddd;border-radius:2pt;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto;margin-left:auto;margin-right:auto}.thumbnail .caption{padding:9px;color:#27292d}.Select-input,.media-right,.media>.pull-right{padding-left:10px}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#23c890}.alert{border:1px solid transparent}.alert h4{margin-top:0;color:inherit}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.modal,.modal-backdrop{right:0;bottom:0;left:0}.alert-success{background-color:#dff0d8;border-color:#d6e9c6;color:#3c763d}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{background-color:#d9edf7;border-color:#bce8f1;color:#31708f}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{background-color:#fcf8e3;border-color:#faebcc;color:#8a6d3b}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{background-color:#f2dede;color:#a94442}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;background-color:#f5f5f5;border-radius:2pt;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar,.progress-bar-success{background-color:#23c890}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-striped .progress-bar-success{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-striped .progress-bar-info,.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{zoom:1;overflow:hidden}.media-body{width:10000px}.media-object.img-thumbnail{max-width:none}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{margin-bottom:20px;padding-left:0}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-right-radius:2pt;border-top-left-radius:2pt}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:2pt;border-bottom-left-radius:2pt}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{text-decoration:none;color:#555;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{background-color:#eee;color:#777;cursor:not-allowed}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#23c890;border-color:#23c890}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#c3f4e4}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.panel-heading>.dropdown .dropdown-toggle,.panel-title,.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:2pt;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-title,.panel>.list-group,.panel>.panel-collapse>.list-group,.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel-body{padding:15px}.panel-body:after,.panel-body:before{content:\" \";display:table}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-right-radius:1pt;border-top-left-radius:1pt}.panel-title{margin-top:0;font-size:16px}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:1pt;border-bottom-left-radius:1pt}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel-group .panel-heading,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-responsive:last-child>.table:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-left-radius:1pt;border-bottom-right-radius:1pt}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-right-radius:1pt;border-top-left-radius:1pt}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:1pt;border-bottom-left-radius:1pt}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-right-radius:0;border-top-left-radius:0}.panel>.table-responsive:first-child>.table:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-right-radius:1pt;border-top-left-radius:1pt}.list-group+.panel-footer,.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-left:15px;padding-right:15px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:1pt}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:1pt}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:1pt}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:1pt}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-responsive{border:0;margin-bottom:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:2pt}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#23c890}.panel-primary>.panel-heading{color:#fff;background-color:#23c890;border-color:#23c890}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#23c890}.panel-primary>.panel-heading .badge{color:#23c890;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#23c890}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;left:0;bottom:0;height:100%;width:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:2pt;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well-lg,.well-sm{border-radius:2pt}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px}.well-sm{padding:9px}.close{float:right;font-size:21px;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.popover,.tooltip{font-style:normal;letter-spacing:normal;line-break:auto;line-height:1.42857;text-shadow:none;text-transform:none;white-space:normal;word-break:normal;word-spacing:normal;word-wrap:normal;text-decoration:none}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none}.modal-content,.popover{background-clip:padding-box}.modal{display:none;position:fixed;z-index:1050;-webkit-overflow-scrolling:touch;outline:0}.modal-footer:after,.modal-footer:before,.modal-header:after,.modal-header:before{display:table;content:\" \"}.modal.fade .modal-dialog{-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%);-webkit-transition:-webkit-transform .3s ease-out;-moz-transition:-moz-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:2pt;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5);outline:0}.modal-backdrop{position:fixed;top:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:.5;filter:alpha(opacity=50)}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:400;text-align:left;text-align:start;font-size:12px;opacity:0;filter:alpha(opacity=0)}.tooltip.in{opacity:.9;filter:alpha(opacity=90)}.tooltip.top{margin-top:-3px;padding:5px 0}.tooltip.right{margin-left:3px;padding:0 5px}.tooltip.bottom{margin-top:3px;padding:5px 0}.tooltip.left{margin-left:-3px;padding:0 5px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:2pt}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow,.tooltip.top-left .tooltip-arrow,.tooltip.top-right .tooltip-arrow{bottom:0;border-width:5px 5px 0;border-top-color:#000}.tooltip.top .tooltip-arrow{left:50%;margin-left:-5px}.tooltip.top-left .tooltip-arrow{right:5px;margin-bottom:-5px}.tooltip.top-right .tooltip-arrow{left:5px;margin-bottom:-5px}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow,.tooltip.bottom-left .tooltip-arrow,.tooltip.bottom-right .tooltip-arrow{border-width:0 5px 5px;border-bottom-color:#000;top:0}.tooltip.bottom .tooltip-arrow{left:50%;margin-left:-5px}.tooltip.bottom-left .tooltip-arrow{right:5px;margin-top:-5px}.tooltip.bottom-right .tooltip-arrow{left:5px;margin-top:-5px}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-weight:400;text-align:left;text-align:start;font-size:14px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:2pt;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2)}.carousel-caption,.carousel-control{color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.6);text-align:center}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{margin:0;padding:8px 14px;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:1pt 1pt 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.carousel,.carousel-inner{position:relative}.popover>.arrow{border-width:11px}.popover>.arrow:after{border-width:10px;content:\"\"}.popover.top>.arrow{left:50%;margin-left:-11px;border-bottom-width:0;border-top-color:#999;border-top-color:rgba(0,0,0,.25);bottom:-11px}.popover.top>.arrow:after{content:\" \";bottom:1px;margin-left:-10px;border-bottom-width:0;border-top-color:#fff}.popover.left>.arrow:after,.popover.right>.arrow:after{content:\" \";bottom:-10px}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-left-width:0;border-right-color:#999;border-right-color:rgba(0,0,0,.25)}.popover.right>.arrow:after{left:1px;border-left-width:0;border-right-color:#fff}.popover.bottom>.arrow{left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25);top:-11px}.popover.bottom>.arrow:after{content:\" \";top:1px;margin-left:-10px;border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;border-right-width:0;border-left-color:#fff}.carousel-inner{overflow:hidden;width:100%}.carousel-inner>.item{display:none;position:relative;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>a>img,.carousel-inner>.item>img{display:block;max-width:100%;height:auto;line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-moz-transition:-moz-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;-moz-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;-moz-perspective:1000px;perspective:1000px}.carousel-inner>.item.active.right,.carousel-inner>.item.next{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);left:0}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);left:0}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);left:0}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;left:0;bottom:0;width:15%;opacity:.5;filter:alpha(opacity=50);font-size:20px;background-color:transparent}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1)}.carousel-control.right{left:auto;right:0;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1)}.carousel-control:focus,.carousel-control:hover{outline:0;color:#fff;text-decoration:none;opacity:.9;filter:alpha(opacity=90)}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;margin-top:-10px;z-index:5;display:inline-block}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;line-height:1;font-family:serif}.carousel-control .icon-prev:before{content:'\\2039'}.carousel-control .icon-next:before{content:'\\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;margin-left:-30%;padding-left:0;list-style:none;text-align:center}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;border:1px solid #fff;border-radius:10px;cursor:pointer;background-color:#000\\9;background-color:transparent}.carousel-indicators .active{margin:0;width:12px;height:12px;background-color:#fff}.carousel-caption{position:absolute;left:15%;right:15%;bottom:20px;z-index:10;padding-top:20px;padding-bottom:20px}.carousel-caption .btn,.text-hide{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{left:20%;right:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:after,.clearfix:before{content:\" \";display:table}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right!important}.pull-left{float:left!important}.daterangepicker.single .calendar,.daterangepicker.single .ranges,.ranges{float:none}.hide{display:none!important}.show{display:block!important}.hidden,.visible-lg,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;background-color:transparent;border:0}.affix{position:fixed}@-ms-viewport{width:device-width}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}.visible-xs-block{display:block!important}.visible-xs-inline{display:inline!important}.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}.visible-sm-block{display:block!important}.visible-sm-inline{display:inline!important}.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}.visible-md-block{display:block!important}.visible-md-inline{display:inline!important}.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}.visible-lg-block{display:block!important}.visible-lg-inline{display:inline!important}.visible-lg-inline-block{display:inline-block!important}.hidden-lg{display:none!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}.hidden-print{display:none!important}}.ReactVirtualized__Table__headerRow{font-weight:700;text-transform:uppercase;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.ReactVirtualized__Table__row{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.ReactVirtualized__Table__headerTruncatedText{display:inline-block;max-width:100%;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.ReactVirtualized__Table__headerColumn,.ReactVirtualized__Table__rowColumn{margin-right:10px;min-width:0}.ReactVirtualized__Table__rowColumn{text-overflow:ellipsis;white-space:nowrap}.ReactVirtualized__Table__headerColumn:first-of-type,.ReactVirtualized__Table__rowColumn:first-of-type{margin-left:10px}.ReactVirtualized__Table__sortableHeaderColumn{cursor:pointer}.ReactVirtualized__Table__sortableHeaderIconContainer{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.ReactVirtualized__Table__sortableHeaderIcon{-webkit-box-flex:0;-ms-flex:0 0 24px;flex:0 0 24px;height:1em;width:1em;fill:currentColor}.Table{width:100%;margin-top:15px}.evenRow,.headerRow,.oddRow{border-bottom:1px solid #e0e0e0}.oddRow{background-color:#fafafa}.headerColumn{text-transform:none}.exampleColumn{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.checkboxLabel{margin-left:.5rem}.checkboxLabel:first-of-type{margin-left:0}.noRows{position:absolute;top:0;bottom:0;left:0;right:0;display:flex;align-items:center;justify-content:center;font-size:1em;color:#bdbdbd}.Select,.Select-control,.placeholder{position:relative}.placeholder{display:inline-block;height:1em;animation-duration:2s;animation-fill-mode:forwards;animation-iteration-count:infinite;animation-name:placeHolderShimmer;animation-timing-function:linear;background:#f6f7f8;background:linear-gradient(to right,#eee 8%,#ddd 18%,#eee 33%);background-size:800px 104px}@keyframes placeHolderShimmer{0%{background-position:-468px 0}100%{background-position:468px 0}}.Select,.Select div,.Select input,.Select span{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.Select.is-disabled>.Select-control{background-color:#f9f9f9}.Select.is-disabled>.Select-control:hover{box-shadow:none}.Select.is-disabled .Select-arrow-zone{cursor:default;pointer-events:none;opacity:.35}.Select-control{background-color:#fff;border-radius:5px;border:1px solid #9ea3a6;color:#333;cursor:default;display:table;border-spacing:0;height:34px;outline:0;overflow:hidden;width:100%}.is-searchable.is-focused:not(.is-open)>.Select-control,.is-searchable.is-open>.Select-control{cursor:text}.Select-control:hover{box-shadow:0 1px 0 rgba(0,0,0,.06)}.Select-control .Select-input:focus{outline:0}.is-open>.Select-control{border-bottom-right-radius:0;border-bottom-left-radius:0;background:#fff;border-color:#b3b3b3 #ccc #d9d9d9}.is-open>.Select-control>.Select-arrow{border-color:transparent transparent #999;border-width:0 5px 5px}.is-focused:not(.is-open)>.Select-control{border-color:#66afe9;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 0 3px rgba(0,126,255,.1)}.Select--single>.Select-control .Select-value,.Select-placeholder{bottom:0;color:#aaa;left:0;line-height:32px;padding-left:10px;padding-right:10px;position:absolute;right:0;top:0;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.has-value.Select--single>.Select-control .Select-value .Select-value-label,.has-value.is-pseudo-focused.Select--single>.Select-control .Select-value .Select-value-label{color:#333}.has-value.Select--single>.Select-control .Select-value a.Select-value-label,.has-value.is-pseudo-focused.Select--single>.Select-control .Select-value a.Select-value-label{cursor:pointer;text-decoration:none}.has-value.Select--single>.Select-control .Select-value a.Select-value-label:focus,.has-value.Select--single>.Select-control .Select-value a.Select-value-label:hover,.has-value.is-pseudo-focused.Select--single>.Select-control .Select-value a.Select-value-label:focus,.has-value.is-pseudo-focused.Select--single>.Select-control .Select-value a.Select-value-label:hover{color:#007eff;outline:0;text-decoration:underline}.Select-input{height:32px;padding-right:10px;vertical-align:middle}.Select-input>input{width:100%;background:none;border:0;box-shadow:none;cursor:default;display:inline-block;font-family:inherit;font-size:inherit;margin:0;outline:0;line-height:14px;padding:8px 0 12px;-webkit-appearance:none}.Select-loading,.Select-loading-zone{width:16px;position:relative;vertical-align:middle}.is-focused .Select-input>input{cursor:text}.has-value.is-pseudo-focused .Select-input{opacity:0}.Select-control:not(.is-searchable)>.Select-input{outline:0}.Select-loading-zone{cursor:pointer;display:table-cell;text-align:center}.Select-loading{-webkit-animation:Select-animation-spin .4s infinite linear;-o-animation:Select-animation-spin .4s infinite linear;animation:Select-animation-spin .4s infinite linear;height:16px;box-sizing:border-box;border-radius:50%;border:2px solid #ccc;border-right-color:#333;display:inline-block}.Select-clear-zone{-webkit-animation:Select-animation-fadeIn .2s;-o-animation:Select-animation-fadeIn .2s;animation:Select-animation-fadeIn .2s;color:#999;cursor:pointer;display:table-cell;position:relative;text-align:center;vertical-align:middle;width:17px}.Select-clear-zone:hover{color:#D0021B}.Select-clear{display:inline-block;font-size:18px;line-height:1}.Select--multi .Select-clear-zone{width:17px}.Select-arrow-zone{cursor:pointer;display:table-cell;position:relative;text-align:center;vertical-align:middle;width:25px;padding-right:5px}.Select--multi .Select-multi-value-wrapper,.Select-arrow{display:inline-block}.Select-arrow{border-color:#999 transparent transparent;border-style:solid;border-width:5px 5px 2.5px;height:0;width:0}.Select-arrow-zone:hover>.Select-arrow,.is-open .Select-arrow{border-top-color:#666}.Select .Select-aria-only{display:inline-block;height:1px;width:1px;margin:-1px;clip:rect(0,0,0,0);overflow:hidden}.Select-noresults,.Select-option{box-sizing:border-box;display:block;padding:8px 10px}@-webkit-keyframes Select-animation-fadeIn{from{opacity:0}to{opacity:1}}@keyframes Select-animation-fadeIn{from{opacity:0}to{opacity:1}}.Select-menu-outer{border-bottom-right-radius:4px;border-bottom-left-radius:4px;background-color:#fff;border:1px solid #ccc;border-top-color:#e6e6e6;box-shadow:0 1px 0 rgba(0,0,0,.06);box-sizing:border-box;margin-top:-1px;max-height:200px;position:absolute;top:100%;width:100%;z-index:1;-webkit-overflow-scrolling:touch}.Select-menu{max-height:198px;overflow-y:auto}.Select-option{background-color:#fff;color:#666;cursor:pointer}.Select-option:last-child{border-bottom-right-radius:4px;border-bottom-left-radius:4px}.Select-option.is-selected{background-color:#f5faff;background-color:rgba(0,126,255,.04);color:#333}.Select-option.is-focused{background-color:#ebf5ff;background-color:rgba(0,126,255,.08);color:#333}.Select-option.is-disabled{color:#ccc;cursor:default}.Select-noresults{color:#999;cursor:default}.Select--multi .Select-input{vertical-align:middle;margin-left:10px;padding:0}.Select--multi.has-value .Select-input{margin-left:5px}.Select--multi .Select-value{background-color:#ebf5ff;background-color:rgba(0,126,255,.08);border-radius:2px;border:1px solid #c2e0ff;border:1px solid rgba(0,126,255,.24);color:#007eff;display:inline-block;font-size:.9em;line-height:1.4;margin-left:5px;margin-top:5px;vertical-align:top}.Select--multi .Select-value-icon,.Select--multi .Select-value-label{display:inline-block;vertical-align:middle}.Select--multi .Select-value-label{border-bottom-right-radius:2px;border-top-right-radius:2px;cursor:default;padding:2px 5px}.Select--multi a.Select-value-label{color:#007eff;cursor:pointer;text-decoration:none}.Select--multi a.Select-value-label:hover{text-decoration:underline}.Select--multi .Select-value-icon{cursor:pointer;border-bottom-left-radius:2px;border-top-left-radius:2px;border-right:1px solid #c2e0ff;border-right:1px solid rgba(0,126,255,.24);padding:1px 5px 3px}.Select--multi .Select-value-icon:focus,.Select--multi .Select-value-icon:hover{background-color:#d8eafd;background-color:rgba(0,113,230,.08);color:#0071e6}.Select--multi .Select-value-icon:active{background-color:#c2e0ff;background-color:rgba(0,126,255,.24)}.Select--multi.is-disabled .Select-value{background-color:#fcfcfc;border:1px solid #e3e3e3;color:#333}.Select--multi.is-disabled .Select-value-icon{cursor:not-allowed;border-right:1px solid #e3e3e3}.Select--multi.is-disabled .Select-value-icon:active,.Select--multi.is-disabled .Select-value-icon:focus,.Select--multi.is-disabled .Select-value-icon:hover{background-color:#fcfcfc}@keyframes Select-animation-spin{to{transform:rotate(1turn)}}@-webkit-keyframes Select-animation-spin{to{-webkit-transform:rotate(1turn)}}.daterangepicker{position:absolute;color:inherit;background:#fff;border-radius:4px;width:278px;padding:4px;margin-top:1px;top:100px;left:20px}.daterangepicker:after,.daterangepicker:before{position:absolute;display:inline-block;content:''}.daterangepicker:before{top:-7px;border-right:7px solid transparent;border-left:7px solid transparent;border-bottom:7px solid #ccc}.daterangepicker:after{top:-6px;border-right:6px solid transparent;border-bottom:6px solid #fff;border-left:6px solid transparent}.daterangepicker.opensleft:before{right:9px}.daterangepicker.opensleft:after{right:10px}.daterangepicker.openscenter:after,.daterangepicker.openscenter:before{left:0;right:0;width:0;margin-left:auto;margin-right:auto}.daterangepicker.opensright:before{left:9px}.daterangepicker.opensright:after{left:10px}.daterangepicker.dropup{margin-top:-5px}.daterangepicker.dropup:before{top:initial;bottom:-7px;border-bottom:initial;border-top:7px solid #ccc}.daterangepicker.dropup:after{top:initial;bottom:-6px;border-bottom:initial;border-top:6px solid #fff}.daterangepicker.dropdown-menu{max-width:none;z-index:3001}.daterangepicker.show-calendar .calendar{display:block}.daterangepicker .calendar{display:none;max-width:270px;margin:4px}.daterangepicker .calendar.single .calendar-table{border:none}.daterangepicker .calendar td,.daterangepicker .calendar th{white-space:nowrap;text-align:center;min-width:32px}.daterangepicker .calendar-table{border:1px solid #fff;padding:4px;border-radius:4px;background:#fff}.daterangepicker table{width:100%;margin:0}.daterangepicker td,.daterangepicker th{text-align:center;width:20px;height:20px;border-radius:4px;border:1px solid transparent;white-space:nowrap;cursor:pointer}.daterangepicker td.available:hover,.daterangepicker th.available:hover{background-color:#eee;border-color:transparent;color:inherit}.daterangepicker td.week,.daterangepicker th.week{font-size:80%;color:#ccc}.daterangepicker td.off,.daterangepicker td.off.end-date,.daterangepicker td.off.in-range,.daterangepicker td.off.start-date{background-color:#fff;border-color:transparent;color:#999}.daterangepicker td.in-range{background-color:#ebf4f8;border-color:transparent;color:#000;border-radius:0}.daterangepicker td.start-date{border-radius:4px 0 0 4px}.daterangepicker td.end-date{border-radius:0 4px 4px 0}.daterangepicker td.start-date.end-date{border-radius:4px}.daterangepicker td.active,.daterangepicker td.active:hover{background-color:#357ebd;border-color:transparent;color:#fff}.daterangepicker th.month{width:auto}.daterangepicker option.disabled,.daterangepicker td.disabled{color:#999;cursor:not-allowed;text-decoration:line-through}.daterangepicker select.monthselect,.daterangepicker select.yearselect{font-size:12px;padding:1px;height:auto;margin:0;cursor:default}.daterangepicker select.monthselect{margin-right:2%;width:56%}.daterangepicker select.yearselect{width:40%}.daterangepicker select.ampmselect,.daterangepicker select.hourselect,.daterangepicker select.minuteselect,.daterangepicker select.secondselect{width:50px;margin-bottom:0}.daterangepicker .input-mini{border:1px solid #ccc;border-radius:4px;color:#555;height:30px;line-height:30px;display:block;vertical-align:middle;margin:0 0 5px;padding:0 6px 0 28px;width:100%}.daterangepicker .input-mini.active{border:1px solid #08c;border-radius:4px}.daterangepicker .daterangepicker_input{position:relative}.daterangepicker .daterangepicker_input i{position:absolute;left:8px;top:8px}.daterangepicker.rtl .input-mini{padding-right:28px;padding-left:6px}.daterangepicker.rtl .daterangepicker_input i{left:auto;right:8px}.daterangepicker .calendar-time{text-align:center;margin:5px auto;line-height:30px;position:relative;padding-left:28px}.daterangepicker .calendar-time select.disabled{color:#ccc;cursor:not-allowed}.ranges{font-size:11px;margin:4px;text-align:left}.ranges ul{list-style:none;margin:0 auto;padding:0;width:100%}.ranges li{font-size:13px;background:#f5f5f5;border:1px solid #f5f5f5;border-radius:4px;color:#08c;padding:3px 12px;margin-bottom:8px;cursor:pointer}.ranges li.active,.ranges li:hover{background:#08c;border:1px solid #08c;color:#fff}@media (min-width:564px){.daterangepicker.ltr .calendar.right .calendar-table,.daterangepicker.rtl .calendar.left .calendar-table{border-left:none;border-top-left-radius:0;border-bottom-left-radius:0}.daterangepicker.ltr .calendar.left .calendar-table,.daterangepicker.rtl .calendar.right .calendar-table{border-right:none;border-top-right-radius:0;border-bottom-right-radius:0}.daterangepicker{width:auto}.daterangepicker .ranges ul{width:160px}.daterangepicker.single .ranges ul{width:100%}.daterangepicker.single .calendar.left{clear:none}.daterangepicker.single.ltr .calendar,.daterangepicker.single.ltr .ranges{float:left}.daterangepicker.single.rtl .calendar,.daterangepicker.single.rtl .ranges{float:right}.daterangepicker.ltr{direction:ltr;text-align:left}.daterangepicker.ltr .calendar.left{clear:left;margin-right:0}.daterangepicker.ltr .calendar.right{margin-left:0}.daterangepicker.ltr .calendar.left .calendar-table,.daterangepicker.ltr .left .daterangepicker_input{padding-right:12px}.daterangepicker.ltr .calendar,.daterangepicker.ltr .ranges{float:left}.daterangepicker.rtl{direction:rtl;text-align:right}.daterangepicker.rtl .calendar.left{clear:right;margin-left:0}.daterangepicker.rtl .calendar.right{margin-right:0}.daterangepicker.rtl .calendar.left .calendar-table,.daterangepicker.rtl .left .daterangepicker_input{padding-left:12px}.daterangepicker.rtl .calendar,.daterangepicker.rtl .ranges{text-align:right;float:right}}.login-container,.login-container .alert{display:block;margin-left:auto;margin-right:auto;max-width:224pt}@media (min-width:730px){.daterangepicker .ranges{width:auto}.daterangepicker.ltr .ranges{float:left}.daterangepicker.rtl .ranges{float:right}.daterangepicker .calendar.left{clear:none!important}}.login{background-color:#23c890}.login-container{margin-top:80pt}.login-container .login-content{box-shadow:0 0 40px 0 rgba(0,0,0,.2)}.login-container .fb-login{background:#3c5a97;color:#FFF}.login .navbar-nav{display:none}@font-face{font-family:BlynkFont;src:url(/static/fonts/ufonts.com_pfdindisplaypro-thin.eot);src:url(/static/fonts/ufonts.com_pfdindisplaypro-thin.woff) format(\"woff\"),url(/static/fonts/ufonts.com_pfdindisplaypro-thin.ttf) format(\"truetype\")}.popover,body{font-family:BlynkFont}input{border-width:.5pt}label{font-weight:400}ul{padding-left:20px}.navbar{background:#23c890}.navbar-right{margin-right:0}.android-option,.ios-option{background-image:url(../img/sprite-de7502efa8464674e2ceb42829ba4bdc.png);margin-right:17px}.navbar-brand{font-size:16pt}.form-heading{font-weight:500;margin-bottom:16pt}.help-block{margin-top:10pt}.btn{margin-top:15pt}.alert{padding:5px 15px;margin-bottom:5px;border-radius:3pt}.alert-danger{border:none}.preview-container{display:inline-block;padding:10px;height:600px;width:300px;margin-top:40px;vertical-align:top}.ios-option,.platform-row .col-sm-6{height:48px}.service-container .form-group{padding-top:5px}.service-container .service-options .service-option{padding:20px}.service-container .service-options .service-option ul{min-height:114px}.service-container .service-options .service-option h4{font-weight:900}.plan-billing-form{margin-top:36px}.terms{margin-top:271px}.plan-options-form{margin-top:20px;padding:20px}.plan-options-form h3{margin-top:0}.ios-option{background-position:-256px -160px;width:41px;float:left}.android-option{background-position:-256px -208px;width:40px;height:48px;float:left}.add-new-product,.list-item-logo,.sample-icon{float:left;margin-right:32px}.platform-row{margin-top:10px;margin-bottom:40px}.platform-row h4{margin-top:16px}.plan-total{margin-top:20px;font-weight:bolder}#app-subscribe{margin-bottom:20px}.color-picker .color{width:36px;height:14px;border-radius:2px}.color-picker .swatch{padding:5px;background:#fff;border-radius:1px;box-shadow:0 0 0 1px rgba(0,0,0,.1);display:inline-block;cursor:pointer}.color-picker .color-popover{position:absolute;z-index:2}.color-picker .cover{position:fixed;top:0;right:0;bottom:0;left:0}.payment-form{margin-bottom:40px;margin-top:10px;padding:20px}.payment-form h3{margin-top:0}.payment-trial-details{margin-top:56px}.preloader-wrapper{position:absolute;left:0;right:0;bottom:0;top:0;z-index:3;background-color:rgba(248,248,248,.6)}.payment-section{padding-top:5px;position:relative}.payment-icon-wrapper div{top:8px;right:8px;position:absolute;width:44px;height:28px;background-image:url(/img/card_types-8f8705ee2516623a5faab39b59e2d86b.png);text-indent:-999em;background-position:0 -296px}.add-new-product,.list-item-default-logo,.list-item-logo,.sample-icon{width:128px;height:128px}@media only screen and (-webkit-min-device-pixel-ratio:1.5),only screen and (min--moz-device-pixel-ratio:1.5),only screen and (min-resolution:240dpi){.payment-icon-wrapper div{background-image:url(/img/card_types@2x-62e20de12606920a5a552e025d653cb6.png);background-size:86px auto}}.add-new-product,.list-item-default-logo,.sample-icon,.setup-container .design,.setup-container .market,.setup-container .project,.setup-container .wifi{background-image:url(../img/sprite-de7502efa8464674e2ceb42829ba4bdc.png)}.payment-icon-wrapper .visa{background-position:0 -380px}.payment-icon-wrapper .maestro{background-position:0 -240px}.payment-icon-wrapper .master-card{background-position:0 -268px}.payment-icon-wrapper .jcb{background-position:0 -212px}.payment-icon-wrapper .discover{background-position:0 -156px}.payment-icon-wrapper .american-express{background-position:0 -352px}.payment-icon-wrapper .coinbase{background-position:0 -324px}.payment-icon-wrapper .paypal{background-position:0 -408px}.payment-icon-wrapper .diners-club,.sample-icon{background-position:0 -128px}.braintree-hosted-fields-invalid{border-color:#d4435c}.braintree-hosted-fields-focused{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}#payment-form .col-sm-4:first-of-type{padding-right:7.5px}#payment-form .col-sm-4:last-of-type{padding-left:7.5px}#payment-form .col-sm-4:nth-of-type(2){padding-right:7.5px;padding-left:7.5px}#payment-form ul{padding-left:5px}#payment-form li{list-style-type:none}#payment-form li:before{content:'✔ '}.add-new-product{background-position:0 0}.list-item-logo{border-radius:26px;border-style:solid;border-width:0}.analytics-form .apply,.chart-type-wrapper,.price{float:right}.analytics-form .form-control,.h-breadcrumb{margin-right:20px}.list-item-default-logo{background-position:-128px 0}.app-list-item{margin-top:15px}.app-list-item-link{color:#27292d}.app-list-item-link:focus,.app-list-item-link:hover{color:#27292d;text-decoration:none}.home-container .col-sm-5{height:128px}.app-list-item div,.app-list-item h4{cursor:default}.appImg,.glyphicon{cursor:pointer}.app-list-item .remove-app{display:none;right:2px;top:12px}.app-list-item:hover .remove-app{display:block}.h-breadcrumbs{margin-bottom:32px}.v-breadcrumbs{padding-left:0}.h-breadcrumb-selected{border-bottom:solid 2px #23c890}.h-breadcrumb{margin-top:27px;font-size:18px;display:inline-block;color:#27292d;text-decoration:none}.h-breadcrumb:focus,.h-breadcrumb:hover{color:#27292d;text-decoration:none}.invisible .h-breadcrumb{border-bottom:solid 2px #fff}.v-breadcrumb{display:block;height:80px;width:60px;font-size:12px;text-decoration:none;color:#27292d}.v-breadcrumb:focus,.v-breadcrumb:hover{color:#27292d;text-decoration:none}.v-breadcrumb-label{padding-top:50px}.setup-container{margin-bottom:40px}.setup-container .project{background-position:-256px 0;width:60px;height:80px}.setup-container .wifi{background-position:-256px -80px;width:60px;height:80px}.setup-container .design{background-position:-188px -128px;width:60px;height:80px}.setup-container .market{background-position:-128px -128px;width:60px;height:80px}.setup-form{min-height:495px;padding:20px}.setup-form .desc{font-size:11px;margin-bottom:20px;margin-top:20px}.setup-form .form-group:last-of-type{margin-top:50px}.back-link{color:#27292d;text-decoration:underline}.review h4{margin-bottom:30px}.review .review-status-msg-1,.review .review-status-msg-2{margin-top:30px}.publish-container{min-height:495px;padding-top:30px}.publish-container .row{margin-top:20px}.publish-container .button-container{margin-top:0}.publish-container .confirm-check{margin-top:90px}.analytics-form:nth-of-type(2){margin-top:10px}.analytics-form label{padding-right:10px}.analytics-form .btn{margin-top:0}.analytics-form .dash,.analytics-form .deviceId{width:150px!important}.chart-section{position:relative;min-height:400px}.selected-date-range-btn{background-color:#fff!important;border:1px solid #9ea3a6;border-radius:5px;margin-left:10px}.selected-date-range-btn .glyphicon{margin-right:8px}.selected-date-range-btn .caret{margin-left:4px}@media (max-width:768px){.selected-date-range-btn{margin-left:0}.analytics-form .dash{width:100%!important}}.chart-type-wrapper .chart-type{background-color:#fff;margin-left:10px}.btn-container,.form-build,.form-checkout,.msg-container .msg{display:block;margin-left:auto;margin-right:auto}.dropdown-menu{min-width:70px!important}.pin{padding:0;border:none!important}.Select-control{min-width:146px}.checkbox{padding-left:15px;padding-top:7px;height:34px}.configuration-wrapper,.content-wrapper,.form-content,.login-container .login-content,.msg-container .msg{background:#f8f8f8;padding:15px;border-radius:3pt}.form-section,.payment-form,.plan-options-form,.service-container .service-options .service-option,.setup-form,.v-breadcrumb-selected{background:#f8f8f8;border-radius:3pt}.msg-container{margin-top:80pt}.msg-container .msg{max-width:224pt}.btn-container,.form-build,.form-checkout{max-width:260pt}.form-build{max-width:623px}.container-configuration{margin-top:40px;margin-bottom:40px;display:inline-block;width:300px;margin-right:10px}.btn-wrapper{width:50%}.grey,.grey a{color:#ccc}.popover p{font-size:9pt;font-weight:600;margin:10px 0 0}.popover span{font-size:8pt}#userMenu{color:#27292d}#userMenu:focus,#userMenu:hover{color:#27292d;text-decoration:none}.info-link,.not-configured,.underline{text-decoration:underline}.appImg{outline:0;width:80px;height:80px;filter:alpha(opacity=0);opacity:0}.appImgDropZone,.imgPreview{width:60pt;height:60pt;border-radius:5pt}.appImgDropZone{background:#23c890;text-align:center;cursor:pointer;font-size:7pt;color:#FFF;display:inline-block;position:absolute}.appImgDropZone.image-preview{background:#f8f8f8}.selectFile{position:relative;margin-top:10px;margin-bottom:10px}.error-block{width:400px;text-align:center;margin-top:10px}.has-error{color:#d4435c;border-color:#d4435c}.has-error .tooltip-inner{background-color:#d4435c}.has-error .tooltip-arrow{border-right-color:#d4435c!important}.has-error:focus{border-color:#d4435c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #e796a4;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #e796a4}#tooltip{min-width:172px}input.has-error,select.has-error,text-area.has-error{text-align:left!important;color:#000!important}.form-signin .has-error{text-align:center}.appImgError{position:absolute;margin-top:50px;margin-left:15px}.checkout-container{margin-top:40px;margin-bottom:40px}.b-radio{border:1px solid #9ea3a6;background-color:#fff;border-radius:2px;padding:10px;margin-bottom:15px}.b-radio input{margin-top:1px}.b-radio label{margin-top:2px;width:100%}.checked{background-color:rgba(35,200,144,.1)!important;border-color:#23c890!important}.preloader{position:absolute;left:50%;margin-left:-22.5px;top:50%;margin-top:-7px;z-index:4}.button-container a,.setup-button-container a{margin-left:10px}.preloader span{display:block;bottom:0;width:9px;height:5px;background:#9ea3a6;position:absolute;animation:preloader 1.5s infinite ease-in-out}.preloader span:nth-child(2){left:11px;animation-delay:.2s}.preloader span:nth-child(3){left:22px;animation-delay:.4s}.preloader span:nth-child(4){left:33px;animation-delay:.6s}.preloader span:nth-child(5){left:44px;animation-delay:.8s}@keyframes preloader{0%,100%,50%{height:5px;transform:translateY(0);background:#9ea3a6}25%{height:30px;transform:translateY(15px);background:#23c890}}.bold,.payment-trial-details div{font-weight:bolder}.note{font-size:10px}.ready{color:#23c890}.not-configured{color:#d4435c}.button-container{padding-right:0}.setup-button-container{position:absolute;bottom:20px;right:20px}.info-link{color:#27292d}.modal{top:150px}.modal .btn-default{margin-top:0}.navbar-brand{margin-right:12px}.active-menu-item,.analytics .analytics-menu-item,.home .home-menu-item,.navbar-nav .active a{background-color:#23c890!important;color:#27292d!important}"
  },
  {
    "path": "server/http-admin/src/main/resources/static/js/admin.js",
    "content": "var app = angular.module('app', ['ng-admin']);\napp.config(['NgAdminConfigurationProvider', function (nga) {\n    // create an admin application\n    var admin = nga.application('Blynk Administration', false)\n        .baseApiUrl(location.protocol + '//' + window.location.hostname + (location.port == 80 ? '' : (':' + location.port)) + location.pathname + '/'); // main API endpoint\n    // create a user entity\n    // the API endpoint for this entity will be 'http://jsonplaceholder.typicode.com/users/:id\n    var users = nga.entity('users').identifier(nga.field('id'));\n    // set the fields of the user entity list view\n    users.listView()\n        .sortField('lastModifiedTs')\n        .fields([\n            nga.field('email').isDetailLink(true),\n            nga.field('appName'),\n            nga.field('# of projects').map(function (value, entry) {\n                if (entry[\"profile.dashBoards\"]) {\n                    return entry[\"profile.dashBoards\"].length;\n                } else {\n                    return 0;\n                }\n            }),\n            nga.field('lastModifiedTs', 'datetime')\n        ])\n        .filters([\n            nga.field('name').label('').pinned(true)\n                .template('<div class=\"input-group\"><input type=\"text\" ng-model=\"value\" placeholder=\"Search\" class=\"form-control\"></input><span class=\"input-group-addon\"><i class=\"glyphicon glyphicon-search\"></i></span></div>')\n        ]);\n\n    users.editionView()\n        .title('Edit user \"{{entry.values.email}}\"')\n        .fields(\n            nga.field('email'),\n            nga.field('name'),\n            nga.field('pass', 'password'),\n            nga.field('lastModifiedTs'),\n            nga.field('energy'),\n            nga.field('appName').editable(false),\n            nga.field('region').editable(false),\n            nga.field('lastLoggedIP').editable(false),\n            nga.field('profile.dashBoards', 'embedded_list')\n                .targetFields([\n                    nga.field('id'),\n                    nga.field('name'),\n                    nga.field('createdAt'),\n                    nga.field('theme', 'choice')\n                        .choices([\n                            {value: 'Blynk', label: 'Blynk'},\n                            {value: 'SparkFun', label: 'SparkFun'}\n                        ]),\n                    nga.field('metadata', 'json'),\n                    nga.field('widgets', 'embedded_list')\n                        .targetFields([\n                            nga.field('id', 'number'),\n                            nga.field('label', 'string'),\n                            nga.field('x').editable(false),\n                            nga.field('y').editable(false),\n                            nga.field('type').editable(false),\n                            nga.field('width').editable(false),\n                            nga.field('height').editable(false),\n                            nga.field('value')\n                        ]),\n                    nga.field('devices', 'embedded_list')\n                        .targetFields([\n                            nga.field('id', 'number'),\n                            nga.field('name', 'string'),\n                            nga.field('boardType', 'choice')\n                              .choices([\n                                {value: 'Arduino 101', label: 'Arduino 101'},\n                                {value: 'Arduino Due', label: 'Arduino Due'},\n                                {value: 'Arduino Leonardo', label: 'Arduino Leonardo'},\n                                {value: 'Arduino Mega', label: 'Arduino Mega'},\n                                {value: 'Arduino Micro', label: 'Arduino Micro'},\n                                {value: 'Arduino Mini', label: 'Arduino Mini'},\n                                {value: 'Arduino MKR1000', label: 'Arduino MKR1000'},\n                                {value: 'Arduino Nano', label: 'Arduino Nano'},\n                                {value: 'Arduino Pro Micro', label: 'Arduino Pro Micro'},\n                                {value: 'Arduino Pro Mini', label: 'Arduino Pro Mini'},\n                                {value: 'Arduino UNO', label: 'Arduino UNO'},\n                                {value: 'Arduino Yun', label: 'Arduino Yun'},\n                                {value: 'Arduino Zero', label: 'Arduino Zero'},\n                                {value: 'ESP8266', label: 'ESP8266'},\n                                {value: 'Generic Board', label: 'Generic Board'},\n                                {value: 'Intel Edison', label: 'Intel Edison'},\n                                {value: 'Intel Galileo', label: 'Intel Galileo'},\n                                {value: 'LinkIt ONE', label: 'LinkIt ONE'},\n                                {value: 'Microduino Core+', label: 'Microduino Core+'},\n                                {value: 'Microduino Core', label: 'Microduino Core'},\n                                {value: 'Microduino CoreRF', label: 'Microduino CoreRF'},\n                                {value: 'Microduino CoreUSB', label: 'Microduino CoreUSB'},\n                                {value: 'NodeMCU', label: 'NodeMCU'},\n                                {value: 'Particle Core', label: 'Particle Core'},\n                                {value: 'Particle Electron', label: 'Particle Electron'},\n                                {value: 'Particle Photon', label: 'Particle Photon'},\n                                {value: 'Raspberry Pi 3 B', label: 'Raspberry Pi 3 B'},\n                                {value: 'Raspberry Pi 2/A+/B+', label: 'Raspberry Pi 2/A+/B+'},\n                                {value: 'Raspberry Pi B (Rev1)', label: 'Raspberry Pi B (Rev1)'},\n                                {value: 'Raspberry Pi A/B (Rev2)', label: 'Raspberry Pi A/B (Rev2)'},\n                                {value: 'RedBearLab CC3200/Mini', label: 'RedBearLab CC3200/Mini'},\n                                {value: 'Seeed Wio Link', label: 'Seeed Wio Link'},\n                                {value: 'SparkFun Blynk Board', label: 'SparkFun Blynk Board'},\n                                {value: 'SparkFun ESP8266 Thing', label: 'SparkFun ESP8266 Thing'},\n                                {value: 'SparkFun Photon RedBoard', label: 'SparkFun Photon RedBoard'},\n                                {value: 'TI CC3200-LaunchXL', label: 'TI CC3200-LaunchXL'},\n                                {value: 'TI Tiva C Connected', label: 'TI Tiva C Connected'},\n                                {value: 'TinyDuino', label: 'TinyDuino'},\n                                {value: 'WeMos D1', label: 'WeMos D1'},\n                                {value: 'WeMos D1 mini', label: 'WeMos D1 mini'},\n                                {value: 'Wildfire v2', label: 'Wildfire v2'},\n                                {value: 'Wildfire v3', label: 'Wildfire v3'},\n                                {value: 'Wildfire v4', label: 'Wildfire v4'},\n                                {value: 'WiPy', label: 'WiPy'}\n                            ]),\n                            nga.field('token').editable(false),\n                            nga.field('lastLoggedIP').editable(false),\n                            nga.field('connectionType', 'choice')\n                                .choices([\n                                    {value: 'ETHERNET', label: 'ETHERNET'},\n                                    {value: 'WI_FI', label: 'WI_FI'},\n                                    {value: 'USB', label: 'USB'},\n                                    {value: 'BLUETOOTH', label: 'BLUETOOTH'},\n                                    {value: 'BLE', label: 'BLE'},\n                                    {value: 'GSM', label: 'GSM'}\n                                    ]\n                            )\n                        ]),\n                    nga.field('tags', 'embedded_list')\n                        .targetFields([\n                            nga.field('id', 'number'),\n                            nga.field('name', 'string')\n                        ]),\n                    nga.field('keepScreenOn', 'boolean'),\n                    nga.field('isShared', 'boolean'),\n                    nga.field('isActive', 'boolean'),\n                    nga.field('isAppConnectedOn', 'boolean')\n                ]),\n            nga.field('dashShareTokens', 'json')\n        );\n\n\n    var libraryVersion = nga.entity('libraryVersion').identifier(nga.field('name')).url('hardwareInfo/blynkVersion').readOnly();\n    libraryVersion.listView()\n        .title('Blynk library versions')\n        .perPage(50)\n        .batchActions([])\n        .sortField('count')\n        .fields([\n            nga.field('name').label('Blynk library version'),\n            nga.field('count').label('Count')\n        ]);\n\n    var cpuType = nga.entity('cpuType').identifier(nga.field('name')).url('hardwareInfo/cpuType').readOnly();\n    cpuType.listView()\n        .title('CPU types')\n        .perPage(50)\n        .batchActions([])\n        .sortField('count')\n        .fields([\n            nga.field('name').label('CPU type'),\n            nga.field('count').label('Count')\n        ]);\n\n    var connectionType = nga.entity('connectionType').identifier(nga.field('name')).url('hardwareInfo/connectionType').readOnly();\n    connectionType.listView()\n        .title('Connection types')\n        .perPage(50)\n        .batchActions([])\n        .sortField('count')\n        .fields([\n            nga.field('name').label('Connection type'),\n            nga.field('count').label('Count')\n        ]);\n\n    var hardwareBoards = nga.entity('hardwareBoards').identifier(nga.field('name')).url('hardwareInfo/boards').readOnly();\n    hardwareBoards.listView()\n        .title('Board types')\n        .perPage(50)\n        .batchActions([])\n        .sortField('count')\n        .fields([\n            nga.field('name').label('Hardware board type'),\n            nga.field('count').label('Count')\n        ]);\n\n\n    var realtime = nga.entity('realtime').url('stats/realtime').readOnly();\n    realtime.listView()\n        .title('Realtime stats')\n        .batchActions([])\n        .sortField('count')\n        .fields([\n            nga.field('oneMinRate').label('1 min request rate'),\n            nga.field('total').label('Total registrations'),\n            nga.field('active').label('Logged in 24h'),\n            nga.field('active3').label('Logged in 72h'),\n            nga.field('connected').label('Hard and App connected'),\n            nga.field('onlineApps').label('App connections'),\n            nga.field('onlineHards').label('Hardware connections')\n            ]);\n\n    var requestsPerUser = nga.entity('requestsPerUser').identifier(nga.field('name')).url('stats/requestsPerUser').readOnly();\n    requestsPerUser.listView()\n        .title('Requests per user')\n        .perPage(50)\n        .batchActions([])\n        .sortField('hardRate')\n        .fields([\n            nga.field('name').label('User'),\n            nga.field('hardRate').label('Hardware requests per second'),\n            nga.field('appRate').label('Application requests per second')\n        ]);\n\n    var messages = nga.entity('messages').identifier(nga.field('name')).url('stats/messages').readOnly();\n    messages.listView()\n        .title('Messages')\n        .perPage(50)\n        .batchActions([])\n        .sortField('count')\n        .fields([\n            nga.field('name').label('Message'),\n            nga.field('count').label('Count')\n        ]);\n\n    var boards = nga.entity('boards').identifier(nga.field('name')).url('stats/boards').readOnly();\n    boards.listView()\n        .title('Board Types')\n        .perPage(50)\n        .batchActions([])\n        .sortField('count')\n        .fields([\n            nga.field('name').label('Board Name'),\n            nga.field('count').label('Count')\n        ]);\n\n    var facebookLogins = nga.entity('facebookLogins').identifier(nga.field('name')).url('stats/facebookLogins').readOnly();\n    facebookLogins.listView()\n        .title('Login Types')\n        .perPage(50)\n        .batchActions([])\n        .sortField('count')\n        .fields([\n            nga.field('name').label('Login Type'),\n            nga.field('count').label('Count')\n        ]);\n\n\n    var widgets = nga.entity('widgets').identifier(nga.field('name')).url('stats/widgets').readOnly();\n    widgets.listView()\n        .title('Widgets')\n        .perPage(50)\n        .batchActions([])\n        .sortField('count')\n        .fields([\n            nga.field('name').label('Widget'),\n            nga.field('count').label('Count')\n        ]);\n\n    var projectsPerUser = nga.entity('projectPerUser').identifier(nga.field('name')).url('stats/projectsPerUser').readOnly();\n    projectsPerUser.listView()\n        .title('Project per user')\n        .perPage(50)\n        .batchActions([])\n        .sortField('count')\n        .fields([\n            nga.field('name').label('# of project per user'),\n            nga.field('count').label('Count')\n        ]);\n\n\n    var filledSpace = nga.entity('filledSpace').identifier(nga.field('name')).url('stats/filledSpace').readOnly();\n    filledSpace.listView()\n        .title('Filled space')\n        .perPage(100)\n        .batchActions([])\n        .sortField('count')\n        .fields([\n            nga.field('name').label('# of cells per project'),\n            nga.field('count').label('Count')\n        ]);\n\n    var userProfileSize = nga.entity('userProfileSize').identifier(nga.field('name')).url('stats/userProfileSize').readOnly();\n    userProfileSize.listView()\n        .title('Size of user profile')\n        .perPage(50)\n        .batchActions([])\n        .sortField('count')\n        .fields([\n            nga.field('name').label('User'),\n            nga.field('count').label('Size in bytes')\n        ]);\n\n    var webHookHosts = nga.entity('webHookHosts').identifier(nga.field('name')).url('stats/webHookHosts').readOnly();\n    webHookHosts.listView()\n        .title('Webhook hosts')\n        .perPage(50)\n        .batchActions([])\n        .sortField('count')\n        .fields([\n            nga.field('name').label('Host'),\n            nga.field('count').label('Number')\n        ]);\n\n    var ips = nga.entity('ips').identifier(nga.field('id')).url('stats/ips').readOnly();\n    ips.listView()\n        .title('IP List')\n        .perPage(50)\n        .batchActions([])\n        .sortField('id')\n        .fields([\n            nga.field('id'),\n            nga.field('ip'),\n            nga.field('name'),\n            nga.field('type')\n        ])\n        .filters([\n            nga.field('ip').label('').pinned(true)\n                .template('<div class=\"input-group\"><input type=\"text\" ng-model=\"value\" placeholder=\"Search\" class=\"form-control\"></input><span class=\"input-group-addon\"><i class=\"glyphicon glyphicon-search\"></i></span></div>')\n        ]);\n\n    var config = nga.entity('config').identifier(nga.field('name'));\n    config.listView()\n        .title('Configurations')\n        .sortField('name')\n        .batchActions([])\n        .fields([\n            nga.field('name').isDetailLink(true)\n        ]);\n\n\n    config.editionView()\n        .title('Edit configuration \"{{entry.values.name}}\"')\n        .fields(\n            nga.field('name').editable(false),\n            nga.field('body', 'text')\n        );\n\n        // customize menu\n    admin.menu(nga.menu().autoClose(false)\n            .addChild(nga.menu(users).icon('<span class=\"glyphicon glyphicon-user\"></span>'))\n            .addChild(nga.menu().title('Stats')\n                .addChild(nga.menu(realtime).title('Realtime').icon(''))\n                .addChild(nga.menu(requestsPerUser).title('Request per user').icon(''))\n                .addChild(nga.menu(messages).title('Messages').icon(''))\n                .addChild(nga.menu(boards).title('Board types').icon(''))\n                .addChild(nga.menu(facebookLogins).title('Login types').icon(''))\n                .addChild(nga.menu(widgets).title('Widgets').icon(''))\n                .addChild(nga.menu(projectsPerUser).title('Projects per user').icon(''))\n                .addChild(nga.menu(filledSpace).title('Cells per project').icon(''))\n                .addChild(nga.menu(userProfileSize).title('Size of user profile').icon(''))\n                .addChild(nga.menu(webHookHosts).title('Webhook hosts').icon(''))\n                .addChild(nga.menu(ips).title('IPs').icon(''))\n            )\n            .addChild(nga.menu().title('Hardware Info')\n                .addChild(nga.menu(libraryVersion).title('Blynk library versions').icon(''))\n                .addChild(nga.menu(cpuType).title('CPU types').icon(''))\n                .addChild(nga.menu(hardwareBoards).title('Hardware boards').icon(''))\n                .addChild(nga.menu(connectionType).title('Connection types').icon(''))\n            )\n            .addChild(nga.menu(config))\n    );\n\n\n    // add the user entity to the admin application\n    admin.addEntity(users);\n    admin.addEntity(requestsPerUser);\n    admin.addEntity(realtime);\n    admin.addEntity(messages);\n    admin.addEntity(boards);\n    admin.addEntity(facebookLogins);\n    admin.addEntity(widgets);\n    admin.addEntity(projectsPerUser);\n    admin.addEntity(filledSpace);\n    admin.addEntity(userProfileSize);\n    admin.addEntity(libraryVersion);\n    admin.addEntity(cpuType);\n    admin.addEntity(hardwareBoards);\n    admin.addEntity(connectionType);\n    admin.addEntity(webHookHosts);\n    admin.addEntity(ips);\n    admin.addEntity(config);\n\n    admin.dashboard(nga.dashboard().addCollection(\n            nga.collection(realtime)\n                .title('Realtime stats')\n                .batchActions([])\n                .sortField('count')\n                .fields([\n                    nga.field('oneMinRate').label('1 min request rate'),\n                    nga.field('total').label('Total registrations'),\n                    nga.field('active').label('Logged in 24h'),\n                    nga.field('active3').label('Logged in 72h'),\n                    nga.field('connected').label('Hard and App connected'),\n                    nga.field('onlineApps').label('App connections'),\n                    nga.field('onlineHards').label('Hardware connections')\n                ])\n\n    )\n    );\n\n    // attach the admin application to the DOM and execute it\n    nga.configure(admin);\n}]);"
  },
  {
    "path": "server/http-admin/src/main/resources/static/js/core-min.js",
    "content": "/*\nCryptoJS v3.1.2\ncode.google.com/p/crypto-js\n(c) 2009-2013 by Jeff Mott. All rights reserved.\ncode.google.com/p/crypto-js/wiki/License\n*/\nvar CryptoJS=CryptoJS||function(h,r){var k={},l=k.lib={},n=function(){},f=l.Base={extend:function(a){n.prototype=this;var b=new n;a&&b.mixIn(a);b.hasOwnProperty(\"init\")||(b.init=function(){b.$super.init.apply(this,arguments)});b.init.prototype=b;b.$super=this;return b},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var b in a)a.hasOwnProperty(b)&&(this[b]=a[b]);a.hasOwnProperty(\"toString\")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}},\nj=l.WordArray=f.extend({init:function(a,b){a=this.words=a||[];this.sigBytes=b!=r?b:4*a.length},toString:function(a){return(a||s).stringify(this)},concat:function(a){var b=this.words,d=a.words,c=this.sigBytes;a=a.sigBytes;this.clamp();if(c%4)for(var e=0;e<a;e++)b[c+e>>>2]|=(d[e>>>2]>>>24-8*(e%4)&255)<<24-8*((c+e)%4);else if(65535<d.length)for(e=0;e<a;e+=4)b[c+e>>>2]=d[e>>>2];else b.push.apply(b,d);this.sigBytes+=a;return this},clamp:function(){var a=this.words,b=this.sigBytes;a[b>>>2]&=4294967295<<\n32-8*(b%4);a.length=h.ceil(b/4)},clone:function(){var a=f.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var b=[],d=0;d<a;d+=4)b.push(4294967296*h.random()|0);return new j.init(b,a)}}),m=k.enc={},s=m.Hex={stringify:function(a){var b=a.words;a=a.sigBytes;for(var d=[],c=0;c<a;c++){var e=b[c>>>2]>>>24-8*(c%4)&255;d.push((e>>>4).toString(16));d.push((e&15).toString(16))}return d.join(\"\")},parse:function(a){for(var b=a.length,d=[],c=0;c<b;c+=2)d[c>>>3]|=parseInt(a.substr(c,\n2),16)<<24-4*(c%8);return new j.init(d,b/2)}},p=m.Latin1={stringify:function(a){var b=a.words;a=a.sigBytes;for(var d=[],c=0;c<a;c++)d.push(String.fromCharCode(b[c>>>2]>>>24-8*(c%4)&255));return d.join(\"\")},parse:function(a){for(var b=a.length,d=[],c=0;c<b;c++)d[c>>>2]|=(a.charCodeAt(c)&255)<<24-8*(c%4);return new j.init(d,b)}},t=m.Utf8={stringify:function(a){try{return decodeURIComponent(escape(p.stringify(a)))}catch(b){throw Error(\"Malformed UTF-8 data\");}},parse:function(a){return p.parse(unescape(encodeURIComponent(a)))}},\nq=l.BufferedBlockAlgorithm=f.extend({reset:function(){this._data=new j.init;this._nDataBytes=0},_append:function(a){\"string\"==typeof a&&(a=t.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var b=this._data,d=b.words,c=b.sigBytes,e=this.blockSize,f=c/(4*e),f=a?h.ceil(f):h.max((f|0)-this._minBufferSize,0);a=f*e;c=h.min(4*a,c);if(a){for(var g=0;g<a;g+=e)this._doProcessBlock(d,g);g=d.splice(0,a);b.sigBytes-=c}return new j.init(g,c)},clone:function(){var a=f.clone.call(this);\na._data=this._data.clone();return a},_minBufferSize:0});l.Hasher=q.extend({cfg:f.extend(),init:function(a){this.cfg=this.cfg.extend(a);this.reset()},reset:function(){q.reset.call(this);this._doReset()},update:function(a){this._append(a);this._process();return this},finalize:function(a){a&&this._append(a);return this._doFinalize()},blockSize:16,_createHelper:function(a){return function(b,d){return(new a.init(d)).finalize(b)}},_createHmacHelper:function(a){return function(b,d){return(new u.HMAC.init(a,\nd)).finalize(b)}}});var u=k.algo={};return k}(Math);\n"
  },
  {
    "path": "server/http-admin/src/main/resources/static/js/enc-base64-min.js",
    "content": "/*\nCryptoJS v3.1.2\ncode.google.com/p/crypto-js\n(c) 2009-2013 by Jeff Mott. All rights reserved.\ncode.google.com/p/crypto-js/wiki/License\n*/\n(function(){var h=CryptoJS,j=h.lib.WordArray;h.enc.Base64={stringify:function(b){var e=b.words,f=b.sigBytes,c=this._map;b.clamp();b=[];for(var a=0;a<f;a+=3)for(var d=(e[a>>>2]>>>24-8*(a%4)&255)<<16|(e[a+1>>>2]>>>24-8*((a+1)%4)&255)<<8|e[a+2>>>2]>>>24-8*((a+2)%4)&255,g=0;4>g&&a+0.75*g<f;g++)b.push(c.charAt(d>>>6*(3-g)&63));if(e=c.charAt(64))for(;b.length%4;)b.push(e);return b.join(\"\")},parse:function(b){var e=b.length,f=this._map,c=f.charAt(64);c&&(c=b.indexOf(c),-1!=c&&(e=c));for(var c=[],a=0,d=0;d<\ne;d++)if(d%4){var g=f.indexOf(b.charAt(d-1))<<2*(d%4),h=f.indexOf(b.charAt(d))>>>6-2*(d%4);c[a>>>2]|=(g|h)<<24-8*(a%4);a++}return j.create(c,a)},_map:\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\"}})();\n"
  },
  {
    "path": "server/http-admin/src/main/resources/static/js/login.js",
    "content": "function isFormValid(){var a=!0;return $(\".form-control\").each(function(r,o){$(o).removeClass(\"has-error\"),$(o).val()||($(o).addClass(\"has-error\"),a=!1)}),a}window.history&&window.history.replaceState&&window.history.replaceState({},\"Blynk\",location.pathname),jQuery(function(a){a(\".form-signin\").on(\"submit\",function(r){if(!isFormValid())return!1;var o=a(\"#inputEmail\").val(),t=a(\"#inputPassword\").val(),i=CryptoJS.algo.SHA256.create();i.update(t,\"utf-8\"),i.update(CryptoJS.SHA256(o.toLowerCase()),\"utf-8\");var n=i.finalize(),e=n.toString(CryptoJS.enc.Base64);a(\"#finalPass\").val(e)})});"
  },
  {
    "path": "server/http-admin/src/main/resources/static/js/sha256-min.js",
    "content": "/*\nCryptoJS v3.1.2\ncode.google.com/p/crypto-js\n(c) 2009-2013 by Jeff Mott. All rights reserved.\ncode.google.com/p/crypto-js/wiki/License\n*/\n(function(k){for(var g=CryptoJS,h=g.lib,v=h.WordArray,j=h.Hasher,h=g.algo,s=[],t=[],u=function(q){return 4294967296*(q-(q|0))|0},l=2,b=0;64>b;){var d;a:{d=l;for(var w=k.sqrt(d),r=2;r<=w;r++)if(!(d%r)){d=!1;break a}d=!0}d&&(8>b&&(s[b]=u(k.pow(l,0.5))),t[b]=u(k.pow(l,1/3)),b++);l++}var n=[],h=h.SHA256=j.extend({_doReset:function(){this._hash=new v.init(s.slice(0))},_doProcessBlock:function(q,h){for(var a=this._hash.words,c=a[0],d=a[1],b=a[2],k=a[3],f=a[4],g=a[5],j=a[6],l=a[7],e=0;64>e;e++){if(16>e)n[e]=\nq[h+e]|0;else{var m=n[e-15],p=n[e-2];n[e]=((m<<25|m>>>7)^(m<<14|m>>>18)^m>>>3)+n[e-7]+((p<<15|p>>>17)^(p<<13|p>>>19)^p>>>10)+n[e-16]}m=l+((f<<26|f>>>6)^(f<<21|f>>>11)^(f<<7|f>>>25))+(f&g^~f&j)+t[e]+n[e];p=((c<<30|c>>>2)^(c<<19|c>>>13)^(c<<10|c>>>22))+(c&d^c&b^d&b);l=j;j=g;g=f;f=k+m|0;k=b;b=d;d=c;c=m+p|0}a[0]=a[0]+c|0;a[1]=a[1]+d|0;a[2]=a[2]+b|0;a[3]=a[3]+k|0;a[4]=a[4]+f|0;a[5]=a[5]+g|0;a[6]=a[6]+j|0;a[7]=a[7]+l|0},_doFinalize:function(){var d=this._data,b=d.words,a=8*this._nDataBytes,c=8*d.sigBytes;\nb[c>>>5]|=128<<24-c%32;b[(c+64>>>9<<4)+14]=k.floor(a/4294967296);b[(c+64>>>9<<4)+15]=a;d.sigBytes=4*b.length;this._process();return this._hash},clone:function(){var b=j.clone.call(this);b._hash=this._hash.clone();return b}});g.SHA256=j._createHelper(h);g.HmacSHA256=j._createHmacHelper(h)})(Math);\n"
  },
  {
    "path": "server/http-admin/src/main/resources/static/login.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en-US\" class=\"gr__publish_blynk_cc\"><head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n\n    <title>Blynk</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <link rel=\"stylesheet\" href=\"/static/css/blynk.css\">\n</head>\n<body class=\"login\" data-gr-c-s-loaded=\"true\">\n<script src=\"/static/js/jquery-2.2.2.min.js\" integrity=\"sha256-36cp2Co+/62rEAAYHLmRCPIych47CvdM+uTBJwSzWjI=\" crossorigin=\"anonymous\"></script>\n<script src=\"/static/js/bootstrap.min.js\" integrity=\"sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS\" crossorigin=\"anonymous\"></script>\n<script src=\"/static/js/core-min.js\"></script>\n<script src=\"/static/js/sha256-min.js\"></script>\n<script src=\"/static/js/enc-base64-min.js\"></script>\n\n<nav class=\"navbar navbar-default\">\n    <div class=\"container-fluid\">\n        <div class=\"navbar-header\">\n            <a class=\"navbar-brand\" href=\"https://www.blynk.cc\">\n                Blynk Administration\n            </a>\n        </div>\n        <div class=\"navbar-text navbar-right\">\n            <div class=\"dropdown\">\n                <ul class=\"dropdown-menu\" aria-labelledby=\"dLabel\">\n                    <li><a href=\"/logout\">Log Out</a></li>\n                </ul>\n            </div>\n        </div>\n    </div>\n\n</nav>\n\n\n\n<div class=\"container login-container\">\n    <div class=\"login-content\">\n        <form class=\"form-signin\" method=\"post\" onsubmit=\"get_action(this);\">\n            <h4 class=\"form-signin-heading\">Log In</h4>\n            <label>\n                Use your Admin account to log in\n            </label>\n            <div class=\"form-group\">\n                <input type=\"email\" name=\"email\" id=\"inputEmail\" class=\"form-control\" placeholder=\"Email address\" required=\"\" autofocus=\"\">\n            </div>\n            <div class=\"form-group\">\n                <input type=\"password\" id=\"inputPassword\" class=\"form-control\" placeholder=\"Password\" required=\"\">\n                <input type=\"hidden\" name=\"password\" id=\"finalPass\">\n            </div>\n            <button class=\"btn btn-primary btn-block\" type=\"submit\">Sign in</button>\n        </form>\n\n    </div>\n\n</div>\n\n<script>\n    function get_action(form) {\n        form.action = window.location.pathname + \"/login\";\n    }\n</script>\n\n<script src=\"/static/js/login.js\"></script>\n\n</body></html>"
  },
  {
    "path": "server/http-admin/src/test/java/cc/blynk/server/admin/http/handlers/IpFilterHandlerTest.java",
    "content": "package cc.blynk.server.admin.http.handlers;\n\nimport org.junit.Test;\n\nimport java.net.InetSocketAddress;\n\nimport static org.junit.Assert.*;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 26.01.16.\n */\npublic class IpFilterHandlerTest {\n\n    private static InetSocketAddress newSockAddress(String ipAddress) {\n        return new InetSocketAddress(ipAddress, 1234);\n    }\n\n    @Test\n    public void testSingleIPFilterWork() throws Exception {\n        String[] data = new String[1];\n        data[0] = \"192.168.0.50\";\n        IpFilterHandler ipFilterHandler = new IpFilterHandler(data);\n\n        assertTrue(ipFilterHandler.accept(null, newSockAddress(\"192.168.0.50\")));\n        assertFalse(ipFilterHandler.accept(null, newSockAddress(\"192.168.0.51\")));\n        assertFalse(ipFilterHandler.accept(null, newSockAddress(\"192.168.1.50\")));\n    }\n\n    @Test\n    public void testCIDRNotationIPFilterWork() throws Exception {\n        String[] data = new String[2];\n        data[0] = \"192.168.100.100\";\n        data[1] = \"192.168.0.50/24\";\n        IpFilterHandler ipFilterHandler = new IpFilterHandler(data);\n\n        for (int i = 0; i <= 255; i++) {\n            assertTrue(ipFilterHandler.accept(null, (newSockAddress(String.format(\"192.168.0.%d\", i)))));\n        }\n        assertTrue(ipFilterHandler.accept(null, (newSockAddress(\"192.168.100.100\"))));\n\n        assertFalse(ipFilterHandler.accept(null, (newSockAddress(\"192.168.1.0\"))));\n        assertFalse(ipFilterHandler.accept(null, (newSockAddress(\"192.168.100.0\"))));\n        assertFalse(ipFilterHandler.accept(null, (newSockAddress(\"192.168.100.101\"))));\n    }\n\n}\n"
  },
  {
    "path": "server/http-admin/src/test/java/cc/blynk/server/admin/http/logic/admin/UsersLogicTest.java",
    "content": "package cc.blynk.server.admin.http.logic.admin;\n\nimport cc.blynk.core.http.Response;\nimport cc.blynk.server.admin.http.logic.UsersLogic;\nimport cc.blynk.server.core.dao.FileManager;\nimport cc.blynk.server.core.dao.ReportingDiskDao;\nimport cc.blynk.server.core.dao.SessionDao;\nimport cc.blynk.server.core.dao.UserDao;\nimport cc.blynk.server.core.dao.UserKey;\nimport cc.blynk.server.core.model.auth.Session;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.db.DBManager;\nimport cc.blynk.utils.AppNameUtil;\nimport io.netty.channel.EventLoop;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.Mock;\nimport org.mockito.Spy;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\n\nimport static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;\nimport static io.netty.handler.codec.http.HttpResponseStatus.OK;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertFalse;\nimport static org.junit.Assert.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\n@RunWith(MockitoJUnitRunner.class)\npublic class UsersLogicTest {\n\n    @Mock\n    private UserDao userDao;\n    @Spy\n    private SessionDao sessionDao;\n    @Mock\n    private DBManager dbManager;\n    @Mock\n    private ReportingDiskDao reportingDao;\n\n    private User user;\n\n    @Mock\n    private Session session;\n\n    private UsersLogic usersLogic;\n    private static final String TEST_USER = \"test_user\";\n    private static Path userFile;\n    private static Path deletedUserFile;\n    private static final String DELETED_DATA_DIR_NAME = \"deleted\";\n\n    @Before\n    public void setUp() throws Exception {\n        user = new User(TEST_USER, \"123\", AppNameUtil.BLYNK, \"local\", \"127.0.0.1\", false, false);\n        when(userDao.delete(any())).thenReturn(user);\n        sessionDao.getOrCreateSessionByUser(new UserKey(user), mock(EventLoop.class));\n        FileManager fileManager = new FileManager(null, null);\n        usersLogic = new UsersLogic(userDao, sessionDao, dbManager, fileManager, null, reportingDao, \"admin\");\n\n        userFile = Paths.get(System.getProperty(\"java.io.tmpdir\"), \"blynk\", TEST_USER + \".Blynk.user\");\n        deletedUserFile = Paths.get(System.getProperty(\"java.io.tmpdir\"), \"blynk\", DELETED_DATA_DIR_NAME, TEST_USER + \".Blynk.user\");\n        Files.deleteIfExists(userFile);\n        Files.deleteIfExists(deletedUserFile);\n\n        Files.createFile(userFile);\n    }\n\n    @Test\n    public void deleteUserByName() throws Exception {\n        Response response = usersLogic.deleteUserByName(TEST_USER + \"-\" + AppNameUtil.BLYNK);\n\n        assertEquals(OK, response.status());\n        assertFalse(Files.exists(userFile));\n        assertTrue(Files.exists(deletedUserFile));\n    }\n\n    @Test\n    public void deleteFakeUserByName() throws Exception {\n        Response response = usersLogic.deleteUserByName(\"fake user\" + \"-\" + AppNameUtil.BLYNK);\n\n        assertEquals(NOT_FOUND, response.status());\n    }\n\n}"
  },
  {
    "path": "server/http-admin/src/test/java/cc/blynk/server/reset/SHA256UtilTest.java",
    "content": "package cc.blynk.server.reset;\n\nimport cc.blynk.utils.SHA256Util;\nimport org.junit.Test;\n\nimport static org.junit.Assert.*;\n\npublic class SHA256UtilTest {\n\n    @Test\n    public void testPasswordHash() {\n        final String hashedPassword = SHA256Util.makeHash(\"123\", \"test@gmail.com\");\n        assertEquals(\"/pyQf3JCj5XoczfsYJ4LUb+y0DONGMl/AFzLiBTo8LA=\", hashedPassword);\n    }\n}\n"
  },
  {
    "path": "server/http-api/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <artifactId>server</artifactId>\n        <groupId>cc.blynk.server</groupId>\n        <version>0.41.17-SNAPSHOT</version>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n\n    <groupId>cc.blynk.server.api</groupId>\n    <artifactId>http-api</artifactId>\n\n    <dependencies>\n        <dependency>\n            <groupId>cc.blynk.server</groupId>\n            <artifactId>core</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n\n        <dependency>\n            <groupId>cc.blynk.server.api.core</groupId>\n            <artifactId>http-core</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n\n        <dependency>\n            <groupId>cc.blynk.server.admin</groupId>\n            <artifactId>http-admin</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n\n        <dependency>\n            <groupId>cc.blynk.server.hardware</groupId>\n            <artifactId>tcp-hardware-server</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n\n        <dependency>\n            <groupId>cc.blynk.server.application</groupId>\n            <artifactId>tcp-app-server</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n\n        <dependency>\n            <groupId>com.github.kenglxn.qrgen</groupId>\n            <artifactId>javase</artifactId>\n            <version>${qrgen.version}</version>\n        </dependency>\n\n    </dependencies>\n\n</project>"
  },
  {
    "path": "server/http-api/src/main/java/cc/blynk/server/api/http/handlers/BaseHttpAndBlynkUnificationHandler.java",
    "content": "package cc.blynk.server.api.http.handlers;\n\nimport io.netty.buffer.ByteBuf;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.ChannelPipeline;\nimport io.netty.handler.codec.ByteToMessageDecoder;\n\nimport java.util.List;\n\nimport static cc.blynk.server.core.protocol.handlers.DefaultExceptionHandler.handleUnexpectedException;\n\n/**\n * Base handler that detects protocol between http and blynk app protocol.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 15.02.18.\n */\npublic abstract class BaseHttpAndBlynkUnificationHandler extends ByteToMessageDecoder {\n\n    @Override\n    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {\n        // Will use the first 5 bytes to detect a protocol.\n        if (in.readableBytes() < 5) {\n            return;\n        }\n\n        //we can't simply read 5 bytes as 1 number, so split on 4 bytes and 1 byte read\n        long header4Bytes = in.getUnsignedInt(0);\n        short lastByteOfHeader = in.getUnsignedByte(4);\n\n        ChannelPipeline pipeline = ctx.pipeline();\n        buildPipeline(pipeline, header4Bytes, lastByteOfHeader).remove(this);\n    }\n\n    private ChannelPipeline buildPipeline(ChannelPipeline pipeline, long header4Bytes, short lastByteOfHeader) {\n        if (isHttp(header4Bytes)) {\n            return buildHttpPipeline(pipeline);\n        }\n        if (isHardwarePipeline(header4Bytes, lastByteOfHeader)) {\n            return buildHardwarePipeline(pipeline);\n        }\n        return buildAppPipeline(pipeline);\n    }\n\n    private static boolean isHardwarePipeline(long header4Bytes, short lastByteOfHeader) {\n        return lastByteOfHeader == 32 && (header4Bytes == 486539520L || header4Bytes == 33554688L);\n    }\n\n    /**\n     * See HttpSignatureTest for more details\n     */\n    private static boolean isHttp(long httpHeader4Bytes) {\n        return\n                httpHeader4Bytes == 1195725856L || // 'GET '\n                httpHeader4Bytes == 1347375956L || // 'POST'\n                httpHeader4Bytes == 1347769376L || // 'PUT '\n                httpHeader4Bytes == 1212498244L || // 'HEAD'\n                httpHeader4Bytes == 1330664521L || // 'OPTI'\n                httpHeader4Bytes == 1346458691L || // 'PATC'\n                httpHeader4Bytes == 1145392197L || // 'DELE'\n                httpHeader4Bytes == 1414676803L || // 'TRAC'\n                httpHeader4Bytes == 1129270862L;   // 'CONN'\n    }\n\n    public abstract ChannelPipeline buildHttpPipeline(ChannelPipeline pipeline);\n\n    public abstract ChannelPipeline buildAppPipeline(ChannelPipeline pipeline);\n\n    public abstract ChannelPipeline buildHardwarePipeline(ChannelPipeline pipeline);\n\n    @Override\n    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {\n        handleUnexpectedException(ctx, cause);\n    }\n}\n"
  },
  {
    "path": "server/http-api/src/main/java/cc/blynk/server/api/http/handlers/BaseWebSocketUnificator.java",
    "content": "package cc.blynk.server.api.http.handlers;\n\nimport io.netty.channel.ChannelHandler;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.ChannelInboundHandlerAdapter;\n\nimport static cc.blynk.server.core.protocol.handlers.DefaultExceptionHandler.handleUnexpectedException;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 16.02.18.\n */\n@ChannelHandler.Sharable\npublic abstract class BaseWebSocketUnificator extends ChannelInboundHandlerAdapter {\n\n    @Override\n    public abstract void channelRead(ChannelHandlerContext ctx, Object msg);\n\n    @Override\n    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {\n        handleUnexpectedException(ctx, cause);\n    }\n}\n"
  },
  {
    "path": "server/http-api/src/main/java/cc/blynk/server/api/http/handlers/LetsEncryptHandler.java",
    "content": "package cc.blynk.server.api.http.handlers;\n\nimport cc.blynk.server.acme.ContentHolder;\nimport io.netty.buffer.ByteBuf;\nimport io.netty.buffer.Unpooled;\nimport io.netty.channel.ChannelFuture;\nimport io.netty.channel.ChannelFutureListener;\nimport io.netty.channel.ChannelHandler;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.ChannelInboundHandlerAdapter;\nimport io.netty.handler.codec.http.DefaultFullHttpResponse;\nimport io.netty.handler.codec.http.DefaultHttpResponse;\nimport io.netty.handler.codec.http.FullHttpRequest;\nimport io.netty.handler.codec.http.FullHttpResponse;\nimport io.netty.handler.codec.http.HttpHeaderValues;\nimport io.netty.handler.codec.http.HttpMethod;\nimport io.netty.handler.codec.http.HttpResponse;\nimport io.netty.handler.codec.http.HttpResponseStatus;\nimport io.netty.handler.codec.http.HttpUtil;\nimport io.netty.handler.codec.http.LastHttpContent;\nimport io.netty.util.ReferenceCountUtil;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.nio.charset.StandardCharsets;\n\nimport static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION;\nimport static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE;\nimport static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;\nimport static io.netty.handler.codec.http.HttpResponseStatus.OK;\nimport static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 10.12.15.\n */\n@ChannelHandler.Sharable\npublic class LetsEncryptHandler extends ChannelInboundHandlerAdapter {\n\n    private static final Logger log = LogManager.getLogger(LetsEncryptHandler.class);\n\n    private static final String LETS_ENCRYPT_PATH = \"/.well-known/acme-challenge/\";\n\n    private final ContentHolder contentHolder;\n\n    public LetsEncryptHandler(ContentHolder contentHolder) {\n        this.contentHolder = contentHolder;\n    }\n\n    private static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {\n        FullHttpResponse response = new DefaultFullHttpResponse(\n                HTTP_1_1, status, Unpooled.copiedBuffer(\"Failure: \" + status + \"\\r\\n\", StandardCharsets.UTF_8));\n        response.headers().set(CONTENT_TYPE, \"text/plain; charset=UTF-8\");\n\n        // Close the connection as soon as the error message is sent.\n        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);\n    }\n\n    @Override\n    public void channelRead(ChannelHandlerContext ctx, Object msg) {\n        if (!(msg instanceof FullHttpRequest)) {\n            return;\n        }\n\n        FullHttpRequest req = (FullHttpRequest) msg;\n\n        if (req.uri().startsWith(LETS_ENCRYPT_PATH)) {\n            try {\n                serveContent(ctx, req);\n            } finally {\n                ReferenceCountUtil.release(req);\n            }\n            return;\n        }\n\n        ctx.fireChannelRead(req);\n    }\n\n    private void serveContent(ChannelHandlerContext ctx, FullHttpRequest request) {\n        if (!request.decoderResult().isSuccess()) {\n            sendError(ctx, BAD_REQUEST);\n            return;\n        }\n\n        if (request.method() != HttpMethod.GET) {\n            return;\n        }\n\n        final String content = contentHolder.content;\n\n        if (content == null) {\n            log.error(\"No content for certificate.\");\n            return;\n        }\n\n        log.info(\"Delivering content {}\", content);\n\n        HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK);\n        HttpUtil.setContentLength(response, content.length());\n        response.headers().set(CONTENT_TYPE, \"text/html\");\n\n        if (HttpUtil.isKeepAlive(request)) {\n            response.headers().set(CONNECTION, HttpHeaderValues.KEEP_ALIVE);\n        }\n\n        ctx.write(response);\n\n        // Write the content.\n        ChannelFuture lastContentFuture;\n        ByteBuf buf = ctx.alloc().buffer(content.length());\n        buf.writeBytes(content.getBytes());\n        ctx.write(buf);\n        lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);\n\n        // Decide whether to close the connection or not.\n        if (!HttpUtil.isKeepAlive(request)) {\n            // Close the connection when the whole content is written out.\n            lastContentFuture.addListener(ChannelFutureListener.CLOSE);\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/http-api/src/main/java/cc/blynk/server/api/http/logic/HttpAPILogic.java",
    "content": "package cc.blynk.server.api.http.logic;\n\nimport cc.blynk.core.http.Response;\nimport cc.blynk.core.http.TokenBaseHttpHandler;\nimport cc.blynk.core.http.annotation.Consumes;\nimport cc.blynk.core.http.annotation.EnumQueryParam;\nimport cc.blynk.core.http.annotation.GET;\nimport cc.blynk.core.http.annotation.Metric;\nimport cc.blynk.core.http.annotation.POST;\nimport cc.blynk.core.http.annotation.PUT;\nimport cc.blynk.core.http.annotation.Path;\nimport cc.blynk.core.http.annotation.PathParam;\nimport cc.blynk.core.http.annotation.QueryParam;\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.api.http.pojo.EmailPojo;\nimport cc.blynk.server.api.http.pojo.PinData;\nimport cc.blynk.server.api.http.pojo.PushMessagePojo;\nimport cc.blynk.server.core.BlockingIOProcessor;\nimport cc.blynk.server.core.dao.FileManager;\nimport cc.blynk.server.core.dao.ReportingDiskDao;\nimport cc.blynk.server.core.dao.TokenValue;\nimport cc.blynk.server.core.dao.UserKey;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.DataStream;\nimport cc.blynk.server.core.model.auth.Session;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.enums.WidgetProperty;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.model.storage.key.DashPinStorageKey;\nimport cc.blynk.server.core.model.storage.value.PinStorageValue;\nimport cc.blynk.server.core.model.storage.value.SinglePinStorageValue;\nimport cc.blynk.server.core.model.widgets.MultiPinWidget;\nimport cc.blynk.server.core.model.widgets.OnePinWidget;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.notifications.Mail;\nimport cc.blynk.server.core.model.widgets.notifications.Notification;\nimport cc.blynk.server.core.model.widgets.others.rtc.RTC;\nimport cc.blynk.server.core.model.widgets.ui.tiles.DeviceTiles;\nimport cc.blynk.server.core.processors.EventorProcessor;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandBodyException;\nimport cc.blynk.server.core.protocol.exceptions.NoDataException;\nimport cc.blynk.server.db.DBManager;\nimport cc.blynk.server.notifications.mail.MailWrapper;\nimport cc.blynk.server.notifications.push.GCMWrapper;\nimport cc.blynk.utils.NumberUtil;\nimport cc.blynk.utils.StringUtils;\nimport cc.blynk.utils.TokenGeneratorUtil;\nimport cc.blynk.utils.http.MediaType;\nimport io.netty.channel.ChannelHandler;\nimport net.glxn.qrgen.core.image.ImageType;\nimport net.glxn.qrgen.javase.QRCode;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.util.AbstractMap;\n\nimport static cc.blynk.core.http.Response.badRequest;\nimport static cc.blynk.core.http.Response.ok;\nimport static cc.blynk.core.http.Response.redirect;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE;\nimport static cc.blynk.server.core.protocol.enums.Command.HTTP_EMAIL;\nimport static cc.blynk.server.core.protocol.enums.Command.HTTP_GET_HISTORY_DATA;\nimport static cc.blynk.server.core.protocol.enums.Command.HTTP_GET_PIN_DATA;\nimport static cc.blynk.server.core.protocol.enums.Command.HTTP_GET_PROJECT;\nimport static cc.blynk.server.core.protocol.enums.Command.HTTP_IS_APP_CONNECTED;\nimport static cc.blynk.server.core.protocol.enums.Command.HTTP_IS_HARDWARE_CONNECTED;\nimport static cc.blynk.server.core.protocol.enums.Command.HTTP_NOTIFY;\nimport static cc.blynk.server.core.protocol.enums.Command.HTTP_QR;\nimport static cc.blynk.server.core.protocol.enums.Command.HTTP_UPDATE_PIN_DATA;\nimport static cc.blynk.server.core.protocol.enums.Command.SET_WIDGET_PROPERTY;\nimport static cc.blynk.utils.StringUtils.BODY_SEPARATOR;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 25.12.15.\n */\n@Path(\"/\")\n@ChannelHandler.Sharable\npublic class HttpAPILogic extends TokenBaseHttpHandler {\n\n    private static final Logger log = LogManager.getLogger(HttpAPILogic.class);\n    private final BlockingIOProcessor blockingIOProcessor;\n    private final MailWrapper mailWrapper;\n    private final GCMWrapper gcmWrapper;\n    private final ReportingDiskDao reportingDao;\n    private final EventorProcessor eventorProcessor;\n    private final DBManager dbManager;\n    private final FileManager fileManager;\n    private final String host;\n    private final String httpsPort;\n\n    public HttpAPILogic(Holder holder) {\n        super(holder.tokenManager, holder.sessionDao, holder.stats, \"\");\n        this.blockingIOProcessor = holder.blockingIOProcessor;\n        this.mailWrapper = holder.mailWrapper;\n        this.gcmWrapper = holder.gcmWrapper;\n        this.reportingDao = holder.reportingDiskDao;\n        this.eventorProcessor = holder.eventorProcessor;\n        this.dbManager = holder.dbManager;\n        this.fileManager = holder.fileManager;\n        this.host = holder.props.host;\n        this.httpsPort = holder.props.getHttpsPortAsString();\n    }\n\n    private static String makeBody(DashBoard dash, int deviceId, short pin, PinType pinType, String pinValue) {\n        Widget widget = dash.findWidgetByPin(deviceId, pin, pinType);\n        if (widget instanceof OnePinWidget) {\n            return ((OnePinWidget) widget).makeHardwareBody();\n        } else if (widget instanceof MultiPinWidget) {\n            return ((MultiPinWidget) widget).makeHardwareBody(pin, pinType);\n        }\n\n        return DataStream.makeHardwareBody(pinType, pin, pinValue);\n    }\n\n    @GET\n    @Path(\"{token}/project\")\n    @Metric(HTTP_GET_PROJECT)\n    public Response getDashboard(@PathParam(\"token\") String token) {\n        TokenValue tokenValue = tokenManager.getTokenValueByToken(token);\n\n        if (tokenValue == null) {\n            log.debug(\"Requested token {} not found.\", token);\n            return badRequest(\"Invalid token.\");\n        }\n\n        return ok(JsonParser.toJsonRestrictiveDashboardForHTTP(tokenValue.dash));\n    }\n\n    @GET\n    @Path(\"{token}/isHardwareConnected\")\n    @Metric(HTTP_IS_HARDWARE_CONNECTED)\n    public Response isHardwareConnected(@PathParam(\"token\") String token) {\n        TokenValue tokenValue = tokenManager.getTokenValueByToken(token);\n\n        if (tokenValue == null) {\n            log.debug(\"Requested token {} not found.\", token);\n            return badRequest(\"Invalid token.\");\n        }\n\n        User user = tokenValue.user;\n        int dashId = tokenValue.dash.id;\n        int deviceId = tokenValue.device.id;\n\n        Session session = sessionDao.get(new UserKey(user));\n\n        return ok(session.isHardwareConnected(dashId, deviceId));\n    }\n\n    @GET\n    @Path(\"{token}/isAppConnected\")\n    @Metric(HTTP_IS_APP_CONNECTED)\n    public Response isAppConnected(@PathParam(\"token\") String token) {\n        TokenValue tokenValue = tokenManager.getTokenValueByToken(token);\n\n        if (tokenValue == null) {\n            log.debug(\"Requested token {} not found.\", token);\n            return badRequest(\"Invalid token.\");\n        }\n\n        User user = tokenValue.user;\n        Session session = sessionDao.get(new UserKey(user));\n\n        return ok(tokenValue.dash.isActive && session.isAppConnected());\n    }\n\n    @GET\n    @Path(\"{token}/get/{pin}\")\n    @Metric(HTTP_GET_PIN_DATA)\n    public Response getWidgetPinDataNew(@PathParam(\"token\") String token,\n                                        @PathParam(\"pin\") String pinString) {\n        TokenValue tokenValue = tokenManager.getTokenValueByToken(token);\n\n        if (tokenValue == null) {\n            log.debug(\"Requested token {} not found.\", token);\n            return badRequest(\"Invalid token.\");\n        }\n\n        User user = tokenValue.user;\n        int deviceId = tokenValue.device.id;\n        DashBoard dash = tokenValue.dash;\n\n        PinType pinType;\n        short pin;\n\n        try {\n            pinType = PinType.getPinType(pinString.charAt(0));\n            pin = NumberUtil.parsePin(pinString.substring(1));\n        } catch (NumberFormatException | IllegalCommandBodyException e) {\n            log.debug(\"Wrong pin format. {}\", pinString);\n            return badRequest(\"Wrong pin format.\");\n        }\n\n        Widget widget = dash.findWidgetByPin(deviceId, pin, pinType);\n\n        if (widget == null) {\n            PinStorageValue value = user.profile.pinsStorage.get(\n                    new DashPinStorageKey(dash.id, deviceId, pinType, pin));\n            if (value == null) {\n                log.debug(\"Requested pin {} not found. User {}\", pinString, user.email);\n                return badRequest(\"Requested pin doesn't exist in the app.\");\n            }\n            if (value instanceof SinglePinStorageValue) {\n                return ok(JsonParser.valueToJsonAsString((SinglePinStorageValue) value));\n            } else {\n                return ok(JsonParser.valueToJsonAsString(value.values()));\n            }\n        }\n\n        if (widget instanceof DeviceTiles) {\n            String value = ((DeviceTiles) widget).getValue(deviceId, pin, pinType);\n            if (value == null) {\n                log.debug(\"Requested pin {} not found. User {}\", pinString, user.email);\n                return badRequest(\"Requested pin doesn't exist in the app.\");\n            }\n            return ok(value);\n        }\n\n        return ok(widget.getJsonValue());\n    }\n\n    @GET\n    @Path(\"{token}/rtc\")\n    @Metric(HTTP_GET_PIN_DATA)\n    public Response getWidgetPinData(@PathParam(\"token\") String token) {\n        TokenValue tokenValue = tokenManager.getTokenValueByToken(token);\n\n        if (tokenValue == null) {\n            log.debug(\"Requested token {} not found.\", token);\n            return badRequest(\"Invalid token.\");\n        }\n\n        User user = tokenValue.user;\n\n        RTC rtc = tokenValue.dash.getWidgetByType(RTC.class);\n\n        if (rtc == null) {\n            log.debug(\"Requested rtc widget not found. User {}\", user.email);\n            return badRequest(\"Requested rtc not exists in app.\");\n        }\n\n        return ok(rtc.getJsonValue());\n    }\n\n    @GET\n    @Path(\"{token}/qr\")\n    @Metric(HTTP_QR)\n    public Response getQR(@PathParam(\"token\") String token) {\n        TokenValue tokenValue = tokenManager.getTokenValueByToken(token);\n\n        if (tokenValue == null) {\n            log.debug(\"Requested token {} not found.\", token);\n            return badRequest(\"Invalid token.\");\n        }\n\n        DashBoard dash = tokenValue.dash;\n\n        String qrToken = TokenGeneratorUtil.generateNewToken();\n        String json = JsonParser.toJsonRestrictiveDashboard(dash);\n\n        blockingIOProcessor.executeDB(() -> {\n            try {\n                boolean insertStatus = dbManager.insertClonedProject(qrToken, json);\n                if (!insertStatus && !fileManager.writeCloneProjectToDisk(qrToken, json)) {\n                    log.error(\"Creating clone project failed for {}\", tokenValue.user.email);\n                }\n            } catch (Exception e) {\n                log.error(\"Error cloning project for {}.\", tokenValue.user.email, e);\n            }\n        });\n\n        //todo generate QR on client side.\n        String cloneQrString = \"blynk://token/clone/\" + qrToken + \"?server=\" + host + \"&port=\" + httpsPort;\n        byte[] qrDataBinary = QRCode.from(cloneQrString).to(ImageType.PNG).stream().toByteArray();\n        return ok(qrDataBinary, \"image/png\");\n    }\n\n    @GET\n    @Path(\"{token}/data/{pin}\")\n    @Metric(HTTP_GET_HISTORY_DATA)\n    public Response getPinHistoryData(@PathParam(\"token\") String token,\n                                      @PathParam(\"pin\") String pinString) {\n        TokenValue tokenValue = tokenManager.getTokenValueByToken(token);\n\n        if (tokenValue == null) {\n            log.debug(\"Requested token {} not found.\", token);\n            return badRequest(\"Invalid token.\");\n        }\n\n        User user = tokenValue.user;\n        int dashId = tokenValue.dash.id;\n        int deviceId = tokenValue.device.id;\n\n        PinType pinType;\n        short pin;\n\n        try {\n            pinType = PinType.getPinType(pinString.charAt(0));\n            pin = NumberUtil.parsePin(pinString.substring(1));\n        } catch (NumberFormatException | IllegalCommandBodyException e) {\n            log.debug(\"Wrong pin format. {}\", pinString);\n            return badRequest(\"Wrong pin format.\");\n        }\n\n        //todo may be optimized\n        try {\n            java.nio.file.Path path = reportingDao.csvGenerator.createCSV(\n                    user, dashId, deviceId, pinType, pin, deviceId);\n            return redirect(\"/\" + path.getFileName().toString());\n        } catch (NoDataException | IllegalStateException noData) {\n            log.debug(noData.getMessage());\n            return badRequest(noData.getMessage());\n        } catch (Exception e) {\n            log.debug(\"Error getting pin data.\", e);\n            return badRequest(\"Error getting pin data.\");\n        }\n    }\n\n    public Response updateWidgetProperty(String token,\n                                         String pinString,\n                                         WidgetProperty property,\n                                         String value) {\n        if (value == null) {\n            log.debug(\"No properties for update provided.\");\n            return badRequest(\"No properties for update provided.\");\n        }\n\n        TokenValue tokenValue = tokenManager.getTokenValueByToken(token);\n\n        if (tokenValue == null) {\n            log.debug(\"Requested token {} not found.\", token);\n            return badRequest(\"Invalid token.\");\n        }\n\n        User user = tokenValue.user;\n        int deviceId = tokenValue.device.id;\n        DashBoard dash = tokenValue.dash;\n\n        //todo add test for this use case\n        if (!dash.isActive) {\n            return badRequest(\"Project is not active.\");\n        }\n\n        PinType pinType;\n        short pin;\n        try {\n            pinType = PinType.getPinType(pinString.charAt(0));\n            pin = NumberUtil.parsePin(pinString.substring(1));\n        } catch (NumberFormatException | IllegalCommandBodyException e) {\n            log.debug(\"Wrong pin format. {}\", pinString);\n            return badRequest(\"Wrong pin format.\");\n        }\n\n        //for now supporting only virtual pins\n        Widget widget = null;\n        for (Widget dashWidget : dash.widgets) {\n            if (dashWidget.isSame(deviceId, pin, pinType)) {\n                //todo for now supporting only single property\n                if (!dashWidget.setProperty(property, value)) {\n                    log.debug(\"Property {} with value {} not supported.\", property, value);\n                    return badRequest(\"Error setting widget property.\");\n                }\n                widget = dashWidget;\n            }\n        }\n\n        if (widget == null) {\n            log.debug(\"No widget for SetWidgetProperty command.\");\n            return badRequest(\"No widget for SetWidgetProperty command.\");\n        }\n\n        Session session = sessionDao.get(new UserKey(user));\n        session.sendToApps(SET_WIDGET_PROPERTY, 111, dash.id,\n                deviceId, \"\" + pin + BODY_SEPARATOR + property + BODY_SEPARATOR + value);\n        return ok();\n    }\n\n    //todo it is a bit ugly right now. could be simplified by passing map of query params.\n    @GET\n    @Path(\"{token}/update/{pin}\")\n    @Consumes(value = MediaType.APPLICATION_JSON)\n    @Metric(HTTP_UPDATE_PIN_DATA)\n    public Response updateWidgetPinDataViaGet(@PathParam(\"token\") String token,\n                                              @PathParam(\"pin\") String pinString,\n                                              @QueryParam(\"value\") String[] pinValues,\n                                              @EnumQueryParam(WidgetProperty.class)\n                                                          AbstractMap.SimpleImmutableEntry<WidgetProperty, String>\n                                                          widgetProperty) {\n\n        if (pinValues != null) {\n            return updateWidgetPinData(token, pinString, pinValues);\n        }\n        if (widgetProperty != null) {\n            return updateWidgetProperty(token, pinString, widgetProperty.getKey(), widgetProperty.getValue());\n        }\n\n        return badRequest(\"Wrong request format.\");\n    }\n\n    @PUT\n    @Path(\"{token}/update/{pin}\")\n    @Consumes(value = MediaType.APPLICATION_JSON)\n    @Metric(HTTP_UPDATE_PIN_DATA)\n    public Response updateWidgetPinDataNew(@PathParam(\"token\") String token,\n                                           @PathParam(\"pin\") String pinString,\n                                           String[] pinValues) {\n        return updateWidgetPinData(token, pinString, pinValues);\n    }\n\n    //todo remove later?\n    @PUT\n    @Path(\"{token}/pin/{pin}\")\n    @Consumes(value = MediaType.APPLICATION_JSON)\n    @Metric(HTTP_UPDATE_PIN_DATA)\n    public Response updateWidgetPinData(@PathParam(\"token\") String token,\n                                        @PathParam(\"pin\") String pinString,\n                                        String[] pinValues) {\n\n        if (pinValues.length == 0) {\n            log.debug(\"No pin for update provided.\");\n            return badRequest(\"No pin for update provided.\");\n        }\n\n        TokenValue tokenValue = tokenManager.getTokenValueByToken(token);\n\n        if (tokenValue == null) {\n            log.debug(\"Requested token {} not found.\", token);\n            return badRequest(\"Invalid token.\");\n        }\n\n        User user = tokenValue.user;\n        int dashId = tokenValue.dash.id;\n        int deviceId = tokenValue.device.id;\n\n        DashBoard dash = tokenValue.dash;\n\n        PinType pinType;\n        short pin;\n\n        try {\n            pinType = PinType.getPinType(pinString.charAt(0));\n            pin = NumberUtil.parsePin(pinString.substring(1));\n        } catch (NumberFormatException | IllegalCommandBodyException e) {\n            log.debug(\"Wrong pin format. {}\", pinString);\n            return badRequest(\"Wrong pin format.\");\n        }\n\n        final long now = System.currentTimeMillis();\n\n        String pinValue = String.join(StringUtils.BODY_SEPARATOR_STRING, pinValues);\n\n        reportingDao.process(user, dash, deviceId, pin, pinType, pinValue, now);\n\n        user.profile.update(dash, deviceId, pin, pinType, pinValue, now);\n        tokenValue.device.dataReceivedAt = now;\n\n        String body = makeBody(dash, deviceId, pin, pinType, pinValue);\n\n        Session session = sessionDao.get(new UserKey(user));\n        if (session == null) {\n            log.debug(\"No session for user {}.\", user.email);\n            return ok();\n        }\n\n        eventorProcessor.process(user, session, dash, deviceId, pin, pinType, pinValue, now);\n\n        session.sendMessageToHardware(dashId, HARDWARE, 111, body, deviceId);\n\n        if (dash.isActive) {\n            session.sendToApps(HARDWARE, 111, dashId, deviceId, body);\n        }\n\n        return ok();\n    }\n\n    @PUT\n    @Path(\"{token}/extra/pin/{pin}\")\n    @Consumes(value = MediaType.APPLICATION_JSON)\n    @Metric(HTTP_UPDATE_PIN_DATA)\n    public Response updateWidgetPinData(@PathParam(\"token\") String token,\n                                        @PathParam(\"pin\") String pinString,\n                                        PinData[] pinsData) {\n\n        if (pinsData.length == 0) {\n            log.debug(\"No pin for update provided.\");\n            return badRequest(\"No pin for update provided.\");\n        }\n\n        TokenValue tokenValue = tokenManager.getTokenValueByToken(token);\n\n        if (tokenValue == null) {\n            log.debug(\"Requested token {} not found.\", token);\n            return badRequest(\"Invalid token.\");\n        }\n\n        User user = tokenValue.user;\n        int dashId = tokenValue.dash.id;\n        int deviceId = tokenValue.device.id;\n\n        DashBoard dash = tokenValue.dash;\n\n        PinType pinType;\n        short pin;\n\n        try {\n            pinType = PinType.getPinType(pinString.charAt(0));\n            pin = NumberUtil.parsePin(pinString.substring(1));\n        } catch (NumberFormatException | IllegalCommandBodyException e) {\n            log.debug(\"Wrong pin format. {}\", pinString);\n            return badRequest(\"Wrong pin format.\");\n        }\n\n        for (PinData pinData : pinsData) {\n            reportingDao.process(user, dash, deviceId, pin, pinType, pinData.value, pinData.timestamp);\n        }\n\n        long now = System.currentTimeMillis();\n        user.profile.update(dash, deviceId, pin, pinType, pinsData[0].value, now);\n\n        String body = makeBody(dash, deviceId, pin, pinType, pinsData[0].value);\n\n        if (body != null) {\n            Session session = sessionDao.get(new UserKey(user));\n            if (session == null) {\n                log.error(\"No session for user {}.\", user.email);\n                return ok();\n            }\n            session.sendMessageToHardware(dashId, HARDWARE, 111, body, deviceId);\n\n            if (dash.isActive) {\n                session.sendToApps(HARDWARE, 111, dashId, deviceId, body);\n            }\n        }\n\n        return ok();\n    }\n\n    @POST\n    @Path(\"{token}/notify\")\n    @Consumes(value = MediaType.APPLICATION_JSON)\n    @Metric(HTTP_NOTIFY)\n    public Response notify(@PathParam(\"token\") String token,\n                           PushMessagePojo message) {\n\n        TokenValue tokenValue = tokenManager.getTokenValueByToken(token);\n\n        if (tokenValue == null) {\n            log.debug(\"Requested token {} not found.\", token);\n            return badRequest(\"Invalid token.\");\n        }\n\n        User user = tokenValue.user;\n\n        if (message == null || Notification.isWrongBody(message.body)) {\n            log.debug(\"Notification body is wrong. '{}'\", message == null ? \"\" : message.body);\n            return badRequest(\"Body is empty or larger than 255 chars.\");\n        }\n\n        DashBoard dash = tokenValue.dash;\n\n        if (!dash.isActive) {\n            log.debug(\"Project is not active.\");\n            return badRequest(\"Project is not active.\");\n        }\n\n        Notification notification = dash.getNotificationWidget();\n\n        if (notification == null || notification.hasNoToken()) {\n            log.debug(\"No notification tokens.\");\n            if (notification == null) {\n                return badRequest(\"No notification widget.\");\n            } else {\n                return badRequest(\"Notification widget not initialized.\");\n            }\n        }\n\n        log.trace(\"Sending push for user {}, with message : '{}'.\", user.email, message.body);\n        notification.push(gcmWrapper, message.body, dash.id);\n\n        return ok();\n    }\n\n    @POST\n    @Path(\"{token}/email\")\n    @Consumes(value = MediaType.APPLICATION_JSON)\n    @Metric(HTTP_EMAIL)\n    public Response email(@PathParam(\"token\") String token,\n                          EmailPojo message) {\n\n        TokenValue tokenValue = tokenManager.getTokenValueByToken(token);\n\n        if (tokenValue == null) {\n            log.debug(\"Requested token {} not found.\", token);\n            return badRequest(\"Invalid token.\");\n        }\n\n        DashBoard dash = tokenValue.dash;\n\n        if (dash == null || !dash.isActive) {\n            log.debug(\"Project is not active.\");\n            return badRequest(\"Project is not active.\");\n        }\n\n        Mail mail = dash.getMailWidget();\n\n        if (mail == null) {\n            log.debug(\"No email widget.\");\n            return badRequest(\"No email widget.\");\n        }\n\n        if (message == null\n                || message.subj == null || message.subj.isEmpty()\n                || message.to == null || message.to.isEmpty()) {\n            log.debug(\"Email body empty. '{}'\", message);\n            return badRequest(\"Email body is wrong. Missing or empty fields 'to', 'subj'.\");\n        }\n\n        log.trace(\"Sending Mail for user {}, with message : '{}'.\", tokenValue.user.email, message.subj);\n        mail(tokenValue.user.email, message.to, message.subj, message.title);\n\n        return ok();\n    }\n\n    private void mail(String email, String to, String subj, String body) {\n        blockingIOProcessor.execute(() -> {\n            try {\n                mailWrapper.sendText(to, subj, body);\n            } catch (Exception e) {\n                log.error(\"Error sending email from HTTP. From : '{}', to : '{}'. Reason : {}\",\n                        email, to, e.getMessage());\n            }\n        });\n    }\n\n}\n"
  },
  {
    "path": "server/http-api/src/main/java/cc/blynk/server/api/http/logic/ResetPasswordHttpLogic.java",
    "content": "package cc.blynk.server.api.http.logic;\n\nimport cc.blynk.core.http.BaseHttpHandler;\nimport cc.blynk.core.http.Response;\nimport cc.blynk.core.http.annotation.Consumes;\nimport cc.blynk.core.http.annotation.Context;\nimport cc.blynk.core.http.annotation.FormParam;\nimport cc.blynk.core.http.annotation.GET;\nimport cc.blynk.core.http.annotation.Metric;\nimport cc.blynk.core.http.annotation.POST;\nimport cc.blynk.core.http.annotation.Path;\nimport cc.blynk.core.http.annotation.PathParam;\nimport cc.blynk.core.http.annotation.QueryParam;\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.core.BlockingIOProcessor;\nimport cc.blynk.server.core.dao.FileManager;\nimport cc.blynk.server.core.dao.UserDao;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.db.DBManager;\nimport cc.blynk.server.internal.token.BaseToken;\nimport cc.blynk.server.internal.token.ResetPassToken;\nimport cc.blynk.server.internal.token.TokensPool;\nimport cc.blynk.server.notifications.mail.MailWrapper;\nimport cc.blynk.utils.AppNameUtil;\nimport cc.blynk.utils.FileLoaderUtil;\nimport cc.blynk.utils.TokenGeneratorUtil;\nimport cc.blynk.utils.http.MediaType;\nimport cc.blynk.utils.properties.Placeholders;\nimport cc.blynk.utils.validators.BlynkEmailValidator;\nimport io.netty.channel.ChannelHandler;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.core.http.Response.badRequest;\nimport static cc.blynk.core.http.Response.noResponse;\nimport static cc.blynk.core.http.Response.notFound;\nimport static cc.blynk.core.http.Response.ok;\nimport static cc.blynk.core.http.Response.serverError;\nimport static cc.blynk.server.core.protocol.enums.Command.HTTP_CLONE;\nimport static cc.blynk.utils.http.MediaType.TEXT_PLAIN;\n\n/**\n * The Blynk project\n * Created by Andrew Zakordonets\n * Date : 12/05/2015.\n */\n@Path(\"/\")\n@ChannelHandler.Sharable\npublic class ResetPasswordHttpLogic extends BaseHttpHandler {\n\n    private static final Logger log = LogManager.getLogger(ResetPasswordHttpLogic.class);\n\n    private final UserDao userDao;\n    private final TokensPool tokensPool;\n    private final String emailBody;\n    private final String emailSubj;\n    private final MailWrapper mailWrapper;\n    private final String resetPassUrl;\n    private final String pageContent;\n    private final String newResetPage;\n    private final BlockingIOProcessor blockingIOProcessor;\n    private final DBManager dbManager;\n    private final FileManager fileManager;\n    private final String resetClickHost;\n\n    public ResetPasswordHttpLogic(Holder holder) {\n        super(holder, \"\");\n        this.userDao = holder.userDao;\n        this.tokensPool = holder.tokensPool;\n        String productName = holder.props.productName;\n        this.emailSubj = \"Password reset request for the \" + productName + \" app.\";\n        this.emailBody = FileLoaderUtil.readResetEmailTemplateAsString()\n                .replace(Placeholders.PRODUCT_NAME, productName);\n        this.newResetPage = holder.textHolder.appResetEmailTemplate\n                .replace(Placeholders.PRODUCT_NAME, productName);\n        this.mailWrapper = holder.mailWrapper;\n\n        String host = holder.props.host;\n\n        //using https for private servers as they have valid certificates.\n        String protocol = host.endsWith(\".blynk.cc\") ? \"https://\" : \"http://\";\n        this.resetPassUrl = protocol + host + \"/landing?token=\";\n        this.pageContent = holder.textHolder.resetPassLandingTemplate;\n        this.blockingIOProcessor = holder.blockingIOProcessor;\n        this.dbManager = holder.dbManager;\n        this.fileManager = holder.fileManager;\n        this.resetClickHost = holder.props.getRestoreHost();\n    }\n\n    private static String generateToken() {\n        return TokenGeneratorUtil.generateNewToken() + TokenGeneratorUtil.generateNewToken();\n    }\n\n    @POST\n    @Consumes(value = MediaType.APPLICATION_FORM_URLENCODED)\n    @Path(\"resetPassword\")\n    public Response sendResetPasswordEmail(@Context ChannelHandlerContext ctx,\n                                           @FormParam(\"email\") String email,\n                                           @FormParam(\"appName\") String appName) {\n\n        if (BlynkEmailValidator.isNotValidEmail(email)) {\n            return badRequest(email + \" email has not valid format.\");\n        }\n\n        String trimmedEmail = email.trim().toLowerCase();\n        appName = (appName == null ? AppNameUtil.BLYNK : appName);\n\n        User user = userDao.getByName(trimmedEmail, appName);\n\n        if (user == null) {\n            return badRequest(\"Sorry, this account does not exist.\");\n        }\n\n        String token = generateToken();\n        log.info(\"{} trying to reset pass.\", trimmedEmail);\n\n        ResetPassToken userToken = new ResetPassToken(trimmedEmail, appName);\n        tokensPool.addToken(token, userToken);\n        String message = emailBody.replace(Placeholders.RESET_URL, resetPassUrl + token);\n        log.info(\"Sending token to {} address\", trimmedEmail);\n\n        blockingIOProcessor.execute(() -> {\n            Response response;\n            try {\n                mailWrapper.sendHtml(trimmedEmail, emailSubj, message);\n                log.info(\"{} mail sent.\", trimmedEmail);\n                response = ok(\"Email was sent.\");\n            } catch (Exception e) {\n                log.info(\"Error sending mail for {}. Reason : {}\", trimmedEmail, e.getMessage());\n                response = badRequest(\"Error sending reset email.\");\n            }\n            if (ctx.channel().isActive() && ctx.channel().isWritable()) {\n                ctx.writeAndFlush(response, ctx.voidPromise());\n            }\n        });\n\n        return noResponse();\n    }\n\n    @GET\n    @Path(\"landing\")\n    public Response generateResetPage(@QueryParam(\"token\") String token) {\n        BaseToken baseToken = tokensPool.getBaseToken(token);\n        if (baseToken == null) {\n            return badRequest(\"Your token was not found or it is outdated. Please try again.\");\n        }\n\n        if (TokenGeneratorUtil.isNotValidResetToken(token)) {\n            return badRequest(\"Invalid request parameters.\");\n        }\n\n        log.info(\"{} landed.\", baseToken.email);\n        String page = pageContent.replace(Placeholders.EMAIL, baseToken.email).replace(Placeholders.TOKEN, token);\n        return ok(page, MediaType.TEXT_HTML);\n    }\n\n    @GET\n    @Path(\"restore\")\n    public Response getNewResetPage(@QueryParam(\"token\") String token, @QueryParam(\"email\") String email) {\n        //we do not check token here, as we used single host but we may have many servers\n\n        //ResetPassToken user = tokensPool.getBaseToken(token);\n        //if (user == null) {\n        //    return badRequest(\"Your token was not found or it is outdated. Please try again.\");\n        //}\n\n        if (TokenGeneratorUtil.isNotValidResetToken(token)\n                || (email != null && BlynkEmailValidator.isNotValidEmail(email))) {\n            return badRequest(\"Invalid request parameters.\");\n        }\n\n        log.info(\"{} landed.\", email);\n        String resetUrl = \"http://\" + resetClickHost + \"/restore?token=\" + token + \"&email=\" + email;\n        String body = newResetPage.replace(Placeholders.RESET_URL, resetUrl);\n        return ok(body, MediaType.TEXT_HTML);\n    }\n\n    @POST\n    @Consumes(value = MediaType.APPLICATION_FORM_URLENCODED)\n    @Path(\"updatePassword\")\n    public Response updatePassword(@FormParam(\"password\") String passHash,\n                                   @FormParam(\"token\") String token) {\n        ResetPassToken resetPassToken = tokensPool.getResetPassToken(token);\n        if (resetPassToken == null) {\n            return badRequest(\"Invalid token. Please repeat all steps.\");\n        }\n\n        log.info(\"Resetting pass for {}\", resetPassToken.email);\n        User user = userDao.getByName(resetPassToken.email, resetPassToken.appName);\n\n        if (user == null) {\n            log.warn(\"No user with email {}\", resetPassToken.email);\n            return notFound();\n        }\n\n        user.resetPass(passHash);\n\n        log.info(\"{} password was reset.\", user.email);\n        tokensPool.removeToken(token);\n        return ok(\"Password was successfully reset.\", TEXT_PLAIN);\n    }\n\n    @GET\n    @Path(\"{token}/clone\")\n    @Metric(HTTP_CLONE)\n    public Response getClone(@Context ChannelHandlerContext ctx,\n                             @PathParam(\"token\") String token) {\n\n        blockingIOProcessor.executeDB(() -> {\n            try {\n                String json = dbManager.selectClonedProject(token);\n                //no cloned project in DB, checking local storage on disk\n                if (json == null) {\n                    json = fileManager.readClonedProjectFromDisk(token);\n                }\n                if (json == null) {\n                    log.debug(\"Requested QR not found. {}\", token);\n                    ctx.writeAndFlush(serverError(\"Requested QR not found.\"), ctx.voidPromise());\n                } else {\n                    ctx.writeAndFlush(ok(json), ctx.voidPromise());\n                }\n            } catch (Exception e) {\n                log.error(\"Error cloning project.\", e);\n                ctx.writeAndFlush(serverError(\"Error getting cloned project.\"), ctx.voidPromise());\n            }\n        });\n\n        return null;\n    }\n\n}\n"
  },
  {
    "path": "server/http-api/src/main/java/cc/blynk/server/api/http/logic/business/AdminAuthHandler.java",
    "content": "package cc.blynk.server.api.http.logic.business;\n\nimport cc.blynk.core.http.BaseHttpHandler;\nimport cc.blynk.core.http.Response;\nimport cc.blynk.core.http.annotation.Consumes;\nimport cc.blynk.core.http.annotation.FormParam;\nimport cc.blynk.core.http.annotation.POST;\nimport cc.blynk.core.http.annotation.Path;\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.core.dao.SessionDao;\nimport cc.blynk.server.core.dao.UserDao;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.utils.AppNameUtil;\nimport cc.blynk.utils.http.MediaType;\nimport io.netty.channel.ChannelHandler;\nimport io.netty.handler.codec.http.cookie.Cookie;\nimport io.netty.handler.codec.http.cookie.DefaultCookie;\nimport io.netty.handler.codec.http.cookie.ServerCookieEncoder;\n\nimport static cc.blynk.core.http.Response.redirect;\nimport static io.netty.handler.codec.http.HttpHeaderNames.SET_COOKIE;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 09.12.15.\n */\n@Path(\"\")\n@ChannelHandler.Sharable\npublic class AdminAuthHandler extends BaseHttpHandler {\n\n    //1 month\n    private static final int COOKIE_EXPIRE_TIME = 30 * 60 * 60 * 24;\n\n    private final UserDao userDao;\n\n    public AdminAuthHandler(Holder holder, String adminRootPath) {\n        super(holder, adminRootPath);\n        this.userDao = holder.userDao;\n    }\n\n    @POST\n    @Consumes(value = MediaType.APPLICATION_FORM_URLENCODED)\n    @Path(\"/login\")\n    public Response login(@FormParam(\"email\") String email,\n                          @FormParam(\"password\") String password) {\n\n        if (email == null || password == null) {\n            return redirect(rootPath);\n        }\n\n        User user = userDao.getByName(email, AppNameUtil.BLYNK);\n\n        if (user == null || !user.isSuperAdmin) {\n            return redirect(rootPath);\n        }\n\n        if (!password.equals(user.pass)) {\n            return redirect(rootPath);\n        }\n\n        Response response = redirect(rootPath);\n\n        log.debug(\"Admin login is successful. Redirecting to {}\", rootPath);\n\n        Cookie cookie = makeDefaultSessionCookie(sessionDao.generateNewSession(user), COOKIE_EXPIRE_TIME);\n        response.headers().add(SET_COOKIE, ServerCookieEncoder.STRICT.encode(cookie));\n\n        return response;\n    }\n\n    @POST\n    @Path(\"/logout\")\n    public Response logout() {\n        Response response = redirect(rootPath);\n        Cookie cookie = makeDefaultSessionCookie(\"\", 0);\n        response.headers().add(SET_COOKIE, ServerCookieEncoder.STRICT.encode(cookie));\n        return response;\n    }\n\n    private static Cookie makeDefaultSessionCookie(String sessionId, int maxAge) {\n        DefaultCookie cookie = new DefaultCookie(SessionDao.SESSION_COOKIE, sessionId);\n        cookie.setMaxAge(maxAge);\n        return cookie;\n    }\n\n}\n"
  },
  {
    "path": "server/http-api/src/main/java/cc/blynk/server/api/http/logic/business/AuthCookieHandler.java",
    "content": "package cc.blynk.server.api.http.logic.business;\n\nimport cc.blynk.server.core.dao.SessionDao;\nimport cc.blynk.server.core.model.auth.User;\nimport io.netty.channel.ChannelHandler;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.ChannelInboundHandlerAdapter;\nimport io.netty.handler.codec.http.FullHttpRequest;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 13.05.16.\n */\n@ChannelHandler.Sharable\npublic class AuthCookieHandler extends ChannelInboundHandlerAdapter {\n\n    private final SessionDao sessionDao;\n\n    public AuthCookieHandler(SessionDao sessionDao) {\n        this.sessionDao = sessionDao;\n    }\n\n    @Override\n    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {\n        if (msg instanceof FullHttpRequest) {\n            FullHttpRequest request = (FullHttpRequest) msg;\n\n            if (request.uri().equals(\"/admin/logout\")) {\n                ctx.channel().attr(SessionDao.userAttributeKey).set(null);\n            } else {\n                User user = sessionDao.getUserFromCookie(request);\n                ctx.channel().attr(SessionDao.userAttributeKey).set(user);\n            }\n        }\n        super.channelRead(ctx, msg);\n    }\n\n}\n"
  },
  {
    "path": "server/http-api/src/main/java/cc/blynk/server/api/http/pojo/EmailPojo.java",
    "content": "package cc.blynk.server.api.http.pojo;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 29.12.15.\n */\npublic class EmailPojo {\n\n    public String to;\n    public String title;\n    public String subj;\n\n    public EmailPojo() {\n    }\n\n    public EmailPojo(String to, String title, String subj) {\n        this.to = to;\n        this.title = title;\n        this.subj = subj;\n    }\n\n    @Override\n    public String toString() {\n        return \"EmailPojo{\"\n                + \"to='\" + to + '\\''\n                + \", title='\" + title + '\\''\n                + \", subj='\" + subj + '\\''\n                + '}';\n    }\n}\n"
  },
  {
    "path": "server/http-api/src/main/java/cc/blynk/server/api/http/pojo/PinData.java",
    "content": "package cc.blynk.server.api.http.pojo;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 04.08.16.\n */\npublic class PinData {\n\n    public String value;\n\n    public long timestamp;\n\n}\n"
  },
  {
    "path": "server/http-api/src/main/java/cc/blynk/server/api/http/pojo/PushMessagePojo.java",
    "content": "package cc.blynk.server.api.http.pojo;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 28.12.15.\n */\npublic class PushMessagePojo {\n\n    public String body;\n\n    public PushMessagePojo() {\n    }\n\n    public PushMessagePojo(String body) {\n        this.body = body;\n    }\n\n    @Override\n    public String toString() {\n        return \"PushMessagePojo{\"\n                + \"body='\" + body + '\\''\n                + '}';\n    }\n}\n"
  },
  {
    "path": "server/http-api/src/main/java/cc/blynk/server/api/websockets/handlers/WSHandler.java",
    "content": "package cc.blynk.server.api.websockets.handlers;\n\nimport cc.blynk.server.core.protocol.enums.Command;\nimport cc.blynk.server.core.stats.GlobalStats;\nimport io.netty.channel.ChannelHandler;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.ChannelInboundHandlerAdapter;\nimport io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;\nimport io.netty.handler.codec.http.websocketx.WebSocketHandshakeException;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 11.01.16.\n */\n@ChannelHandler.Sharable\npublic class WSHandler extends ChannelInboundHandlerAdapter {\n\n    private static final Logger log = LogManager.getLogger(WSHandler.class);\n\n    private final GlobalStats globalStats;\n\n    public WSHandler(GlobalStats globalStats) {\n        this.globalStats = globalStats;\n    }\n\n    @Override\n    public void channelRead(ChannelHandlerContext ctx, Object msg) {\n        globalStats.markWithoutGlobal(Command.WEB_SOCKETS);\n        if (msg instanceof BinaryWebSocketFrame) {\n            ctx.fireChannelRead(((BinaryWebSocketFrame) msg).content());\n        }\n    }\n\n    @Override\n    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {\n        if (cause instanceof WebSocketHandshakeException) {\n            log.debug(\"Web Socket Handshake Exception.\", cause);\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/http-api/src/main/java/cc/blynk/server/api/websockets/handlers/WSWrapperEncoder.java",
    "content": "package cc.blynk.server.api.websockets.handlers;\n\nimport io.netty.buffer.ByteBuf;\nimport io.netty.channel.ChannelHandler;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.ChannelOutboundHandlerAdapter;\nimport io.netty.channel.ChannelPromise;\nimport io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;\n\n/**\n * Just wraps ByteBuf into WebSockets frame.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 15.01.16.\n */\n@ChannelHandler.Sharable\npublic class WSWrapperEncoder extends ChannelOutboundHandlerAdapter {\n\n    @Override\n    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {\n        if (ctx.channel().isWritable()) {\n            if (msg instanceof ByteBuf) {\n                super.write(ctx, new BinaryWebSocketFrame((ByteBuf) msg), promise);\n            } else {\n                super.write(ctx, msg, promise);\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/http-api/src/main/resources/static/reset/enterNewPassword.html",
    "content": "<!DOCTYPE html>\n<html>\n<div style=\"position: absolute !important; visibility: hidden !important\"></div>\n<head>\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge,chrome=1\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    \n    <meta charset=\"utf-8\">\n    <title>PasswordRestore — Blynk</title>\n    <link rel=\"icon\" type=\"image/png\" href=\"http://www.blynk.cc/favicon.ico\">\n\n    <style type=\"text/css\"></style>\n\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"//static1.squarespace.com/static/sitecss/54765ba7e4b0d055ee0b47a6/81/52a74dafe4b073a80cd253c5/547662ffe4b0ace1b3020903/918-05142015/1431607402131/site.css?&filterFeatures=false&part=2\"/><link rel=\"stylesheet\" type=\"text/css\" href=\"//static1.squarespace.com/static/sitecss/54765ba7e4b0d055ee0b47a6/81/52a74dafe4b073a80cd253c5/547662ffe4b0ace1b3020903/918-05142015/1431607402131/site.css?&filterFeatures=false&part=3\"/><link rel=\"stylesheet\" type=\"text/css\" href=\"//static1.squarespace.com/static/sitecss/54765ba7e4b0d055ee0b47a6/81/52a74dafe4b073a80cd253c5/547662ffe4b0ace1b3020903/918-05142015/1431607402131/site.css?&filterFeatures=false&part=4\"/><![endif]-->\n<!--[if lt IE 9]><script src=\"//static.squarespace.com\"></script><link rel=\"stylesheet\" type=\"text/css\" href=\"//static1.squarespace.com/static/sitecss/54765ba7e4b0d055ee0b47a6/81/52a74dafe4b073a80cd253c5/547662ffe4b0ace1b3020903/918-05142015/1431607402131/site.css?&filterFeatures=false&noMedia=true&part=1\"/><link rel=\"stylesheet\" type=\"text/css\" href=\"//static1.squarespace.com/static/sitecss/54765ba7e4b0d055ee0b47a6/81/52a74dafe4b073a80cd253c5/547662ffe4b0ace1b3020903/918-05142015/1431607402131/site.css?&filterFeatures=false&noMedia=true&part=2\"/><link rel=\"stylesheet\" type=\"text/css\" href=\"//static1.squarespace.com/static/sitecss/54765ba7e4b0d055ee0b47a6/81/52a74dafe4b073a80cd253c5/547662ffe4b0ace1b3020903/918-05142015/1431607402131/site.css?&filterFeatures=false&noMedia=true&part=3\"/><link rel=\"stylesheet\" type=\"text/css\" href=\"//static1.squarespace.com/static/sitecss/54765ba7e4b0d055ee0b47a6/81/52a74dafe4b073a80cd253c5/547662ffe4b0ace1b3020903/918-05142015/1431607402131/site.css?&filterFeatures=false&noMedia=true&part=4\"/><![endif]-->\n<!--[if !IE]> --><link rel=\"stylesheet\" type=\"text/css\" href=\"//static1.squarespace.com/static/sitecss/54765ba7e4b0d055ee0b47a6/81/52a74dafe4b073a80cd253c5/547662ffe4b0ace1b3020903/918-05142015/1431607402131/site.css\"><!-- <![endif]-->\n\n\n    <style type=\"text/css\">.disable-hover:not(.sqs-layout-editing), .disable-hover:not(.sqs-layout-editing) * { pointer-events: none  ; }</style>\n\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/components/core-min.js\"></script>\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/components/sha256-min.js\"></script>\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/components/enc-base64-min.js\"></script>\n\n</head>\n\n  <body id=\"collection-555496aae4b071d194a427ab\" class=\"nav-button-style-raised nav-button-corner-style-rounded banner-button-style-raised banner-button-corner-style-rounded banner-slideshow-controls-arrows meta-priority-date  hide-entry-author hide-list-entry-footer    hide-blog-sidebar   center-navigation--info  gallery-design-grid aspect-ratio-auto lightbox-style-light gallery-navigation-bullets gallery-info-overlay-show-on-hover gallery-aspect-ratio-32-standard gallery-arrow-style-no-background gallery-transitions-fade gallery-show-arrows gallery-auto-crop   product-design-version-is-legacy product-list-gallery-grid product-list-layout-ratio-thirds product-item-size-11-square product-image-auto-crop product-list-titles-under product-list-alignment-center product-list-text-alignment-left  show-product-price product-item-gallery-slideshow product-item-image-alignment-left product-item-carousel-crop product-gallery-size-11-square  product-item-stacked-banner product-item-image-zoom product-item-thumbnails-left product-item-text-alignment-left product-item-excerpt-alignment-left product-item-excerpt-position-middle show-product-item-nav product-social-sharing product-list-status-rectangle product-list-status-placement-top-right product-list-status-flag-placement-top-right product-list-status-banner-placement-top   event-show-past-events event-thumbnails event-thumbnail-size-32-standard event-date-label  event-list-show-cats event-list-date event-list-time event-list-address   event-icalgcal-links  event-excerpts       hide-opentable-icons opentable-style-dark newsletter-style-light small-button-style-outline small-button-shape-rounded medium-button-style-raised medium-button-shape-rounded large-button-style-raised large-button-shape-rounded button-style-raised button-corner-style-rounded native-currency-code-usd collection-555496aae4b071d194a427ab collection-type-page collection-layout-default mobile-style-available\">\n    <a href=\"http://www.blynk.cc/password#\" class=\"body-overlay\"></a>\n    <div class=\"sqs-announcement-bar-dropzone\"></div>\n    <div id=\"sidecarNav\">\n      <div id=\"mobileNavWrapper\" class=\"nav-wrapper\" data-content-field=\"navigation-mobileNav\">\n          <nav id=\"mobileNavigation\">\n              <div class=\"collection homepage\">\n                <a href=\"http://www.blynk.cc/\">\n                  Home\n                </a>\n              </div>\n\n              <div class=\"collection\">\n                <a href=\"http://www.blynk.cc/getting-started/\">\n                  Getting started\n                </a>\n              </div>\n\n              <div class=\"collection\">\n                <a href=\"http://www.blynk.cc/faq/\">\n                  FAQ\n                </a>\n              </div>\n\n              <div class=\"external\">\n                <a href=\"http://community.blynk.cc/\">\n                  Community\n                </a>\n              </div>\n          </nav>\n      </div>\n    </div>\n\n    <div id=\"siteWrapper\" class=\"clearfix\">\n        <div class=\"sqs-cart-dropzone\" style=\"top: 95px;\">\n            <div id=\"yui_3_17_2_1_1431850947577_187\" class=\"yui3-widget sqs-widget sqs-pill-shopping-cart sqs-scalable-hidden\" style=\"visibility: hidden; opacity: 0; transform: scale(0.94);\">\n                <div id=\"yui_3_17_2_1_1431850947577_189\" class=\"sqs-pill-shopping-cart-content dark\">\n                    <div class=\"icon\">\n                    </div>\n                    <div class=\"details\">\n                        Cart&nbsp;-&nbsp;\n                        <span class=\"total-quantity\">0</span>\n                        <span class=\"suffix\">items</span>\n                    </div>\n\n                    <span class=\"subtotal\">\n                      <span class=\"price\"><span class=\"sqs-money-native\">0.00</span></span>\n                    </span>\n                </div>\n            </div>\n        </div>\n\n      <header id=\"header\" class=\"show-on-scroll\" data-offset-el=\".index-section\" data-offset-behavior=\"bottom\" role=\"banner\">\n        <div class=\"header-inner\">\n          <div id=\"siteTitleWrapper\" class=\"wrapper\" data-content-field=\"site-title\">\n            \n              <h1 id=\"siteTitle\" class=\"site-title\"><a href=\"http://www.blynk.cc/\">Blynk</a></h1>\n            \n          </div>\n            <div id=\"headerNav\"><div id=\"mainNavWrapper\" class=\"nav-wrapper\" data-content-field=\"navigation-mainNav\">\n                <nav id=\"mainNavigation\" data-content-field=\"navigation-mainNav\">\n\n                  <div class=\"collection homepage\">\n                    <a href=\"http://www.blynk.cc/\">\n                      Home\n                    </a>\n                  </div>\n\n                  <div class=\"collection\">\n                    <a href=\"http://www.blynk.cc/getting-started/\">\n                      Getting started\n                    </a>\n                  </div>\n\n                  <div class=\"collection\">\n                    <a href=\"http://www.blynk.cc/faq/\">\n                      FAQ\n                    </a>\n                  </div>\n\n                    <div class=\"external\">\n                      <a href=\"http://community.blynk.cc/\">\n                        Community\n                      </a>\n                    </div>\n                </nav>\n\n            </div>\n\n<!-- style below blocks out the mobile nav toggle only when nav is loaded -->\n<style>.mobile-nav-toggle-label { display: inline-block !important; }</style>\n\n\n</div>\n        </div>\n      </header>\n\n                <div id=\"promotedGalleryWrapper\" class=\"sqs-layout promoted-gallery-wrapper\">\n                    <div class=\"row\">\n                        <div class=\"col\">\n\n                        </div>\n                    </div>\n                </div>\n\n\n      <main id=\"page\" role=\"main\">\n        \n        <!-- comment the linebreak between these two elements because science\n        --><!-- comment the linebreak between these two elements because science\n        --><!-- comment the linebreak between these two elements because science\n        --><div id=\"content\" class=\"main-content\" data-content-field=\"main-content\" data-collection-id=\"555496aae4b071d194a427ab\" data-edit-main-image=\"Banner\">\n         <div class=\"sqs-layout sqs-grid-12 columns-12\" data-type=\"page\" data-updated-on=\"1431610820672\" id=\"page-555496aae4b071d194a427ab\"><div class=\"row sqs-row\" id=\"yui_3_17_2_1_1431850947577_298\"><div class=\"col sqs-col-12 span-12\" id=\"yui_3_17_2_1_1431850947577_297\"><div class=\"sqs-block spacer-block sqs-block-spacer sized vsize-1\" data-block-type=\"21\" id=\"block-yui_3_17_2_4_1431606871172_25466\"><div class=\"sqs-block-content\">&nbsp;</div></div><div class=\"sqs-block html-block sqs-block-html\" data-block-type=\"2\" id=\"block-yui_3_17_2_4_1431606871172_24372\"><div class=\"sqs-block-content\"><h2>reset your password</h2></div></div><div class=\"row sqs-row\" id=\"yui_3_17_2_1_1431850947577_296\"><div class=\"col sqs-col-6 span-6\" id=\"yui_3_17_2_1_1431850947577_295\"><div class=\"sqs-block form-block sqs-block-form\" data-block-type=\"9\" id=\"block-yui_3_17_2_4_1431606871172_4671\"><div class=\"sqs-block-content\" id=\"yui_3_17_2_1_1431850947577_294\">\n\n<div class=\"form-wrapper\">\n\n    <div class=\"form-inner-wrapper\" id=\"yui_3_17_2_1_1431850947577_292\">\n\n      <form autocomplete=\"on\" action=\"updatePassword\" method=\"POST\" id=\"form_login\" onsubmit=\"encryptPassword();\">\n          <div class=\"field-list clear\">\n                <div class=\"form-item field password required\">\n                  <label class=\"title\" for=\"password\">New Password <span class=\"required\">*</span></label>\n                  \n                  <input class=\"field-element\" type=\"password\" id=\"password\" name=\"password\">\n                </div>\n\n                <div class=\"form-item field password required\">\n                  <label class=\"title\" for=\"password_re\">Confirm New Password <span class=\"required\">*</span></label>\n                  \n                  <input class=\"field-element\" type=\"password\" id=\"password_re\" onkeyup=\"checkPass(); return false;\">\n                    <span id=\"confirmMessage\" class=\"confirmMessage\"></span>\n                </div>\n                </div>\n\n              <div>\n                  <input type=\"hidden\" id=\"token\" name=\"token\" value=\"{TOKEN}\">\n              </div>\n              <div>\n                  <input type=\"hidden\" id=\"email\" name=\"email\" value=\"{EMAIL}\">\n              </div>\n\n        <div class=\"form-button-wrapper form-button-wrapper--align-left\">\n          <input class=\"button sqs-system-button sqs-editable-button\" type=\"submit\" value=\"Reset Password\">\n        </div>\n        \n\n        <div class=\"hidden form-submission-text\"><p>Your password has been reset</p></div>\n\n        <div class=\"hidden form-submission-html\" data-submission-html=\"\"></div>\n      </form>\n\n    </div>\n</div>\n</div>\n         </div>\n         </div>\n             <div class=\"col sqs-col-6 span-6\" id=\"yui_3_17_2_1_1431850947577_338\">\n                 <div class=\"sqs-block spacer-block sqs-block-spacer\" data-aspect-ratio=\"47.288503253796094\" data-block-type=\"21\" id=\"block-yui_3_17_2_4_1431606871172_20828\">\n                     <div class=\"sqs-block-content sqs-intrinsic\" id=\"yui_3_17_2_1_1431850947577_170\" style=\"padding-bottom: 47.2885032537961%;\">&nbsp;\n                     </div>\n                 </div>\n             </div>\n         </div>\n         </div>\n         </div>\n         </div>\n        </div>\n        \n      </main>\n\n      <div id=\"preFooter\">\n        <div class=\"pre-footer-inner\">\n          <div class=\"sqs-layout sqs-grid-12 columns-12 empty\" data-layout-label=\"Pre-Footer Content\" data-type=\"block-field\" data-updated-on=\"1420592867852\">\n              <div class=\"row sqs-row\">\n                  <div class=\"col sqs-col-12 span-12\">\n                  </div>\n              </div>\n          </div>\n        </div>\n      </div>\n\n      <footer id=\"footer\" role=\"contentinfo\">\n        <div class=\"footer-inner\">\n          <div class=\"nav-wrapper back-to-top-nav\"><nav><div class=\"back-to-top\"><a href=\"http://www.blynk.cc/password#header\">Back to Top</a></div></nav></div>\n          \n\n\n          \n          <div class=\"sqs-layout sqs-grid-12 columns-12\" data-layout-label=\"Footer Content\" data-type=\"block-field\" data-updated-on=\"1431552584967\">\n              <div class=\"row sqs-row\"><div class=\"col sqs-col-12 span-12\"><div class=\"row sqs-row\"><div class=\"col sqs-col-2 span-2\">\n                  <div class=\"sqs-block html-block sqs-block-html\" data-block-type=\"2\" id=\"block-yui_3_17_2_21_1424011885501_52324\">\n                      <div class=\"sqs-block-content\"><p><strong>Blynk</strong></p><p><a href=\"http://blynk.cc/\">Home</a><br><a href=\"http://community.blynk.cc/\">Forum</a><br>Blog (soon)</p>\n                      </div>\n                  </div>\n              </div>\n                  <div class=\"col sqs-col-3 span-3\">\n                  <div class=\"sqs-block html-block sqs-block-html\" data-block-type=\"2\" id=\"block-yui_3_17_2_21_1424011885501_8783\">\n                      <div class=\"sqs-block-content\"><p><strong>Help (Soon)</strong></p><p><a href=\"http://www.blynk.cc/getting-started/\">Getting started</a><br><a target=\"_blank\" href=\"https://github.com/blynkkk\">Downloads</a><br>Documentation<br><a href=\"http://community.blynk.cc/t/hardware-supported-by-blynk/16\">Supported Hardware</a></p>\n                      </div>\n                  </div>\n                  </div>\n                  <div class=\"col sqs-col-7 span-7\">\n                      <div class=\"sqs-block html-block sqs-block-html\" data-block-type=\"2\" id=\"block-yui_3_17_2_21_1424011885501_5446\">\n                          <div class=\"sqs-block-content\"><p><strong>Social Media</strong></p><p><a target=\"_blank\" href=\"https://www.facebook.com/blynkapp\">Facebook</a><br><a target=\"_blank\" href=\"https://twitter.com/blynk_app\"><span>Twitter</span></a></p><p>&nbsp;</p>\n                          </div>\n                      </div>\n                  </div>\n              </div>\n              </div>\n              </div>\n          </div>\n        </div>\n      </footer>\n    </div>\n  <script type=\"text/javascript\">\n      function checkPass()\n      {\n          //Store the password field objects into variables ...\n          var pass1 = document.getElementById('password');\n          var pass2 = document.getElementById('password_re');\n          //Store the Confimation StringMessage Object ...\n          var message = document.getElementById('confirmMessage');\n          //Set the colors we will be using ...\n          var goodColor = \"#66cc66\";\n          var badColor = \"#ff6666\";\n          //Compare the values in the password field\n          //and the confirmation field\n          if(pass1.value == pass2.value){\n              //The passwords match.\n              //Set the color to the good color and inform\n              //the user that they have entered the correct password\n              message.style.color = goodColor;\n              message.innerHTML = \"Passwords Match!\"\n          }else{\n              //The passwords do not match.\n              //Set the color to the bad color and\n              //notify the user.\n              message.style.color = badColor;\n              message.innerHTML = \"Passwords Do Not Match!\"\n          }\n      }\n  </script>\n  <script type=\"text/javascript\">\n      function encryptPassword() {\n          var email = document.getElementById(\"email\").value;\n          var password = document.getElementById(\"password\").value;\n          var algo = CryptoJS.algo.SHA256.create();\n          algo.update(password, 'utf-8');\n          algo.update(CryptoJS.SHA256(email.toLowerCase()), 'utf-8');\n          var hash = algo.finalize();\n          var final = hash.toString(CryptoJS.enc.Base64);\n          document.getElementById(\"password\").value = final;\n      }\n  </script>\n</body>\n</html>\n"
  },
  {
    "path": "server/http-api/src/main/resources/static/reset/reset-email-app-confirmation.html",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"\n        \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n    <meta name=\"viewport\" content=\"width=device-width\"/>\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"/>\n    <style type=\"text/css\">\n        * {\n            margin: 0;\n            padding: 0;\n            font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;\n            font-size: 100%;\n            line-height: 1.6;\n        }\n\n        img {\n            max-width: 100%;\n        }\n\n        body {\n            -webkit-font-smoothing: antialiased;\n            -webkit-text-size-adjust: none;\n            width: 100% !important;\n            height: 100%;\n        }\n\n        a {\n            color: #348eda;\n        }\n\n        .btn-primary {\n            text-decoration: none;\n            color: #FFF;\n            background-color: #24C48E;\n            border: solid #24C48E;\n            border-width: 10px 20px;\n            line-height: 1;\n            font-weight: bold;\n            text-align: center;\n            cursor: pointer;\n            display: inline-block;\n            border-radius: 25px;\n        }\n\n        .padding {\n            padding: 10px 0;\n        }\n\n        table.body-wrap {\n            width: 100%;\n            padding: 20px;\n        }\n\n        table.body-wrap .container {\n            border: 1px solid #f0f0f0;\n        }\n\n        .footer-wrap .container p {\n            font-size: 12px;\n            color: #666;\n\n        }\n\n        table.footer-wrap a {\n            color: #999;\n        }\n\n        h1, h2, h3 {\n            font-family: \"Helvetica Neue\", Helvetica, Arial, \"Lucida Grande\", sans-serif;\n            color: #000;\n            margin: 40px 0 10px;\n            line-height: 1.2;\n            font-weight: 200;\n        }\n\n        h1 {\n            font-size: 36px;\n        }\n\n        h2 {\n            font-size: 28px;\n        }\n\n        h3 {\n            font-size: 22px;\n        }\n\n        p, ul, ol {\n            margin-bottom: 10px;\n            font-weight: normal;\n            font-size: 14px;\n        }\n\n        ul li, ol li {\n            margin-left: 5px;\n            list-style-position: inside;\n        }\n\n        .container {\n            display: block !important;\n            max-width: 600px !important;\n            margin: 0 auto !important; /* makes it centered */\n            clear: both !important;\n        }\n\n        .body-wrap .container {\n            padding: 20px;\n        }\n\n        .content {\n            max-width: 600px;\n            margin: 0 auto;\n            display: block;\n        }\n\n        .content table {\n            width: 100%;\n        }\n    </style>\n</head>\n\n<body bgcolor=\"#f6f6f6\">\n\n<table class=\"body-wrap\" bgcolor=\"#f6f6f6\">\n    <tr>\n        <td></td>\n        <td class=\"container\" bgcolor=\"#FFFFFF\">\n            <div class=\"content\">\n                <table>\n                    <tr>\n                        <td>\n                            <p><b>You have changed your password on {PRODUCT_NAME}.</b> Please, keep it in your records so you don't forget it.</p>\n\n                            <p>Your email address: {EMAIL}</p>\n\n                            <p>Thank you,<br>\n                               The {PRODUCT_NAME} team</p>\n                        </td>\n                    </tr>\n                </table>\n            </div>\n        </td>\n        <td></td>\n    </tr>\n</table>\n\n</body>\n</html>"
  },
  {
    "path": "server/http-api/src/main/resources/static/reset/reset-email-app.html",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"\n        \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n    <meta name=\"viewport\" content=\"width=device-width\"/>\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"/>\n    <style type=\"text/css\">\n        * {\n            margin: 0;\n            padding: 0;\n            font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;\n            font-size: 100%;\n            line-height: 1.6;\n        }\n\n        img {\n            max-width: 100%;\n        }\n\n        body {\n            -webkit-font-smoothing: antialiased;\n            -webkit-text-size-adjust: none;\n            width: 100% !important;\n            height: 100%;\n        }\n\n        a {\n            color: #348eda;\n        }\n\n        .btn-primary {\n            text-decoration: none;\n            color: #FFF;\n            background-color: #24C48E;\n            border: solid #24C48E;\n            border-width: 10px 20px;\n            line-height: 1;\n            font-weight: bold;\n            text-align: center;\n            cursor: pointer;\n            display: inline-block;\n            border-radius: 25px;\n        }\n\n        .padding {\n            padding: 10px 0;\n        }\n\n        table.body-wrap {\n            width: 100%;\n            padding: 20px;\n        }\n\n        table.body-wrap .container {\n            border: 1px solid #f0f0f0;\n        }\n\n        .footer-wrap .container p {\n            font-size: 12px;\n            color: #666;\n\n        }\n\n        table.footer-wrap a {\n            color: #999;\n        }\n\n        h1, h2, h3 {\n            font-family: \"Helvetica Neue\", Helvetica, Arial, \"Lucida Grande\", sans-serif;\n            color: #000;\n            margin: 40px 0 10px;\n            line-height: 1.2;\n            font-weight: 200;\n        }\n\n        h1 {\n            font-size: 36px;\n        }\n\n        h2 {\n            font-size: 28px;\n        }\n\n        h3 {\n            font-size: 22px;\n        }\n\n        p, ul, ol {\n            margin-bottom: 10px;\n            font-weight: normal;\n            font-size: 14px;\n        }\n\n        ul li, ol li {\n            margin-left: 5px;\n            list-style-position: inside;\n        }\n\n        .container {\n            display: block !important;\n            max-width: 600px !important;\n            margin: 0 auto !important; /* makes it centered */\n            clear: both !important;\n        }\n\n        .body-wrap .container {\n            padding: 20px;\n        }\n\n        .content {\n            max-width: 600px;\n            margin: 0 auto;\n            display: block;\n        }\n\n        .content table {\n            width: 100%;\n        }\n    </style>\n</head>\n\n<body bgcolor=\"#f6f6f6\">\n\n<table class=\"body-wrap\" bgcolor=\"#f6f6f6\">\n    <tr>\n        <td></td>\n        <td class=\"container\" bgcolor=\"#FFFFFF\">\n            <div class=\"content\">\n                <table>\n                    <tr>\n                        <td>\n                            <p>Hi there,</p>\n\n                            <p>We got a request to reset your {PRODUCT_NAME} password.\n                            If you didn't request password change simply ignore it, otherwise click on the button below from <b>your smartphone or tablet</b>\n                                and select {PRODUCT_NAME} app.\n                            </p>\n                            <table>\n                                <tr>\n                                    <td class=\"padding\">\n                                        <p><a href=\"{RESET_URL}\" class=\"btn-primary\">Reset Now</a></p>\n                                    </td>\n                                </tr>\n                            </table>\n\n                            <p>You can also scan the attached QR code from {PRODUCT_NAME} app.</p>\n\n                            <p>Yours, {PRODUCT_NAME} team.</p>\n                        </td>\n                    </tr>\n                </table>\n            </div>\n        </td>\n        <td></td>\n    </tr>\n</table>\n\n</body>\n</html>"
  },
  {
    "path": "server/http-api/src/main/resources/static/reset/reset-email.html",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"\n        \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n    <meta name=\"viewport\" content=\"width=device-width\"/>\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"/>\n    <style type=\"text/css\">\n        /* -------------------------------------\n                GLOBAL\n        ------------------------------------- */\n        * {\n            margin: 0;\n            padding: 0;\n            font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;\n            font-size: 100%;\n            line-height: 1.6;\n        }\n\n        img {\n            max-width: 100%;\n        }\n\n        body {\n            -webkit-font-smoothing: antialiased;\n            -webkit-text-size-adjust: none;\n            width: 100% !important;\n            height: 100%;\n        }\n\n        /* -------------------------------------\n                ELEMENTS\n        ------------------------------------- */\n        a {\n            color: #348eda;\n        }\n\n        .btn-primary {\n            text-decoration: none;\n            color: #FFF;\n            background-color: #24C48E;\n            border: solid #24C48E;\n            border-width: 10px 20px;\n            line-height: 1;\n            font-weight: bold;\n            text-align: center;\n            cursor: pointer;\n            display: inline-block;\n            border-radius: 25px;\n        }\n\n        .padding {\n            padding: 10px 0;\n        }\n\n        /* -------------------------------------\n                BODY\n        ------------------------------------- */\n        table.body-wrap {\n            width: 100%;\n            padding: 20px;\n        }\n\n        table.body-wrap .container {\n            border: 1px solid #f0f0f0;\n        }\n\n        /* -------------------------------------\n                FOOTER\n        ------------------------------------- */\n\n        .footer-wrap .container p {\n            font-size: 12px;\n            color: #666;\n\n        }\n\n        table.footer-wrap a {\n            color: #999;\n        }\n\n        /* -------------------------------------\n                TYPOGRAPHY\n        ------------------------------------- */\n        h1, h2, h3 {\n            font-family: \"Helvetica Neue\", Helvetica, Arial, \"Lucida Grande\", sans-serif;\n            color: #000;\n            margin: 40px 0 10px;\n            line-height: 1.2;\n            font-weight: 200;\n        }\n\n        h1 {\n            font-size: 36px;\n        }\n\n        h2 {\n            font-size: 28px;\n        }\n\n        h3 {\n            font-size: 22px;\n        }\n\n        p, ul, ol {\n            margin-bottom: 10px;\n            font-weight: normal;\n            font-size: 14px;\n        }\n\n        ul li, ol li {\n            margin-left: 5px;\n            list-style-position: inside;\n        }\n\n        /* ---------------------------------------------------\n                RESPONSIVENESS\n                Nuke it from orbit. It's the only way to be sure.\n        ------------------------------------------------------ */\n        /* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */\n        .container {\n            display: block !important;\n            max-width: 600px !important;\n            margin: 0 auto !important; /* makes it centered */\n            clear: both !important;\n        }\n\n        /* Set the padding on the td rather than the div for Outlook compatibility */\n        .body-wrap .container {\n            padding: 20px;\n        }\n\n        /* This should also be a block element, so that it will fill 100% of the .container */\n        .content {\n            max-width: 600px;\n            margin: 0 auto;\n            display: block;\n        }\n\n        /* Let's make sure tables in the content area are 100% wide */\n        .content table {\n            width: 100%;\n        }\n    </style>\n</head>\n\n<body bgcolor=\"#f6f6f6\">\n\n<!-- body -->\n<table class=\"body-wrap\" bgcolor=\"#f6f6f6\">\n    <tr>\n        <td></td>\n        <td class=\"container\" bgcolor=\"#FFFFFF\">\n\n            <!-- content -->\n            <div class=\"content\">\n                <table>\n                    <tr>\n                        <td>\n                            <p>Hi there,</p>\n\n                            <p>You recently made a request to reset your password for the {PRODUCT_NAME} app. To complete the process, click the link below.</p>\n                            <p>If you did not request a password reset from {PRODUCT_NAME}, please ignore this message.</p>\n\n                            <table>\n                                <tr>\n                                    <td class=\"padding\">\n                                        <p><a href=\"{RESET_URL}\" class=\"btn-primary\">Reset Now</a></p>\n                                    </td>\n                                </tr>\n                            </table>\n                        </td>\n                    </tr>\n                </table>\n            </div>\n            <!-- /content -->\n\n        </td>\n        <td></td>\n    </tr>\n</table>\n<!-- /body -->\n\n</body>\n</html>"
  },
  {
    "path": "server/http-api/src/main/resources/static/reset/site.css",
    "content": "/*! Squarespace LESS Compiler  (less.js language v1.3.3)  */\n/*! normalize.css v2.1.3 | MIT License | git.io/normalize */\narticle,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,video{display:inline-block}audio:not([controls]){display:none;height:0}[hidden],template{display:none}html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}a{background:transparent}a:focus{outline:thin dotted}a:active,a:hover{outline:0}h1{font-size:2em;margin:.67em 0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}mark{background:#ff0;color:#000}code,kbd,pre,samp{font-family:monospace,serif;font-size:1em}pre{white-space:pre-wrap}q{quotes:\"\\201C\" \"\\201D\" \"\\2018\" \"\\2019\"}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:0}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}button,input,select,textarea{font-family:inherit;font-size:100%;margin:0}button,input{line-height:normal}button,select{text-transform:none}button,html input[type=\"button\"],input[type=\"reset\"],input[type=\"submit\"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}input[type=\"checkbox\"],input[type=\"radio\"]{box-sizing:border-box;padding:0}input[type=\"search\"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type=\"search\"]::-webkit-search-cancel-button,input[type=\"search\"]::-webkit-search-decoration{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}textarea{overflow:auto;vertical-align:top}table{border-collapse:collapse;border-spacing:0}nav ul{list-style-type:none;margin:0;padding:0}\n/*! Squarespace LESS Compiler  (less.js language v1.3.3)  */\n.clear{zoom:1}.clear:after{display:block;visibility:hidden;font-size:0;height:0;clear:both;content:\".\"}@-webkit-keyframes spin-frames{from{-webkit-transform:rotate(0deg);-webkit-animation-timing-function:linear}to{-webkit-transform:rotate(360deg);-webkit-animation-timing-function:linear}}@-moz-keyframes spin-frames{from{-moz-transform:rotate(0deg);-moz-animation-timing-function:linear}to{-moz-transform:rotate(360deg);-moz-animation-timing-function:linear}}.sqs-lightbox-signup-spinner{position:fixed !important;left:50% !important;margin-top:-150px !important;margin-left:-150px !important;width:300px !important;height:300px !important}.squarespace-signup-text{font-family:'proxima-nova','Helvetica Neue',Helvetica,Arial,sans-serif;color:#fff;width:300px;text-align:center;padding-top:15px;line-height:21px;font-size:15px;padding-bottom:100px}.squarespace-signup-text .join-thank-you{font-weight:bold;padding-bottom:20px}.squarespace-signup-spinner{background:transparent url('//static.squarespace.com/universal/images-v6/big-gear.png') center center no-repeat;width:300px !important;height:220px !important;-webkit-animation-duration:2s;-moz-animation-duration:2s;-o-animation-duration:2s;animation-duration:2s;-webkit-animation-iteration-count:infinite;-moz-animation-iteration-count:infinite;-o-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-name:spin-frames;-moz-animation-name:spin-frames;-o-animation-name:spin-frames;animation-name:spin-frames}.squarespace-signup-spinner.stopped{-webkit-animation-name:stopped;-moz-animation-name:stopped;-o-animation-name:stopped;animation-name:stopped}.sqs-lightbox.light .squarespace-signup-text{font:12px / 22px 'Gotham SSm A','Gotham SSm B','Gotham SSm','Gotham','Proxima Nova','Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif !important;background-color:transparent !important;letter-spacing:0 !important;display:block !important;float:none !important;color:#000 !important;height:auto !important;width:auto !important;margin:0 !important;padding:0 !important;text-transform:none !important;color:#3e3e3e}.sqs-lightbox.light .squarespace-signup-spinner{background:transparent url('//static.squarespace.com/universal/images-v6/big-gear-dark.png') center center no-repeat}.sqs-g{letter-spacing:-.31em;*letter-spacing:normal;*word-spacing:-.43em;text-rendering:optimizespeed}.opera-only :-o-prefocus,.sqs-g{word-spacing:-.43em}.yui3-u,.sqs-u{display:inline-block;zoom:1;*display:inline;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.sqs-u-1,.sqs-u-1-2,.sqs-u-1-3,.sqs-u-2-3,.sqs-u-1-4,.sqs-u-3-4,.sqs-u-1-5,.sqs-u-2-5,.sqs-u-3-5,.sqs-u-4-5,.sqs-u-1-6,.sqs-u-5-6,.sqs-u-1-8,.sqs-u-3-8,.sqs-u-5-8,.sqs-u-7-8,.sqs-u-1-12,.sqs-u-5-12,.sqs-u-7-12,.sqs-u-11-12,.sqs-u-1-24,.sqs-u-5-24,.sqs-u-7-24,.sqs-u-11-24,.sqs-u-13-24,.sqs-u-17-24,.sqs-u-19-24,.sqs-u-23-24{display:inline-block;zoom:1;*display:inline;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.sqs-u-1{display:block}.sqs-u-1-2{width:50%}.sqs-u-1-3{width:33.33333%}.sqs-u-2-3{width:66.66666%}.sqs-u-1-4{width:25%}.sqs-u-3-4{width:75%}.sqs-u-1-5{width:20%}.sqs-u-2-5{width:40%}.sqs-u-3-5{width:60%}.sqs-u-4-5{width:80%}.sqs-u-1-6{width:16.656%}.sqs-u-5-6{width:83.33%}.sqs-u-1-8{width:12.5%}.sqs-u-3-8{width:37.5%}.sqs-u-5-8{width:62.5%}.sqs-u-7-8{width:87.5%}.sqs-u-1-12{width:8.3333%}.sqs-u-5-12{width:41.6666%}.sqs-u-7-12{width:58.3333%}.sqs-u-11-12{width:91.6666%}.sqs-u-1-24{width:4.1666%}.sqs-u-5-24{width:20.8333%}.sqs-u-7-24{width:29.1666%}.sqs-u-11-24{width:45.8333%}.sqs-u-13-24{width:54.1666%}.sqs-u-17-24{width:70.8333%}.sqs-u-19-24{width:79.1666%}.sqs-u-23-24{width:95.8333%}#sqs-css-stamp.cssgrids{display:none}.yui3-widget-hidden{display:none}.yui3-widget-content{overflow:hidden}.yui3-widget-content-expanded{box-sizing:border-box;height:100%}.yui3-widget-tmp-forcesize{overflow:hidden !important}.sqs-panel{position:absolute}.sqs-panel-hidden{visibility:hidden}.sqs-widget-tmp-forcesize .sqs-panel-content{overflow:hidden !important}.sqs-panel .sqs-widget-hd{position:relative}.sqs-panel .sqs-widget-hd .sqs-widget-buttons{position:absolute;top:0;right:0}.sqs-panel .sqs-widget-ft .sqs-widget-buttons{display:inline-block;*display:inline;zoom:1}.yui3-slider,.yui3-slider-rail{display:-moz-inline-stack;display:inline-block;*display:inline;zoom:1;vertical-align:middle}.yui3-slider-content{position:relative;display:block}.yui3-slider-rail{position:relative}.yui3-slider-rail-cap-top,.yui3-slider-rail-cap-left,.yui3-slider-rail-cap-bottom,.yui3-slider-rail-cap-right,.yui3-slider-thumb,.yui3-slider-thumb-image,.yui3-slider-thumb-shadow{position:absolute}.yui3-slider-thumb{overflow:hidden}.sqs-aclist,.yui3-aclist{position:absolute;z-index:10}.sqs-aclist-hidden,.yui3-aclist-hidden{visibility:hidden}.sqs-aclist-aria,.yui3-aclist-aria{left:-9999px;position:absolute}.sqs-aclist-list,.yui3-aclist-list{list-style:none;margin:0;overflow:hidden;padding:0}.sqs-aclist-item,.yui3-aclist-item{cursor:pointer;list-style:none;padding:2px 5px}.sqs-aclist-item-active,.yui3-aclist-item-active{outline:#afafaf dotted thin}body.native-currency-code-usd .sqs-money-native:before{content:'$'}body.native-currency-code-cad .sqs-money-native:before{content:'$'}body.native-currency-code-cad .sqs-money-native:after{content:' CAD'}body.native-currency-code-gbp .sqs-money-native:before{content:'£'}body.native-currency-code-eur .sqs-money-native:before{content:'€'}body.native-currency-code-aud .sqs-money-native:before{content:'$'}body.native-currency-code-aud .sqs-money-native:after{content:' AUD'}body.native-currency-code-chf .sqs-money-native:before{content:'CHF'}.sqs-follow-button-hidden{display:none}.sqs-system-error{color:#3e3e3e !important;background:transparent url('//static.squarespace.com/universal/images-v6/damask/error-dark.png') center center no-repeat;background-position:12px 12px}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:1.5dppx){.sqs-system-error{background-image:url('//static.squarespace.com/universal/images-v6/damask/error-dark@2x.png');background-size:44px}}.sqs-system-error input{cursor:pointer;outline:none;background:#3e3e3e;padding:11px;text-align:center;-webkit-transition:background-color .1s ease-in-out;transition:background-color .1s ease-in-out;line-height:22px;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none;font-family:inherit;-webkit-appearance:none;-moz-appearance:none;appearance:none}.sqs-system-error input,.sqs-system-error input>*{color:#fff !important;-webkit-appearance:none;border:0;text-transform:uppercase;outline:none;font-size:11px;font-weight:500}.sqs-system-error input:hover{background-color:#000;box-shadow:none}.sqs-system-error-overlay.dialog-screen-overlay{background:rgba(242,242,242,.98)}.password-prompt-overlay{z-index:50499;position:fixed;left:0;top:0;right:0;bottom:0;opacity:0;-webkit-transition:all .2s cubic-bezier(.645,.045,.355,1);transition:all .2s cubic-bezier(.645,.045,.355,1);background:#000;background:-webkit-gradient(radial,50% 25%,0,50% 25%,800,from(rgba(0,0,0,.85)),to(#000)) transparent;background:-moz-radial-gradient(center 45deg,circle cover,rgba(0,0,0,.85) 0%,#000 100%) transparent}.sqs-password-prompt{background:#f2f2f2;box-shadow:0 4px 33px rgba(0,0,0,.22),0 0 0 1px rgba(0,0,0,.04);position:absolute;left:50%;top:100px;margin-left:-160px;z-index:50500;-webkit-transition:all .2s cubic-bezier(.645,.045,.355,1);transition:all .2s cubic-bezier(.645,.045,.355,1);height:190px;width:320px;opacity:0;-webkit-transform:scale(.98);-moz-transform:scale(.98);-ms-transform:scale(.98);transform:scale(.98)}.sqs-password-prompt.shown{opacity:1;-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);transform:scale(1)}.sqs-password-prompt-content .title{text-transform:uppercase;font-weight:500;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none;border-bottom:1px solid #e2e2e2}.sqs-password-prompt-content .password{box-sizing:border-box;position:relative;background-color:#fff;padding:11px;line-height:22px;height:44px;width:100%;color:#3e3e3e;border:none}.sqs-password-prompt-content .password:focus{outline:none}.sqs-password-prompt-content .title,.sqs-password-prompt-content .fields{padding:16.5px 33px}.sqs-password-prompt-content .buttons{position:absolute;bottom:0;width:100%;border-top:1px solid #e4e4e4;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex}.sqs-password-prompt-content .buttons:empty{border-top:0}.sqs-password-prompt-content .buttons>*{-webkit-box-flex:1;-moz-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1;border-left:1px solid #e4e4e4 !important}.sqs-password-prompt-content .buttons>*:first-child{border-left:none !important}.sqs-password-prompt-content .buttons input,.sqs-password-prompt-content .buttons button{background:transparent}.sqs-password-prompt-content .buttons a{border-bottom:none}.sqs-password-prompt-content .buttons>*{cursor:pointer;outline:none;background:#f2f2f2;padding:11px;text-align:center;-webkit-transition:background-color .1s ease-in-out;transition:background-color .1s ease-in-out;line-height:22px;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none;font-family:inherit;-webkit-appearance:none;-moz-appearance:none;appearance:none}.sqs-password-prompt-content .buttons>*,.sqs-password-prompt-content .buttons>*>*{color:#3e3e3e !important;-webkit-appearance:none;border:0;text-transform:uppercase;outline:none;font-size:11px;font-weight:500}.sqs-password-prompt-content .buttons>*:hover{background-color:#fff;box-shadow:none}.sqs-block.form-block .pre-submit-back,.sqs-block.form-block .pre-submit-topics,.sqs-block.form-block .pre-submit-content,.sqs-block.form-block .pre-submit-form{display:none}.sqs-form-pre-submit-flow-content .pre-submit-back,.sqs-form-pre-submit-flow-content .pre-submit-topics,.sqs-form-pre-submit-flow-content .pre-submit-content,.sqs-form-pre-submit-flow-content .pre-submit-form{display:none}.sqs-form-pre-submit-flow-content[data-edit-mode=\"topics\"] .pre-submit-topics{display:block}.sqs-form-pre-submit-flow-content[data-edit-mode=\"content\"] .pre-submit-back,.sqs-form-pre-submit-flow-content[data-edit-mode=\"content\"] .pre-submit-content{display:block}.sqs-form-pre-submit-flow-content[data-edit-mode=\"form\"] .pre-submit-back,.sqs-form-pre-submit-flow-content[data-edit-mode=\"form\"] .pre-submit-form{display:block}.sqs-form-pre-submit-flow-content .pre-submit-topics .topic-wrapper .sub-topics{display:none}.sqs-form-pre-submit-flow-content .pre-submit-topics .topic-wrapper.open .sub-topics{display:block}.sqs-form-pre-submit-topic{margin-bottom:5.5px}.sqs-form-pre-submit-topic-content{position:relative}.sqs-form-pre-submit-topic-content .topic-control-bar{background:#e4e4e4;padding-left:55px;padding-right:88px;padding-top:5.5px;padding-bottom:5.5px}.sqs-form-pre-submit-topic-content .topic-control-bar input{padding:5.5px;height:33px;background:transparent;-webkit-transition:background .1s ease-in-out;transition:background .1s ease-in-out}.sqs-form-pre-submit-topic-content .topic-control-bar input:hover{background:rgba(255,255,255,.5)}.sqs-form-pre-submit-topic-content .topic-control-bar input:focus{background:#fff}.sqs-form-pre-submit-topic-content .editor-wrapper{padding:11px;border:1px solid #e4e4e4}.sqs-form-pre-submit-topic-content .sqs-dialog-field:first-child{margin-top:0}.sqs-form-pre-submit-topic-content .sqs-dialog-field:last-child{margin-bottom:0}.sqs-form-pre-submit-topic-content .edit-topic{position:absolute;right:38.5px;background:rgba(0,0,0,.1);top:11px;width:44px;text-align:center;cursor:pointer;text-transform:uppercase;font-size:9px;font-weight:500}.sqs-form-pre-submit-topic-content .remove-topic{position:absolute;right:0;top:0;height:44px;width:44px;text-indent:-9000px;overflow:hidden;background:transparent url('//static.squarespace.com/universal/images-v6/damask/trash-9-dark.png') center center no-repeat;cursor:pointer}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:1.5dppx){.sqs-form-pre-submit-topic-content .remove-topic{background-image:url('//static.squarespace.com/universal/images-v6/damask/trash-9-dark@2x.png');background-size:9px 11px}}.sqs-form-pre-submit-topic-content .remove-topic:hover{background:transparent url('//static.squarespace.com/universal/images-v6/damask/trash-9-red.png') center center no-repeat}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:1.5dppx){.sqs-form-pre-submit-topic-content .remove-topic:hover{background-image:url('//static.squarespace.com/universal/images-v6/damask/trash-9-red@2x.png');background-size:9px 11px}}.sqs-form-pre-submit-topic-content .topic-drag-handle{position:absolute;left:0;top:0;height:44px;width:22px;background:transparent url('//static.squarespace.com/universal/images-v6/damask/handle.png') center center no-repeat;opacity:.2;cursor:ns-resize}.sqs-form-pre-submit-topic-content.editing-mode-empty .topic-children .topic-child-controls{display:block}.sqs-form-pre-submit-topic-content.editing-mode-sub-topics .topic-children .topic-sub-topics{display:block}.sqs-form-pre-submit-topic-content.editing-mode-content .topic-children .topic-content{display:block}.sqs-form-pre-submit-topic .topic-children{padding:22px;background-color:#e4e4e4}.sqs-form-pre-submit-topic .topic-children .topic-label{padding-bottom:10px;border-bottom:1px solid #333;margin-bottom:10px}.sqs-form-pre-submit-topic .topic-children .topic-label .sqs-dialog-field.sqs-text.name-topicLabel input{font-size:11px;margin:0}.sqs-form-pre-submit-topic .topic-children .topic-child-controls,.sqs-form-pre-submit-topic .topic-children .topic-sub-topics,.sqs-form-pre-submit-topic .topic-children .topic-content{display:none}.sqs-form-pre-submit-topic .topic-children .topic-child-controls{zoom:1}.sqs-form-pre-submit-topic .topic-children .topic-child-controls:after{display:block;visibility:hidden;font-size:0;height:0;clear:both;content:\".\"}.sqs-form-pre-submit-topic .topic-children .topic-child-controls .topic-child-control-wrapper{float:left;width:50%;padding:20px 40px;box-sizing:border-box}.sqs-form-pre-submit-topic .topic-children .topic-child-controls .topic-child-control-wrapper .topic-child-control{text-align:center;background-color:#d0d0d0;cursor:pointer;padding:10px;display:block}.sqs-form-pre-submit-topic .topic-children .topic-sub-topics .sqs-form-pre-submit-topic-content .topic-control-bar{border:1px solid #d0d0d0;padding-left:30px}.sqs-form-pre-submit-topic .topic-children .topic-sub-topics .sqs-form-pre-submit-topic-content .topic-control-bar .topic-title .sqs-dialog-field.sqs-text input{width:215px}.sqs-form-pre-submit-topic .topic-children .topic-sub-topics .sqs-form-pre-submit-topic-content .topic-control-bar .edit-topic{width:70px}.sqs-form-pre-submit-topic .topic-children .topic-sub-topics .sqs-form-pre-submit-topic-content .topic-children .topic-child-controls,.sqs-form-pre-submit-topic .topic-children .topic-sub-topics .sqs-form-pre-submit-topic-content .topic-children .topic-sub-topics,.sqs-form-pre-submit-topic .topic-children .topic-sub-topics .sqs-form-pre-submit-topic-content .topic-children .topic-content{display:none}.sqs-form-pre-submit-topic .topic-children .topic-sub-topics .sqs-form-pre-submit-topic-content.editing-mode-empty .topic-child-controls{display:block}.sqs-form-pre-submit-topic .topic-children .topic-sub-topics .sqs-form-pre-submit-topic-content.editing-mode-sub-topics .topic-sub-topics{display:block}.sqs-form-pre-submit-topic .topic-children .topic-sub-topics .sqs-form-pre-submit-topic-content.editing-mode-content .topic-content{display:block}.sqs-form-pre-submit-topic .topic-children .topic-sub-topics .add-topic{padding:7px 7px 7px 25px;font-size:10px}.sqs-form-pre-submit-topic .topic-children .topic-sub-topics .add-topic .add-icon{top:9px;left:9px}@font-face{font-family:'squarespace-ui-font';src:url('//static.squarespace.com/universal/fonts/squarespace-ui-font.eot');src:url('//static.squarespace.com/universal/fonts/squarespace-ui-font.eot?#iefix') format('embedded-opentype'),url('//static.squarespace.com/universal/fonts/squarespace-ui-font.svg#squarespace-ui-font') format('svg'),url('//static.squarespace.com/universal/fonts/squarespace-ui-font.woff') format('woff'),url('//static.squarespace.com/universal/fonts/squarespace-ui-font.ttf') format('truetype');font-weight:normal;font-style:normal}.sqs-ui-font-family{font-family:'squarespace-ui-font';font-style:normal;speak:none;font-weight:normal;-webkit-font-smoothing:antialiased}[class^=\"sqs-ui-font-\"]:before,[class*=\" sqs-ui-font-\"]:before{font-family:'squarespace-ui-font';font-style:normal;speak:none;font-weight:normal;-webkit-font-smoothing:antialiased}[data-icon]:before{font-family:'squarespace-ui-font';font-style:normal;speak:none;font-weight:normal;-webkit-font-smoothing:antialiased;content:attr(data-icon)}.sqs-gallery-container a{border-bottom:0 !important}.sqs-gallery-container iframe{width:100%;height:100%;background:transparent;display:block}.sqs-gallery-controls .previous,.sqs-gallery-controls .next{position:absolute;top:50%;outline:none;color:#fff !important;z-index:999;font-size:14px;line-height:40px;margin-top:-30px;background-color:rgba(0,0,0,.12);display:inline-block;padding:10px;-webkit-transition:all 200ms cubic-bezier(.25,.46,.45,.94);-moz-transition:all 200ms cubic-bezier(.25,.46,.45,.94);-ms-transition:all 200ms cubic-bezier(.25,.46,.45,.94);-o-transition:all 200ms cubic-bezier(.25,.46,.45,.94);transition:all 200ms cubic-bezier(.25,.46,.45,.94)}.sqs-gallery-controls .previous:hover,.sqs-gallery-controls .next:hover{background-color:rgba(0,0,0,.2);color:#fff}.sqs-gallery-controls .previous{left:0px}.sqs-gallery-controls .previous:before{font-family:'squarespace-ui-font';font-style:normal;speak:none;font-weight:normal;-webkit-font-smoothing:antialiased;content:\"\\e02c\";text-align:center;display:inline-block;vertical-align:middle}.sqs-gallery-controls .previous:before{font-size:32px;width:32px;height:32px;line-height:32px}.sqs-gallery-controls .next{right:0px}.sqs-gallery-controls .next:before{font-family:'squarespace-ui-font';font-style:normal;speak:none;font-weight:normal;-webkit-font-smoothing:antialiased;content:\"\\e02d\";text-align:center;display:inline-block;vertical-align:middle}.sqs-gallery-controls .next:before{font-size:32px;width:32px;height:32px;line-height:32px}.sqs-gallery-controls .next:before,.sqs-gallery-controls .previous:before{font-size:24px;width:24px;height:24px;line-height:24px}.sqs-gallery-design-stacked{position:relative;text-align:left}.sqs-gallery-design-stacked-slide{position:absolute;top:0;left:0;width:100%;height:100%}.sqs-gallery-design-stacked-slide img{box-shadow:#000 0em 0em 0em}.sqs-gallery-design-stacked-slide.normal img{height:100%}.sqs-gallery-design-stacked-slide:only-child{cursor:default}.sqs-gallery-design-stacked-scrollHorz,.sqs-gallery-design-stacked-swipe{overflow:hidden}.sqs-gallery-design-stacked-scrollHorz .sqs-gallery-design-stacked-slide,.sqs-gallery-design-stacked-swipe .sqs-gallery-design-stacked-slide{position:relative;float:left}.sqs-gallery-design-stacked-swipe-wrapper{overflow-x:scroll;-webkit-transform:translatez(0);-ms-overflow-style:none;-ms-scroll-chaining:none;-ms-scroll-snap-type:mandatory;-ms-scroll-snap-points-x:snapinterval(0%,100%)}.sqs-gallery-design-strip{position:relative;overflow:hidden;height:100%}.sqs-gallery-design-strip .sqs-wrapper{position:relative;height:100%}.sqs-gallery-design-strip-slide{float:left;height:100% !important;max-width:none !important;width:auto !important;cursor:pointer;position:relative}.sqs-gallery-design-strip-slide .sqs-video-wrapper{height:100% !important}.sqs-gallery-design-strip-slide:only-child{cursor:default}.sqs-gallery-design-autocolumns{position:relative}.sqs-gallery-design-autocolumns-slide{position:absolute}.sqs-gallery-design-autocolumns-slide img{width:100%;display:inline-block;-webkit-transition:opacity .2s;transition:opacity .2s;opacity:1}.sqs-gallery-design-autocolumns-slide img.loading{opacity:0}.sqs-gallery-design-autocolumns-slide.content-fit img,.sqs-gallery-design-autocolumns-slide .content-fit img{width:auto}.sqs-gallery-design-autocolumns-slide.slide-stretched img{height:100%}.sqs-gallery-design-carousel .sqs-gallery-controls{overflow:hidden}.sqs-gallery-design-carousel .sqs-gallery-controls .next,.sqs-gallery-design-carousel .sqs-gallery-controls .previous{display:block;float:right;position:relative;top:auto;left:auto;right:auto;bottom:auto;margin:0 0 15px 0;padding:0;background-color:transparent;color:inherit !important;font-size:16px;line-height:16px;cursor:pointer}.sqs-gallery-design-carousel .sqs-gallery-controls.show-hover-effect .previous:hover,.sqs-gallery-design-carousel .sqs-gallery-controls.show-hover-effect .next:hover{background-color:transparent;color:#1d1d1d;opacity:1}.sqs-gallery-design-carousel .sqs-gallery-controls.show-hover-effect .sqs-disabled:hover{cursor:default;opacity:.4}.sqs-gallery-design-carousel .sqs-gallery-controls .next:before{font-family:'squarespace-ui-font';font-style:normal;speak:none;font-weight:normal;-webkit-font-smoothing:antialiased;content:\"\\e02d\";text-align:center;display:inline-block;vertical-align:middle}.sqs-gallery-design-carousel .sqs-gallery-controls .next:before{font-size:32px;width:32px;height:32px;line-height:32px}.sqs-gallery-design-carousel .sqs-gallery-controls .next:before{font-size:16px;width:16px;height:16px;line-height:16px}.sqs-gallery-design-carousel .sqs-gallery-controls .previous{margin-right:10px}.sqs-gallery-design-carousel .sqs-gallery-controls .previous:before{font-family:'squarespace-ui-font';font-style:normal;speak:none;font-weight:normal;-webkit-font-smoothing:antialiased;content:\"\\e02c\";text-align:center;display:inline-block;vertical-align:middle}.sqs-gallery-design-carousel .sqs-gallery-controls .previous:before{font-size:32px;width:32px;height:32px;line-height:32px}.sqs-gallery-design-carousel .sqs-gallery-controls .previous:before{font-size:16px;width:16px;height:16px;line-height:16px}.sqs-gallery-design-carousel .sqs-gallery-controls .sqs-disabled{cursor:default;opacity:.4}.sqs-gallery-design-carousel .sqs-gallery-controls .sqs-hidden{display:none}.sqs-gallery-design-carousel .sqs-gallery-container{width:100%;overflow:hidden}.sqs-gallery-design-carousel .sqs-gallery{margin:0 0 0 -1% !important;white-space:nowrap;vertical-align:top;font-size:0;-webkit-transition:-webkit-transform .4s ease;-moz-transition:-moz-transform .4s ease;-ms-transition:-ms-transform .4s ease;-o-transition:-o-transform .4s ease;transition:transform .4s ease}.sqs-gallery-design-carousel .sqs-gallery-design-carousel-slide{display:inline-block;width:33.66666666666667%;padding:0 1%;white-space:nowrap;vertical-align:top;font-size:0}.sqs-gallery-design-carousel .sqs-gallery-design-carousel-slide img{width:100%;height:auto}.sqs-gallery-design-carousel .sqs-gallery-design-carousel-slide *{white-space:normal}.sqs-gallery-design-carousel.sqs-gallery-design-carousel-slides-in-view-1 .sqs-gallery-design-carousel-slide{width:101%}.sqs-gallery-design-carousel.sqs-gallery-design-carousel-slides-in-view-2 .sqs-gallery-design-carousel-slide{width:50.5%}.sqs-gallery-design-carousel.sqs-gallery-design-carousel-slides-in-view-3 .sqs-gallery-design-carousel-slide:nth-child(3n+1),.sqs-gallery-design-carousel.sqs-gallery-design-carousel-slides-in-view-3 .sqs-gallery-design-carousel-slide:nth-child(3n+2){width:33.66%}.sqs-gallery-design-carousel.sqs-gallery-design-carousel-slides-in-view-3 .sqs-gallery-design-carousel-slide:nth-child(3n+3){width:33.68%}.sqs-gallery-design-carousel.sqs-gallery-design-carousel-slides-in-view-4 .sqs-gallery-design-carousel-slide{width:25.25%}.sqs-gallery-design-carousel.sqs-gallery-design-carousel-slides-in-view-5 .sqs-gallery-design-carousel-slide{width:20.2%}@media screen and (max-width:724px){.sqs-gallery-design-carousel.sqs-gallery-design-carousel-slides-in-view-4 .sqs-gallery-design-carousel-slide:nth-child(3n+1),.sqs-gallery-design-carousel.sqs-gallery-design-carousel-slides-in-view-4 .sqs-gallery-design-carousel-slide:nth-child(3n+2),.sqs-gallery-design-carousel.sqs-gallery-design-carousel-slides-in-view-5 .sqs-gallery-design-carousel-slide:nth-child(3n+1),.sqs-gallery-design-carousel.sqs-gallery-design-carousel-slides-in-view-5 .sqs-gallery-design-carousel-slide:nth-child(3n+2){width:33.66%}.sqs-gallery-design-carousel.sqs-gallery-design-carousel-slides-in-view-4 .sqs-gallery-design-carousel-slide:nth-child(3n+3),.sqs-gallery-design-carousel.sqs-gallery-design-carousel-slides-in-view-5 .sqs-gallery-design-carousel-slide:nth-child(3n+3){width:33.68%}}@media screen and (max-width:480px){.sqs-gallery-design-carousel.sqs-gallery-design-carousel-slides-in-view-3 .sqs-gallery-design-carousel-slide,.sqs-gallery-design-carousel.sqs-gallery-design-carousel-slides-in-view-4 .sqs-gallery-design-carousel-slide,.sqs-gallery-design-carousel.sqs-gallery-design-carousel-slides-in-view-5 .sqs-gallery-design-carousel-slide{width:50.5% !important}}.sqs-gallery-design-list .sqs-gallery-design-list-slide{overflow:hidden;margin-bottom:17px !important;padding-bottom:17px !important}.sqs-gallery-design-list .sqs-gallery-image-container{float:left;width:25%;padding-right:20px;box-sizing:border-box;-webkit-box-sizing:border-box;-moz-box-sizing:border-box}.sqs-gallery-design-list .sqs-gallery-meta-container{float:left;width:75%;box-sizing:border-box;-webkit-box-sizing:border-box;-moz-box-sizing:border-box}.sqs-gallery-design-list .sqs-gallery-design-list-slide.no-image .sqs-gallery-image-container{width:0 !important}.sqs-gallery-design-list .sqs-gallery-design-list-slide.no-image .sqs-gallery-meta-container{width:100% !important}@media screen and (max-width:480px){.sqs-gallery-design-list .sqs-gallery-design-list-slide:not(.no-image) .sqs-gallery-image-container{width:35% !important}.sqs-gallery-design-list .sqs-gallery-design-list-slide:not(.no-image) .sqs-gallery-meta-container{width:65% !important}}.sqs-gallery-design-autorows .sqs-gallery-design-autorows-slide{float:left;cursor:pointer;overflow:hidden}.sqs-gallery-design-autorows .sqs-gallery-design-autorows-slide img{height:100%}.sqs-gallery-design-autorows .sqs-gallery-design-autorows-slide .meta{display:none}.sqs-gallery-design-autogrid{zoom:1}.sqs-gallery-design-autogrid:after{display:block;visibility:hidden;font-size:0;height:0;clear:both;content:\".\"}.sqs-gallery-design-autogrid-slide{position:relative;float:left}.sqs-gallery-design-autogrid-slide .img-wrapper{height:0}.sqs-gallery-design-autogrid-slide img{width:100%}.yui3-lightbox2{-moz-user-select:text;-webkit-user-select:text;-ms-user-select:text;user-select:text}.yui3-lightbox2 .yui3-lightbox2-content{height:100%;left:0;position:absolute;width:100%;overflow:hidden}.yui3-lightbox2 .sqs-lightbox-slideshow{height:100%;opacity:0;z-index:100000001}.yui3-lightbox2 .sqs-lightbox-slideshow .sqs-lightbox-padder{position:absolute;text-align:left;top:2%;left:2%;bottom:2%;right:2%}.yui3-lightbox2 .sqs-lightbox-overlay{position:absolute;opacity:0;top:0;left:0;background:#000;height:100%;width:100%}.yui3-lightbox2 .sqs-lightbox-meta{position:absolute;padding:20px;color:#fff;z-index:100000001;margin:20px auto 0;-ms-filter:\"progid:DXImageTransform.Microsoft.Alpha(Opacity=0)\";opacity:0;-webkit-transition:opacity ease-out .2s;transition:opacity ease-out .2s}.yui3-lightbox2 .sqs-lightbox-meta.overlay-description-visible{background:#000;-ms-filter:\"progid:DXImageTransform.Microsoft.Alpha(Opacity=70)\";background:rgba(0,0,0,.7);opacity:1 !important}.yui3-lightbox2 .sqs-lightbox-meta p:first-child{margin-top:0}.yui3-lightbox2 .sqs-lightbox-meta p:last-child{margin-bottom:0}.yui3-lightbox2 .sqs-lightbox-meta h1{font-size:1em;color:#fff;margin:0 0 10px}.yui3-lightbox2 .sqs-lightbox-meta p a{color:#fff;text-decoration:underline}.yui3-lightbox2 .sqs-lightbox-close,.yui3-lightbox2 .sqs-lightbox-previous,.yui3-lightbox2 .sqs-lightbox-next,.yui3-lightbox2 .sqs-lightbox-meta-trigger{position:absolute;z-index:100000002;display:inline-block;color:#ccc;height:20px;width:20px;font-size:26px;cursor:pointer;outline:none}.yui3-lightbox2 .sqs-lightbox-next,.yui3-lightbox2 .sqs-lightbox-previous{padding:12px;opacity:0;top:50%;margin-top:-22px;-webkit-transition:opacity .2s;transition:opacity .2s}.yui3-lightbox2 .sqs-lightbox-next.mouseover,.yui3-lightbox2 .sqs-lightbox-previous.mouseover{opacity:1}.yui3-lightbox2 .sqs-lightbox-next{right:2%}.yui3-lightbox2 .sqs-lightbox-next:before{font-family:'squarespace-ui-font';font-style:normal;speak:none;font-weight:normal;-webkit-font-smoothing:antialiased;content:\"\\e02d\";text-align:center;display:inline-block;vertical-align:middle}.yui3-lightbox2 .sqs-lightbox-next:before{font-size:32px;width:32px;height:32px;line-height:32px}.yui3-lightbox2 .sqs-lightbox-previous{left:2%}.yui3-lightbox2 .sqs-lightbox-previous:before{font-family:'squarespace-ui-font';font-style:normal;speak:none;font-weight:normal;-webkit-font-smoothing:antialiased;content:\"\\e02c\";text-align:center;display:inline-block;vertical-align:middle}.yui3-lightbox2 .sqs-lightbox-previous:before{font-size:32px;width:32px;height:32px;line-height:32px}.yui3-lightbox2 .sqs-lightbox-next::before,.yui3-lightbox2 .sqs-lightbox-previous::before{font-size:22px}.yui3-lightbox2 .sqs-lightbox-close{padding:2px;right:2%;top:2%;text-align:right}.yui3-lightbox2 .sqs-lightbox-close:before{font-family:'squarespace-ui-font';font-style:normal;speak:none;font-weight:normal;-webkit-font-smoothing:antialiased;content:\"\\e02e\";text-align:center;display:inline-block;vertical-align:middle}.yui3-lightbox2 .sqs-lightbox-close:before{font-size:32px;width:32px;height:32px;line-height:32px}.yui3-lightbox2 .sqs-lightbox-meta-trigger{bottom:0;right:0;padding:2%;text-align:center;font-size:26px;line-height:.5;text-align:right}body.sqs-lightbox-open{position:static !important;overflow-y:hidden}.sqs-gallery img:not([src]){opacity:0}.fadeable-plugged.display-status-hidden{display:none}body.no-scroll{height:100%;position:fixed}.no-scroll{overflow:hidden !important}.sqs-lightbox-overlay{position:fixed;opacity:0;top:0;left:0;background:#000;height:100%;width:100%}.sqs-lightbox-overlay.sqs-lightbox-overlay-style-orb{background:-webkit-gradient(radial,50% 25%,0,50% 25%,800,from(rgba(0,0,0,.75)),to(#000));background:-moz-radial-gradient(center 45deg,circle cover,rgba(0,0,0,.75) 0%,#000 100%)}.sqs-lightbox-overlay.light{background:rgba(242,242,242,.98) !important;color:#3e3e3e}.sqs-lightbox-overlay.white.sqs-lightbox-overlay-style-orb{background:-webkit-gradient(radial,50% 25%,0,50% 25%,800,from(rgba(255,255,255,.96)),to(#fff));background:-moz-radial-gradient(center 45deg,circle cover,from(rgba(255,255,255,.96)),to(#fff))}.sqsp-tooltip{color:inherit;background-color:#f2f2f2;padding:22px 33px;box-shadow:0 4px 33px rgba(0,0,0,.22),0 0 0 1px rgba(0,0,0,.04);position:absolute;overflow:hidden;text-align:left !important;max-width:250px}.sqsp-tooltip .title{text-transform:uppercase;font-weight:500;margin-bottom:11px}.sqsp-tooltip .description{margin:11px 0}.sqsp-tooltip .buttons{margin:22px -33px -22px;border-top:1px solid #e4e4e4;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex}.sqsp-tooltip .buttons:empty{border-top:0}.sqsp-tooltip .buttons>*{-webkit-box-flex:1;-moz-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1;border-left:1px solid #e4e4e4 !important}.sqsp-tooltip .buttons>*:first-child{border-left:none !important}.sqsp-tooltip .buttons input,.sqsp-tooltip .buttons button{background:transparent}.sqsp-tooltip .buttons a{border-bottom:none}.sqsp-tooltip .buttons a:not(.reject){cursor:pointer;outline:none;background:#f2f2f2;padding:11px;text-align:center;-webkit-transition:background-color .1s ease-in-out;transition:background-color .1s ease-in-out;line-height:22px;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none;font-family:inherit;-webkit-appearance:none;-moz-appearance:none;appearance:none}.sqsp-tooltip .buttons a:not(.reject),.sqsp-tooltip .buttons a:not(.reject)>*{color:#3e3e3e !important;-webkit-appearance:none;border:0;text-transform:uppercase;outline:none;font-size:11px;font-weight:500}.sqsp-tooltip .buttons a:not(.reject):hover{background-color:#fff;box-shadow:none}.sqsp-tooltip .buttons a.reject{cursor:pointer;outline:none;background:#f2f2f2;padding:11px;text-align:center;-webkit-transition:background-color .1s ease-in-out;transition:background-color .1s ease-in-out;line-height:22px;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none;font-family:inherit;-webkit-appearance:none;-moz-appearance:none;appearance:none}.sqsp-tooltip .buttons a.reject,.sqsp-tooltip .buttons a.reject>*{color:#3e3e3e !important;-webkit-appearance:none;border:0;text-transform:uppercase;outline:none;font-size:11px;font-weight:500}.sqsp-tooltip .buttons a.reject:hover{background-color:#000;box-shadow:none}.sqsp-tooltip .buttons a.reject:hover{background-color:#f0523d}.sqsp-tooltip .buttons a.reject:hover,.sqsp-tooltip .buttons a.reject:hover *{color:#fff !important}.sqs-action-overlay{position:absolute;top:0;right:0;white-space:nowrap;-webkit-transition:opacity .1s ease-out;transition:opacity .1s ease-out;opacity:0;background-color:#3e3e3e;overflow:hidden;z-index:50;height:32px;border-radius:3px}.sqs-action-overlay.loading{opacity:1}.sqs-action-overlay.bottom{top:auto;bottom:0}.sqs-action-overlay>div{display:inline-block;height:32px;width:33px;opacity:.3;cursor:pointer}.sqs-action-overlay>div:hover{opacity:.9}.sqs-action-overlay>div:active,.sqs-action-overlay>div:focus{opacity:1}.sqs-action-overlay>div.edit-image,.sqs-action-overlay>div.edit{background:transparent url('//static.squarespace.com/universal/images-v6/damask/edit-16-light.png') center center no-repeat}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:1.5dppx){.sqs-action-overlay>div.edit-image,.sqs-action-overlay>div.edit{background-image:url('//static.squarespace.com/universal/images-v6/damask/edit-32-light.png');background-size:16px}}.sqs-action-overlay>div.edit.loading{background:none}.sqs-action-overlay>div.image-info{background:transparent url('//static.squarespace.com/universal/images-v6/damask/settings-16-light.png') center center no-repeat}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:1.5dppx){.sqs-action-overlay>div.image-info{background-image:url('//static.squarespace.com/universal/images-v6/damask/settings-32-light.png');background-size:16px}}.sqs-action-overlay>div.remove,.sqs-action-overlay>div.remove-image{background:transparent url('//static.squarespace.com/universal/images-v6/damask/trash-9-light.png') center center no-repeat;cursor:pointer}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:1.5dppx){.sqs-action-overlay>div.remove,.sqs-action-overlay>div.remove-image{background-image:url('//static.squarespace.com/universal/images-v6/damask/trash-9-light@2x.png');background-size:9px 11px}}.sqs-action-overlay>div.remove:hover,.sqs-action-overlay>div.remove-image:hover{background:transparent url('//static.squarespace.com/universal/images-v6/damask/trash-9-red.png') center center no-repeat}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:1.5dppx){.sqs-action-overlay>div.remove:hover,.sqs-action-overlay>div.remove-image:hover{background-image:url('//static.squarespace.com/universal/images-v6/damask/trash-9-red@2x.png');background-size:9px 11px}}.sqs-action-overlay>div.remove:hover,.sqs-action-overlay>div.remove-image:hover{background:#f0523d url('//static.squarespace.com/universal/images-v6/damask/trash-9-light.png') center center no-repeat}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:1.5dppx){.sqs-action-overlay>div.remove:hover,.sqs-action-overlay>div.remove-image:hover{background-image:url('//static.squarespace.com/universal/images-v6/damask/trash-9-light@2x.png');background-size:9px 11px}}.sqs-action-overlay>div.video-info{background:transparent url('//static.squarespace.com/universal/images-v6/damask/settings-16-light.png') center center no-repeat}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:1.5dppx){.sqs-action-overlay>div.video-info{background-image:url('//static.squarespace.com/universal/images-v6/damask/settings-32-light.png');background-size:16px}}.sqs-action-overlay>div.getty{background:transparent url('//static.squarespace.com/universal/images-v6/damask/getty-16-light.png') center center no-repeat}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:1.5dppx){.sqs-action-overlay>div.getty{background-image:url('//static.squarespace.com/universal/images-v6/damask/getty-32-light.png');background-size:16px}}.sqs-action-overlay>div.buy{background:transparent url('//static.squarespace.com/universal/images-v6/damask/shopping-cart-16-light.png') center center no-repeat}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:1.5dppx){.sqs-action-overlay>div.buy{background-image:url('//static.squarespace.com/universal/images-v6/damask/shopping-cart-32-light.png');background-size:16px}}.sqs-action-overlay>div.remove-video{background:transparent url('//static.squarespace.com/universal/images-v6/damask/trash-9-light.png') center center no-repeat;cursor:pointer}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:1.5dppx){.sqs-action-overlay>div.remove-video{background-image:url('//static.squarespace.com/universal/images-v6/damask/trash-9-light@2x.png');background-size:9px 11px}}.sqs-action-overlay>div.remove-video:hover{background:transparent url('//static.squarespace.com/universal/images-v6/damask/trash-9-red.png') center center no-repeat}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:1.5dppx){.sqs-action-overlay>div.remove-video:hover{background-image:url('//static.squarespace.com/universal/images-v6/damask/trash-9-red@2x.png');background-size:9px 11px}}.sqs-action-overlay>div.remove-video:hover{background:#f0523d url('//static.squarespace.com/universal/images-v6/damask/trash-9-light.png') center center no-repeat}/*IE9_SPLIT_MARKER*/\n@media (-webkit-min-device-pixel-ratio:2),(min-resolution:1.5dppx){.sqs-action-overlay>div.remove-video:hover{background-image:url('//static.squarespace.com/universal/images-v6/damask/trash-9-light@2x.png');background-size:9px 11px}}.sqs-action-overlay>div.loading{background:none;position:relative;opacity:1}.sqs-action-overlay>div.loading .sqs-spin.default{position:relative;top:50%;left:50%;-webkit-transform:translatex(-50%) translatey(-50%);-moz-transform:translatex(-50%) translatey(-50%);-ms-transform:translatex(-50%) translatey(-50%);transform:translatex(-50%) translatey(-50%)}.sqs-action-overlay-container:hover .sqs-action-overlay{opacity:1}.touch .sqs-action-overlay{opacity:1}.image-focal-point{border-radius:14px;height:14px;width:14px;margin-left:-10px;margin-top:-10px;position:absolute;border:3px solid rgba(255,255,255,.8);background:rgba(0,0,0,.2);cursor:move;opacity:0}.sqs-loading-overlay-node{background:rgba(255,255,255,.9)}.sqs-loading-overlay-node .sqs-spin{position:absolute;top:50%;left:50%}.sqs-loading-overlay-node .sqs-spin.large{margin-top:-11px;margin-left:-11px}.sqs-loading-overlay-node .sqs-spin.extra-large{margin-top:-20px;margin-left:-20px}.sqs-loading-overlay-node.has-title .title{position:absolute;top:50%;width:100%;text-align:center;margin-top:22px;color:#999;font-size:14px}.sqs-loading-overlay-node.has-title .sqs-spin{margin-top:-22px}body>.login-wrapper{position:fixed;top:0;left:0;height:100%;width:100%;z-index:30100;transition:all .5s ease-in-out}body>.login-wrapper.hidden{opacity:0}.sqs-video-wrapper .intrinsic{max-width:100%}.sqs-video-wrapper.video-none{position:relative}.sqs-video-wrapper.video-fill{position:absolute;width:100%;height:100%}.sqs-video-wrapper.video-fit{position:absolute;width:100%}.sqs-video-wrapper.video-fit .intrinsic{width:100%}.sqs-video-wrapper.video-fit .intrinsic-inner{position:relative}.sqs-video-wrapper iframe{position:absolute;top:0;left:0;width:100%;height:100%}.sqs-video-wrapper object,.sqs-video-wrapper embed{position:absolute;top:0;left:0;width:100%;height:100%}.sqs-video-wrapper .sqs-video-overlay{display:block;position:absolute;top:0;left:0;width:100%;height:100%;background-size:cover;color:#000;background-position:center center;background-repeat:no-repeat}.sqs-video-wrapper .sqs-video-overlay .sqs-video-opaque{position:absolute;bottom:0;width:100%;height:100%;background:#000;opacity:0}.sqs-video-wrapper .sqs-video-overlay.no-thumb .sqs-video-opaque{opacity:1}.sqs-video-wrapper .sqs-video-overlay .sqs-video-icon{opacity:.8;position:absolute;top:50%;left:50%;background-image:url('//static.squarespace.com/universal/images-v6/icons/icon-video-48-light-solid.png');background-position:center center;background-repeat:no-repeat;height:48px;width:48px;margin-left:-24px;margin-top:-24px;cursor:pointer}html.blogapp .sqs-video-wrapper .sqs-video-overlay .sqs-video-icon{background-image:url('gallery-play-big.png');height:80px;width:80px;margin-left:-40px;margin-top:-40px;opacity:.75}@media only screen and (-webkit-min-device-pixel-ratio:2),only screen and (min-resolution:192dpi){html.blogapp .sqs-video-wrapper .sqs-video-overlay .sqs-video-icon{background-image:url('gallery-play-big@2x.png');background-size:80px}}.sqs-video-wrapper.video-invalid{position:static !important;height:48px !important}.sqs-video-wrapper .sqs-video-invalid-wrapper{position:absolute;top:0;left:0;width:100%;height:100%;overflow:hidden}body.sqs-search-ui{background:red;overflow:hidden;color:red}body.sqs-search-ui-fullscreen.no-scroll{position:static}.sqs-search-ui-input-box{padding-bottom:10px}body.sqs-search-ui-fullscreen .sqs-search-ui{background:#fff;position:fixed;top:0;left:0;right:0;bottom:0;z-index:100000;padding:100px 100px 50px;font-family:\"Helvetica Neue\",Helvetica,Arial,sans-serif}body.sqs-search-ui-fullscreen .sqs-search-ui.display-status-hidden{position:absolute}body.sqs-search-ui-fullscreen .sqs-search-ui-close{-webkit-transition:opacity .1s ease-out;transition:opacity .1s ease-out;position:absolute;top:0;right:0;height:60px;width:60px;background:transparent url('//static.squarespace.com/universal/images-v6/icons/icon-closethin-15-dark.png') center center no-repeat;z-index:10100;opacity:.4;cursor:pointer}body.sqs-search-ui-fullscreen .sqs-search-ui-close:hover{opacity:1}body.sqs-search-ui-fullscreen .sqs-search-ui input{position:fixed;top:0;left:0;right:0;margin-left:96px;margin-top:60px;font-weight:500;font-family:inherit}body.sqs-search-ui-fullscreen .sqs-search-ui .yui3-aclist,body.sqs-search-ui-fullscreen .sqs-search-ui .yui3-scrollingautocompletelist{margin-left:96px;font-size:11px;line-height:18px;padding-left:5px;position:fixed;top:130px;left:0;color:#000;background:#fff;width:300px}body.sqs-search-ui-fullscreen .sqs-search-ui .yui3-aclist .yui3-aclist-item,body.sqs-search-ui-fullscreen .sqs-search-ui .yui3-scrollingautocompletelist .yui3-aclist-item{list-style:none;margin-top:2px}body.sqs-search-ui-fullscreen .sqs-search-ui .yui3-aclist .yui3-aclist-item-active,body.sqs-search-ui-fullscreen .sqs-search-ui .yui3-scrollingautocompletelist .yui3-aclist-item-active{outline:none;color:#000;font-weight:bold}body.sqs-search-ui-fullscreen .sqs-search-ui-list{position:absolute;top:200px;left:0;right:0;bottom:0;margin:-1px 85px 0}body.sqs-search-ui-fullscreen .sqs-search-ui-list .search-results{position:absolute;width:100%}body.sqs-search-ui-fullscreen .sqs-search-ui-pagination{display:none}body.sqs-search-ui-fullscreen .sqs-search-ui-item{padding:16px}body.sqs-search-ui-fullscreen .sqs-search-ui-item.active{background-color:#fcfcfc}body.sqs-search-ui-fullscreen .sqs-search-ui-item img{height:50px;width:50px;float:left;margin-right:16px}body.sqs-search-ui-fullscreen .sqs-search-ui-item .sqs-title .record-type{font-weight:200;color:#888;font-size:11px;padding-left:7px;display:none}body.sqs-search-ui-fullscreen .sqs-search-ui-item .sqs-title .edit{-webkit-transition:color,background-color .1s ease-out;transition:color,background-color .1s ease-out;background:#f2f2f2;color:#111;font-size:10px;padding:2px 10px;border-radius:10px;margin-left:6px}body.sqs-search-ui-fullscreen .sqs-search-ui-item .sqs-title .edit:hover{background:#111;color:#fff}@media screen and (max-width:600px){body.sqs-search-ui-fullscreen .sqs-search-ui{padding:8em 2em 0}body.sqs-search-ui-fullscreen .sqs-search-ui input{margin-left:1em}body.sqs-search-ui-fullscreen .sqs-search-ui .yui3-aclist,body.sqs-search-ui-fullscreen .sqs-search-ui .yui3-scrollingautocompletelist{display:none}body.sqs-search-ui-fullscreen .sqs-search-ui-list{top:150px;margin:0 2em}}.sqs-search-container .search-notice{font-size:12px;color:#000}.sqs-search-container .search-notice.error{color:#d10000}.sqs-search-container .search-notice.hide{display:none}.sqs-search-container-waiting{background:#fff;height:100%;width:100%;position:absolute;opacity:.5}.sqs-search-container a{color:#999;text-decoration:none}.sqs-search-container input{background:none;border:none;outline:none;font-size:30px}.sqs-search-container input::-webkit-input-placeholder{color:#eee}.sqs-search-container input:-moz-placeholder{color:#eee}.sqs-search-container input::selection{color:#fff;background-color:#000}.sqs-search-container input:focus{box-shadow:none;border:none}.sqs-search-container-list{overflow-y:auto;overflow-x:hidden}.sqs-search-container-list .search-results{border-top:1px solid rgba(200,200,200,.35);border-bottom:1px solid rgba(200,200,200,.35)}.sqs-search-container-list .search-results.empty{border:none}.search-result:first-child .sqs-search-container-item{border-top:none}.sqs-search-container-item{position:relative;border-top:1px solid rgba(200,200,200,.35);zoom:1;cursor:pointer}.sqs-search-container-item:after{display:block;visibility:hidden;font-size:0;height:0;clear:both;content:\".\"}.sqs-search-container-item:first-child{margin-top:0}.sqs-search-container-item mark{font-weight:bold}.sqs-search-container-item em{font-style:italic}.sqs-search-container-item .sqs-title{font-size:1.5em;font-weight:400;line-height:1.3em;margin-bottom:.5em}.sqs-search-container-item .sqs-content{font-weight:400;font-size:1em;line-height:1.4em}.sqs-search-container-item .sqs-content span{margin:2px 0}.sqs-search-container .loading{opacity:.75}.sqs-search-container .loading .desc{display:block;float:left;padding-top:4px;padding-left:12px}.sqs-search-container .loading .spinner-wrapper{display:block;float:left}@media screen and (max-width:600px){.sqs-search-container-list{top:160px}.sqs-search-container-list .search-results{margin-right:0px}.sqs-search-container input{font-size:24px !important}}.sqs-search-page{width:100%}.sqs-search-page-input{position:relative;margin:0;padding:15px 15px 15px 60px;background:url(/universal/images-v6/icons/icon-searchqueries-32-dark.png) no-repeat 15px 50%;background-color:#fff;border:1px solid #aaa}.sqs-search-page-input.loading{background-image:none}.sqs-search-page-input input{display:block;width:100%;font-weight:400}.sqs-search-page-input .spinner-wrapper{position:absolute;left:15px;top:50%;margin-top:-14px}.sqs-search-page-result{padding:50px 0}.sqs-search-page-more.hide{display:none !important}.sqs-search-page-more.loading span{display:none}.sqs-search-page-item{padding:1.5em 0}.sqs-search-page-item .sqs-main-image{position:absolute;top:0;left:0;right:0;bottom:0}.sqs-search-page-item .sqs-main-image-container{width:20%;padding-right:20px;float:left;box-sizing:border-box}.sqs-search-page-item .sqs-main-image-intrinsic{position:relative;width:100%;height:0;padding-bottom:100%}.sqs-search-page-item .sqs-main-content{float:left;width:80%;box-sizing:border-box}.sqs-search-page-more-wrapper{width:100%;text-align:center;margin:10px 0}@media screen and (max-width:600px){.sqs-search-page-result{padding:0 20px}.sqs-search-page-more-wrapper{font-size:10px}}.sqs-simple-like{cursor:pointer;-webkit-user-select:none;line-height:18px;display:inline-block}.sqs-simple-like .like-icon{-webkit-transition:background-color,opacity .5s ease-out;transition:background-color,opacity .5s ease-out;float:left;margin-right:5px;height:18px;width:18px;border-radius:50%;background:#999 url('//static.squarespace.com/universal/images-v6/comments/icon_like_12_light.png') center center no-repeat;background-size:10px;display:block}.sqs-simple-like:hover .like-icon{background-color:#d10000;-webkit-animation:beat 1s infinite;-webkit-animation-timing-function:ease-out}.sqs-simple-like.float .like-icon{background-color:#d10000;-webkit-animation:float 2s infinite;-webkit-animation-timing-function:ease-out;-webkit-animation-iteration-count:1}.sqs-simple-like.clicked .like-icon{background-color:#d10000;-webkit-animation:none;-webkit-transition:all 1s ease-in-out}@-webkit-keyframes float{0%{opacity:0;-webkit-transform:scale(1.15);-webkit-animation-timing-function:ease-in}30%{-webkit-animation-timing-function:linear;opacity:.6}60%{opacity:.8;-webkit-transform:scale(1.45);-webkit-animation-timing-function:linear}100%{-webkit-animation-timing-function:ease-out;opacity:0}}@-webkit-keyframes beat{0%{-webkit-transform:scale(1)}10%{-webkit-transform:scale(1.25)}20%{-webkit-transform:scale(1.1)}30%{-webkit-transform:scale(1.25)}100%{-webkit-transform:scale(1)}}.squarespace-social-buttons{-webkit-user-select:none;-moz-user-select:none;position:relative}.squarespace-social-buttons .sqs-socialbutton-content{position:relative}.squarespace-social-buttons.empty{display:none}.squarespace-social-buttons.button-style .ss-social-button-wrapper{display:block;width:55px;background-color:#fff;background-color:#222;background-image:-moz-linear-gradient(#555,#222);background-image:-ms-linear-gradient(#555,#222);background-image:-webkit-linear-gradient(#555,#222);background-image:linear-gradient(#555,#222);border-radius:3px;padding:1px 6px 0px 2px;box-shadow:inset 0 0 0 1px rgba(255,255,255,.04),inset 0 -1px 0 rgba(0,0,0,.24);cursor:pointer}.squarespace-social-buttons.button-style .ss-social-button-wrapper:hover{background-color:#6d6d6d;background-color:#222;background-image:-moz-linear-gradient(#666,#222);background-image:-ms-linear-gradient(#666,#222);background-image:-webkit-linear-gradient(#666,#222);background-image:linear-gradient(#666,#222)}.squarespace-social-buttons.button-style .ss-social-button-wrapper .ss-social-button{display:inline-block;font:11px 'Helvetica Neue',Helvetica,Arial,sans-serif;font-weight:600;color:#fff;height:21px;line-height:20px;padding:0px 0px 0px 22px;cursor:pointer;background:2px 2px no-repeat url('//static.squarespace.com/universal/images-v6/standard/icon_social_button_10_light.png');background-position:10% 50%}.squarespace-social-buttons.button-style .ss-social-button-wrapper .ss-social-button:hover+.ss-social-button-list{display:block}.squarespace-social-buttons.inline-style{display:inline-block;cursor:pointer}.squarespace-social-buttons.inline-style .ss-social-button-icon{float:left;margin-right:5px;height:18px;width:18px;border-radius:50%;background:#999 url('//static.squarespace.com/universal/images-v6/standard/icon_social_button_10_light.png') center center no-repeat;background-size:8px;display:block}.squarespace-social-buttons.inline-style :hover .ss-social-button-icon{background-color:#222}.squarespace-social-buttons .ss-social-list-wrapper{height:0;position:absolute;overflow:hidden;z-index:10000}.squarespace-social-buttons .ss-social-list-wrapper .ss-social-button-list{padding:14px;text-align:left;min-width:108px;box-shadow:0 1px 3px rgba(0,0,0,.05);background-color:rgba(254,254,254,.9);border-width:1px;border-style:solid;border-color:rgba(242,242,242,.05);border:1px solid #ccc;border-radius:5px}.squarespace-social-buttons .ss-social-list-wrapper .ss-social-button-list .ss-social-button-container{margin-bottom:10px;min-height:28px}.squarespace-social-buttons .ss-social-list-wrapper .ss-social-button-list .ss-social-button-container:last-child{margin-bottom:0px}.sqs-ss-badge{position:fixed;height:44px;overflow:hidden;border-radius:44px;width:44px;background:#000;opacity:0;cursor:pointer;z-index:10001;-webkit-transition:all .4s cubic-bezier(.23,1,.32,1);transition:all .4s cubic-bezier(.23,1,.32,1)}.sqs-ss-badge-content{white-space:nowrap}.sqs-ss-badge .badge-closed,.sqs-ss-badge .badge-open{display:inline-block;vertical-align:top;height:44px;-webkit-transform:scale(1) translatez(0);-moz-transform:scale(1) translatez(0);-ms-transform:scale(1) translatez(0);transform:scale(1) translatez(0);-webkit-transition:all .4s cubic-bezier(.23,1,.32,1);transition:all .4s cubic-bezier(.23,1,.32,1)}.sqs-ss-badge .badge-closed{width:44px;background:transparent url('//static.squarespace.com/universal/images-v6/icons/icon-squarespace-16-light.png') center center no-repeat}.sqs-ss-badge .badge-open{width:0;opacity:0}.sqs-ss-badge .badge-open .badge-open-inner{white-space:nowrap}.sqs-ss-badge .badge-open .badge-open-inner h2{color:#e2e2e2 !important;font:300 10px 'proxima-nova','HelveticaNeue-Light','Helvetica Neue Light','Helvetica Neue',Helvetica,Arial,'Lucida Grande',sans-serif !important;line-height:44px !important;letter-spacing:1px;text-transform:uppercase;margin:0 !important}.sqs-ss-badge[data-position=\"top-left\"]{top:22px;left:22px}.sqs-ss-badge[data-position=\"top-center\"]{right:0;top:20px;left:0;margin:auto;-webkit-transform:translatey(-100px);-moz-transform:translatey(-100px);-ms-transform:translatey(-100px);transform:translatey(-100px)}.sqs-ss-badge[data-position=\"top-right\"]{top:22px;right:22px}.sqs-ss-badge[data-position=\"bottom-left\"]{bottom:22px;left:22px}.sqs-ss-badge[data-position=\"bottom-center\"]{right:0;bottom:20px;left:0;margin:auto}.sqs-ss-badge[data-position=\"bottom-right\"]{right:22px;bottom:22px}.sqs-ss-badge.badge-auto-hide[data-position=\"top-left\"],.sqs-ss-badge.badge-auto-hide[data-position=\"top-center\"],.sqs-ss-badge.badge-auto-hide[data-position=\"top-right\"]{-webkit-transform:translatey(-100px);-moz-transform:translatey(-100px);-ms-transform:translatey(-100px);transform:translatey(-100px)}.sqs-ss-badge.badge-auto-hide[data-position=\"bottom-left\"],.sqs-ss-badge.badge-auto-hide[data-position=\"bottom-center\"],.sqs-ss-badge.badge-auto-hide[data-position=\"bottom-right\"]{-webkit-transform:translatey(100px);-moz-transform:translatey(100px);-ms-transform:translatey(100px);transform:translatey(100px)}.sqs-ss-badge.is-mobile[data-devices=\"desktop-only\"]{display:none}.sqs-ss-badge[data-type=\"white\"]{background:#fff}.sqs-ss-badge[data-type=\"white\"] .badge-open .badge-open-inner h2{color:#111 !important}.sqs-ss-badge[data-type=\"white\"] .badge-closed{background-image:url(\"//static.squarespace.com/universal/images-v6/icons/icon-squarespace-16-dark.png\")}.sqs-ss-badge.badge-visible{opacity:1;-webkit-transform:translatez(0) !important;-moz-transform:translatez(0) !important;-ms-transform:translatez(0) !important;transform:translatez(0) !important}.sqs-ss-badge:not(.is-mobile):hover{width:220px;border-radius:0}.sqs-ss-badge:not(.is-mobile):hover .badge-open{transform:none;opacity:1}.sqs-ss-badge-mobile-info-bar-present[data-position=\"bottom-left\"],.sqs-ss-badge-mobile-info-bar-present[data-position=\"bottom-center\"],.sqs-ss-badge-mobile-info-bar-present[data-position=\"bottom-right\"]{bottom:72px}.sqs-ss-badge:not(.is-mobile):hover+.sqs-ss-badge-cover{visibility:visible;opacity:1}.sqs-ss-badge[data-position=\"top-left\"]+.sqs-ss-badge-cover{background:-moz-radial-gradient(top left,circle cover,rgba(0,0,0,.1) 0%,rgba(0,0,0,.5) 100%);background:-ms-radial-gradient(top left,circle cover,rgba(0,0,0,.1) 0%,rgba(0,0,0,.5) 100%);background:radial-gradient(circle at top left,rgba(0,0,0,.1) 0%,rgba(0,0,0,.5) 100%)}.sqs-ss-badge[data-position=\"top-center\"]+.sqs-ss-badge-cover{background:-moz-radial-gradient(top center,circle cover,rgba(0,0,0,.1) 0%,rgba(0,0,0,.5) 100%);background:-ms-radial-gradient(top center,circle cover,rgba(0,0,0,.1) 0%,rgba(0,0,0,.5) 100%);background:radial-gradient(circle at top center,rgba(0,0,0,.1) 0%,rgba(0,0,0,.5) 100%)}.sqs-ss-badge[data-position=\"top-right\"]+.sqs-ss-badge-cover{background:-moz-radial-gradient(top right,circle cover,rgba(0,0,0,.1) 0%,rgba(0,0,0,.5) 100%);background:-ms-radial-gradient(top right,circle cover,rgba(0,0,0,.1) 0%,rgba(0,0,0,.5) 100%);background:radial-gradient(circle at top right,rgba(0,0,0,.1) 0%,rgba(0,0,0,.5) 100%)}.sqs-ss-badge[data-position=\"bottom-left\"]+.sqs-ss-badge-cover{background:-moz-radial-gradient(bottom left,circle cover,rgba(0,0,0,.1) 0%,rgba(0,0,0,.5) 100%);background:-ms-radial-gradient(bottom left,circle cover,rgba(0,0,0,.1) 0%,rgba(0,0,0,.5) 100%);background:radial-gradient(circle at bottom left,rgba(0,0,0,.1) 0%,rgba(0,0,0,.5) 100%)}.sqs-ss-badge[data-position=\"bottom-center\"]+.sqs-ss-badge-cover{background:-moz-radial-gradient(bottom center,circle cover,rgba(0,0,0,.1) 0%,rgba(0,0,0,.5) 100%);background:-ms-radial-gradient(bottom center,circle cover,rgba(0,0,0,.1) 0%,rgba(0,0,0,.5) 100%);background:radial-gradient(circle at bottom center,rgba(0,0,0,.1) 0%,rgba(0,0,0,.5) 100%)}.sqs-ss-badge[data-position=\"bottom-right\"]+.sqs-ss-badge-cover{background:-moz-radial-gradient(bottom right,circle cover,rgba(0,0,0,.1) 0%,rgba(0,0,0,.5) 100%);background:-ms-radial-gradient(bottom right,circle cover,rgba(0,0,0,.1) 0%,rgba(0,0,0,.5) 100%);background:radial-gradient(circle at bottom right,rgba(0,0,0,.1) 0%,rgba(0,0,0,.5) 100%)}body.sqs-style-mode[data-position=\"top-left\"]{top:22px;left:242px}body.sqs-style-mode[data-position=\"bottom-left\"]{bottom:22px;left:242px}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:192dppx){.sqs-ss-badge .badge-closed{background-size:16px;background-image:url(\"//static.squarespace.com/universal/images-v6/icons/icon-squarespace-32-light.png\")}.sqs-ss-badge[data-type=\"white\"] .badge-closed{background-image:url(\"//static.squarespace.com/universal/images-v6/icons/icon-squarespace-32-dark.png\")}}.sqs-ss-badge-cover{position:fixed;width:100%;height:100%;top:0;left:0;opacity:0;visibility:hidden;background-color:rgba(0,0,0,.6);z-index:10000;-webkit-transition:all .3s ease;transition:all .3s ease}.sqs-licensed-asset-preview-bar{z-index:9999;position:fixed;left:0;right:0;bottom:0;height:88px;background-color:#3e3e3e;color:#fff;padding:11px;box-sizing:border-box}.sqs-licensed-asset-preview-bar-content{text-align:center;position:absolute;top:50%;width:100%;-webkit-transform:translate(0,-50%);-moz-transform:translate(0,-50%);-ms-transform:translate(0,-50%);transform:translate(0,-50%)}.sqs-licensed-asset-preview-bar-content span{font-family:helvetica,sans-serif;text-transform:uppercase;letter-spacing:1px;font-weight:500;font-size:11px}.sqs-mobile-info-bar{position:fixed;z-index:10000;bottom:0;left:0;width:100%;background:#ebebeb;-webkit-transition:all .2s cubic-bezier(.23,.47,.32,1);transition:all .2s cubic-bezier(.23,.47,.32,1)}.sqs-mobile-info-bar-content{-webkit-backface-visibility:hidden}.sqs-mobile-info-bar-triggers{font-size:0;padding:0 20px;text-align:center}.sqs-mobile-info-bar-trigger{cursor:pointer;display:inline-block;width:25%;padding:15px 0;text-align:center}.sqs-mobile-info-bar-trigger a{display:block}.sqs-mobile-info-bar-trigger-icon{display:block;width:16px;height:16px;margin:0 auto 8px auto;background-size:contain;background-repeat:no-repeat}.sqs-mobile-info-bar-trigger-label{display:block;font-size:10px;line-height:1em;letter-spacing:1px;color:#222;text-transform:uppercase;font-family:'proxima-nova',arial,sans-serif}.sqs-mobile-info-bar-trigger[data-type=\"location\"] .sqs-mobile-info-bar-trigger-icon{background-image:url(//static.squarespace.com/universal/images-v6/mobile-info-bar/map.png)}.sqs-mobile-info-bar-trigger[data-type=\"contactEmail\"] .sqs-mobile-info-bar-trigger-icon{background-image:url(//static.squarespace.com/universal/images-v6/mobile-info-bar/email.png)}.sqs-mobile-info-bar-trigger[data-type=\"contactPhoneNumber\"] .sqs-mobile-info-bar-trigger-icon{background-image:url(//static.squarespace.com/universal/images-v6/mobile-info-bar/call.png)}.sqs-mobile-info-bar-trigger[data-type=\"businessHours\"] .sqs-mobile-info-bar-trigger-icon{background-image:url(//static.squarespace.com/universal/images-v6/mobile-info-bar/hours.png)}.sqs-mobile-info-bar-overlay{visibility:hidden;position:fixed;top:0;left:0;width:100%;height:100%;opacity:0;background:#ebebeb;color:#222;-webkit-transition:opacity .2s cubic-bezier(.23,.47,.32,1);transition:opacity .2s cubic-bezier(.23,.47,.32,1)}.sqs-mobile-info-bar-overlay-content,.sqs-mobile-info-bar-overlay-content>div{position:absolute !important;top:0;left:0;width:100%;height:100%}.sqs-mobile-info-bar-overlay-content>div{display:none}.sqs-mobile-info-bar-overlay-content .sqs-business-hours{top:10%;left:10%;width:80%;height:80%}.sqs-mobile-info-bar-overlay-content .sqs-mobile-info-bar-map{top:0;left:0;width:100%;height:100%}.sqs-mobile-info-bar-overlay-content .sqs-mobile-info-bar-address{position:absolute;width:100%;height:auto;color:#aaa;background:#ebebeb;bottom:0;padding:20px;font-size:12px;line-height:19px;font-family:'proxima-nova',arial,sans-serif;box-sizing:border-box}.sqs-mobile-info-bar-overlay-content .sqs-mobile-info-bar-address [data-type=\"addressTitle\"]{color:#222;font-size:14px;line-height:14px;margin:2px 0 7px 0}.sqs-mobile-info-bar-overlay-content .sqs-mobile-info-bar-address-link{background:url(//static.squarespace.com/universal/images-v6/icons/icon-external-link-18-dark.png) no-repeat;position:absolute;width:18px;height:18px;top:50%;right:20px;margin-top:-9px}.sqs-mobile-info-bar-overlay-close{cursor:pointer;position:fixed;background:#ebebeb;top:10px;right:10px;padding:13px}.sqs-mobile-info-bar-overlay-close:after{content:'×';display:block;font-family:helvetica,arial,sans-serif;font-weight:100;font-size:19px;line-height:15px;padding:0;color:#222}.sqs-mobile-info-bar-show-overlay{z-index:10010}.sqs-mobile-info-bar-show-overlay .sqs-mobile-info-bar-overlay{opacity:1;visibility:visible}.sqs-mobile-info-bar-dark{background:#222}.sqs-mobile-info-bar-dark .sqs-mobile-info-bar-overlay{background:#222;color:#fff}.sqs-mobile-info-bar-dark .sqs-mobile-info-bar-address{background:#222}.sqs-mobile-info-bar-dark .sqs-mobile-info-bar-address [data-type=\"addressTitle\"]{color:#fff}.sqs-mobile-info-bar-dark .sqs-mobile-info-bar-address-link{background-image:url(//static.squarespace.com/universal/images-v6/icons/icon-external-link-18-light.png)}.sqs-mobile-info-bar-dark .sqs-mobile-info-bar-trigger-label{color:#fff}.sqs-mobile-info-bar-dark .sqs-mobile-info-bar-trigger[data-type=\"location\"] .sqs-mobile-info-bar-trigger-icon{background-image:url(//static.squarespace.com/universal/images-v6/mobile-info-bar/map-light.png)}.sqs-mobile-info-bar-dark .sqs-mobile-info-bar-trigger[data-type=\"contactEmail\"] .sqs-mobile-info-bar-trigger-icon{background-image:url(//static.squarespace.com/universal/images-v6/mobile-info-bar/email-light.png)}.sqs-mobile-info-bar-dark .sqs-mobile-info-bar-trigger[data-type=\"contactPhoneNumber\"] .sqs-mobile-info-bar-trigger-icon{background-image:url(//static.squarespace.com/universal/images-v6/mobile-info-bar/call-light.png)}.sqs-mobile-info-bar-dark .sqs-mobile-info-bar-trigger[data-type=\"businessHours\"] .sqs-mobile-info-bar-trigger-icon{background-image:url(//static.squarespace.com/universal/images-v6/mobile-info-bar/hours-light.png)}.sqs-mobile-info-bar-dark .sqs-mobile-info-bar-overlay-close,.sqs-mobile-info-bar-overlay-close-dark{background:#222}.sqs-mobile-info-bar-dark .sqs-mobile-info-bar-overlay-close:after,.sqs-mobile-info-bar-overlay-close-dark:after{color:#fff}.sqs-style-mode .sqs-mobile-info-bar,.sqs-mobile-info-bar-hide{-webkit-transform:translate3d(0,100px,0);-moz-transform:translate3d(0,100px,0);-ms-transform:translate3d(0,100px,0);transform:translate3d(0,100px,0)}.sqs-business-hours{max-width:250px;margin-left:auto;margin-right:auto}.sqs-business-hours-day{margin:.5em 0;font-family:'proxima-nova',sans-serif;font-size:16px;line-height:1.4;font-style:normal;letter-spacing:1px;zoom:1}.sqs-business-hours-day-label{color:#aaa;float:left;position:relative;top:2px;width:35%;margin-right:10%;font-size:13px;text-transform:uppercase;text-align:right}.sqs-business-hours-day-hours{float:right;width:55%}.sqs-business-hours-day .closed{color:#999}.sqs-business-hours-day:after{display:block;visibility:hidden;font-size:0;height:0;clear:both;content:\".\"}.sqs-business-hours-store{text-align:center;margin:1em 0 3em 0;color:#aaa;font-family:'proxima-nova',sans-serif;font-size:16px;line-height:1.65}.sqs-business-hours-store span{text-transform:uppercase;letter-spacing:2px;color:#222;font-size:20px}.sqs-business-hours-dark .sqs-business-hours-store span{color:#fff}.sqs-widgets-confirmation{position:absolute;z-index:1000000;font-size:12px}.sqs-widgets-confirmation-content{color:inherit;background-color:#f2f2f2;padding:22px 33px;text-align:center;box-shadow:0 4px 33px rgba(0,0,0,.22),0 0 0 1px rgba(0,0,0,.04)}.sqs-widgets-confirmation-content>.title{text-transform:uppercase;font-weight:500;margin-bottom:11px}.sqs-widgets-confirmation-content .message{margin:11px 0;line-height:22px}.sqs-widgets-confirmation-content .fields{margin-bottom:11px}.sqs-widgets-confirmation-content .fields .check-field-wrapper{padding:0}.sqs-widgets-confirmation-content .fields .check-field-wrapper .field-description{background:none}.sqs-widgets-confirmation-content .buttons{border-top:1px solid #e4e4e4;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;margin:22px -33px -22px}.sqs-widgets-confirmation-content .buttons:empty{border-top:0}.sqs-widgets-confirmation-content .buttons>*{-webkit-box-flex:1;-moz-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1;border-left:1px solid #e4e4e4 !important}.sqs-widgets-confirmation-content .buttons>*:first-child{border-left:none !important}.sqs-widgets-confirmation-content .buttons input,.sqs-widgets-confirmation-content .buttons button{background:transparent}.sqs-widgets-confirmation-content .buttons a{border-bottom:none}.sqs-widgets-confirmation-content .buttons .confirmation-button:not(.reject){cursor:pointer;outline:none;background:#f2f2f2;padding:11px;text-align:center;-webkit-transition:background-color .1s ease-in-out;transition:background-color .1s ease-in-out;line-height:22px;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none;font-family:inherit;-webkit-appearance:none;-moz-appearance:none;appearance:none}.sqs-widgets-confirmation-content .buttons .confirmation-button:not(.reject),.sqs-widgets-confirmation-content .buttons .confirmation-button:not(.reject)>*{color:#3e3e3e !important;-webkit-appearance:none;border:0;text-transform:uppercase;outline:none;font-size:11px;font-weight:500}.sqs-widgets-confirmation-content .buttons .confirmation-button:not(.reject):hover{background-color:#fff;box-shadow:none}.sqs-widgets-confirmation-content .buttons .confirmation-button.reject{cursor:pointer;outline:none;background:#f2f2f2;padding:11px;text-align:center;-webkit-transition:background-color .1s ease-in-out;transition:background-color .1s ease-in-out;line-height:22px;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none;font-family:inherit;-webkit-appearance:none;-moz-appearance:none;appearance:none}.sqs-widgets-confirmation-content .buttons .confirmation-button.reject,.sqs-widgets-confirmation-content .buttons .confirmation-button.reject>*{color:#3e3e3e !important;-webkit-appearance:none;border:0;text-transform:uppercase;outline:none;font-size:11px;font-weight:500}.sqs-widgets-confirmation-content .buttons .confirmation-button.reject:hover{background-color:#000;box-shadow:none}.sqs-widgets-confirmation-content .buttons .confirmation-button.reject:hover{background-color:#f0523d}.sqs-widgets-confirmation-content .buttons .confirmation-button.reject:hover,.sqs-widgets-confirmation-content .buttons .confirmation-button.reject:hover *{color:#fff !important}.sqs-widgets-confirmation.sqs-widgets-data-confirmation .sqs-widgets-confirmation-content{text-align:left}.sqs-widgets-confirmation.danger-zone .sqs-widgets-confirmation-content{color:#fff !important;background-color:#f0523d}.sqs-widgets-confirmation.danger-zone .sqs-widgets-confirmation-content .buttons .confirmation-button{background-color:#f0523d;color:#fff !important}.sqs-widgets-confirmation.danger-zone .sqs-widgets-confirmation-content .buttons .confirmation-button:hover{background-color:#e4351e}.sqs-widgets-confirmation.dangerous-confirmation-button .sqs-widgets-confirmation-content .buttons .confirm:hover{background-color:#f0523d;color:#fff !important}.sqs-widgets-confirmation.reject-warning .buttons .confirmation-button.reject:hover{background-color:#f0523d;color:#fff}.sqs-widgets-confirmation.delete-collection .confirmation-button.confirm:hover{background-color:#f0523d;color:#fff !important}.sqs-widgets-confirmation.with-media .title:empty,.sqs-widgets-confirmation.with-media .message:empty{display:none}.sqs-widgets-confirmation.with-media .title:empty+.message:empty+.media{margin-top:-22px}.sqs-widgets-confirmation.with-media .media{display:block;position:relative;margin:0px -33px}.sqs-widgets-confirmation.with-media .media>*{display:block;position:relative;margin:0 auto}.sqs-widgets-confirmation.with-media .buttons{margin-top:0px}.sqs-widgets-confirmation.shown .media>*{width:100%}.sqs-widgets-confirmation{opacity:0;-webkit-transform:scale(.96);-moz-transform:scale(.96);-ms-transform:scale(.96);transform:scale(.96)}.sqs-widgets-confirmation.mobile{-webkit-transform:translatey(-50%);-moz-transform:translatey(-50%);-ms-transform:translatey(-50%);transform:translatey(-50%)}.sqs-widgets-confirmation.shown{opacity:1;-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);transform:scale(1);-webkit-animation-name:show-confirmation;-moz-animation-name:show-confirmation;-o-animation-name:show-confirmation;animation-name:show-confirmation;-webkit-animation-iteration-count:1;-moz-animation-iteration-count:1;-o-animation-iteration-count:1;animation-iteration-count:1;-webkit-animation-duration:.3s;-moz-animation-duration:.3s;-o-animation-duration:.3s;animation-duration:.3s}.sqs-widgets-confirmation.shown.mobile{-webkit-transform:translatey(0);-moz-transform:translatey(0);-ms-transform:translatey(0);transform:translatey(0);-webkit-animation-name:show-confirmation-mobile;-moz-animation-name:show-confirmation-mobile;-o-animation-name:show-confirmation-mobile;animation-name:show-confirmation-mobile}.sqs-widgets-confirmation.hiding{opacity:0;-webkit-animation-name:none;-moz-animation-name:none;-o-animation-name:none;animation-name:none;-webkit-transition-property:all;transition-property:all;-webkit-transition-duration:.3s;transition-duration:.3s;-webkit-transform:scale(.96);-moz-transform:scale(.96);-ms-transform:scale(.96);transform:scale(.96)}.sqs-widgets-confirmation.hiding.mobile{-webkit-transform:translatey(-50%);-moz-transform:translatey(-50%);-ms-transform:translatey(-50%);transform:translatey(-50%)}.sqs-widgets-confirmation-hidden{display:none}@-webkit-keyframes show-confirmation{from{opacity:0;-webkit-transform:scale(.96);-moz-transform:scale(.96);-ms-transform:scale(.96);transform:scale(.96)}to{opacity:1;-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);transform:scale(1)}}@-moz-keyframes show-confirmation{from{opacity:0;-webkit-transform:scale(.96);-moz-transform:scale(.96);-ms-transform:scale(.96);transform:scale(.96)}to{opacity:1;-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);transform:scale(1)}}@keyframes show-confirmation{from{opacity:0;-webkit-transform:scale(.96);-moz-transform:scale(.96);-ms-transform:scale(.96);transform:scale(.96)}to{opacity:1;-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);transform:scale(1)}}@-webkit-keyframes show-confirmation-mobile{from{-webkit-transform:translatey(-50%);-moz-transform:translatey(-50%);-ms-transform:translatey(-50%);transform:translatey(-50%)}to{-webkit-transform:translatey(0);-moz-transform:translatey(0);-ms-transform:translatey(0);transform:translatey(0)}}@keyframes show-confirmation-mobile{from{-webkit-transform:translatey(-50%);-moz-transform:translatey(-50%);-ms-transform:translatey(-50%);transform:translatey(-50%)}to{-webkit-transform:translatey(0);-moz-transform:translatey(0);-ms-transform:translatey(0);transform:translatey(0)}}.sqs-widgets-confirmation-overlay{display:block;background:#000;position:fixed;top:0;left:0;width:100%;height:100%;opacity:.4;z-index:999999}.ReactModal__Overlay{-webkit-perspective:600;perspective:600;opacity:0;overflow-x:hidden;overflow-y:auto;background-color:rgba(0,0,0,.4);z-index:999999}.ReactModal__Overlay--after-open{opacity:1;-webkit-transition:opacity 150ms;transition:opacity 150ms}.ReactModal__Content{position:fixed;top:50%;left:50%;transform-origin:top left;-webkit-transform:scale(.96) translate(-50%,-50%);-moz-transform:scale(.96) translate(-50%,-50%);-ms-transform:scale(.96) translate(-50%,-50%);transform:scale(.96) translate(-50%,-50%)}.ReactModal__Content:focus{outline:none}.ReactModal__Content--after-open{-webkit-transform:scale(1) translate(-50%,-50%);-moz-transform:scale(1) translate(-50%,-50%);-ms-transform:scale(1) translate(-50%,-50%);transform:scale(1) translate(-50%,-50%);-webkit-transition:all 300ms;transition:all 300ms}.ReactModal__Overlay--before-close{opacity:0}.ReactModal__Content--before-close{-webkit-transform:scale(.96) translate(-50%,-50%);-moz-transform:scale(.96) translate(-50%,-50%);-ms-transform:scale(.96) translate(-50%,-50%);transform:scale(.96) translate(-50%,-50%);-webkit-transition:all 300ms;transition:all 300ms}.ReactModal__Content.modal-dialog{border:none;background-color:transparent}.Modal{color:inherit;background-color:#f2f2f2;padding:22px 33px;box-sizing:border-box;box-shadow:0 4px 33px rgba(0,0,0,.22),0 0 0 1px rgba(0,0,0,.04)}.Modal-header{text-transform:uppercase;font-weight:500;margin-bottom:11px}.Modal-body{margin:11px 0}.Modal-fields{margin-bottom:11px}.Modal-fields .check-field-wrapper{padding:0}.Modal-fields .check-field-wrapper .field-description{background:none}.Modal-footer{border-top:1px solid #e4e4e4;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;margin:22px -33px -22px}.Modal-footer:empty{border-top:0}.Modal-footer>*{-webkit-box-flex:1;-moz-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1;border-left:1px solid #e4e4e4 !important}.Modal-footer>*:first-child{border-left:none !important}.Modal-footer input,.Modal-footer button{background:transparent}.Modal-footer a{border-bottom:none}.Modal-button{cursor:pointer;outline:none;background:#f2f2f2;padding:11px;text-align:center;-webkit-transition:background-color .1s ease-in-out;transition:background-color .1s ease-in-out;line-height:22px;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none;font-family:inherit;-webkit-appearance:none;-moz-appearance:none;appearance:none}.Modal-button,.Modal-button>*{color:#3e3e3e !important;-webkit-appearance:none;border:0;text-transform:uppercase;outline:none;font-size:11px;font-weight:500}.Modal-button:hover{background-color:#fff;box-shadow:none}.Modal-button--danger{cursor:pointer;outline:none;background:#f2f2f2;padding:11px;text-align:center;-webkit-transition:background-color .1s ease-in-out;transition:background-color .1s ease-in-out;line-height:22px;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none;font-family:inherit;-webkit-appearance:none;-moz-appearance:none;appearance:none}.Modal-button--danger,.Modal-button--danger>*{color:#3e3e3e !important;-webkit-appearance:none;border:0;text-transform:uppercase;outline:none;font-size:11px;font-weight:500}.Modal-button--danger:hover{background-color:#000;box-shadow:none}.Modal-button--danger:hover{background-color:#f0523d}.Modal-button--danger:hover,.Modal-button--danger:hover *{color:#fff !important}.Survey-body{overflow:hidden}.Survey .expand-enter{max-height:0;-webkit-transition:max-height .6s ease-in-out;transition:max-height .6s ease-in-out}.Survey .expand-enter.expand-enter-active{max-height:500px}.CancellationSurvey-title{text-transform:uppercase;font-weight:500;font-size:16px}.CancellationSurvey .Survey-prompt{margin-top:11px;font-size:12px;color:#797979}.sqs-spin{background-color:transparent;border-radius:150px;display:inline-block;vertical-align:middle;-webkit-animation:sqs-spin 1s infinite linear;-moz-animation:sqs-spin 1s infinite linear;-ms-animation:sqs-spin 1s infinite linear;-o-animation:sqs-spin 1s infinite linear;animation:sqs-spin 1s infinite linear}.sqs-spin.light{border:2px solid rgba(255,255,255,.7);border-top-color:rgba(255,255,255,.15);border-left-color:rgba(255,255,255,.15)}.sqs-spin.dark{border:2px solid rgba(0,0,0,.75);border-top-color:rgba(0,0,0,.08);border-left-color:rgba(0,0,0,.08)}.sqs-spin.extra-small{width:4px;height:4px}.sqs-spin.small{width:8px;height:8px}.sqs-spin.default{width:12px;height:12px}.sqs-spin.large{width:22px;height:22px}.sqs-spin.extra-large{width:40px;height:40px}.sqs-spin.xx-large{width:80px;height:80px}.sqs-spin.degraded{border:0px;border-radius:0px;-webkit-animation:none;-moz-animation:none;-ms-animation:none;-o-animation:none;animation:none}.sqs-spin.degraded img{width:100%;height:100%;border:0 !important;outline:0 !important;box-shadow:none !important}@-webkit-keyframes sqs-spin{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg)}}@-moz-keyframes sqs-spin{0%{-moz-transform:rotate(0deg)}100%{-moz-transform:rotate(360deg)}}@-ms-keyframes sqs-spin{0%{-ms-transform:rotate(0deg)}100%{-ms-transform:rotate(360deg)}}@-o-keyframes sqs-spin{0%{-o-transform:rotate(0deg)}100%{-o-transform:rotate(360deg)}}@keyframes sqs-spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}/*IE9_SPLIT_MARKER*/\n\n/*! Squarespace LESS Compiler  (less.js language v1.3.3)  */\n.sqs-block.vsize-1 .sqs-block-content{height:34px}.sqs-block.vsize-2 .sqs-block-content{height:68px}.sqs-block.vsize-3 .sqs-block-content{height:102px}.sqs-block.vsize-4 .sqs-block-content{height:136px}.sqs-block.vsize-5 .sqs-block-content{height:170px}.sqs-block.vsize-6 .sqs-block-content{height:204px}.sqs-block.vsize-7 .sqs-block-content{height:238px}.sqs-block.vsize-8 .sqs-block-content{height:272px}.sqs-block.vsize-9 .sqs-block-content{height:306px}.sqs-block.vsize-10 .sqs-block-content{height:340px}.sqs-block.vsize-11 .sqs-block-content{height:374px}.sqs-block.vsize-12 .sqs-block-content{height:408px}.sqs-block.vsize-13 .sqs-block-content{height:442px}.sqs-block.vsize-14 .sqs-block-content{height:476px}.sqs-block.vsize-15 .sqs-block-content{height:510px}.sqs-block.vsize-16 .sqs-block-content{height:544px}.sqs-block.vsize-17 .sqs-block-content{height:578px}.sqs-block.vsize-18 .sqs-block-content{height:612px}.sqs-block.vsize-19 .sqs-block-content{height:646px}.sqs-block.vsize-20 .sqs-block-content{height:680px}.sqs-block.vsize-21 .sqs-block-content{height:714px}.sqs-block.vsize-22 .sqs-block-content{height:748px}.sqs-block.vsize-23 .sqs-block-content{height:782px}.sqs-block.vsize-24 .sqs-block-content{height:816px}.sqs-block.vsize-25 .sqs-block-content{height:850px}.sqs-block.vsize-26 .sqs-block-content{height:884px}.sqs-block.vsize-27 .sqs-block-content{height:918px}.sqs-block.vsize-28 .sqs-block-content{height:952px}.sqs-block.vsize-29 .sqs-block-content{height:986px}.sqs-block.vsize-30 .sqs-block-content{height:1020px}.sqs-row{width:auto !important;*zoom:1}.sqs-row:before,.sqs-row:after{content:\"\";display:table}.sqs-row:after{clear:both}[class*=sqs-col]{float:left}[class*=sqs-col] .sqs-block{padding-left:17px;padding-right:17px}[class*=sqs-col]:last-child{padding-right:0}.sqs-col-12{width:100%}.sqs-col-12 .sqs-col-12{width:100%}.sqs-col-12 .sqs-col-11{width:91.6667%}.sqs-col-12 .sqs-col-10{width:83.3333%}.sqs-col-12 .sqs-col-9{width:75%}.sqs-col-12 .sqs-col-8{width:66.6667%}.sqs-col-12 .sqs-col-7{width:58.3333%}.sqs-col-12 .sqs-col-6{width:50%}.sqs-col-12 .sqs-col-5{width:41.6667%}.sqs-col-12 .sqs-col-4{width:33.3333%}.sqs-col-12 .sqs-col-3{width:25%}.sqs-col-12 .sqs-col-2{width:16.6667%}.sqs-col-12 .sqs-col-1{width:8.3333%}.sqs-col-11{width:91.6667%}.sqs-col-11 .sqs-col-11{width:100%}.sqs-col-11 .sqs-col-10{width:90.9091%}.sqs-col-11 .sqs-col-9{width:81.8182%}.sqs-col-11 .sqs-col-8{width:72.7273%}.sqs-col-11 .sqs-col-7{width:63.6364%}.sqs-col-11 .sqs-col-6{width:54.5455%}.sqs-col-11 .sqs-col-5{width:45.4545%}.sqs-col-11 .sqs-col-4{width:36.3636%}.sqs-col-11 .sqs-col-3{width:27.2727%}.sqs-col-11 .sqs-col-2{width:18.1818%}.sqs-col-11 .sqs-col-1{width:9.0909%}.sqs-col-10{width:83.3333%}.sqs-col-10 .sqs-col-10{width:100%}.sqs-col-10 .sqs-col-9{width:90%}.sqs-col-10 .sqs-col-8{width:80%}.sqs-col-10 .sqs-col-7{width:70%}.sqs-col-10 .sqs-col-6{width:60%}.sqs-col-10 .sqs-col-5{width:50%}.sqs-col-10 .sqs-col-4{width:40%}.sqs-col-10 .sqs-col-3{width:30%}.sqs-col-10 .sqs-col-2{width:20%}.sqs-col-10 .sqs-col-1{width:10%}.sqs-col-9{width:75%}.sqs-col-9 .sqs-col-9{width:100%}.sqs-col-9 .sqs-col-8{width:88.8889%}.sqs-col-9 .sqs-col-7{width:77.7778%}.sqs-col-9 .sqs-col-6{width:66.6667%}.sqs-col-9 .sqs-col-5{width:55.5556%}.sqs-col-9 .sqs-col-4{width:44.4444%}.sqs-col-9 .sqs-col-3{width:33.3333%}.sqs-col-9 .sqs-col-2{width:22.2222%}.sqs-col-9 .sqs-col-1{width:11.1111%}.sqs-col-8{width:66.6667%}.sqs-col-8 .sqs-col-8{width:100%}.sqs-col-8 .sqs-col-7{width:87.5%}.sqs-col-8 .sqs-col-6{width:75%}.sqs-col-8 .sqs-col-5{width:62.5%}.sqs-col-8 .sqs-col-4{width:50%}.sqs-col-8 .sqs-col-3{width:37.5%}.sqs-col-8 .sqs-col-2{width:25%}.sqs-col-8 .sqs-col-1{width:12.5%}.sqs-col-7{width:58.3333%}.sqs-col-7 .sqs-col-7{width:100%}.sqs-col-7 .sqs-col-6{width:85.7143%}.sqs-col-7 .sqs-col-5{width:71.4286%}.sqs-col-7 .sqs-col-4{width:57.1429%}.sqs-col-7 .sqs-col-3{width:42.8571%}.sqs-col-7 .sqs-col-2{width:28.5714%}.sqs-col-7 .sqs-col-1{width:14.2857%}.sqs-col-6{width:50%}.sqs-col-6 .sqs-col-6{width:100%}.sqs-col-6 .sqs-col-5{width:83.3333%}.sqs-col-6 .sqs-col-4{width:66.6667%}.sqs-col-6 .sqs-col-3{width:50%}.sqs-col-6 .sqs-col-2{width:33.3333%}.sqs-col-6 .sqs-col-1{width:16.6667%}.sqs-col-5{width:41.6667%}.sqs-col-5 .sqs-col-5{width:100%}.sqs-col-5 .sqs-col-4{width:80%}.sqs-col-5 .sqs-col-3{width:60%}.sqs-col-5 .sqs-col-2{width:40%}.sqs-col-5 .sqs-col-1{width:20%}.sqs-col-4{width:33.3333%}.sqs-col-4 .sqs-col-4{width:100%}.sqs-col-4 .sqs-col-3{width:75%}.sqs-col-4 .sqs-col-2{width:50%}.sqs-col-4 .sqs-col-1{width:25%}.sqs-col-3{width:25%}.sqs-col-3 .sqs-col-3{width:100%}.sqs-col-3 .sqs-col-2{width:66.6667%}.sqs-col-3 .sqs-col-1{width:33.3333%}.sqs-col-2{width:16.6667%}.sqs-col-2 .sqs-col-2{width:100%}.sqs-col-2 .sqs-col-1{width:50%}.sqs-col-1{width:8.3333%}.sqs-col-1 .sqs-col-1{width:100%}.sqs-layout > .sqs-row{margin-left:-17px;margin-right:-17px}.sqs-layout:not(.sqs-editing) .sqs-row .sqs-block:not(.float):first-child{padding-top:0}.sqs-layout:not(.sqs-editing) .sqs-block+.sqs-row .sqs-block:not(.float):first-child{padding-top:17px}.sqs-layout:not(.sqs-editing) .sqs-row+.sqs-row .sqs-block:not(.float):first-child{padding-top:17px}.sqs-layout:not(.sqs-editing)>.sqs-row:first-child>[class*=sqs-col]:first-child>.sqs-block:last-child,.sqs-layout:not(.sqs-editing) .sqs-block+.sqs-row .sqs-block:not(.float):last-child{padding-bottom:17px}.sqs-layout:not(.sqs-editing) .sqs-row+.sqs-row:not(:last-child) .sqs-block:last-child{padding-bottom:17px}.sqs-block.sized .sqs-block-content{overflow:hidden}.text-align-center{text-align:center}.text-align-right{text-align:right}.columns-1 [class*=sqs-col-]{width:100% !important}.sqs-block .state-message,.sqs-state-message{font:400 normal 12px / 22px 'Gotham SSm A','Gotham SSm B','Gotham SSm','Gotham','Proxima Nova','Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;letter-spacing:normal;padding:19px;padding-left:60px;color:#3e3e3e;position:relative;background-color:rgba(128,128,128,.15000000000000002)}.sqs-block .state-message:after,.sqs-state-message:after{content:\" \";position:absolute;top:0;left:0;height:60px;width:60px;background:transparent url(/universal/images-v6/icons/block-indicator-dark.png) no-repeat center}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:1.5dppx){.sqs-block .state-message:after,.sqs-state-message:after{background-image:url('/universal/images-v6/icons/block-indicator-dark@2x.png');background-size:22px}}.sqs-block .state-message.information,.sqs-state-message.information{background:#222;padding:30px 20px;text-align:center;color:#999;font-size:11px}.sqs-block .state-message .title,.sqs-state-message .title{padding-bottom:8px;font-size:14px}html.cameron .sqs-block .state-message .title,html.cameron .sqs-state-message .title{color:#eee}.sqs-block .state-message>.sqs-state-message-button,.sqs-state-message>.sqs-state-message-button,.sqs-block .state-message .sqs-state-message-buttons-wrapper,.sqs-state-message .sqs-state-message-buttons-wrapper{margin-top:19px;margin-left:-41px;display:block !important;position:relative}.sqs-block .state-message .sqs-state-message-button,.sqs-state-message .sqs-state-message-button{cursor:pointer;outline:none;background:#3e3e3e;padding:11px;-webkit-transition:background-color .1s ease-in-out;-moz-transition:background-color .1s ease-in-out;-ms-transition:background-color .1s ease-in-out;-o-transition:background-color .1s ease-in-out;transition:background-color .1s ease-in-out;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none;font-family:inherit;-webkit-appearance:none;-moz-appearance:none;appearance:none;line-height:22px;text-align:center;display:inline-block;position:relative}.sqs-block .state-message .sqs-state-message-button,.sqs-state-message .sqs-state-message-button,.sqs-block .state-message .sqs-state-message-button>*,.sqs-state-message .sqs-state-message-button>*{color:#fff !important;-webkit-appearance:none;border:0;text-transform:uppercase;outline:none;font-size:11px;font-weight:500}.sqs-block .state-message .sqs-state-message-button:hover,.sqs-state-message .sqs-state-message-button:hover{background-color:#000;box-shadow:none}.sqs-layout.sqs-editing .sqs-block .sqs-block .state-message .sqs-state-message-button,.sqs-layout.sqs-editing .sqs-block .sqs-state-message .sqs-state-message-button{z-index:1001}.sqs-col-0{width:0;display:none}.sqs-block{position:relative;height:auto;outline:1px solid transparent;-webkit-transition:box-shadow .1s ease-in-out;-moz-transition:box-shadow .1s ease-in-out;-ms-transition:box-shadow .1s ease-in-out;-o-transition:box-shadow .1s ease-in-out;transition:box-shadow .1s ease-in-out;padding-top:17px;padding-bottom:17px}.sqs-block:not(.sqs-block-html):not(.sqs-block-markdown){clear:both}.sqs-layout.sqs-editing .sqs-block.sqs-block-focused:not(.sqs-block-html),.sqs-layout.sqs-editing .sqs-block.sqs-block-editing:not(.sqs-block-html),html:not(.blogapp) .sqs-layout.sqs-editing .sqs-block.sqs-block.sqs-selected,html:not(.blogapp) .sqs-layout.sqs-editing .sqs-block.sqs-block-editable:hover,.sqs-layout.sqs-editing .sqs-block.sqs-confirmation-open{box-shadow:inset 0 0 0 1px rgba(0,0,0,.1)}.sqs-layout.sqs-editing .sqs-block.sqs-block-focused.sqs-block-html:hover,.sqs-layout.sqs-editing .sqs-block.sqs-block.sqs-selected.sqs-block-html.sqs-block-editing{box-shadow:none !important}.sqs-layout.sqs-editing .sqs-block.sqs-dd-dragging,.sqs-layout.sqs-editing .sqs-block.yui3-dd-dragging{z-index:9995 !important;opacity:.3;-webkit-transition:opacity .15s ease-in-out, -webkit-transform .15s ease-in-out;-moz-transition:opacity .15s ease-in-out, -webkit-transform .15s ease-in-out;-ms-transition:opacity .15s ease-in-out, -webkit-transform .15s ease-in-out;-o-transition:opacity .15s ease-in-out, -webkit-transform .15s ease-in-out;transition:opacity .15s ease-in-out, -webkit-transform .15s ease-in-out;box-sizing:border-box}.sqs-block iframe.embedded-scripts-preview{display:block;position:relative;border:0}.sqs-block .removed-script{display:block;opacity:.6}.sqs-block .removed-script::before{content:'Script Disabled';font-style:italic}html.blogapp .sqs-block{-webkit-transition:none !important;-moz-transition:none !important;-ms-transition:none !important;-o-transition:none !important;transition:none !important}html .sqs-block.sqs-block-editable:not(.sqs-block-editing){cursor:url(/universal/images-v6/grab.cur) 8 8,move;cursor:-webkit-grab;cursor:-moz-grab}html .sqs-block.sqs-block-editable:not(.sqs-block-editing) .sqs-dd-invalid-handle{cursor:default}html.sqs-dragging-block *{cursor:url(/universal/images-v6/grabbing.cur) 8 8,move;cursor:-webkit-grabbing;cursor:-moz-grabbing}html .sqs-locked-layout .sqs-block{cursor:default !important}html .sqs-block.sqs-block-html .sqs-block-content{cursor:auto}.sqs-block.sqs-block-code img{max-width:100%}.sqs-block-hidden{height:0;overflow:hidden}.yui3-overlay-hidden{display:none}.sqs-editing-overlay{position:absolute;top:0;left:0;right:0;bottom:0;z-index:1000;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none}.sqs-locked-height .sqs-editing-overlay{background-color:rgba(255,255,255,.5)}html.browser-msie .sqs-editing-overlay{background-color:rgba(128,128,128,.05)}body.sqs-dragging .sqs-layout .sqs-content-overlay{display:block !important}.sqs-content-overlay{position:absolute;left:0;width:100%}[class*=aspect-overlay]{padding-bottom:20px;position:absolute}[class*=aspect-overlay] .yui3-overlay-content{background:rgba(0,0,0,.9);color:#fff;font:12px/24px 'Helvetica Neue',Helvetica,Arial,sans-serif;text-align:center;width:50px;border-radius:5px}.sqs-block .yui3-resize-handle{display:none;position:absolute;height:50px;width:50px}.sqs-block .yui3-resize-handles-wrapper{z-index:10}.sqs-block .yui3-resize-handle-inner{position:absolute;top:50%;left:50%;margin-top:-7px;margin-left:-7px;height:13px;width:13px;border-radius:100px;background-color:grey}.sqs-block .yui3-resize-handle-b{margin-bottom:-25px;margin-left:-25px;bottom:-1px;left:50%;cursor:row-resize}.sqs-block .yui3-resize-handle-l{left:0;top:0;bottom:0;width:10px;cursor:col-resize}.sqs-block .yui3-resize-handle-r{right:0;top:0;bottom:0;width:10px;cursor:col-resize}.sqs-block .yui3-resize-handle.sqs-dd-dragging .yui3-resize-handle,.sqs-block .yui3-resize-handle.yui3-dd-dragging .yui3-resize-handle{display:none}.sqs-block[class*=focused] .yui3-resize-handle,.sqs-block.sqs-block-editing .yui3-resize-handle{display:block;z-index:9999}.sqs-block[class*=float]{z-index:10 !important;box-sizing:border-box;clear:none}.sqs-block[class*=float-left]{float:left;margin-right:17px}.sqs-block[class*=float-left]+ .sqs-block[class*=float-left]{clear:left}.sqs-block[class*=float-right]{float:right;margin-left:17px}.sqs-block[class*=float-right]+ .sqs-block[class*=float-right]{clear:right}.sqs-remove-button{position:absolute !important;border-radius:100px;background:#111 url('icon_close_14_light.png') center center no-repeat;background-size:7px;cursor:pointer}body.sqs-index .sqs-remove-button{background:#111 url('/universal/images-v6/icons/icon_close_14_light.png') center center no-repeat}.sqs-block-error{padding-top:12px;padding-bottom:12px}.sqs-block-error .sqs-block-content{border:1px solid #ddd;background:#eee;color:#333}html.blogapp .sqs-block-error{padding-top:17px;padding-bottom:17px}html.blogapp .sqs-block-error .sqs-block-content{padding:6px 12px}html.blogapp .sqs-state-message,html.blogapp .state-message{display:block;border:1px solid #ddd;padding:6px 12px;background:#eee;text-align:center;color:#333}.sqs-block .sqs-intrinsic{position:relative !important}.sqs-block .sqs-intrinsic .sqs-intrinsic-content{position:absolute !important;top:0;left:0;height:100%;max-width:none;width:100%}@font-face{font-family:'squarespace-ui-font';src:url('//static.squarespace.com/universal/fonts/squarespace-ui-font.eot');src:url('//static.squarespace.com/universal/fonts/squarespace-ui-font.eot?#iefix') format('embedded-opentype'),url('//static.squarespace.com/universal/fonts/squarespace-ui-font.svg#squarespace-ui-font') format('svg'),url('//static.squarespace.com/universal/fonts/squarespace-ui-font.woff') format('woff'),url('//static.squarespace.com/universal/fonts/squarespace-ui-font.ttf') format('truetype');font-weight:normal;font-style:normal}.sqs-ui-font-family{font-family:'squarespace-ui-font';font-style:normal;speak:none;font-weight:normal;-webkit-font-smoothing:antialiased}[class^=\"sqs-ui-font-\"]:before,[class*=\" sqs-ui-font-\"]:before{font-family:'squarespace-ui-font';font-style:normal;speak:none;font-weight:normal;-webkit-font-smoothing:antialiased}[data-icon]:before{font-family:'squarespace-ui-font';font-style:normal;speak:none;font-weight:normal;-webkit-font-smoothing:antialiased;content:attr(data-icon)}#list-paging a,#item-paging a{text-decoration:none}#list-paging a.newer .pagination-icon:before,#item-paging a.newer .pagination-icon:before{font-family:'squarespace-ui-font';font-style:normal;speak:none;font-weight:normal;-webkit-font-smoothing:antialiased;content:\"\\e000\";text-align:center;display:inline-block;vertical-align:middle}#list-paging a.newer .pagination-icon:before,#item-paging a.newer .pagination-icon:before{font-size:16px;width:16px;height:16px;line-height:16px}#list-paging a.newer .pagination-icon:before,#item-paging a.newer .pagination-icon:before{font-size:inherit;width:16px;height:16px;line-height:16px}#list-paging a.older .pagination-icon:after,#item-paging a.older .pagination-icon:after{font-family:'squarespace-ui-font';font-style:normal;speak:none;font-weight:normal;-webkit-font-smoothing:antialiased;content:\"\\e003\";text-align:center;display:inline-block;vertical-align:middle}#list-paging a.older .pagination-icon:after,#item-paging a.older .pagination-icon:after{font-size:16px;width:16px;height:16px;line-height:16px}#list-paging a.older .pagination-icon:after,#item-paging a.older .pagination-icon:after{font-size:inherit;width:16px;height:16px;line-height:16px}#list-paging,#item-paging{border-top:1px solid #e3e3e3;border-bottom:1px solid #e3e3e3;margin:3em 0 0 0}#list-paging a,#item-paging a{padding:1.5em 0;display:inline-block;width:48%}#list-paging a.newer .pagination-icon,#item-paging a.newer .pagination-icon{margin-right:.5em}#list-paging a.newer:after,#item-paging a.newer:after{content:\"Newer\"}#list-paging a.older,#item-paging a.older{float:right;text-align:right}#list-paging a.older .pagination-icon,#item-paging a.older .pagination-icon{margin-left:.5em}#list-paging a.older:before,#item-paging a.older:before{content:\"Older\"}#list-paging a.disabled,#item-paging a.disabled{color:#ddd}.like-share{float:right}.like-share .sqs-simple-like{display:inline-block;margin-right:.5em}.like-share .sqs-simple-like .like-icon{float:none;display:inline-block;vertical-align:middle}.like-share .squarespace-social-buttons{display:inline-block;margin-right:.5em}.like-share .squarespace-social-buttons .ss-social-button-icon{float:none;display:inline-block;vertical-align:middle}.like-share.empty{display:none}body:not(.event-show-past-events) .eventlist.eventlist--past{display:none}.eventlist-event{position:relative;margin:68px 0 0 0;padding:0}.eventlist-event:first-of-type{margin:0}.eventlist-column-thumbnail{display:block;float:left;width:35%;height:0;padding-bottom:23.333333333333332%;text-decoration:none !important;background:rgba(110,110,110,.05)}.eventlist-column-thumbnail img{-webkit-transition:opacity .3s ease-in;-moz-transition:opacity .3s ease-in;-ms-transition:opacity .3s ease-in;-o-transition:opacity .3s ease-in;transition:opacity .3s ease-in}.eventlist-column-thumbnail img:not(.loaded){opacity:0}body:not(.event-thumbnails) .eventlist-column-thumbnail{display:none}.event-disable-item-pages .eventlist-column-thumbnail{cursor:default;pointer-events:none}.event-thumbnail-size-11-square .eventlist-column-thumbnail{padding-bottom:35%}.event-thumbnail-size-32-standard .eventlist-column-thumbnail{padding-bottom:23.333333333333332%}.event-thumbnail-size-23-standard-vertical .eventlist-column-thumbnail{padding-bottom:52.5%}.event-thumbnail-size-43-four-thirds .eventlist-column-thumbnail{padding-bottom:26.25%}.event-thumbnail-size-169-widescreen .eventlist-column-thumbnail{padding-bottom:19.6875%}.event-thumbnail-size-2401-anamorphic-widescreen .eventlist-column-thumbnail{padding-bottom:14.583333333333334%}.eventlist-column-thumbnail:empty{height:auto;min-height:100px;padding-bottom:0 !important;background:transparent}.eventlist-column-date{display:block;position:absolute;top:0;left:0;width:35%;margin:0;padding:0;color:#333 !important;text-decoration:none !important}.event-disable-item-pages .eventlist-column-date{cursor:default;pointer-events:none}body:not(.event-date-label) .eventlist-column-date{display:none}body:not(.event-thumbnails) .eventlist-column-date{position:static;float:left;width:70px}.eventlist-datetag{display:table;position:absolute;top:10px;right:10px;height:auto;min-height:70px;width:70px;margin:0;padding:0;background:#fff;color:#333;font-size:14px;line-height:14px;text-align:center;box-sizing:border-box}body:not(.event-thumbnails) .eventlist-datetag{position:static;background:#e8ecec}.eventlist-event:not(.eventlist-event--hasimg) .eventlist-datetag{top:0;background:#e8ecec}.eventlist-datetag-inner{display:table-cell;vertical-align:middle;margin:0;padding:6px;color:inherit;font-size:0;line-height:0;letter-spacing:0}.eventlist-datetag-startdate--month,.eventlist-datetag-startdate--day,.eventlist-datetag-time,.eventlist-datetag-enddate{margin:3px 0;line-height:1em;text-transform:uppercase;white-space:nowrap}.eventlist-event--past .eventlist-datetag-startdate--month,.eventlist-event--past .eventlist-datetag-startdate--day,.eventlist-event--past .eventlist-datetag-time,.eventlist-event--past .eventlist-datetag-enddate{opacity:.3}.eventlist-datetag-time,.eventlist-datetag-enddate{border-top:1px solid #ddd;margin:6px 0 0 0;padding-top:6px;font-size:11px}.eventlist-datetag-startdate--month{font-size:14px;margin-top:6px}.eventlist-datetag-startdate--day{font-size:26px}body:not(.event-date-label-time) .eventlist-datetag-time{display:none}.eventlist-datetag-enddate:before{content:\"to \"}.eventlist-datetag-status{display:none;position:absolute;top:0px;left:35px;width:1px;height:70px;background:#000;-webkit-transform:rotate(45deg);-moz-transform:rotate(45deg);-ms-transform:rotate(45deg);-o-transform:rotate(45deg);transform:rotate(45deg)}.eventlist-event--past .eventlist-datetag-status{display:block}.eventlist-event--past.eventlist-event--multiday .eventlist-datetag-status,body.event-date-label-time .eventlist-event--past .eventlist-datetag-status{top:0px;height:85px;-webkit-transform:rotate(38deg);-moz-transform:rotate(38deg);-ms-transform:rotate(38deg);-o-transform:rotate(38deg);transform:rotate(38deg)}.eventlist-column-info{float:left;width:65%;padding:0 0 0 34px;box-sizing:border-box}body:not(.event-thumbnails) .eventlist-column-info{width:calc(100% -  70px);width:-webkit-calc(100% -  70px);width:-moz-calc(100% -  70px)}body:not(.event-thumbnails):not(.event-date-label) .eventlist-column-info{width:100%;padding-left:0}.eventlist-cats{margin:0 0 4.25px 0;padding:0;font-size:14px;line-height:1.4em}.eventlist-cats a{color:inherit !important;text-decoration:none !important}body:not(.event-list-show-cats) .eventlist-cats{display:none}.eventlist-title{margin:0 0 17px 0 !important;padding:0 !important;font-size:28px !important;line-height:1.2em !important}.eventlist-title .eventlist-title-link{margin:0 !important;padding:0 !important;color:inherit !important;text-decoration:none !important;font-size:inherit !important;line-height:inherit !important}.eventlist-title .eventlist-title-link:empty:before{content:\"Untitled Event\"}.event-disable-item-pages .eventlist-title .eventlist-title-link{cursor:default;pointer-events:none}.eventlist-meta{list-style-type:none;margin:0 0 17px 0;padding:0}.eventlist-meta-item{margin:0;padding:0;text-align:left}.event-icons .eventlist-meta-item{position:relative;padding-left:25.5px}.event-icons .eventlist-meta-item:before{opacity:.5;position:absolute;top:3px;left:-2px}.event-icons .eventlist-meta-item.eventlist-meta-date:before{font-family:'squarespace-ui-font';font-style:normal;speak:none;font-weight:normal;-webkit-font-smoothing:antialiased;content:\"\\e015\";text-align:center;display:inline-block;vertical-align:middle}.event-icons .eventlist-meta-item.eventlist-meta-date:before{font-size:16px;width:16px;height:16px;line-height:16px}.event-icons .eventlist-meta-item.eventlist-meta-time:before{font-family:'squarespace-ui-font';font-style:normal;speak:none;font-weight:normal;-webkit-font-smoothing:antialiased;content:\"\\e00c\";text-align:center;display:inline-block;vertical-align:middle}.event-icons .eventlist-meta-item.eventlist-meta-time:before{font-size:16px;width:16px;height:16px;line-height:16px}.event-icons .eventlist-meta-item.eventlist-meta-address:before{font-family:'squarespace-ui-font';font-style:normal;speak:none;font-weight:normal;-webkit-font-smoothing:antialiased;content:\"\\e02f\";text-align:center;display:inline-block;vertical-align:middle}.event-icons .eventlist-meta-item.eventlist-meta-address:before{font-size:16px;width:16px;height:16px;line-height:16px}body:not(.event-list-date) .eventlist-meta-date,body:not(.event-list-time) .eventlist-meta-time,body:not(.event-list-address) .eventlist-meta-address{display:none}.event-list-time .eventlist-event--multiday .eventlist-meta-date .event-date:after{content:\", \"}.event-list-time .eventlist-event--multiday .eventlist-meta-time{display:inline-block}.eventlist-meta-address-line:after{content:\", \"}.eventlist-meta-address-line:last-of-type:after{content:none}.eventlist-meta-address-maplink:before{content:\"(map)\"}body:not(.event-list-icalgcal-links) .eventlist-meta-export{display:none}.eventlist-meta-export-google:before{content:'Google Calendar'}.eventlist-meta-export-divider{margin:0 4px}.eventlist-meta-export-divider:before{content:\"\\00B7\"}.eventlist-meta-export-ical:before{content:'ICS'}body:not(.event-excerpts) .eventlist-description,body:not(.event-excerpts) .eventlist-excerpt{display:none}.eventlist-excerpt{margin:0 0 17px 0}.eventlist-button{margin:5.666666666666667px 0 25.5px 0;display:inline-block;width:auto;height:auto;padding:1em 2.5em;color:#fff;background-color:#272727;border-width:0;font-family:\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-size:12px;line-height:1em;font-weight:normal;font-style:normal;text-transform:uppercase;letter-spacing:0px;text-align:center;text-decoration:none;cursor:pointer;outline:none;-webkit-appearance:none;-moz-appearance:none;appearance:none}.eventlist-button:before{content:\"View Event \\2192\"}body:not(.event-list-cta-button) .eventlist-button{display:none !important}body:not(.event-list-like-and-share-buttons) .eventlist-actions{display:none}.eventlist-actions .sqs-simple-like{line-height:inherit}.eventlist-actions .sqs-simple-like .like-count{margin-right:1.2em}.eventlist-actions .sqs-simple-like .like-count:before{font-family:'squarespace-ui-font';font-style:normal;speak:none;font-weight:normal;-webkit-font-smoothing:antialiased;content:\"\\e012\";text-align:center;display:inline-block;vertical-align:middle}.eventlist-actions .sqs-simple-like .like-count:before{font-size:16px;width:16px;height:16px;line-height:16px}.eventlist-actions .sqs-simple-like .like-count:before{margin-right:.2em;position:relative;top:.13em;font-size:1.2em;width:auto;height:auto;line-height:inherit;text-align:left;vertical-align:initial}.eventlist-actions .sqs-simple-like .like-icon{display:none}.eventlist-actions .ss-social-button:before{font-family:'squarespace-ui-font';font-style:normal;speak:none;font-weight:normal;-webkit-font-smoothing:antialiased;content:\"\\e02b\";text-align:center;display:inline-block;vertical-align:middle}.eventlist-actions .ss-social-button:before{font-size:16px;width:16px;height:16px;line-height:16px}.eventlist-actions .ss-social-button:before{margin-right:.4em;font-size:.85em;width:auto;height:auto;line-height:inherit;text-align:left;vertical-align:initial}.eventlist-actions .ss-social-button div{display:inline-block}.eventlist-actions .ss-social-button-icon{display:none !important}.eventlist-filter{font-size:18px;line-height:1em;margin:0 0 51px 0}.eventlist-filter:before{content:\"Filtering by: \"}.eventlist-past-upcoming-divider{display:none;height:0;border:none;border-top:1px solid rgba(230,230,230,.8);font-size:68px;line-height:68px}.eventlist--upcoming+.eventlist--past .eventlist-past-upcoming-divider{display:block}.event-datetime-divider:before{content:\" \\2013 \"}.eventlist-empty:before{content:\"There are no upcoming events at this time.\"}.eventitem-backlink{display:inline-block;margin:0 0 51px 0;text-transform:uppercase}.eventitem-backlink:before{content:\"\\2190\\0020 Back to All Events\"}body:not(.event-item-back-link) .eventitem-backlink{display:none}.eventitem{position:relative}.eventitem-column-meta{float:left;width:30%;box-sizing:border-box}.eventitem-title{margin:0 0 34px 0 !important;padding:0 !important;font-size:28px !important;line-height:1.2em !important}.eventitem-title:empty:before{content:\"Untitled Event\"}.eventitem-meta{list-style-type:none;margin:0 0 17px 0;padding:0}.eventitem-meta-item{margin:0;padding:0;font-size:.9em;line-height:1.6em}.eventitem--multiday .eventitem-meta-date .event-date:after{content:\", \"}.eventitem--multiday .eventitem-meta-time{display:inline-block}.eventitem-meta-address-line:after{content:\", \"}.eventitem-meta-address-line:last-of-type:after{content:none}.eventitem-meta-address-line.eventitem-meta-address-line--title{display:block}.eventitem-meta-address-line.eventitem-meta-address-line--title:after{content:none}.eventitem-meta-address-maplink:before{content:\"(map)\"}body:not(.event-icalgcal-links) .event-meta-addtocalendar-container{display:none}.eventitem-meta-export-google:before{content:\"Google Calendar\"}.eventitem-meta-export-divider{margin:0 4px}.eventitem-meta-export-divider:before{content:\"\\00B7\"}.eventitem-meta-export-ical:before{content:\"ICS\"}.eventitem-meta-cats:before{content:\"Posted in \"}.eventitem-meta-tags:before{content:\"Tagged \"}.event-meta-socialicon-container .sqs-simple-like{line-height:inherit}.event-meta-socialicon-container .sqs-simple-like .like-count{margin-right:1.2em}.event-meta-socialicon-container .sqs-simple-like .like-count:before{font-family:'squarespace-ui-font';font-style:normal;speak:none;font-weight:normal;-webkit-font-smoothing:antialiased;content:\"\\e012\";text-align:center;display:inline-block;vertical-align:middle}.event-meta-socialicon-container .sqs-simple-like .like-count:before{font-size:16px;width:16px;height:16px;line-height:16px}.event-meta-socialicon-container .sqs-simple-like .like-count:before{margin-right:.2em;position:relative;top:.13em;font-size:1.2em;width:auto;height:auto;line-height:inherit;text-align:left;vertical-align:initial}.event-meta-socialicon-container .sqs-simple-like .like-icon{display:none}.event-meta-socialicon-container .ss-social-button:before{font-family:'squarespace-ui-font';font-style:normal;speak:none;font-weight:normal;-webkit-font-smoothing:antialiased;content:\"\\e02b\";text-align:center;display:inline-block;vertical-align:middle}.event-meta-socialicon-container .ss-social-button:before{font-size:16px;width:16px;height:16px;line-height:16px}.event-meta-socialicon-container .ss-social-button:before{margin-right:.4em;font-size:.85em;width:auto;height:auto;line-height:inherit;text-align:left;vertical-align:initial}.event-meta-socialicon-container .ss-social-button div{display:inline-block}.event-meta-socialicon-container .ss-social-button-icon{display:none !important}body:not(.event-like-and-share-buttons) .event-meta-socialicon-container{display:none}.eventitem-column-content{float:left;width:70%;padding-left:34px;box-sizing:border-box}.eventitem-content-footer{margin:17px 0 0 0}.eventitem-content-footer .eventitem-sourceurl{margin:0 0 8.5px 0}.eventitem-content-footer .eventitem-sourceurl:before{content:\"Source: \"}.eventitem-content-footer .eventitem-meta{margin:0 0 8.5px 0}.eventitem-content-footer .eventitem-meta>*{font-size:inherit}.eventitem-pager{margin:170px 0 0 0}.eventitem-pager-newer,.eventitem-pager-older{float:left;display:inline-block;text-decoration:none;box-sizing:border-box}.eventitem-pager-older .eventitem-pager-date:before{content:\"Earlier Event: \"}.eventitem-pager-newer{float:right;text-align:right}.eventitem-pager-newer .eventitem-pager-date:before{content:\"Later Event: \"}.eventitem-pager-disabled{opacity:.4}.event-list-compact-view .eventlist-column-thumbnail,.event-list-compact-view .eventlist-column-date,.event-list-compact-view .eventlist-column-info{width:100% !important}.event-list-compact-view .eventlist-column-thumbnail:empty{min-height:0}.event-list-compact-view.event-thumbnail-size-11-square .eventlist-column-thumbnail{padding-bottom:100%}.event-list-compact-view.event-thumbnail-size-32-standard .eventlist-column-thumbnail{padding-bottom:66.666%}.event-list-compact-view.event-thumbnail-size-23-standard-vertical .eventlist-column-thumbnail{padding-bottom:150%}.event-list-compact-view.event-thumbnail-size-43-four-thirds .eventlist-column-thumbnail{padding-bottom:75%}.event-list-compact-view.event-thumbnail-size-169-widescreen .eventlist-column-thumbnail{padding-bottom:56.25%}.event-list-compact-view.event-thumbnail-size-2401-anamorphic-widescreen .eventlist-column-thumbnail{padding-bottom:41.666%}.event-list-compact-view .eventlist-datetag{left:0;right:auto}.event-list-compact-view.event-thumbnails .eventlist-event--hasimg .eventlist-datetag{left:10px}.event-list-compact-view.event-thumbnails .eventlist-event:not(.eventlist-event--hasimg) .eventlist-column-date{position:static;float:left;width:70px}.event-list-compact-view.event-thumbnails .eventlist-event:not(.eventlist-event--hasimg) .eventlist-column-date .eventlist-datetag{position:static}.event-list-compact-view .eventlist-column-info{margin:25.5px 0 0 0;padding:0}.event-list-compact-viewbody:not(.event-thumbnails):not(.event-date-label) .eventlist-event{margin-top:34px}.event-list-compact-viewbody:not(.event-thumbnails):not(.event-date-label) .eventlist-column-info{margin-top:0}.event-list-compact-view .eventitem-title{margin-bottom:17px !important}.event-list-compact-view .eventitem-column-meta{margin-bottom:34px}.event-list-compact-view .eventitem-column-meta,.event-list-compact-view .eventitem-column-content{float:none;width:auto;padding:0}.event-item-compact-view .eventitem-title{margin-bottom:17px !important}.event-item-compact-view .eventitem-column-meta{margin-bottom:34px}.event-item-compact-view .eventitem-column-meta,.event-item-compact-view .eventitem-column-content{float:none;width:auto;padding:0}@media only screen and (max-width:639px){.eventlist-column-thumbnail,.eventlist-column-date,.eventlist-column-info{width:100% !important}.eventlist-column-thumbnail:empty{min-height:0}.event-thumbnail-size-11-square .eventlist-column-thumbnail{padding-bottom:100%}.event-thumbnail-size-32-standard .eventlist-column-thumbnail{padding-bottom:66.666%}.event-thumbnail-size-23-standard-vertical .eventlist-column-thumbnail{padding-bottom:150%}.event-thumbnail-size-43-four-thirds .eventlist-column-thumbnail{padding-bottom:75%}.event-thumbnail-size-169-widescreen .eventlist-column-thumbnail{padding-bottom:56.25%}.event-thumbnail-size-2401-anamorphic-widescreen .eventlist-column-thumbnail{padding-bottom:41.666%}.eventlist-datetag{left:0;right:auto}.event-thumbnails .eventlist-event--hasimg .eventlist-datetag{left:10px}.event-thumbnails .eventlist-event:not(.eventlist-event--hasimg) .eventlist-column-date{position:static;float:left;width:70px}.event-thumbnails .eventlist-event:not(.eventlist-event--hasimg) .eventlist-column-date .eventlist-datetag{position:static}.eventlist-column-info{margin:25.5px 0 0 0;padding:0}body:not(.event-thumbnails):not(.event-date-label) .eventlist-event{margin-top:34px}body:not(.event-thumbnails):not(.event-date-label) .eventlist-column-info{margin-top:0}.eventitem-title{margin-bottom:17px !important}.eventitem-column-meta{margin-bottom:34px}.eventitem-column-meta,.eventitem-column-content{float:none;width:auto;padding:0}.eventitem-title{margin-bottom:17px !important}.eventitem-column-meta{margin-bottom:34px}.eventitem-column-meta,.eventitem-column-content{float:none;width:auto;padding:0}}.event-time-24hr{display:none}.event-time-format .event-time-12hr{display:none}.event-time-format .event-time-24hr{display:inline}.collection-type-gallery:not(.gallery-design-grid) .arrow.previous-slide:before{font-family:'squarespace-ui-font';font-style:normal;speak:none;font-weight:normal;-webkit-font-smoothing:antialiased;content:\"\\e000\";text-align:center;display:inline-block;vertical-align:middle}.collection-type-gallery:not(.gallery-design-grid) .arrow.previous-slide:before{font-size:16px;width:16px;height:16px;line-height:16px}.collection-type-gallery:not(.gallery-design-grid) .arrow.previous-slide:before{font-size:16px;width:40px;height:40px;line-height:40px}.collection-type-gallery:not(.gallery-design-grid) .arrow.next-slide:before{font-family:'squarespace-ui-font';font-style:normal;speak:none;font-weight:normal;-webkit-font-smoothing:antialiased;content:\"\\e003\";text-align:center;display:inline-block;vertical-align:middle}.collection-type-gallery:not(.gallery-design-grid) .arrow.next-slide:before{font-size:16px;width:16px;height:16px;line-height:16px}.collection-type-gallery:not(.gallery-design-grid) .arrow.next-slide:before{font-size:16px;width:40px;height:40px;line-height:40px}.collection-type-gallery.gallery-design-grid .dots,.collection-type-gallery.gallery-design-grid .thumbnail-wrapper,.collection-type-gallery.gallery-design-grid .circles,.collection-type-gallery.gallery-design-grid .numbers,.collection-type-gallery.gallery-design-grid .simple{display:none}.collection-type-gallery.gallery-design-grid .slide{cursor:pointer}.collection-type-gallery.gallery-design-grid .slide .slide-meta{display:none}.collection-type-gallery.gallery-design-grid .slide>a{display:block;height:100%}.collection-type-gallery.gallery-design-grid.lightbox-style-light .yui3-lightbox2 .sqs-lightbox-overlay{background:#fff}.collection-type-gallery.gallery-design-grid.lightbox-style-light .yui3-lightbox2 .sqs-lightbox-close,.collection-type-gallery.gallery-design-grid.lightbox-style-light .yui3-lightbox2 .sqs-lightbox-previous,.collection-type-gallery.gallery-design-grid.lightbox-style-light .yui3-lightbox2 .sqs-lightbox-next,.collection-type-gallery.gallery-design-grid.lightbox-style-light .yui3-lightbox2 .sqs-lightbox-meta-trigger{color:#111}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery{cursor:pointer;opacity:0;zoom:1;-ms-filter:\"progid:DXImageTransform.Microsoft.Alpha(Opacity=0)\";-webkit-transition:opacity .2s ease-out;-moz-transition:opacity .2s ease-out;-ms-transition:opacity .2s ease-out;-o-transition:opacity .2s ease-out;transition:opacity .2s ease-out}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .arrow,.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .icons span{-moz-user-select:-moz-none;-webkit-user-select:none;-ms-user-select:none;user-select:none}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .gallery-wrapper{position:relative;width:100%}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .gallery-wrapper .slides{display:block;width:100%;height:100% !important;-webkit-transition:opacity .2s ease-out;-moz-transition:opacity .2s ease-out;-ms-transition:opacity .2s ease-out;-o-transition:opacity .2s ease-out;transition:opacity .2s ease-out}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .gallery-wrapper .slides .slide{opacity:0;zoom:1;-ms-filter:\"progid:DXImageTransform.Microsoft.Alpha(Opacity=0)\";height:100% !important}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .gallery-wrapper .slides .slide>a{display:block;height:100%}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .slides-controls{position:relative;z-index:991;overflow:hidden}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .arrow{z-index:997;position:absolute;width:100%;height:40px;margin-top:-20px;text-align:center;line-height:40px;font-weight:bold;color:#fff;background:#222;-webkit-transition:opacity .1s ease-in;-moz-transition:opacity .1s ease-in;-ms-transition:opacity .1s ease-in;-o-transition:opacity .1s ease-in;transition:opacity .1s ease-in}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .arrow.previous-slide{left:0;margin-left:2%;width:40px;height:40px}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .arrow.next-slide{right:0;margin-right:2%;float:right;width:40px;height:40px}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .arrow.sqs-disabled{opacity:0}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .dots,.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .thumbnail-wrapper,.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .circles,.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .numbers,.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .simple{display:none;margin:20px 0}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .dots.sqs-gallery-controls-disabled,.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .thumbnail-wrapper.sqs-gallery-controls-disabled,.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .circles.sqs-gallery-controls-disabled,.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .numbers.sqs-gallery-controls-disabled,.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .simple.sqs-gallery-controls-disabled{display:none}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .dots{text-align:center}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .dots .dot{font-size:30px;margin:0 5px}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .dots .dot:after{content:\"·\"}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .numbers{text-align:center}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .numbers .number{font-size:12px;margin:0 .5em}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .circles{font-size:0;position:absolute;bottom:0;text-align:center;z-index:999;width:100%;height:16px;margin:40px 0}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .circles .circle{display:inline-block;width:10px;height:10px;border:2px solid #fff;margin:0 5px;border-radius:100%;-webkit-border-radius:999px}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .circles .circle.sqs-active-slide{background:#fff}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .simple{text-align:center;font-size:12px}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .simple .previous.sqs-disabled,.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .simple .next.sqs-disabled{opacity:.5}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .simple .current-index{letter-spacing:2px}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .simple .current-index:after{content:\" / \"}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .simple .previous{float:left}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .simple .previous:after{content:\"Previous\"}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .simple .next{float:right}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .simple .next:after{content:\"Next\"}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .dots .dot,.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .numbers .number,.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .thumbnail-wrapper .thumbnail{opacity:.5}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .dots .dot.sqs-active-slide,.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .numbers .number.sqs-active-slide,.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .thumbnail-wrapper .thumbnail.sqs-active-slide{opacity:1}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .thumbnail-wrapper .thumbnail{width:100px !important}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .slide-meta{display:none;position:absolute;width:100%;bottom:0;z-index:996;height:auto;background:rgba(0,0,0,.7);padding:24px 0}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .slide-meta .title{margin:0;font-size:14px;color:#fff;padding:0 2%}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .slide-meta .description{margin-top:.5em;display:inline-block;padding:0 2%}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .slide-meta .description p,.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .slide-meta .clickthrough a{font-size:13px;line-height:1.4em;color:#999}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .slide-meta .description p,.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .slide-meta .clickthrough{margin:0}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .slide-meta .description p a{color:#999;text-decoration:underline}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .slide-meta .clickthrough{display:inline-block}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .slide-meta .clickthrough a{border-bottom:1px solid}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery .slide-meta .clickthrough a:before{content:\"Read more\"}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery.sqs-system-gallery-init{position:relative}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery.sqs-system-gallery-init>*{display:none}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery.sqs-system-gallery-ready{opacity:.01;opacity:1;zoom:1;-ms-filter:\"progid:DXImageTransform.Microsoft.Alpha(Opacity=10)\"}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery.sqs-system-gallery-interaction .arrow{opacity:0}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery.sqs-system-gallery-interaction.sqs-system-gallery-hover-slides-left .arrow.previous-slide:not(.sqs-disabled),.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery.sqs-system-gallery-video-iframe .arrow.previous-slide:not(.sqs-disabled){opacity:1}.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery.sqs-system-gallery-interaction.sqs-system-gallery-hover-slides-right .arrow.next-slide:not(.sqs-disabled),.collection-type-gallery:not(.gallery-design-grid) .sqs-system-gallery.sqs-system-gallery-video-iframe .arrow.next-slide:not(.sqs-disabled){opacity:1}.collection-type-gallery:not(.gallery-design-grid).dialog-open .arrow.previous-slide:not(.sqs-disabled){opacity:1}.collection-type-gallery:not(.gallery-design-grid).dialog-open .arrow.next-slide:not(.sqs-disabled){opacity:1}.collection-type-gallery:not(.gallery-design-grid).gallery-navigation-thumbnails .sqs-system-gallery .thumbnail-wrapper{display:block}.collection-type-gallery:not(.gallery-design-grid).gallery-navigation-bullets .sqs-system-gallery .dots{display:block}.collection-type-gallery:not(.gallery-design-grid).gallery-navigation-numbers .sqs-system-gallery .numbers{display:block}.collection-type-gallery:not(.gallery-design-grid).gallery-navigation-circles .sqs-system-gallery .slide-meta{bottom:auto;top:0}.collection-type-gallery:not(.gallery-design-grid).gallery-navigation-circles .sqs-system-gallery .circles{display:block}.collection-type-gallery:not(.gallery-design-grid).gallery-navigation-simple .sqs-system-gallery .simple{display:block}.collection-type-gallery:not(.gallery-design-grid).gallery-info-overlay-always-show .sqs-system-gallery .slide-meta.show{display:block}.collection-type-gallery:not(.gallery-design-grid).gallery-info-overlay-show-on-hover .slide:hover .slide-meta.show{display:block}.collection-type-gallery:not(.gallery-design-grid):not(.gallery-show-arrows) .sqs-system-gallery .arrow{opacity:0 !important}.collection-type-gallery:not(.gallery-design-grid).gallery-aspect-ratio-11-square .sqs-system-gallery.sqs-system-gallery-init{padding-bottom:100%}.collection-type-gallery:not(.gallery-design-grid).gallery-aspect-ratio-11-square .thumbnail-wrapper{height:100px}.collection-type-gallery:not(.gallery-design-grid).gallery-aspect-ratio-32-standard .sqs-system-gallery.sqs-system-gallery-init{padding-bottom:66.66%}.collection-type-gallery:not(.gallery-design-grid).gallery-aspect-ratio-32-standard .thumbnail-wrapper{height:66px}.collection-type-gallery:not(.gallery-design-grid).gallery-aspect-ratio-43-four-thirds .sqs-system-gallery.sqs-system-gallery-init{padding-bottom:75%}.collection-type-gallery:not(.gallery-design-grid).gallery-aspect-ratio-43-four-thirds .thumbnail-wrapper{height:75px}.collection-type-gallery:not(.gallery-design-grid).gallery-aspect-ratio-169-widescreen .sqs-system-gallery.sqs-system-gallery-init{padding-bottom:56.25%}.collection-type-gallery:not(.gallery-design-grid).gallery-aspect-ratio-169-widescreen .thumbnail-wrapper{height:56.25px}.collection-type-gallery:not(.gallery-design-grid).gallery-arrow-style-circular .sqs-system-gallery .arrow{border-radius:100%;-webkit-border-radius:999px}.collection-type-gallery:not(.gallery-design-grid).gallery-arrow-style-round-corners .sqs-system-gallery .arrow{border-radius:10%;-webkit-border-radius:4px}.collection-type-gallery:not(.gallery-design-grid).gallery-arrow-style-rectangular .sqs-system-gallery .arrow{border-radius:0;-webkit-border-radius:0}.collection-type-gallery:not(.gallery-design-grid).gallery-arrow-style-no-background .sqs-system-gallery .arrow{border-radius:0;background:none;-webkit-border-radius:0}@media screen and (max-width:480px){.collection-type-gallery .sqs-system-gallery .slide-meta{display:none !important}}.sqs-audio-playlist{zoom:1}.sqs-audio-playlist.loading{visibility:hidden}.sqs-audio-playlist:after{display:block;visibility:hidden;font-size:0;height:0;clear:both;content:\".\"}.sqs-audio-playlist:after{display:block;visibility:hidden;font-size:0;height:0;clear:both;content:\".\"}.sqs-audio-playlist.hidden{display:none}.sqs-audio-playlist .album-info{width:33%;float:left;zoom:1}.sqs-audio-playlist .album-info:after{display:block;visibility:hidden;font-size:0;height:0;clear:both;content:\".\"}.sqs-audio-playlist .album-info:after{display:block;visibility:hidden;font-size:0;height:0;clear:both;content:\".\"}.sqs-audio-playlist .album-cover-wrapper{position:relative;width:90px;height:90px;margin-bottom:30px}.sqs-audio-playlist .album-controls{position:absolute;top:0;right:0;bottom:0;left:0;cursor:pointer}.sqs-audio-playlist .album-controls .button{-webkit-transition:.25s all linear;-moz-transition:.25s all linear;-ms-transition:.25s all linear;-o-transition:.25s all linear;transition:.25s all linear;-moz-border-radius:50%;border-radius:50%;position:absolute;bottom:50%;left:50%;display:block;width:90px;height:90px;margin-bottom:-45px;margin-left:-45px;background:#000}.sqs-audio-playlist .album-controls .icon{display:block;position:relative;left:50%;top:50%;margin-top:-20px;margin-left:-10px;width:0px;height:0px;border-top:18px solid transparent;border-bottom:18px solid transparent;border-left:30px solid #fff;-webkit-transform:translatez();-ms-transform:translatez();transform:translatez()}.sqs-audio-playlist .album-title{font-size:1.5em;margin:0}.sqs-audio-playlist .album-title a{text-decoration:none}.sqs-audio-playlist.playing .album-controls .button .icon{border-width:0px;margin-top:-15px;margin-left:-13px}.sqs-audio-playlist.playing .album-controls .button .icon,.sqs-audio-playlist.playing .album-controls .button .icon:before{height:30px;width:10px;background-color:#fff}.sqs-audio-playlist.playing .album-controls .button .icon:before{content:'';display:block;margin-left:16px}.sqs-audio-playlist.has-album-cover .album-cover-wrapper{position:relative;width:100%;height:0;padding-bottom:100%;margin-bottom:20px}.sqs-audio-playlist.has-album-cover .album-cover{position:absolute;top:0;right:0;bottom:0;left:0}.sqs-audio-playlist.has-album-cover .button{background:rgba(0,0,0,.7);opacity:.9}.sqs-audio-playlist.has-album-cover:hover .button{background:rgba(0,0,0,.9)}.sqs-audio-playlist.has-album-cover.playing .album-controls .button{margin:-15px;bottom:0;left:0;-webkit-transform:scale(.4,.4);-ms-transform:scale(.4,.4);transform:scale(.4,.4)}.sqs-audio-playlist.has-album-cover.playing .album-controls .button .icon{border-width:0px;margin-top:-15px;margin-left:-13px}.sqs-audio-playlist.has-album-cover.playing .album-controls .button .icon,.sqs-audio-playlist.has-album-cover.playing .album-controls .button .icon:before{height:30px;width:10px;background-color:#fff}.sqs-audio-playlist.has-album-cover.playing .album-controls .button .icon:before{content:'';display:block;margin-left:16px}.sqs-audio-playlist.has-album-cover.playing .track{opacity:.4}.sqs-audio-playlist.has-album-cover.playing .track:hover,.sqs-audio-playlist.has-album-cover.playing .track.selected{opacity:1}.sqs-audio-playlist .tracks{float:right;width:60%;margin:0;padding:0}.sqs-audio-playlist .track{list-style-type:none;padding:0;margin:0 0 5%;cursor:pointer;zoom:1;font-style:normal;font-weight:normal;letter-spacing:0;text-transform:none;font-size:13px;line-height:1.4em}.sqs-audio-playlist .track:after{display:block;visibility:hidden;font-size:0;height:0;clear:both;content:\".\"}.sqs-audio-playlist .track:after{display:block;visibility:hidden;font-size:0;height:0;clear:both;content:\".\"}.sqs-audio-playlist .track-progress-bar{clear:both;height:2px;position:relative;cursor:pointer;padding-bottom:2.5%}.sqs-audio-playlist .track-progress-bar .bar{position:absolute;top:0;left:0;height:2px;width:0%}.sqs-audio-playlist .track-progress-bar .bar.bg{width:100%}.sqs-audio-playlist .track-progress-bar .bar.play-bar{position:relative}.sqs-audio-playlist .track-meta{float:right;text-align:right}.sqs-audio-playlist .track-info .title a{font-size:16px}.sqs-audio-playlist.tablet .album-info,.sqs-audio-playlist.tablet .tracks{width:100%;float:none}.sqs-audio-playlist.tablet .album-info{padding-bottom:8%}.sqs-audio-playlist.tablet .album-cover-wrapper{float:left;margin-right:30px}.sqs-audio-playlist.tablet.has-album-cover .album-cover-wrapper{width:40%;padding-bottom:40%}.sqs-audio-playlist.tablet.no-main-image .album-description{margin-left:120px}.sqs-audio-playlist.phone .album-info,.sqs-audio-playlist.phone .tracks{width:100%;float:none}.sqs-audio-playlist.phone .tracks{margin-top:30px}.sqs-audio-playlist.phone .tracks .track{margin-bottom:10%}.sqs-audio-playlist.phone .album-info{padding-bottom:0}.sqs-audio-playlist.phone .album-cover-wrapper{float:none;margin-right:0;margin-bottom:20px}.sqs-audio-playlist.phone.has-album-cover .album-cover-wrapper{width:100%;padding-bottom:100%}.sqs-audio-playlist.phone.no-main-image .album-description{margin-left:0}.sqs-audio-playlist .track-progress-bar{-webkit-tap-highlight-color:rgba(0,0,0,.5)}.sqs-audio-playlist .track-progress-bar .bar{-webkit-tap-highlight-color:rgba(0,0,0,.5)}.sqs-audio-playlist .track-progress-bar .bar.bg{background-color:#000;background-color:rgba(0,0,0,.1)}.sqs-audio-playlist .track-progress-bar .bar.load-bar{background-color:#000;background-color:rgba(0,0,0,.05)}.sqs-audio-playlist .track-progress-bar .bar.play-bar{background-color:#000;background-color:rgba(0,0,0,.8)}.sqs-audio-playlist .track-meta .track-time{color:#000;color:rgba(0,0,0,.5)}.sqs-audio-playlist .track-meta .actions{color:#000;color:rgba(0,0,0,.2)}.sqs-audio-playlist .track-meta .actions a{color:#000;color:rgba(0,0,0,.5)}.sqs-audio-playlist .track-meta .actions a:hover{color:#000;color:rgba(0,0,0,.8)}.sqs-audio-playlist .track-info .title a{color:#000;color:rgba(0,0,0,.85)}.sqs-audio-playlist .track-info .artist{color:#000;color:rgba(0,0,0,.5)}.hide-album-share-link .sqs-audio-playlist .squarespace-social-buttons{display:none}#productList{clear:both;margin-left:-3%;margin-top:-3%;width:103%}#productList .product{cursor:pointer;float:left;margin-left:2.912621359223301%;margin-top:3%;position:relative;width:30.420711974110034%;-webkit-transform:translatez(0)}#productList .product .product-image{-webkit-transition:opacity .14s ease-out}#productList .product .product-image .intrinsic{padding-bottom:100%;line-height:0;position:relative;overflow:hidden}#productList .product .product-image .intrinsic>div{position:absolute;top:0;left:0;bottom:0;right:0;background-color:rgba(0,0,0,0)}#productList .product .product-image img{-webkit-transition:opacity .3s ease-out;-moz-transition:opacity .3s ease-out;transition:opacity .3s ease-out}#productList .product .product-mark{position:absolute;top:15px;right:0;background:#222;padding:6px 8px;color:#fff;line-height:1em;text-transform:uppercase;-webkit-font-smoothing:antialiased}#productList .product .product-title{font-size:15px;line-height:1.5em;margin-top:1em}#productList .product .product-price{font-size:12px;display:none;line-height:1.5em}#productList .product .product-price .original-price{text-decoration:line-through;opacity:.7;filter:alpha(opacity=70)}#productList .product .product-price .strikeout{text-decoration:line-through}#productList .product .product-image img{will-change:opacity}#productList .product:hover .product-image img{opacity:.8;filter:alpha(opacity=80)}#productList .product:nth-child(3n+1){clear:left}#productList ul.pagination{clear:both;margin-top:15px;margin-left:2.912621359223301%}#productList ul.pagination li{display:inline-block}#productList ul.pagination li.previous-page{text-align:left}#productList ul.pagination li.next-page{text-align:right}.product-list-alignment-center #productList .product-title,.product-list-alignment-center #productList .product-price{text-align:center}.product-item-size-11-square #productList .product .product-image .intrinsic{padding-bottom:100%}.product-item-size-32-standard #productList .product .product-image .intrinsic{padding-bottom:66.666%}.product-item-size-23-standard-vertical #productList .product .product-image .intrinsic{padding-bottom:150%}.product-item-size-43-four-thirds #productList .product .product-image .intrinsic{padding-bottom:75%}.product-item-size-169-widescreen #productList .product .product-image .intrinsic{padding-bottom:56.25%}@media only screen and (min-width:700px){.no-touch .product-list-titles-overlay #productList .product .product-image{margin:0}.no-touch .product-list-titles-overlay #productList .product .product-overlay{position:absolute;top:0;left:0;bottom:0;right:0;background:rgba(0,0,0,.6);color:#fff;-webkit-font-smoothing:antialiased}.no-touch .product-list-titles-overlay #productList .product .product-mark{font-size:12px;line-height:normal}.no-touch .product-list-titles-overlay #productList .product .product-meta{position:absolute;width:80%;margin:0 10%;top:50%}.no-touch .product-list-titles-overlay #productList .product .product-title{font-size:16px;font-weight:700;line-height:1.5em;color:#fff}.no-touch .product-list-titles-overlay #productList .product .product-price{font-size:13px;line-height:normal;color:#fff}.no-touch .product-list-titles-overlay #productList .product .product-overlay{opacity:0;-webkit-transition:opacity .3s ease-out;-moz-transition:opacity .3s ease-out;-ms-transition:opacity .3s ease-out;-o-transition:opacity .3s ease-out;transition:opacity .3s ease-out;filter:alpha(opacity=0)}.no-touch .product-list-titles-overlay #productList .product .product-mark{opacity:1;-webkit-transition:opacity .3s ease-out;-moz-transition:opacity .3s ease-out;-ms-transition:opacity .3s ease-out;-o-transition:opacity .3s ease-out;transition:opacity .3s ease-out;filter:alpha(opacity=100)}.no-touch .product-list-titles-overlay #productList .product .product-meta{opacity:0;-webkit-transition:opacity .35s cubic-bezier(0,0,1,1);-moz-transition:opacity .35s cubic-bezier(0,0,1,1);-ms-transition:opacity .35s cubic-bezier(0,0,1,1);-o-transition:opacity .35s cubic-bezier(0,0,1,1);transition:opacity .35s cubic-bezier(0,0,1,1);filter:alpha(opacity=0)}.no-touch .product-list-titles-overlay #productList .product .product-title{margin-top:5px;-webkit-transition:margin .3s cubic-bezier(0,0,.28,1);-moz-transition:margin .3s cubic-bezier(0,0,.28,1);-ms-transition:margin .3s cubic-bezier(0,0,.28,1);-o-transition:margin .3s cubic-bezier(0,0,.28,1);transition:margin .3s cubic-bezier(0,0,.28,1)}.no-touch .product-list-titles-overlay #productList .product .product-title,.no-touch .product-list-titles-overlay #productList .product .product-price{text-align:center}.no-touch .product-list-titles-overlay #productList .product:hover .product-overlay{opacity:1;filter:alpha(opacity=100)}.no-touch .product-list-titles-overlay #productList .product:hover .product-mark{opacity:0;filter:alpha(opacity=0)}.no-touch .product-list-titles-overlay #productList .product:hover .product-meta{opacity:1;filter:alpha(opacity=100)}.no-touch .product-list-titles-overlay #productList .product:hover .product-title{margin-top:0}}.product-list-titles-under .product-meta{margin-top:0 !important}.show-product-price #productList .product .product-price{display:block}.sqs-style-mode .product-overlay{opacity:1 !important}.sqs-style-mode .product-mark{opacity:0 !important}.sqs-style-mode .product-meta{opacity:1 !important}#productNav{text-transform:uppercase;margin-bottom:30px;display:none}.product-title.mobile{display:none}#productDetails{position:relative;float:right;width:48.5%}#productDetails .product-title{margin:0 0 .5em}#productDetails .product-mark{float:right;background:#222;padding:6px 8px;color:#fff;line-height:1em;text-transform:uppercase;-webkit-font-smoothing:subpixel-antialiased;font-size:12px}#productDetails .product-price{margin:1em 0;font-size:16px;line-height:1.5em}#productDetails .product-price input{width:130px;height:30px;padding-left:5px}#productDetails .product-price .minimum-price{margin-top:3px;margin-left:10px}#productDetails .product-price .original-price{text-decoration:line-through;opacity:.7;filter:alpha(opacity=70)}#productDetails .product-price .strikeout{text-decoration:line-through}#productDetails .product-variants .variant-option{margin:1.2em 0}#productDetails .product-variants .variant-out-of-stock{color:#c00}#productDetails .product-quantity-select{margin-top:1.2em 0}#productDetails input{padding:5px 10px;border:1px solid #ccc;-moz-border-radius:3px;border-radius:3px}.product-sharing{display:none}.product-social-sharing .product-sharing{display:block}#productGallery{width:48.5%;float:left}#productGallery .intrinsic{max-width:100%}#productGallery .wrapper{padding-bottom:100%;position:relative}#productGallery #productSlideshow{position:absolute;top:0;bottom:0;left:0;width:100%;background-color:rgba(0,0,0,0)}#productGallery #productSlideshow .slide{height:100%;width:100%;overflow:hidden;cursor:pointer}#productGallery #productThumbnails{margin-left:-5px;visibility:hidden;overflow:hidden}#productGallery #productThumbnails .slide{width:50px;height:50px;margin:5px 0 0 5px;font-size:0;cursor:pointer;float:left;background-color:rgba(0,0,0,0)}.product-gallery-size-11-square #productGallery .intrinsic .wrapper{padding-bottom:100%}.product-gallery-size-32-standard #productGallery .intrinsic .wrapper{padding-bottom:66.666%}.product-gallery-size-23-standard-vertical #productGallery .intrinsic .wrapper{padding-bottom:150%}.product-gallery-size-43-four-thirds #productGallery .intrinsic .wrapper{padding-bottom:75%}.product-gallery-size-169-widescreen #productGallery .intrinsic .wrapper{padding-bottom:56.25%}.product-description{clear:both;margin-top:24px}.show-product-item-nav #productWrapper #productNav{display:block}.sqs-add-to-cart-button-wrapper{visibility:hidden}.sqs-add-to-cart-button{display:inline-block;width:auto;height:auto;padding:1em 2.5em;color:#fff;background-color:#272727;border-width:0;font-family:\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-size:12px;line-height:1em;font-weight:normal;font-style:normal;text-transform:uppercase;letter-spacing:0px;text-align:center;text-decoration:none;cursor:pointer;outline:none;-webkit-appearance:none;-moz-appearance:none;appearance:none;font-size:13px}.sqs-add-to-cart-button-inner{position:relative}.sqs-add-to-cart-button.cart-adding .sqs-spin{position:absolute;top:50%;margin-top:-12px}.sqs-add-to-cart-button.cart-adding .status-text{display:inline-block;margin-left:35px}.sqs-add-to-cart-button.cart-added .status-text{margin-left:0}.collection-type-products .sqs-add-to-cart-button{margin:20px 0;padding:1.5em 4em !important}.sqs-donate-button{display:inline-block;width:auto;height:auto;padding:1em 2.5em;color:#fff;background-color:#272727;border-width:0;font-family:\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-size:12px;line-height:1em;font-weight:normal;font-style:normal;text-transform:uppercase;letter-spacing:0px;text-align:center;text-decoration:none;cursor:pointer;outline:none;-webkit-appearance:none;-moz-appearance:none;appearance:none}@media only screen and (max-width:700px){#productSummary .product-title{display:none}#productSummary .product-title.mobile{display:block}.product-meta{margin-top:0 !important}#productDetails,#productGallery{width:100%;float:none}#productList{width:100%;margin-left:0;margin-top:0}#productList .product{float:left;margin-left:0;margin-top:0;width:100%;cursor:pointer;margin-bottom:3%}#productList .product .product-image{margin-bottom:3%}#productList .product .product-image .content-fit{position:absolute;top:0;left:0;bottom:0;right:0}#productList .product .product-image img{-webkit-transition:opacity .3s ease-out}}.sqs-gallery-block-stacked{padding:0;margin:0}.sqs-gallery-block-stacked a{border:0}.sqs-gallery-block-stacked .image-wrapper{margin:0 0 1px 0;line-height:1px}.sqs-gallery-block-stacked .image-wrapper img{width:100%}.sqs-gallery-block-stacked .meta{display:none;max-width:28em}.sqs-gallery-block-stacked.sqs-gallery-block-show-meta .meta{display:block}.sqs-gallery-block-stacked .meta-inside{margin-bottom:28px;margin-top:14px}.sqs-gallery-block-stacked .meta-title{margin-bottom:.3em}.sqs-gallery-block-stacked .meta-description{font-size:.9em;line-height:1.5em}.sqs-gallery-block-stacked .meta-description p{margin-bottom:0;margin-top:0}.sqs-gallery-block-slideshow{position:relative;background-color:rgba(175,175,175,.1)}.sqs-gallery-block-slideshow .slide>a{position:absolute;top:0;left:0;width:100%;height:100%;display:block}.sqs-gallery-block-slideshow .slide .meta{opacity:0}.sqs-gallery-block-slideshow .meta{position:absolute;opacity:0;background-color:#111;background-color:rgba(0,0,0,.3)}.sqs-gallery-block-slideshow .meta .meta-title{color:#fff}.sqs-gallery-block-slideshow .meta .meta-title{font-size:18px;line-height:1.2em;letter-spacing:1px}.sqs-gallery-block-slideshow .meta .meta-title+.meta-description{margin-top:.3em}.sqs-gallery-block-slideshow .meta .meta-description,.sqs-gallery-block-slideshow .meta .meta-description p{color:#ddd;color:rgba(255,255,255,.95);font-size:14px;line-height:1.5em}.sqs-gallery-block-slideshow .meta .meta-description strong{color:inherit}.sqs-gallery-block-slideshow .meta .meta-description *:first-child{margin-top:0}.sqs-gallery-block-slideshow .meta .meta-description *:last-child{margin-bottom:0}.sqs-gallery-block-slideshow .meta-inside{padding:25px}.sqs-gallery-block-slideshow .meta a,.sqs-gallery-block-slideshow .meta a:hover{color:#fff;text-decoration:underline}.sqs-gallery-block-slideshow .meta.overflow{overflow-y:auto}.sqs-gallery-block-slideshow .slide.loaded .meta{opacity:1}.sqs-gallery-block-slideshow.sqs-gallery-block-meta-hover .meta{opacity:0 !important;-ms-filter:\"progid:DXImageTransform.Microsoft.Alpha(Opacity=0)\"}.sqs-gallery-block-slideshow.sqs-gallery-block-meta-hover .slide:hover .meta{opacity:1 !important;-ms-filter:\"progid:DXImageTransform.Microsoft.Alpha(Opacity=70)\"}.sqs-gallery-block-slideshow .meta{display:none;-ms-filter:\"progid:DXImageTransform.Microsoft.Alpha(Opacity=0)\"}.sqs-gallery-block-slideshow.sqs-gallery-block-show-meta .sqs-active-slide .meta{display:block;opacity:1;-ms-filter:\"progid:DXImageTransform.Microsoft.Alpha(Opacity=70)\"}.sqs-gallery-block-slideshow.sqs-gallery-block-show-meta .mobile-view .slide.loaded>a{line-height:0;height:auto;position:static}.sqs-gallery-block-slideshow.sqs-gallery-block-show-meta .mobile-view .slide.loaded .sqs-video-wrapper{position:static}.sqs-gallery-block-slideshow.sqs-gallery-block-show-meta .mobile-view .slide.loaded .meta{background-color:transparent;color:inherit;padding:20px 0 10px 0;margin:0;max-width:none !important;opacity:1 !important;position:static !important}.sqs-gallery-block-slideshow.sqs-gallery-block-show-meta .mobile-view .slide.loaded .meta .meta-inside{padding:0}.sqs-gallery-block-slideshow.sqs-gallery-block-show-meta .mobile-view .slide.loaded .meta .meta-title,.sqs-gallery-block-slideshow.sqs-gallery-block-show-meta .mobile-view .slide.loaded .meta .meta-description,.sqs-gallery-block-slideshow.sqs-gallery-block-show-meta .mobile-view .slide.loaded .meta .meta-description p{color:inherit}.sqs-gallery-block-slideshow.sqs-gallery-block-show-meta .mobile-view .slide.loaded .meta .meta-title{font-size:.9em}.sqs-gallery-block-slideshow.sqs-gallery-block-show-meta .mobile-view .slide.loaded .meta .meta-description{font-size:.9em}.sqs-gallery-block-slideshow.sqs-gallery-block-show-meta .mobile-view .slide.loaded .meta .meta-description p{font-size:1em;line-height:1.3em}.sqs-gallery-block-slideshow .slide.video-playing .meta{display:none}.sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-top .meta,.sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-top-left .meta,.sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-top-right .meta{top:0px}.sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-top-left .meta,.sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-top-right .meta{max-width:50%;margin:20px}.sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-center .meta{max-width:50%;top:50%;left:50%;text-align:center}.sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-bottom .meta,.sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-bottom-left .meta,.sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-bottom-right .meta{bottom:0px}.sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-bottom .meta{background:-moz-linear-gradient(top,rgba(0,0,0,0) 0%,rgba(30,30,30,.3) 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,rgba(0,0,0,0)),color-stop(100%,rgba(30,30,30,.3)));background:-webkit-linear-gradient(top,rgba(0,0,0,0) 0%,rgba(30,30,30,.3) 100%);background:-o-linear-gradient(top,rgba(0,0,0,0) 0%,rgba(30,30,30,.3) 100%);background:-ms-linear-gradient(top,rgba(0,0,0,0) 0%,rgba(30,30,30,.3) 100%);background:linear-gradient(to bottom,rgba(0,0,0,0) 0%,rgba(30,30,30,.3) 100%)}.sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-bottom .meta-inside{padding:30px 20px 15px}.sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-bottom-left .meta,.sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-bottom-right .meta{max-width:50%;margin:20px}.sqs-gallery-block-slider{position:relative;height:100%}.sqs-gallery-block-grid.sqs-gallery-thumbnails-per-row-1 .sqs-gallery-design-grid-slide{width:100%}.sqs-gallery-block-grid.sqs-gallery-thumbnails-per-row-2 .sqs-gallery-design-grid-slide{width:50%}.sqs-gallery-block-grid.sqs-gallery-thumbnails-per-row-3 .sqs-gallery-design-grid-slide{width:33.333333333333336%}.sqs-gallery-block-grid.sqs-gallery-thumbnails-per-row-4 .sqs-gallery-design-grid-slide{width:25%}.sqs-gallery-block-grid.sqs-gallery-thumbnails-per-row-5 .sqs-gallery-design-grid-slide{width:20%}.sqs-gallery-block-grid.sqs-gallery-thumbnails-per-row-6 .sqs-gallery-design-grid-slide{width:16.666666666666668%}.sqs-gallery-block-grid.sqs-gallery-thumbnails-per-row-7 .sqs-gallery-design-grid-slide{width:14.285714285714286%}.sqs-gallery-block-grid.sqs-gallery-thumbnails-per-row-8 .sqs-gallery-design-grid-slide{width:12.5%}.sqs-gallery-block-grid.sqs-gallery-thumbnails-per-row-9 .sqs-gallery-design-grid-slide{width:11.11111111111111%}.sqs-gallery-block-grid.sqs-gallery-thumbnails-per-row-10 .sqs-gallery-design-grid-slide{width:10%}.sqs-gallery-block-grid.sqs-gallery-aspect-ratio-square .slide .margin-wrapper a.image-slide-anchor,.sqs-gallery-block-grid.sqs-gallery-aspect-ratio-square .slide .margin-wrapper .content-wrapper,.sqs-gallery-block-grid.sqs-gallery-aspect-ratio-square .slide .margin-wrapper .content-wrapper.content-fill .sqs-video-wrapper{padding-bottom:100%}.sqs-gallery-block-grid.sqs-gallery-aspect-ratio-standard .slide .margin-wrapper a.image-slide-anchor,.sqs-gallery-block-grid.sqs-gallery-aspect-ratio-standard .slide .margin-wrapper .content-wrapper,.sqs-gallery-block-grid.sqs-gallery-aspect-ratio-standard .slide .margin-wrapper .content-wrapper.content-fill .sqs-video-wrapper{padding-bottom:66.666%}.sqs-gallery-block-grid.sqs-gallery-aspect-ratio-standard-vertical .slide .margin-wrapper a.image-slide-anchor,.sqs-gallery-block-grid.sqs-gallery-aspect-ratio-standard-vertical .slide .margin-wrapper .content-wrapper,.sqs-gallery-block-grid.sqs-gallery-aspect-ratio-standard-vertical .slide .margin-wrapper .content-wrapper.content-fill .sqs-video-wrapper{padding-bottom:150%}.sqs-gallery-block-grid.sqs-gallery-aspect-ratio-four-three .slide .margin-wrapper a.image-slide-anchor,.sqs-gallery-block-grid.sqs-gallery-aspect-ratio-four-three .slide .margin-wrapper .content-wrapper,.sqs-gallery-block-grid.sqs-gallery-aspect-ratio-four-three .slide .margin-wrapper .content-wrapper.content-fill .sqs-video-wrapper{padding-bottom:75%}.sqs-gallery-block-grid.sqs-gallery-aspect-ratio-three-four-vertical .slide .margin-wrapper a.image-slide-anchor,.sqs-gallery-block-grid.sqs-gallery-aspect-ratio-three-four-vertical .slide .margin-wrapper .content-wrapper,.sqs-gallery-block-grid.sqs-gallery-aspect-ratio-three-four-vertical .slide .margin-wrapper .content-wrapper.content-fill .sqs-video-wrapper{padding-bottom:133.333%}.sqs-gallery-block-grid.sqs-gallery-aspect-ratio-widescreen .slide .margin-wrapper a.image-slide-anchor,.sqs-gallery-block-grid.sqs-gallery-aspect-ratio-widescreen .slide .margin-wrapper .content-wrapper,.sqs-gallery-block-grid.sqs-gallery-aspect-ratio-widescreen .slide .margin-wrapper .content-wrapper.content-fill .sqs-video-wrapper{padding-bottom:56.25%}.sqs-gallery-block-grid.sqs-gallery-aspect-ratio-anamorphic-widescreen .slide .margin-wrapper a.image-slide-anchor,.sqs-gallery-block-grid.sqs-gallery-aspect-ratio-anamorphic-widescreen .slide .margin-wrapper .content-wrapper,.sqs-gallery-block-grid.sqs-gallery-aspect-ratio-anamorphic-widescreen .slide .margin-wrapper .content-wrapper.content-fill .sqs-video-wrapper{padding-bottom:41.666%}.sqs-gallery-block-grid .slide{float:left;width:25%}.sqs-gallery-block-grid .slide .margin-wrapper{position:relative}.sqs-gallery-block-grid .slide .margin-wrapper a.image-slide-anchor{padding-bottom:100%;width:100%;height:0;display:block}.sqs-gallery-block-grid .slide .margin-wrapper a.image-slide-anchor img{display:inline-block}.sqs-gallery-block-grid .slide .margin-wrapper .content-wrapper{padding-bottom:100%;width:100%;display:block}.sqs-gallery-block-grid .slide .margin-wrapper .content-wrapper.content-fill .sqs-video-wrapper{height:0;padding-bottom:100%}.sqs-gallery-block-grid .slide .margin-wrapper .image-slide-title{text-align:center;display:none}.sqs-gallery-block-grid .slide .meta{position:relative}.sqs-gallery-block-grid .slide .meta h1{font-size:12px;letter-spacing:normal;margin:0}.sqs-block .sqs-gallery-thumbnails .sqs-video-thumbnail{position:relative}.sqs-block .sqs-gallery-thumbnails .sqs-video-thumbnail img{height:100%}.sqs-block .sqs-gallery-thumbnails .sqs-video-thumbnail .sqs-video-thumbnail-icon{opacity:1;position:absolute;top:50%;left:50%;background-image:url('//static.squarespace.com/universal/images-v6/icons/icon-video-24-light-solid.png');background-position:center center;height:24px;width:24px;margin-left:-12px;margin-top:-12px}.sqs-block .sqs-gallery-thumbnails .sqs-video-thumbnail.no-image .sqs-video-thumbnail-inner{background-image:url('//static.squarespace.com/universal/images-v6/icons/icon-video-24-light-solid.png');background-position:center center;background-repeat:no-repeat}.sqs-block .sqs-gallery-thumbnails .sqs-video-thumbnail:not(.no-image).loading .sqs-video-thumbnail-icon{opacity:0}.sqs-block .sqs-gallery-thumbnails .sqs-video-thumbnail .sqs-video-thumbnail-inner{height:100%;background:#000}.sqs-block .sqs-gallery-thumbnails .sqs-gallery-design-strip-slide{opacity:.5}.sqs-block .sqs-gallery-thumbnails .sqs-gallery-design-strip-slide.sqs-active-slide{opacity:1}@media only screen and (max-width:480px){.sqs-gallery-block-slideshow .meta{display:none !important}}@media only screen and (device-width:768px){.sqs-gallery-block-slideshow.sqs-gallery-block-show-meta .meta{opacity:1 !important}}.sqs-block.gallery-block .sqs-helper .sqs-handle-bottom{display:none}.sqs-block.gallery-block.sized .sqs-helper .sqs-handle-bottom{display:block}.sqs-layout.editing .sqs-block.gallery-block:hover .sqs-gallery-block-slideshow.sqs-gallery-block-meta-hover .meta{opacity:1 !important}.summary-block ul{list-style-type:none;margin:0;padding:0}.summary-block .summary-item:not(:last-child){margin-bottom:24px}.summary-block .summary-collection-title{display:none}.summary-block .summary-thumbnail{overflow:hidden;height:150px}.summary-block .summary-title{font-size:1.2em}.summary-block .summary-content-below-thumbnail .summary-title{margin:1em 0 0 0}.summary-block .summary-excerpt{margin:.75em 0}.summary-block .summary-excerpt p{font-size:.9em}.summary-block .timestamp{display:block;font-size:.8em;text-transform:uppercase}.summary-block .summary-more-link{display:none;margin-left:3px}.sqs-block-collectionlink .collectionlink-thumbnail,.link-block .collectionlink-thumbnail{overflow:hidden;height:150px}.sqs-block-collectionlink .collectionlink-thumbnail a,.link-block .collectionlink-thumbnail a{display:block;height:100%}.sqs-block-collectionlink .collectionlink-title,.link-block .collectionlink-title{font-size:1.2em}.sqs-block-collectionlink .collectionlink-title a,.link-block .collectionlink-title a{display:block}.sqs-block-collectionlink .collectionlink-content-below-thumbnail .collectionlink-title,.link-block .collectionlink-content-below-thumbnail .collectionlink-title{margin:1em 0 0 0}.sqs-block-collectionlink .collectionlink-description,.link-block .collectionlink-description{margin:.75em 0}.sqs-block-collectionlink .collectionlink-description p,.link-block .collectionlink-description p{font-size:.9em}.sqs-block-collectionlink .collection-more-link,.link-block .collection-more-link{display:none;margin-left:3px}.sqs-svg-icon--wrapper{position:relative;cursor:pointer;overflow:hidden;display:inline-block;width:32px;height:32px;text-decoration:none;-webkit-transform:translatez(0);-moz-transform:translatez(0);-ms-transform:translatez(0);-o-transform:translatez(0);transform:translatez(0)}.sqs-svg-icon--wrapper>div{position:absolute;top:0;left:0;width:100%;height:100%}.sqs-svg-icon--wrapper svg{position:absolute;top:0;left:0;width:100%;height:100%}.sqs-svg-icon--wrapper{border-color:transparent}.social-icons-size-extra-small.social-icons-style-regular .sqs-svg-icon--wrapper{width:16px;height:16px;margin:0 5px}.social-icons-size-small.social-icons-style-regular .sqs-svg-icon--wrapper{width:20px;height:20px;margin:0 6px}.social-icons-size-medium.social-icons-style-regular .sqs-svg-icon--wrapper{width:24px;height:24px;margin:0 7px}.social-icons-size-large.social-icons-style-regular .sqs-svg-icon--wrapper{width:28px;height:28px;margin:0 8px}.social-icons-size-extra-large.social-icons-style-regular .sqs-svg-icon--wrapper{width:32px;height:32px;margin:0 9px}.social-icons-size-extra-small.social-icons-style-border .sqs-svg-icon--wrapper,.social-icons-size-extra-small.social-icons-style-knockout .sqs-svg-icon--wrapper,.social-icons-size-extra-small.social-icons-style-solid .sqs-svg-icon--wrapper{width:24px;height:24px;margin:0 3px}.social-icons-size-small.social-icons-style-border .sqs-svg-icon--wrapper,.social-icons-size-small.social-icons-style-knockout .sqs-svg-icon--wrapper,.social-icons-size-small.social-icons-style-solid .sqs-svg-icon--wrapper{width:28px;height:28px;margin:0 4px}.social-icons-size-medium.social-icons-style-border .sqs-svg-icon--wrapper,.social-icons-size-medium.social-icons-style-knockout .sqs-svg-icon--wrapper,.social-icons-size-medium.social-icons-style-solid .sqs-svg-icon--wrapper{width:32px;height:32px;margin:0 4px}.social-icons-size-large.social-icons-style-border .sqs-svg-icon--wrapper,.social-icons-size-large.social-icons-style-knockout .sqs-svg-icon--wrapper,.social-icons-size-large.social-icons-style-solid .sqs-svg-icon--wrapper{width:36px;height:36px;margin:0 5px}.social-icons-size-extra-large.social-icons-style-border .sqs-svg-icon--wrapper,.social-icons-size-extra-large.social-icons-style-knockout .sqs-svg-icon--wrapper,.social-icons-size-extra-large.social-icons-style-solid .sqs-svg-icon--wrapper{width:48px;height:48px;margin:0 6px}.social-icons-shape-square .sqs-svg-icon--wrapper{border-radius:0}.social-icons-style-border.social-icons-shape-circle .sqs-svg-icon--wrapper,.social-icons-style-solid.social-icons-shape-circle .sqs-svg-icon--wrapper,.social-icons-style-knockout.social-icons-shape-circle .sqs-svg-icon--wrapper{border-radius:50%}.social-icons-style-border.social-icons-shape-rounded .sqs-svg-icon--wrapper,.social-icons-style-solid.social-icons-shape-rounded .sqs-svg-icon--wrapper,.social-icons-style-knockout.social-icons-shape-rounded .sqs-svg-icon--wrapper{border-radius:15%}.social-icons-style-border .sqs-svg-icon--wrapper{border:2px solid;box-sizing:border-box}.social-icons-style-regular .sqs-svg-icon--wrapper>div{-webkit-transform:scale(2);-moz-transform:scale(2);-ms-transform:scale(2);-o-transform:scale(2);transform:scale(2)}.sqs-svg-icon--wrapper:first-of-type{margin-left:0 !important}.sqs-svg-icon--wrapper:last-of-type{margin-right:0 !important}.social-icons-color-standard.social-icons-style-regular .fivehundredpix .sqs-use--icon{fill:#00aeef}.social-icons-color-standard.social-icons-style-regular .fivehundredpix .sqs-use--background,.social-icons-color-standard.social-icons-style-regular .fivehundredpix .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .fivehundredpix .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .fivehundredpix .sqs-use--icon{fill:rgba(0,174,239,.4)}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .fivehundredpix:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .fivehundredpix:hover .sqs-use--icon{fill:#00aeef}.social-icons-color-standard.social-icons-style-border .fivehundredpix{border-color:#00aeef}.social-icons-color-standard.social-icons-style-border .fivehundredpix .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-border .fivehundredpix .sqs-use--icon{fill:#00aeef}.social-icons-color-standard.social-icons-style-border .fivehundredpix .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-border .fivehundredpix:hover{background-color:#00aeef}.social-icons-color-standard.social-icons-style-border .fivehundredpix:hover .sqs-use--icon{fill:#fff}.social-icons-color-standard.social-icons-style-knockout .fivehundredpix .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .fivehundredpix .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .fivehundredpix .sqs-use--mask{fill:#00aeef}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .fivehundredpix .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .fivehundredpix .sqs-use--mask{fill:rgba(0,174,239,.4)}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .fivehundredpix:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .fivehundredpix:hover .sqs-use--mask{fill:#00aeef}.social-icons-color-standard.social-icons-style-solid .fivehundredpix .sqs-use--mask{fill:#00aeef}.social-icons-color-standard.social-icons-style-solid .fivehundredpix .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-solid .fivehundredpix .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .fivehundredpix .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .fivehundredpix .sqs-use--mask{fill:rgba(0,174,239,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .fivehundredpix .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .fivehundredpix .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .fivehundredpix .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .fivehundredpix .sqs-use--background{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .fivehundredpix:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .fivehundredpix:hover .sqs-use--mask{fill:#00aeef}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .fivehundredpix:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .fivehundredpix:hover .sqs-use--icon{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .fivehundredpix:hover .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .fivehundredpix:hover .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-regular .bandsintown .sqs-use--icon{fill:#00b4b3}.social-icons-color-standard.social-icons-style-regular .bandsintown .sqs-use--background,.social-icons-color-standard.social-icons-style-regular .bandsintown .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .bandsintown .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .bandsintown .sqs-use--icon{fill:rgba(0,180,179,.4)}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .bandsintown:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .bandsintown:hover .sqs-use--icon{fill:#00b4b3}.social-icons-color-standard.social-icons-style-border .bandsintown{border-color:#00b4b3}.social-icons-color-standard.social-icons-style-border .bandsintown .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-border .bandsintown .sqs-use--icon{fill:#00b4b3}.social-icons-color-standard.social-icons-style-border .bandsintown .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-border .bandsintown:hover{background-color:#00b4b3}.social-icons-color-standard.social-icons-style-border .bandsintown:hover .sqs-use--icon{fill:#fff}.social-icons-color-standard.social-icons-style-knockout .bandsintown .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .bandsintown .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .bandsintown .sqs-use--mask{fill:#00b4b3}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .bandsintown .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .bandsintown .sqs-use--mask{fill:rgba(0,180,179,.4)}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .bandsintown:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .bandsintown:hover .sqs-use--mask{fill:#00b4b3}.social-icons-color-standard.social-icons-style-solid .bandsintown .sqs-use--mask{fill:#00b4b3}.social-icons-color-standard.social-icons-style-solid .bandsintown .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-solid .bandsintown .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .bandsintown .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .bandsintown .sqs-use--mask{fill:rgba(0,180,179,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .bandsintown .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .bandsintown .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .bandsintown .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .bandsintown .sqs-use--background{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .bandsintown:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .bandsintown:hover .sqs-use--mask{fill:#00b4b3}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .bandsintown:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .bandsintown:hover .sqs-use--icon{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .bandsintown:hover .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .bandsintown:hover .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-regular .behance .sqs-use--icon{fill:#1769ff}.social-icons-color-standard.social-icons-style-regular .behance .sqs-use--background,.social-icons-color-standard.social-icons-style-regular .behance .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .behance .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .behance .sqs-use--icon{fill:rgba(23,105,255,.4)}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .behance:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .behance:hover .sqs-use--icon{fill:#1769ff}.social-icons-color-standard.social-icons-style-border .behance{border-color:#1769ff}.social-icons-color-standard.social-icons-style-border .behance .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-border .behance .sqs-use--icon{fill:#1769ff}.social-icons-color-standard.social-icons-style-border .behance .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-border .behance:hover{background-color:#1769ff}.social-icons-color-standard.social-icons-style-border .behance:hover .sqs-use--icon{fill:#fff}.social-icons-color-standard.social-icons-style-knockout .behance .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .behance .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .behance .sqs-use--mask{fill:#1769ff}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .behance .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .behance .sqs-use--mask{fill:rgba(23,105,255,.4)}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .behance:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .behance:hover .sqs-use--mask{fill:#1769ff}.social-icons-color-standard.social-icons-style-solid .behance .sqs-use--mask{fill:#1769ff}.social-icons-color-standard.social-icons-style-solid .behance .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-solid .behance .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .behance .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .behance .sqs-use--mask{fill:rgba(23,105,255,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .behance .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .behance .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .behance .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .behance .sqs-use--background{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .behance:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .behance:hover .sqs-use--mask{fill:#1769ff}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .behance:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .behance:hover .sqs-use--icon{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .behance:hover .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .behance:hover .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-regular .codepen .sqs-use--icon{fill:#222}.social-icons-color-standard.social-icons-style-regular .codepen .sqs-use--background,.social-icons-color-standard.social-icons-style-regular .codepen .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .codepen .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .codepen .sqs-use--icon{fill:rgba(34,34,34,.4)}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .codepen:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .codepen:hover .sqs-use--icon{fill:#222}.social-icons-color-standard.social-icons-style-border .codepen{border-color:#222}.social-icons-color-standard.social-icons-style-border .codepen .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-border .codepen .sqs-use--icon{fill:#222}.social-icons-color-standard.social-icons-style-border .codepen .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-border .codepen:hover{background-color:#222}.social-icons-color-standard.social-icons-style-border .codepen:hover .sqs-use--icon{fill:#fff}.social-icons-color-standard.social-icons-style-knockout .codepen .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .codepen .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .codepen .sqs-use--mask{fill:#222}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .codepen .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .codepen .sqs-use--mask{fill:rgba(34,34,34,.4)}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .codepen:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .codepen:hover .sqs-use--mask{fill:#222}.social-icons-color-standard.social-icons-style-solid .codepen .sqs-use--mask{fill:#222}.social-icons-color-standard.social-icons-style-solid .codepen .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-solid .codepen .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .codepen .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .codepen .sqs-use--mask{fill:rgba(34,34,34,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .codepen .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .codepen .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .codepen .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .codepen .sqs-use--background{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .codepen:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .codepen:hover .sqs-use--mask{fill:#222}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .codepen:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .codepen:hover .sqs-use--icon{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .codepen:hover .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .codepen:hover .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-regular .dribbble .sqs-use--icon{fill:#ea4c89}.social-icons-color-standard.social-icons-style-regular .dribbble .sqs-use--background,.social-icons-color-standard.social-icons-style-regular .dribbble .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .dribbble .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .dribbble .sqs-use--icon{fill:rgba(234,76,137,.4)}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .dribbble:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .dribbble:hover .sqs-use--icon{fill:#ea4c89}.social-icons-color-standard.social-icons-style-border .dribbble{border-color:#ea4c89}.social-icons-color-standard.social-icons-style-border .dribbble .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-border .dribbble .sqs-use--icon{fill:#ea4c89}.social-icons-color-standard.social-icons-style-border .dribbble .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-border .dribbble:hover{background-color:#ea4c89}.social-icons-color-standard.social-icons-style-border .dribbble:hover .sqs-use--icon{fill:#fff}.social-icons-color-standard.social-icons-style-knockout .dribbble .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .dribbble .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .dribbble .sqs-use--mask{fill:#ea4c89}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .dribbble .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .dribbble .sqs-use--mask{fill:rgba(234,76,137,.4)}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .dribbble:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .dribbble:hover .sqs-use--mask{fill:#ea4c89}.social-icons-color-standard.social-icons-style-solid .dribbble .sqs-use--mask{fill:#ea4c89}.social-icons-color-standard.social-icons-style-solid .dribbble .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-solid .dribbble .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .dribbble .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .dribbble .sqs-use--mask{fill:rgba(234,76,137,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .dribbble .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .dribbble .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .dribbble .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .dribbble .sqs-use--background{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .dribbble:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .dribbble:hover .sqs-use--mask{fill:#ea4c89}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .dribbble:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .dribbble:hover .sqs-use--icon{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .dribbble:hover .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .dribbble:hover .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-regular .dropbox .sqs-use--icon{fill:#007ee5}.social-icons-color-standard.social-icons-style-regular .dropbox .sqs-use--background,.social-icons-color-standard.social-icons-style-regular .dropbox .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .dropbox .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .dropbox .sqs-use--icon{fill:rgba(0,126,229,.4)}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .dropbox:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .dropbox:hover .sqs-use--icon{fill:#007ee5}.social-icons-color-standard.social-icons-style-border .dropbox{border-color:#007ee5}.social-icons-color-standard.social-icons-style-border .dropbox .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-border .dropbox .sqs-use--icon{fill:#007ee5}.social-icons-color-standard.social-icons-style-border .dropbox .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-border .dropbox:hover{background-color:#007ee5}.social-icons-color-standard.social-icons-style-border .dropbox:hover .sqs-use--icon{fill:#fff}.social-icons-color-standard.social-icons-style-knockout .dropbox .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .dropbox .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .dropbox .sqs-use--mask{fill:#007ee5}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .dropbox .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .dropbox .sqs-use--mask{fill:rgba(0,126,229,.4)}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .dropbox:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .dropbox:hover .sqs-use--mask{fill:#007ee5}.social-icons-color-standard.social-icons-style-solid .dropbox .sqs-use--mask{fill:#007ee5}.social-icons-color-standard.social-icons-style-solid .dropbox .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-solid .dropbox .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .dropbox .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .dropbox .sqs-use--mask{fill:rgba(0,126,229,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .dropbox .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .dropbox .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .dropbox .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .dropbox .sqs-use--background{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .dropbox:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .dropbox:hover .sqs-use--mask{fill:#007ee5}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .dropbox:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .dropbox:hover .sqs-use--icon{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .dropbox:hover .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .dropbox:hover .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-regular .email .sqs-use--icon{fill:#222}.social-icons-color-standard.social-icons-style-regular .email .sqs-use--background,.social-icons-color-standard.social-icons-style-regular .email .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .email .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .email .sqs-use--icon{fill:rgba(34,34,34,.4)}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .email:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .email:hover .sqs-use--icon{fill:#222}.social-icons-color-standard.social-icons-style-border .email{border-color:#222}.social-icons-color-standard.social-icons-style-border .email .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-border .email .sqs-use--icon{fill:#222}.social-icons-color-standard.social-icons-style-border .email .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-border .email:hover{background-color:#222}.social-icons-color-standard.social-icons-style-border .email:hover .sqs-use--icon{fill:#fff}.social-icons-color-standard.social-icons-style-knockout .email .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .email .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .email .sqs-use--mask{fill:#222}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .email .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .email .sqs-use--mask{fill:rgba(34,34,34,.4)}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .email:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .email:hover .sqs-use--mask{fill:#222}.social-icons-color-standard.social-icons-style-solid .email .sqs-use--mask{fill:#222}.social-icons-color-standard.social-icons-style-solid .email .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-solid .email .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .email .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .email .sqs-use--mask{fill:rgba(34,34,34,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .email .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .email .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .email .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .email .sqs-use--background{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .email:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .email:hover .sqs-use--mask{fill:#222}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .email:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .email:hover .sqs-use--icon{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .email:hover .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .email:hover .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-regular .facebook .sqs-use--icon{fill:#3b5998}.social-icons-color-standard.social-icons-style-regular .facebook .sqs-use--background,.social-icons-color-standard.social-icons-style-regular .facebook .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .facebook .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .facebook .sqs-use--icon{fill:rgba(59,89,152,.4)}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .facebook:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .facebook:hover .sqs-use--icon{fill:#3b5998}.social-icons-color-standard.social-icons-style-border .facebook{border-color:#3b5998}.social-icons-color-standard.social-icons-style-border .facebook .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-border .facebook .sqs-use--icon{fill:#3b5998}.social-icons-color-standard.social-icons-style-border .facebook .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-border .facebook:hover{background-color:#3b5998}.social-icons-color-standard.social-icons-style-border .facebook:hover .sqs-use--icon{fill:#fff}.social-icons-color-standard.social-icons-style-knockout .facebook .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .facebook .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .facebook .sqs-use--mask{fill:#3b5998}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .facebook .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .facebook .sqs-use--mask{fill:rgba(59,89,152,.4)}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .facebook:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .facebook:hover .sqs-use--mask{fill:#3b5998}.social-icons-color-standard.social-icons-style-solid .facebook .sqs-use--mask{fill:#3b5998}.social-icons-color-standard.social-icons-style-solid .facebook .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-solid .facebook .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .facebook .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .facebook .sqs-use--mask{fill:rgba(59,89,152,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .facebook .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .facebook .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .facebook .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .facebook .sqs-use--background{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .facebook:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .facebook:hover .sqs-use--mask{fill:#3b5998}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .facebook:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .facebook:hover .sqs-use--icon{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .facebook:hover .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .facebook:hover .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-regular .flickr .sqs-use--icon{fill:#0063dc}.social-icons-color-standard.social-icons-style-regular .flickr .sqs-use--background,.social-icons-color-standard.social-icons-style-regular .flickr .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .flickr .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .flickr .sqs-use--icon{fill:rgba(0,99,220,.4)}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .flickr:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .flickr:hover .sqs-use--icon{fill:#0063dc}.social-icons-color-standard.social-icons-style-border .flickr{border-color:#0063dc}.social-icons-color-standard.social-icons-style-border .flickr .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-border .flickr .sqs-use--icon{fill:#0063dc}.social-icons-color-standard.social-icons-style-border .flickr .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-border .flickr:hover{background-color:#0063dc}.social-icons-color-standard.social-icons-style-border .flickr:hover .sqs-use--icon{fill:#fff}.social-icons-color-standard.social-icons-style-knockout .flickr .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .flickr .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .flickr .sqs-use--mask{fill:#0063dc}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .flickr .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .flickr .sqs-use--mask{fill:rgba(0,99,220,.4)}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .flickr:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .flickr:hover .sqs-use--mask{fill:#0063dc}.social-icons-color-standard.social-icons-style-solid .flickr .sqs-use--mask{fill:#0063dc}.social-icons-color-standard.social-icons-style-solid .flickr .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-solid .flickr .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .flickr .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .flickr .sqs-use--mask{fill:rgba(0,99,220,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .flickr .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .flickr .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .flickr .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .flickr .sqs-use--background{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .flickr:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .flickr:hover .sqs-use--mask{fill:#0063dc}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .flickr:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .flickr:hover .sqs-use--icon{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .flickr:hover .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .flickr:hover .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-regular .foursquare .sqs-use--icon{fill:#f94877}.social-icons-color-standard.social-icons-style-regular .foursquare .sqs-use--background,.social-icons-color-standard.social-icons-style-regular .foursquare .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .foursquare .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .foursquare .sqs-use--icon{fill:rgba(249,72,119,.4)}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .foursquare:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .foursquare:hover .sqs-use--icon{fill:#f94877}.social-icons-color-standard.social-icons-style-border .foursquare{border-color:#f94877}.social-icons-color-standard.social-icons-style-border .foursquare .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-border .foursquare .sqs-use--icon{fill:#f94877}.social-icons-color-standard.social-icons-style-border .foursquare .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-border .foursquare:hover{background-color:#f94877}.social-icons-color-standard.social-icons-style-border .foursquare:hover .sqs-use--icon{fill:#fff}.social-icons-color-standard.social-icons-style-knockout .foursquare .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .foursquare .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .foursquare .sqs-use--mask{fill:#f94877}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .foursquare .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .foursquare .sqs-use--mask{fill:rgba(249,72,119,.4)}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .foursquare:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .foursquare:hover .sqs-use--mask{fill:#f94877}.social-icons-color-standard.social-icons-style-solid .foursquare .sqs-use--mask{fill:#f94877}.social-icons-color-standard.social-icons-style-solid .foursquare .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-solid .foursquare .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .foursquare .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .foursquare .sqs-use--mask{fill:rgba(249,72,119,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .foursquare .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .foursquare .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .foursquare .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .foursquare .sqs-use--background{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .foursquare:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .foursquare:hover .sqs-use--mask{fill:#f94877}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .foursquare:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .foursquare:hover .sqs-use--icon{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .foursquare:hover .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .foursquare:hover .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-regular .github .sqs-use--icon{fill:#4183c4}.social-icons-color-standard.social-icons-style-regular .github .sqs-use--background,.social-icons-color-standard.social-icons-style-regular .github .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .github .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .github .sqs-use--icon{fill:rgba(65,131,196,.4)}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .github:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .github:hover .sqs-use--icon{fill:#4183c4}.social-icons-color-standard.social-icons-style-border .github{border-color:#4183c4}.social-icons-color-standard.social-icons-style-border .github .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-border .github .sqs-use--icon{fill:#4183c4}.social-icons-color-standard.social-icons-style-border .github .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-border .github:hover{background-color:#4183c4}.social-icons-color-standard.social-icons-style-border .github:hover .sqs-use--icon{fill:#fff}.social-icons-color-standard.social-icons-style-knockout .github .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .github .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .github .sqs-use--mask{fill:#4183c4}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .github .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .github .sqs-use--mask{fill:rgba(65,131,196,.4)}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .github:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .github:hover .sqs-use--mask{fill:#4183c4}.social-icons-color-standard.social-icons-style-solid .github .sqs-use--mask{fill:#4183c4}.social-icons-color-standard.social-icons-style-solid .github .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-solid .github .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .github .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .github .sqs-use--mask{fill:rgba(65,131,196,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .github .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .github .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .github .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .github .sqs-use--background{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .github:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .github:hover .sqs-use--mask{fill:#4183c4}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .github:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .github:hover .sqs-use--icon{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .github:hover .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .github:hover .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-regular .googleplay .sqs-use--icon{fill:#5adfcb}.social-icons-color-standard.social-icons-style-regular .googleplay .sqs-use--background,.social-icons-color-standard.social-icons-style-regular .googleplay .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .googleplay .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .googleplay .sqs-use--icon{fill:rgba(90,223,203,.4)}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .googleplay:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .googleplay:hover .sqs-use--icon{fill:#5adfcb}.social-icons-color-standard.social-icons-style-border .googleplay{border-color:#5adfcb}.social-icons-color-standard.social-icons-style-border .googleplay .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-border .googleplay .sqs-use--icon{fill:#5adfcb}.social-icons-color-standard.social-icons-style-border .googleplay .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-border .googleplay:hover{background-color:#5adfcb}.social-icons-color-standard.social-icons-style-border .googleplay:hover .sqs-use--icon{fill:#fff}.social-icons-color-standard.social-icons-style-knockout .googleplay .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .googleplay .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .googleplay .sqs-use--mask{fill:#5adfcb}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .googleplay .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .googleplay .sqs-use--mask{fill:rgba(90,223,203,.4)}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .googleplay:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .googleplay:hover .sqs-use--mask{fill:#5adfcb}.social-icons-color-standard.social-icons-style-solid .googleplay .sqs-use--mask{fill:#5adfcb}.social-icons-color-standard.social-icons-style-solid .googleplay .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-solid .googleplay .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .googleplay .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .googleplay .sqs-use--mask{fill:rgba(90,223,203,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .googleplay .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .googleplay .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .googleplay .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .googleplay .sqs-use--background{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .googleplay:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .googleplay:hover .sqs-use--mask{fill:#5adfcb}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .googleplay:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .googleplay:hover .sqs-use--icon{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .googleplay:hover .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .googleplay:hover .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-regular .google .sqs-use--icon{fill:#dd4b39}.social-icons-color-standard.social-icons-style-regular .google .sqs-use--background,.social-icons-color-standard.social-icons-style-regular .google .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .google .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .google .sqs-use--icon{fill:rgba(221,75,57,.4)}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .google:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .google:hover .sqs-use--icon{fill:#dd4b39}.social-icons-color-standard.social-icons-style-border .google{border-color:#dd4b39}.social-icons-color-standard.social-icons-style-border .google .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-border .google .sqs-use--icon{fill:#dd4b39}.social-icons-color-standard.social-icons-style-border .google .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-border .google:hover{background-color:#dd4b39}.social-icons-color-standard.social-icons-style-border .google:hover .sqs-use--icon{fill:#fff}.social-icons-color-standard.social-icons-style-knockout .google .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .google .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .google .sqs-use--mask{fill:#dd4b39}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .google .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .google .sqs-use--mask{fill:rgba(221,75,57,.4)}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .google:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .google:hover .sqs-use--mask{fill:#dd4b39}.social-icons-color-standard.social-icons-style-solid .google .sqs-use--mask{fill:#dd4b39}.social-icons-color-standard.social-icons-style-solid .google .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-solid .google .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .google .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .google .sqs-use--mask{fill:rgba(221,75,57,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .google .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .google .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .google .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .google .sqs-use--background{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .google:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .google:hover .sqs-use--mask{fill:#dd4b39}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .google:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .google:hover .sqs-use--icon{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .google:hover .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .google:hover .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-regular .houzz .sqs-use--icon{fill:#7ac143}.social-icons-color-standard.social-icons-style-regular .houzz .sqs-use--background,.social-icons-color-standard.social-icons-style-regular .houzz .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .houzz .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .houzz .sqs-use--icon{fill:rgba(122,193,67,.4)}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .houzz:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .houzz:hover .sqs-use--icon{fill:#7ac143}.social-icons-color-standard.social-icons-style-border .houzz{border-color:#7ac143}.social-icons-color-standard.social-icons-style-border .houzz .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-border .houzz .sqs-use--icon{fill:#7ac143}.social-icons-color-standard.social-icons-style-border .houzz .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-border .houzz:hover{background-color:#7ac143}.social-icons-color-standard.social-icons-style-border .houzz:hover .sqs-use--icon{fill:#fff}.social-icons-color-standard.social-icons-style-knockout .houzz .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .houzz .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .houzz .sqs-use--mask{fill:#7ac143}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .houzz .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .houzz .sqs-use--mask{fill:rgba(122,193,67,.4)}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .houzz:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .houzz:hover .sqs-use--mask{fill:#7ac143}.social-icons-color-standard.social-icons-style-solid .houzz .sqs-use--mask{fill:#7ac143}.social-icons-color-standard.social-icons-style-solid .houzz .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-solid .houzz .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .houzz .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .houzz .sqs-use--mask{fill:rgba(122,193,67,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .houzz .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .houzz .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .houzz .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .houzz .sqs-use--background{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .houzz:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .houzz:hover .sqs-use--mask{fill:#7ac143}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .houzz:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .houzz:hover .sqs-use--icon{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .houzz:hover .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .houzz:hover .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-regular .instagram .sqs-use--icon{fill:#3f729b}.social-icons-color-standard.social-icons-style-regular .instagram .sqs-use--background,.social-icons-color-standard.social-icons-style-regular .instagram .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .instagram .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .instagram .sqs-use--icon{fill:rgba(63,114,155,.4)}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .instagram:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .instagram:hover .sqs-use--icon{fill:#3f729b}.social-icons-color-standard.social-icons-style-border .instagram{border-color:#3f729b}.social-icons-color-standard.social-icons-style-border .instagram .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-border .instagram .sqs-use--icon{fill:#3f729b}.social-icons-color-standard.social-icons-style-border .instagram .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-border .instagram:hover{background-color:#3f729b}.social-icons-color-standard.social-icons-style-border .instagram:hover .sqs-use--icon{fill:#fff}.social-icons-color-standard.social-icons-style-knockout .instagram .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .instagram .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .instagram .sqs-use--mask{fill:#3f729b}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .instagram .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .instagram .sqs-use--mask{fill:rgba(63,114,155,.4)}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .instagram:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .instagram:hover .sqs-use--mask{fill:#3f729b}.social-icons-color-standard.social-icons-style-solid .instagram .sqs-use--mask{fill:#3f729b}.social-icons-color-standard.social-icons-style-solid .instagram .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-solid .instagram .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .instagram .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .instagram .sqs-use--mask{fill:rgba(63,114,155,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .instagram .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .instagram .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .instagram .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .instagram .sqs-use--background{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .instagram:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .instagram:hover .sqs-use--mask{fill:#3f729b}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .instagram:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .instagram:hover .sqs-use--icon{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .instagram:hover .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .instagram:hover .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-regular .itunes .sqs-use--icon{fill:#ff3241}.social-icons-color-standard.social-icons-style-regular .itunes .sqs-use--background,.social-icons-color-standard.social-icons-style-regular .itunes .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .itunes .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .itunes .sqs-use--icon{fill:rgba(255,50,65,.4)}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .itunes:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .itunes:hover .sqs-use--icon{fill:#ff3241}.social-icons-color-standard.social-icons-style-border .itunes{border-color:#ff3241}.social-icons-color-standard.social-icons-style-border .itunes .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-border .itunes .sqs-use--icon{fill:#ff3241}.social-icons-color-standard.social-icons-style-border .itunes .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-border .itunes:hover{background-color:#ff3241}.social-icons-color-standard.social-icons-style-border .itunes:hover .sqs-use--icon{fill:#fff}.social-icons-color-standard.social-icons-style-knockout .itunes .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .itunes .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .itunes .sqs-use--mask{fill:#ff3241}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .itunes .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .itunes .sqs-use--mask{fill:rgba(255,50,65,.4)}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .itunes:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .itunes:hover .sqs-use--mask{fill:#ff3241}.social-icons-color-standard.social-icons-style-solid .itunes .sqs-use--mask{fill:#ff3241}.social-icons-color-standard.social-icons-style-solid .itunes .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-solid .itunes .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .itunes .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .itunes .sqs-use--mask{fill:rgba(255,50,65,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .itunes .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .itunes .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .itunes .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .itunes .sqs-use--background{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .itunes:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .itunes:hover .sqs-use--mask{fill:#ff3241}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .itunes:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .itunes:hover .sqs-use--icon{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .itunes:hover .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .itunes:hover .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-regular .linkedin .sqs-use--icon{fill:#0976b4}.social-icons-color-standard.social-icons-style-regular .linkedin .sqs-use--background,.social-icons-color-standard.social-icons-style-regular .linkedin .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .linkedin .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .linkedin .sqs-use--icon{fill:rgba(9,118,180,.4)}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .linkedin:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .linkedin:hover .sqs-use--icon{fill:#0976b4}.social-icons-color-standard.social-icons-style-border .linkedin{border-color:#0976b4}.social-icons-color-standard.social-icons-style-border .linkedin .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-border .linkedin .sqs-use--icon{fill:#0976b4}.social-icons-color-standard.social-icons-style-border .linkedin .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-border .linkedin:hover{background-color:#0976b4}.social-icons-color-standard.social-icons-style-border .linkedin:hover .sqs-use--icon{fill:#fff}.social-icons-color-standard.social-icons-style-knockout .linkedin .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .linkedin .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .linkedin .sqs-use--mask{fill:#0976b4}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .linkedin .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .linkedin .sqs-use--mask{fill:rgba(9,118,180,.4)}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .linkedin:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .linkedin:hover .sqs-use--mask{fill:#0976b4}.social-icons-color-standard.social-icons-style-solid .linkedin .sqs-use--mask{fill:#0976b4}.social-icons-color-standard.social-icons-style-solid .linkedin .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-solid .linkedin .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .linkedin .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .linkedin .sqs-use--mask{fill:rgba(9,118,180,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .linkedin .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .linkedin .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .linkedin .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .linkedin .sqs-use--background{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .linkedin:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .linkedin:hover .sqs-use--mask{fill:#0976b4}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .linkedin:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .linkedin:hover .sqs-use--icon{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .linkedin:hover .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .linkedin:hover .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-regular .medium .sqs-use--icon{fill:#222}.social-icons-color-standard.social-icons-style-regular .medium .sqs-use--background,.social-icons-color-standard.social-icons-style-regular .medium .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .medium .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .medium .sqs-use--icon{fill:rgba(34,34,34,.4)}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .medium:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .medium:hover .sqs-use--icon{fill:#222}.social-icons-color-standard.social-icons-style-border .medium{border-color:#222}.social-icons-color-standard.social-icons-style-border .medium .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-border .medium .sqs-use--icon{fill:#222}.social-icons-color-standard.social-icons-style-border .medium .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-border .medium:hover{background-color:#222}.social-icons-color-standard.social-icons-style-border .medium:hover .sqs-use--icon{fill:#fff}.social-icons-color-standard.social-icons-style-knockout .medium .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .medium .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .medium .sqs-use--mask{fill:#222}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .medium .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .medium .sqs-use--mask{fill:rgba(34,34,34,.4)}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .medium:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .medium:hover .sqs-use--mask{fill:#222}.social-icons-color-standard.social-icons-style-solid .medium .sqs-use--mask{fill:#222}.social-icons-color-standard.social-icons-style-solid .medium .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-solid .medium .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .medium .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .medium .sqs-use--mask{fill:rgba(34,34,34,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .medium .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .medium .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .medium .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .medium .sqs-use--background{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .medium:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .medium:hover .sqs-use--mask{fill:#222}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .medium:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .medium:hover .sqs-use--icon{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .medium:hover .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .medium:hover .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-regular .meetup .sqs-use--icon{fill:#e0393e}.social-icons-color-standard.social-icons-style-regular .meetup .sqs-use--background,.social-icons-color-standard.social-icons-style-regular .meetup .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .meetup .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .meetup .sqs-use--icon{fill:rgba(224,57,62,.4)}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .meetup:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .meetup:hover .sqs-use--icon{fill:#e0393e}.social-icons-color-standard.social-icons-style-border .meetup{border-color:#e0393e}.social-icons-color-standard.social-icons-style-border .meetup .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-border .meetup .sqs-use--icon{fill:#e0393e}.social-icons-color-standard.social-icons-style-border .meetup .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-border .meetup:hover{background-color:#e0393e}.social-icons-color-standard.social-icons-style-border .meetup:hover .sqs-use--icon{fill:#fff}.social-icons-color-standard.social-icons-style-knockout .meetup .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .meetup .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .meetup .sqs-use--mask{fill:#e0393e}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .meetup .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .meetup .sqs-use--mask{fill:rgba(224,57,62,.4)}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .meetup:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .meetup:hover .sqs-use--mask{fill:#e0393e}.social-icons-color-standard.social-icons-style-solid .meetup .sqs-use--mask{fill:#e0393e}.social-icons-color-standard.social-icons-style-solid .meetup .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-solid .meetup .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .meetup .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .meetup .sqs-use--mask{fill:rgba(224,57,62,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .meetup .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .meetup .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .meetup .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .meetup .sqs-use--background{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .meetup:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .meetup:hover .sqs-use--mask{fill:#e0393e}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .meetup:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .meetup:hover .sqs-use--icon{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .meetup:hover .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .meetup:hover .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-regular .pinterest .sqs-use--icon{fill:#cc2127}.social-icons-color-standard.social-icons-style-regular .pinterest .sqs-use--background,.social-icons-color-standard.social-icons-style-regular .pinterest .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .pinterest .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .pinterest .sqs-use--icon{fill:rgba(204,33,39,.4)}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .pinterest:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .pinterest:hover .sqs-use--icon{fill:#cc2127}.social-icons-color-standard.social-icons-style-border .pinterest{border-color:#cc2127}.social-icons-color-standard.social-icons-style-border .pinterest .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-border .pinterest .sqs-use--icon{fill:#cc2127}.social-icons-color-standard.social-icons-style-border .pinterest .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-border .pinterest:hover{background-color:#cc2127}.social-icons-color-standard.social-icons-style-border .pinterest:hover .sqs-use--icon{fill:#fff}.social-icons-color-standard.social-icons-style-knockout .pinterest .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .pinterest .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .pinterest .sqs-use--mask{fill:#cc2127}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .pinterest .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .pinterest .sqs-use--mask{fill:rgba(204,33,39,.4)}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .pinterest:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .pinterest:hover .sqs-use--mask{fill:#cc2127}.social-icons-color-standard.social-icons-style-solid .pinterest .sqs-use--mask{fill:#cc2127}.social-icons-color-standard.social-icons-style-solid .pinterest .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-solid .pinterest .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .pinterest .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .pinterest .sqs-use--mask{fill:rgba(204,33,39,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .pinterest .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .pinterest .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .pinterest .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .pinterest .sqs-use--background{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .pinterest:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .pinterest:hover .sqs-use--mask{fill:#cc2127}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .pinterest:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .pinterest:hover .sqs-use--icon{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .pinterest:hover .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .pinterest:hover .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-regular .rdio .sqs-use--icon{fill:#006ed2}.social-icons-color-standard.social-icons-style-regular .rdio .sqs-use--background,.social-icons-color-standard.social-icons-style-regular .rdio .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .rdio .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .rdio .sqs-use--icon{fill:rgba(0,110,210,.4)}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .rdio:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .rdio:hover .sqs-use--icon{fill:#006ed2}.social-icons-color-standard.social-icons-style-border .rdio{border-color:#006ed2}.social-icons-color-standard.social-icons-style-border .rdio .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-border .rdio .sqs-use--icon{fill:#006ed2}.social-icons-color-standard.social-icons-style-border .rdio .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-border .rdio:hover{background-color:#006ed2}.social-icons-color-standard.social-icons-style-border .rdio:hover .sqs-use--icon{fill:#fff}.social-icons-color-standard.social-icons-style-knockout .rdio .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .rdio .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .rdio .sqs-use--mask{fill:#006ed2}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .rdio .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .rdio .sqs-use--mask{fill:rgba(0,110,210,.4)}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .rdio:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .rdio:hover .sqs-use--mask{fill:#006ed2}.social-icons-color-standard.social-icons-style-solid .rdio .sqs-use--mask{fill:#006ed2}.social-icons-color-standard.social-icons-style-solid .rdio .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-solid .rdio .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .rdio .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .rdio .sqs-use--mask{fill:rgba(0,110,210,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .rdio .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .rdio .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .rdio .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .rdio .sqs-use--background{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .rdio:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .rdio:hover .sqs-use--mask{fill:#006ed2}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .rdio:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .rdio:hover .sqs-use--icon{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .rdio:hover .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .rdio:hover .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-regular .reddit .sqs-use--icon{fill:#ff4500}.social-icons-color-standard.social-icons-style-regular .reddit .sqs-use--background,.social-icons-color-standard.social-icons-style-regular .reddit .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .reddit .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .reddit .sqs-use--icon{fill:rgba(255,69,0,.4)}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .reddit:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .reddit:hover .sqs-use--icon{fill:#ff4500}.social-icons-color-standard.social-icons-style-border .reddit{border-color:#ff4500}.social-icons-color-standard.social-icons-style-border .reddit .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-border .reddit .sqs-use--icon{fill:#ff4500}.social-icons-color-standard.social-icons-style-border .reddit .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-border .reddit:hover{background-color:#ff4500}.social-icons-color-standard.social-icons-style-border .reddit:hover .sqs-use--icon{fill:#fff}.social-icons-color-standard.social-icons-style-knockout .reddit .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .reddit .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .reddit .sqs-use--mask{fill:#ff4500}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .reddit .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .reddit .sqs-use--mask{fill:rgba(255,69,0,.4)}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .reddit:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .reddit:hover .sqs-use--mask{fill:#ff4500}.social-icons-color-standard.social-icons-style-solid .reddit .sqs-use--mask{fill:#ff4500}.social-icons-color-standard.social-icons-style-solid .reddit .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-solid .reddit .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .reddit .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .reddit .sqs-use--mask{fill:rgba(255,69,0,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .reddit .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .reddit .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .reddit .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .reddit .sqs-use--background{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .reddit:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .reddit:hover .sqs-use--mask{fill:#ff4500}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .reddit:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .reddit:hover .sqs-use--icon{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .reddit:hover .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .reddit:hover .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-regular .rss .sqs-use--icon{fill:#222}.social-icons-color-standard.social-icons-style-regular .rss .sqs-use--background,.social-icons-color-standard.social-icons-style-regular .rss .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .rss .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .rss .sqs-use--icon{fill:rgba(34,34,34,.4)}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .rss:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .rss:hover .sqs-use--icon{fill:#222}.social-icons-color-standard.social-icons-style-border .rss{border-color:#222}.social-icons-color-standard.social-icons-style-border .rss .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-border .rss .sqs-use--icon{fill:#222}.social-icons-color-standard.social-icons-style-border .rss .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-border .rss:hover{background-color:#222}.social-icons-color-standard.social-icons-style-border .rss:hover .sqs-use--icon{fill:#fff}.social-icons-color-standard.social-icons-style-knockout .rss .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .rss .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .rss .sqs-use--mask{fill:#222}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .rss .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .rss .sqs-use--mask{fill:rgba(34,34,34,.4)}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .rss:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .rss:hover .sqs-use--mask{fill:#222}.social-icons-color-standard.social-icons-style-solid .rss .sqs-use--mask{fill:#222}.social-icons-color-standard.social-icons-style-solid .rss .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-solid .rss .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .rss .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .rss .sqs-use--mask{fill:rgba(34,34,34,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .rss .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .rss .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .rss .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .rss .sqs-use--background{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .rss:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .rss:hover .sqs-use--mask{fill:#222}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .rss:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .rss:hover .sqs-use--icon{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .rss:hover .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .rss:hover .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-regular .smugmug .sqs-use--icon{fill:#7dbb00}.social-icons-color-standard.social-icons-style-regular .smugmug .sqs-use--background,.social-icons-color-standard.social-icons-style-regular .smugmug .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .smugmug .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .smugmug .sqs-use--icon{fill:rgba(125,187,0,.4)}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .smugmug:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .smugmug:hover .sqs-use--icon{fill:#7dbb00}.social-icons-color-standard.social-icons-style-border .smugmug{border-color:#7dbb00}.social-icons-color-standard.social-icons-style-border .smugmug .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-border .smugmug .sqs-use--icon{fill:#7dbb00}.social-icons-color-standard.social-icons-style-border .smugmug .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-border .smugmug:hover{background-color:#7dbb00}.social-icons-color-standard.social-icons-style-border .smugmug:hover .sqs-use--icon{fill:#fff}.social-icons-color-standard.social-icons-style-knockout .smugmug .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .smugmug .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .smugmug .sqs-use--mask{fill:#7dbb00}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .smugmug .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .smugmug .sqs-use--mask{fill:rgba(125,187,0,.4)}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .smugmug:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .smugmug:hover .sqs-use--mask{fill:#7dbb00}.social-icons-color-standard.social-icons-style-solid .smugmug .sqs-use--mask{fill:#7dbb00}.social-icons-color-standard.social-icons-style-solid .smugmug .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-solid .smugmug .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .smugmug .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .smugmug .sqs-use--mask{fill:rgba(125,187,0,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .smugmug .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .smugmug .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .smugmug .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .smugmug .sqs-use--background{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .smugmug:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .smugmug:hover .sqs-use--mask{fill:#7dbb00}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .smugmug:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .smugmug:hover .sqs-use--icon{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .smugmug:hover .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .smugmug:hover .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-regular .soundcloud .sqs-use--icon{fill:#f60}.social-icons-color-standard.social-icons-style-regular .soundcloud .sqs-use--background,.social-icons-color-standard.social-icons-style-regular .soundcloud .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .soundcloud .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .soundcloud .sqs-use--icon{fill:rgba(255,102,0,.4)}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .soundcloud:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .soundcloud:hover .sqs-use--icon{fill:#f60}.social-icons-color-standard.social-icons-style-border .soundcloud{border-color:#f60}.social-icons-color-standard.social-icons-style-border .soundcloud .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-border .soundcloud .sqs-use--icon{fill:#f60}.social-icons-color-standard.social-icons-style-border .soundcloud .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-border .soundcloud:hover{background-color:#f60}.social-icons-color-standard.social-icons-style-border .soundcloud:hover .sqs-use--icon{fill:#fff}.social-icons-color-standard.social-icons-style-knockout .soundcloud .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .soundcloud .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .soundcloud .sqs-use--mask{fill:#f60}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .soundcloud .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .soundcloud .sqs-use--mask{fill:rgba(255,102,0,.4)}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .soundcloud:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .soundcloud:hover .sqs-use--mask{fill:#f60}.social-icons-color-standard.social-icons-style-solid .soundcloud .sqs-use--mask{fill:#f60}.social-icons-color-standard.social-icons-style-solid .soundcloud .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-solid .soundcloud .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .soundcloud .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .soundcloud .sqs-use--mask{fill:rgba(255,102,0,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .soundcloud .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .soundcloud .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .soundcloud .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .soundcloud .sqs-use--background{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .soundcloud:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .soundcloud:hover .sqs-use--mask{fill:#f60}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .soundcloud:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .soundcloud:hover .sqs-use--icon{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .soundcloud:hover .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .soundcloud:hover .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-regular .spotify .sqs-use--icon{fill:#84bd00}.social-icons-color-standard.social-icons-style-regular .spotify .sqs-use--background,.social-icons-color-standard.social-icons-style-regular .spotify .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .spotify .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .spotify .sqs-use--icon{fill:rgba(132,189,0,.4)}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .spotify:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .spotify:hover .sqs-use--icon{fill:#84bd00}.social-icons-color-standard.social-icons-style-border .spotify{border-color:#84bd00}.social-icons-color-standard.social-icons-style-border .spotify .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-border .spotify .sqs-use--icon{fill:#84bd00}.social-icons-color-standard.social-icons-style-border .spotify .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-border .spotify:hover{background-color:#84bd00}.social-icons-color-standard.social-icons-style-border .spotify:hover .sqs-use--icon{fill:#fff}.social-icons-color-standard.social-icons-style-knockout .spotify .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .spotify .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .spotify .sqs-use--mask{fill:#84bd00}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .spotify .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .spotify .sqs-use--mask{fill:rgba(132,189,0,.4)}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .spotify:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .spotify:hover .sqs-use--mask{fill:#84bd00}.social-icons-color-standard.social-icons-style-solid .spotify .sqs-use--mask{fill:#84bd00}.social-icons-color-standard.social-icons-style-solid .spotify .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-solid .spotify .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .spotify .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .spotify .sqs-use--mask{fill:rgba(132,189,0,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .spotify .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .spotify .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .spotify .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .spotify .sqs-use--background{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .spotify:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .spotify:hover .sqs-use--mask{fill:#84bd00}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .spotify:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .spotify:hover .sqs-use--icon{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .spotify:hover .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .spotify:hover .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-regular .squarespace .sqs-use--icon{fill:#222}.social-icons-color-standard.social-icons-style-regular .squarespace .sqs-use--background,.social-icons-color-standard.social-icons-style-regular .squarespace .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .squarespace .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .squarespace .sqs-use--icon{fill:rgba(34,34,34,.4)}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .squarespace:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .squarespace:hover .sqs-use--icon{fill:#222}.social-icons-color-standard.social-icons-style-border .squarespace{border-color:#222}.social-icons-color-standard.social-icons-style-border .squarespace .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-border .squarespace .sqs-use--icon{fill:#222}.social-icons-color-standard.social-icons-style-border .squarespace .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-border .squarespace:hover{background-color:#222}.social-icons-color-standard.social-icons-style-border .squarespace:hover .sqs-use--icon{fill:#fff}.social-icons-color-standard.social-icons-style-knockout .squarespace .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .squarespace .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .squarespace .sqs-use--mask{fill:#222}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .squarespace .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .squarespace .sqs-use--mask{fill:rgba(34,34,34,.4)}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .squarespace:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .squarespace:hover .sqs-use--mask{fill:#222}.social-icons-color-standard.social-icons-style-solid .squarespace .sqs-use--mask{fill:#222}.social-icons-color-standard.social-icons-style-solid .squarespace .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-solid .squarespace .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .squarespace .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .squarespace .sqs-use--mask{fill:rgba(34,34,34,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .squarespace .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .squarespace .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .squarespace .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .squarespace .sqs-use--background{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .squarespace:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .squarespace:hover .sqs-use--mask{fill:#222}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .squarespace:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .squarespace:hover .sqs-use--icon{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .squarespace:hover .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .squarespace:hover .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-regular .stumbleupon .sqs-use--icon{fill:#eb4924}.social-icons-color-standard.social-icons-style-regular .stumbleupon .sqs-use--background,.social-icons-color-standard.social-icons-style-regular .stumbleupon .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .stumbleupon .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .stumbleupon .sqs-use--icon{fill:rgba(235,73,36,.4)}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .stumbleupon:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .stumbleupon:hover .sqs-use--icon{fill:#eb4924}.social-icons-color-standard.social-icons-style-border .stumbleupon{border-color:#eb4924}.social-icons-color-standard.social-icons-style-border .stumbleupon .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-border .stumbleupon .sqs-use--icon{fill:#eb4924}.social-icons-color-standard.social-icons-style-border .stumbleupon .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-border .stumbleupon:hover{background-color:#eb4924}.social-icons-color-standard.social-icons-style-border .stumbleupon:hover .sqs-use--icon{fill:#fff}.social-icons-color-standard.social-icons-style-knockout .stumbleupon .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .stumbleupon .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .stumbleupon .sqs-use--mask{fill:#eb4924}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .stumbleupon .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .stumbleupon .sqs-use--mask{fill:rgba(235,73,36,.4)}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .stumbleupon:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .stumbleupon:hover .sqs-use--mask{fill:#eb4924}.social-icons-color-standard.social-icons-style-solid .stumbleupon .sqs-use--mask{fill:#eb4924}.social-icons-color-standard.social-icons-style-solid .stumbleupon .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-solid .stumbleupon .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .stumbleupon .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .stumbleupon .sqs-use--mask{fill:rgba(235,73,36,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .stumbleupon .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .stumbleupon .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .stumbleupon .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .stumbleupon .sqs-use--background{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .stumbleupon:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .stumbleupon:hover .sqs-use--mask{fill:#eb4924}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .stumbleupon:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .stumbleupon:hover .sqs-use--icon{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .stumbleupon:hover .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .stumbleupon:hover .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-regular .tumblr .sqs-use--icon{fill:#35465d}.social-icons-color-standard.social-icons-style-regular .tumblr .sqs-use--background,.social-icons-color-standard.social-icons-style-regular .tumblr .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .tumblr .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .tumblr .sqs-use--icon{fill:rgba(53,70,93,.4)}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .tumblr:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .tumblr:hover .sqs-use--icon{fill:#35465d}.social-icons-color-standard.social-icons-style-border .tumblr{border-color:#35465d}.social-icons-color-standard.social-icons-style-border .tumblr .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-border .tumblr .sqs-use--icon{fill:#35465d}.social-icons-color-standard.social-icons-style-border .tumblr .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-border .tumblr:hover{background-color:#35465d}.social-icons-color-standard.social-icons-style-border .tumblr:hover .sqs-use--icon{fill:#fff}.social-icons-color-standard.social-icons-style-knockout .tumblr .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .tumblr .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .tumblr .sqs-use--mask{fill:#35465d}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .tumblr .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .tumblr .sqs-use--mask{fill:rgba(53,70,93,.4)}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .tumblr:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .tumblr:hover .sqs-use--mask{fill:#35465d}.social-icons-color-standard.social-icons-style-solid .tumblr .sqs-use--mask{fill:#35465d}.social-icons-color-standard.social-icons-style-solid .tumblr .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-solid .tumblr .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .tumblr .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .tumblr .sqs-use--mask{fill:rgba(53,70,93,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .tumblr .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .tumblr .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .tumblr .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .tumblr .sqs-use--background{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .tumblr:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .tumblr:hover .sqs-use--mask{fill:#35465d}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .tumblr:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .tumblr:hover .sqs-use--icon{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .tumblr:hover .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .tumblr:hover .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-regular .twitch .sqs-use--icon{fill:#6441a5}.social-icons-color-standard.social-icons-style-regular .twitch .sqs-use--background,.social-icons-color-standard.social-icons-style-regular .twitch .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .twitch .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .twitch .sqs-use--icon{fill:rgba(100,65,165,.4)}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .twitch:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .twitch:hover .sqs-use--icon{fill:#6441a5}.social-icons-color-standard.social-icons-style-border .twitch{border-color:#6441a5}.social-icons-color-standard.social-icons-style-border .twitch .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-border .twitch .sqs-use--icon{fill:#6441a5}.social-icons-color-standard.social-icons-style-border .twitch .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-border .twitch:hover{background-color:#6441a5}.social-icons-color-standard.social-icons-style-border .twitch:hover .sqs-use--icon{fill:#fff}.social-icons-color-standard.social-icons-style-knockout .twitch .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .twitch .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .twitch .sqs-use--mask{fill:#6441a5}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .twitch .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .twitch .sqs-use--mask{fill:rgba(100,65,165,.4)}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .twitch:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .twitch:hover .sqs-use--mask{fill:#6441a5}.social-icons-color-standard.social-icons-style-solid .twitch .sqs-use--mask{fill:#6441a5}.social-icons-color-standard.social-icons-style-solid .twitch .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-solid .twitch .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .twitch .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .twitch .sqs-use--mask{fill:rgba(100,65,165,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .twitch .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .twitch .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .twitch .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .twitch .sqs-use--background{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .twitch:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .twitch:hover .sqs-use--mask{fill:#6441a5}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .twitch:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .twitch:hover .sqs-use--icon{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .twitch:hover .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .twitch:hover .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-regular .twitter .sqs-use--icon{fill:#55acee}.social-icons-color-standard.social-icons-style-regular .twitter .sqs-use--background,.social-icons-color-standard.social-icons-style-regular .twitter .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .twitter .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .twitter .sqs-use--icon{fill:rgba(85,172,238,.4)}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .twitter:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .twitter:hover .sqs-use--icon{fill:#55acee}.social-icons-color-standard.social-icons-style-border .twitter{border-color:#55acee}.social-icons-color-standard.social-icons-style-border .twitter .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-border .twitter .sqs-use--icon{fill:#55acee}.social-icons-color-standard.social-icons-style-border .twitter .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-border .twitter:hover{background-color:#55acee}.social-icons-color-standard.social-icons-style-border .twitter:hover .sqs-use--icon{fill:#fff}.social-icons-color-standard.social-icons-style-knockout .twitter .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .twitter .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .twitter .sqs-use--mask{fill:#55acee}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .twitter .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .twitter .sqs-use--mask{fill:rgba(85,172,238,.4)}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .twitter:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .twitter:hover .sqs-use--mask{fill:#55acee}.social-icons-color-standard.social-icons-style-solid .twitter .sqs-use--mask{fill:#55acee}.social-icons-color-standard.social-icons-style-solid .twitter .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-solid .twitter .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .twitter .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .twitter .sqs-use--mask{fill:rgba(85,172,238,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .twitter .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .twitter .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .twitter .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .twitter .sqs-use--background{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .twitter:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .twitter:hover .sqs-use--mask{fill:#55acee}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .twitter:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .twitter:hover .sqs-use--icon{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .twitter:hover .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .twitter:hover .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-regular .vevo .sqs-use--icon{fill:#ff0031}.social-icons-color-standard.social-icons-style-regular .vevo .sqs-use--background,.social-icons-color-standard.social-icons-style-regular .vevo .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .vevo .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .vevo .sqs-use--icon{fill:rgba(255,0,49,.4)}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .vevo:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .vevo:hover .sqs-use--icon{fill:#ff0031}.social-icons-color-standard.social-icons-style-border .vevo{border-color:#ff0031}.social-icons-color-standard.social-icons-style-border .vevo .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-border .vevo .sqs-use--icon{fill:#ff0031}.social-icons-color-standard.social-icons-style-border .vevo .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-border .vevo:hover{background-color:#ff0031}.social-icons-color-standard.social-icons-style-border .vevo:hover .sqs-use--icon{fill:#fff}.social-icons-color-standard.social-icons-style-knockout .vevo .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .vevo .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .vevo .sqs-use--mask{fill:#ff0031}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .vevo .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .vevo .sqs-use--mask{fill:rgba(255,0,49,.4)}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .vevo:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .vevo:hover .sqs-use--mask{fill:#ff0031}.social-icons-color-standard.social-icons-style-solid .vevo .sqs-use--mask{fill:#ff0031}.social-icons-color-standard.social-icons-style-solid .vevo .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-solid .vevo .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .vevo .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .vevo .sqs-use--mask{fill:rgba(255,0,49,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .vevo .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .vevo .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .vevo .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .vevo .sqs-use--background{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .vevo:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .vevo:hover .sqs-use--mask{fill:#ff0031}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .vevo:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .vevo:hover .sqs-use--icon{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .vevo:hover .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .vevo:hover .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-regular .vimeo .sqs-use--icon{fill:#1ab7ea}.social-icons-color-standard.social-icons-style-regular .vimeo .sqs-use--background,.social-icons-color-standard.social-icons-style-regular .vimeo .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .vimeo .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .vimeo .sqs-use--icon{fill:rgba(26,183,234,.4)}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .vimeo:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .vimeo:hover .sqs-use--icon{fill:#1ab7ea}.social-icons-color-standard.social-icons-style-border .vimeo{border-color:#1ab7ea}.social-icons-color-standard.social-icons-style-border .vimeo .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-border .vimeo .sqs-use--icon{fill:#1ab7ea}.social-icons-color-standard.social-icons-style-border .vimeo .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-border .vimeo:hover{background-color:#1ab7ea}.social-icons-color-standard.social-icons-style-border .vimeo:hover .sqs-use--icon{fill:#fff}.social-icons-color-standard.social-icons-style-knockout .vimeo .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .vimeo .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .vimeo .sqs-use--mask{fill:#1ab7ea}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .vimeo .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .vimeo .sqs-use--mask{fill:rgba(26,183,234,.4)}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .vimeo:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .vimeo:hover .sqs-use--mask{fill:#1ab7ea}.social-icons-color-standard.social-icons-style-solid .vimeo .sqs-use--mask{fill:#1ab7ea}.social-icons-color-standard.social-icons-style-solid .vimeo .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-solid .vimeo .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .vimeo .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .vimeo .sqs-use--mask{fill:rgba(26,183,234,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .vimeo .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .vimeo .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .vimeo .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .vimeo .sqs-use--background{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .vimeo:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .vimeo:hover .sqs-use--mask{fill:#1ab7ea}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .vimeo:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .vimeo:hover .sqs-use--icon{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .vimeo:hover .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .vimeo:hover .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-regular .vine .sqs-use--icon{fill:#00b488}.social-icons-color-standard.social-icons-style-regular .vine .sqs-use--background,.social-icons-color-standard.social-icons-style-regular .vine .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .vine .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .vine .sqs-use--icon{fill:rgba(0,180,136,.4)}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .vine:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .vine:hover .sqs-use--icon{fill:#00b488}.social-icons-color-standard.social-icons-style-border .vine{border-color:#00b488}.social-icons-color-standard.social-icons-style-border .vine .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-border .vine .sqs-use--icon{fill:#00b488}.social-icons-color-standard.social-icons-style-border .vine .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-border .vine:hover{background-color:#00b488}.social-icons-color-standard.social-icons-style-border .vine:hover .sqs-use--icon{fill:#fff}.social-icons-color-standard.social-icons-style-knockout .vine .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .vine .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .vine .sqs-use--mask{fill:#00b488}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .vine .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .vine .sqs-use--mask{fill:rgba(0,180,136,.4)}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .vine:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .vine:hover .sqs-use--mask{fill:#00b488}.social-icons-color-standard.social-icons-style-solid .vine .sqs-use--mask{fill:#00b488}.social-icons-color-standard.social-icons-style-solid .vine .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-solid .vine .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .vine .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .vine .sqs-use--mask{fill:rgba(0,180,136,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .vine .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .vine .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .vine .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .vine .sqs-use--background{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .vine:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .vine:hover .sqs-use--mask{fill:#00b488}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .vine:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .vine:hover .sqs-use--icon{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .vine:hover .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .vine:hover .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-regular .vsco .sqs-use--icon{fill:#a9a849}.social-icons-color-standard.social-icons-style-regular .vsco .sqs-use--background,.social-icons-color-standard.social-icons-style-regular .vsco .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .vsco .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .vsco .sqs-use--icon{fill:rgba(169,168,73,.4)}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .vsco:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .vsco:hover .sqs-use--icon{fill:#a9a849}.social-icons-color-standard.social-icons-style-border .vsco{border-color:#a9a849}.social-icons-color-standard.social-icons-style-border .vsco .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-border .vsco .sqs-use--icon{fill:#a9a849}.social-icons-color-standard.social-icons-style-border .vsco .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-border .vsco:hover{background-color:#a9a849}.social-icons-color-standard.social-icons-style-border .vsco:hover .sqs-use--icon{fill:#fff}.social-icons-color-standard.social-icons-style-knockout .vsco .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .vsco .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .vsco .sqs-use--mask{fill:#a9a849}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .vsco .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .vsco .sqs-use--mask{fill:rgba(169,168,73,.4)}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .vsco:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .vsco:hover .sqs-use--mask{fill:#a9a849}.social-icons-color-standard.social-icons-style-solid .vsco .sqs-use--mask{fill:#a9a849}.social-icons-color-standard.social-icons-style-solid .vsco .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-solid .vsco .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .vsco .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .vsco .sqs-use--mask{fill:rgba(169,168,73,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .vsco .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .vsco .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .vsco .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .vsco .sqs-use--background{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .vsco:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .vsco:hover .sqs-use--mask{fill:#a9a849}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .vsco:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .vsco:hover .sqs-use--icon{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .vsco:hover .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .vsco:hover .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-regular .yelp .sqs-use--icon{fill:#c41200}.social-icons-color-standard.social-icons-style-regular .yelp .sqs-use--background,.social-icons-color-standard.social-icons-style-regular .yelp .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .yelp .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .yelp .sqs-use--icon{fill:rgba(196,18,0,.4)}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .yelp:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .yelp:hover .sqs-use--icon{fill:#c41200}.social-icons-color-standard.social-icons-style-border .yelp{border-color:#c41200}.social-icons-color-standard.social-icons-style-border .yelp .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-border .yelp .sqs-use--icon{fill:#c41200}.social-icons-color-standard.social-icons-style-border .yelp .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-border .yelp:hover{background-color:#c41200}.social-icons-color-standard.social-icons-style-border .yelp:hover .sqs-use--icon{fill:#fff}.social-icons-color-standard.social-icons-style-knockout .yelp .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .yelp .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .yelp .sqs-use--mask{fill:#c41200}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .yelp .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .yelp .sqs-use--mask{fill:rgba(196,18,0,.4)}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .yelp:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .yelp:hover .sqs-use--mask{fill:#c41200}.social-icons-color-standard.social-icons-style-solid .yelp .sqs-use--mask{fill:#c41200}.social-icons-color-standard.social-icons-style-solid .yelp .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-solid .yelp .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .yelp .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .yelp .sqs-use--mask{fill:rgba(196,18,0,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .yelp .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .yelp .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .yelp .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .yelp .sqs-use--background{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .yelp:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .yelp:hover .sqs-use--mask{fill:#c41200}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .yelp:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .yelp:hover .sqs-use--icon{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .yelp:hover .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .yelp:hover .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-regular .youtube .sqs-use--icon{fill:#e52d27}.social-icons-color-standard.social-icons-style-regular .youtube .sqs-use--background,.social-icons-color-standard.social-icons-style-regular .youtube .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .youtube .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .youtube .sqs-use--icon{fill:rgba(229,45,39,.4)}.social-icons-color-standard.social-icons-style-regular .sqs-svg-icon--list:hover .youtube:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-regular.sqs-svg-icon--list:hover .youtube:hover .sqs-use--icon{fill:#e52d27}.social-icons-color-standard.social-icons-style-border .youtube{border-color:#e52d27}.social-icons-color-standard.social-icons-style-border .youtube .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-border .youtube .sqs-use--icon{fill:#e52d27}.social-icons-color-standard.social-icons-style-border .youtube .sqs-use--mask{fill:transparent}.social-icons-color-standard.social-icons-style-border .youtube:hover{background-color:#e52d27}.social-icons-color-standard.social-icons-style-border .youtube:hover .sqs-use--icon{fill:#fff}.social-icons-color-standard.social-icons-style-knockout .youtube .sqs-use--background{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .youtube .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-knockout .youtube .sqs-use--mask{fill:#e52d27}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .youtube .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .youtube .sqs-use--mask{fill:rgba(229,45,39,.4)}.social-icons-color-standard.social-icons-style-knockout .sqs-svg-icon--list:hover .youtube:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-knockout.sqs-svg-icon--list:hover .youtube:hover .sqs-use--mask{fill:#e52d27}.social-icons-color-standard.social-icons-style-solid .youtube .sqs-use--mask{fill:#e52d27}.social-icons-color-standard.social-icons-style-solid .youtube .sqs-use--icon{fill:transparent}.social-icons-color-standard.social-icons-style-solid .youtube .sqs-use--background{fill:#fff}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .youtube .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .youtube .sqs-use--mask{fill:rgba(229,45,39,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .youtube .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .youtube .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .youtube .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .youtube .sqs-use--background{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .youtube:hover .sqs-use--mask,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .youtube:hover .sqs-use--mask{fill:#e52d27}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .youtube:hover .sqs-use--icon,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .youtube:hover .sqs-use--icon{fill:rgba(255,255,255,0)}.social-icons-color-standard.social-icons-style-solid .sqs-svg-icon--list:hover .youtube:hover .sqs-use--background,.social-icons-color-standard.social-icons-style-solid.sqs-svg-icon--list:hover .youtube:hover .sqs-use--background{fill:#fff}.sqs-svg-icon--wrapper{-webkit-transition:background-color 170ms ease-in-out;-moz-transition:background-color 170ms ease-in-out;-ms-transition:background-color 170ms ease-in-out;-o-transition:background-color 170ms ease-in-out;transition:background-color 170ms ease-in-out}.sqs-use--icon,.sqs-use--mask,.sqs-use--background{-webkit-transition:fill 170ms ease-in-out;-moz-transition:fill 170ms ease-in-out;-ms-transition:fill 170ms ease-in-out;-o-transition:fill 170ms ease-in-out;transition:fill 170ms ease-in-out}.social-solid-icon-anim-out 0%{fill:#fff}.social-solid-icon-anim-out 100%{fill:rgba(255,255,255,.4)}@-webkit-keyframes social-solid-icon-anim-out{0%{fill:#fff}100%{fill:rgba(255,255,255,.4)}}@keyframes social-solid-icon-anim{0%{fill:#fff}100%{fill:rgba(255,255,255,.4)}}.social-solid-icon-anim-off 0%{fill:rgba(255,255,255,.4)}.social-solid-icon-anim-off 99%{fill:#fff}.social-solid-icon-anim-off 100%{fill:rgba(255,255,255,0)}@-webkit-keyframes social-solid-icon-anim-off{0%{fill:rgba(255,255,255,.4)}99%{fill:#fff}100%{fill:rgba(255,255,255,0)}}@keyframes social-solid-icon-anim{0%{fill:rgba(255,255,255,.4)}99%{fill:#fff}100%{fill:rgba(255,255,255,0)}}.social-solid-icon-anim-in 0%{fill:rgba(255,255,255,.4)}.social-solid-icon-anim-in 100%{fill:#fff}@-webkit-keyframes social-solid-icon-anim-in{0%{fill:rgba(255,255,255,.4)}100%{fill:#fff}}@keyframes social-solid-icon-anim{0%{fill:rgba(255,255,255,.4)}100%{fill:#fff}}.social-icons-style-solid .sqs-use--background{-webkit-transition:fill 0ms 170ms ease-in-out;-moz-transition:fill 0ms 170ms ease-in-out;-ms-transition:fill 0ms 170ms ease-in-out;-o-transition:fill 0ms 170ms ease-in-out;transition:fill 0ms 170ms ease-in-out}.social-icons-style-solid .sqs-use--icon{-webkit-animation:social-solid-icon-anim-off 170ms ease-in-out;animation:social-solid-icon-anim-off 170ms ease-in-out}.social-icons-style-solid .sqs-svg-icon--list:hover .sqs-use--icon,.social-icons-style-solid.sqs-svg-icon--list:hover .sqs-use--icon{-webkit-animation:social-solid-icon-anim-out 170ms ease-in-out;animation:social-solid-icon-anim-out 170ms ease-in-out}.social-icons-style-solid .sqs-svg-icon--list:hover .sqs-use--background,.social-icons-style-solid.sqs-svg-icon--list:hover .sqs-use--background{-webkit-transition:fill 0ms 0ms ease-in-out;-moz-transition:fill 0ms 0ms ease-in-out;-ms-transition:fill 0ms 0ms ease-in-out;-o-transition:fill 0ms 0ms ease-in-out;transition:fill 0ms 0ms ease-in-out}.social-icons-style-solid .sqs-svg-icon--list:hover .sqs-svg-icon--wrapper:hover .sqs-use--icon,.social-icons-style-solid.sqs-svg-icon--list:hover .sqs-svg-icon--wrapper:hover .sqs-use--icon{-webkit-animation:social-solid-icon-anim-in 170ms ease-in-out;animation:social-solid-icon-anim-in 170ms ease-in-out}.social-icons-style-solid .sqs-svg-icon--list:hover .sqs-svg-icon--wrapper:hover .sqs-use--background,.social-icons-style-solid.sqs-svg-icon--list:hover .sqs-svg-icon--wrapper:hover .sqs-use--background{-webkit-transition:fill 0ms 160ms ease-in-out;-moz-transition:fill 0ms 160ms ease-in-out;-ms-transition:fill 0ms 160ms ease-in-out;-o-transition:fill 0ms 160ms ease-in-out;transition:fill 0ms 160ms ease-in-out}.sqs-svg-icon--list.social-icon-alignment-left{text-align:left}.sqs-svg-icon--list.social-icon-alignment-right{text-align:right}.sqs-svg-icon--list.social-icon-alignment-center{text-align:center}.rss-block .social-rss:before,.rss-block .social-rss-square:before,.rss-block .social-rss-round:before{font-family:'social-icon-font';speak:none;font-style:normal;font-weight:normal;line-height:1;-webkit-font-smoothing:antialiased;position:relative;top:0;margin-right:10px;font-size:.7em}.rss-block .social-rss:before{content:\"\\e630\"}/*IE9_SPLIT_MARKER*/\n.social-icons-style-regular .sqs-use--icon{fill:#fff}.social-icons-style-regular .sqs-use--background,.social-icons-style-regular .sqs-use--mask{fill:transparent}.social-icons-style-regular.social-icons-color-white .sqs-use--icon{fill:#fff}.social-icons-style-regular.social-icons-color-white .sqs-use--background,.social-icons-style-regular.social-icons-color-white .sqs-use--mask{fill:transparent}.social-icons-style-regular.social-icons-color-black .sqs-use--icon{fill:#222}.social-icons-style-regular.social-icons-color-black .sqs-use--background,.social-icons-style-regular.social-icons-color-black .sqs-use--mask{fill:transparent}.social-icons-style-border .sqs-svg-icon--wrapper{border-color:#fff}.social-icons-style-border .sqs-use--icon{fill:#fff}.social-icons-style-border .sqs-use--background,.social-icons-style-border .sqs-use--mask{fill:transparent}.social-icons-style-border.social-icons-color-white .sqs-svg-icon--wrapper{border-color:#fff}.social-icons-style-border.social-icons-color-white .sqs-use--icon{fill:#fff}.social-icons-style-border.social-icons-color-white .sqs-use--background,.social-icons-style-border.social-icons-color-white .sqs-use--mask{fill:transparent}.social-icons-style-border.social-icons-color-black .sqs-svg-icon--wrapper{border-color:#222}.social-icons-style-border.social-icons-color-black .sqs-use--icon{fill:#222}.social-icons-style-border.social-icons-color-black .sqs-use--background,.social-icons-style-border.social-icons-color-black .sqs-use--mask{fill:transparent}.social-icons-style-knockout .sqs-use--mask{fill:#fff}.social-icons-style-knockout .sqs-use--background,.social-icons-style-knockout .sqs-use--icon{fill:transparent}.social-icons-style-knockout.social-icons-color-white .sqs-use--mask{fill:#fff}.social-icons-style-knockout.social-icons-color-white .sqs-use--background,.social-icons-style-knockout.social-icons-color-white .sqs-use--icon{fill:transparent}.social-icons-style-knockout.social-icons-color-black .sqs-use--mask{fill:#222}.social-icons-style-knockout.social-icons-color-black .sqs-use--background,.social-icons-style-knockout.social-icons-color-black .sqs-use--icon{fill:transparent}.social-icons-style-solid .sqs-use--icon{fill:#222}.social-icons-style-solid .sqs-use--background{fill:#222}.social-icons-style-solid .sqs-use--mask{fill:#fff}.social-icons-style-solid.social-icons-color-white .sqs-use--icon{fill:#222}.social-icons-style-solid.social-icons-color-white .sqs-use--background{fill:#fff}.social-icons-style-solid.social-icons-color-white .sqs-use--mask{fill:#fff}.social-icons-style-solid.social-icons-color-black .sqs-use--icon{fill:#fff}.social-icons-style-solid.social-icons-color-black .sqs-use--background{fill:#222}.social-icons-style-solid.social-icons-color-black .sqs-use--mask{fill:#222}.social-icons-style-border .sqs-svg-icon--wrapper:hover{background-color:#fff}.social-icons-style-border .sqs-svg-icon--wrapper:hover .sqs-use--icon{fill:#222}.social-icons-style-border.social-icons-color-white .sqs-svg-icon--wrapper:hover{background-color:#fff}.social-icons-style-border.social-icons-color-white .sqs-svg-icon--wrapper:hover .sqs-use--icon{fill:#222}.social-icons-style-border.social-icons-color-black .sqs-svg-icon--wrapper:hover{background-color:#222}.social-icons-style-border.social-icons-color-black .sqs-svg-icon--wrapper:hover .sqs-use--icon{fill:#fff}.social-icons-style-regular:hover .sqs-svg-icon--wrapper .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-style-regular:hover .sqs-svg-icon--wrapper:hover .sqs-use--icon{fill:#fff}.social-icons-style-regular.social-icons-color-white:hover .sqs-svg-icon--wrapper .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-style-regular.social-icons-color-white:hover .sqs-svg-icon--wrapper:hover .sqs-use--icon{fill:#fff}.social-icons-style-regular.social-icons-color-black:hover .sqs-svg-icon--wrapper .sqs-use--icon{fill:rgba(34,34,34,.4)}.social-icons-style-regular.social-icons-color-black:hover .sqs-svg-icon--wrapper:hover .sqs-use--icon{fill:#222}.social-icons-style-knockout:hover .sqs-svg-icon--wrapper .sqs-use--mask{fill:rgba(255,255,255,.4)}.social-icons-style-knockout:hover .sqs-svg-icon--wrapper:hover .sqs-use--mask{fill:#fff}.social-icons-style-knockout.social-icons-color-white:hover .sqs-svg-icon--wrapper .sqs-use--mask{fill:rgba(255,255,255,.4)}.social-icons-style-knockout.social-icons-color-white:hover .sqs-svg-icon--wrapper:hover .sqs-use--mask{fill:#fff}.social-icons-style-knockout.social-icons-color-black:hover .sqs-svg-icon--wrapper .sqs-use--mask{fill:rgba(34,34,34,.4)}.social-icons-style-knockout.social-icons-color-black:hover .sqs-svg-icon--wrapper:hover .sqs-use--mask{fill:#222}.social-icons-style-solid:hover .sqs-svg-icon--wrapper .sqs-use--mask{fill:rgba(255,255,255,.4)}.social-icons-style-solid:hover .sqs-svg-icon--wrapper .sqs-use--icon{fill:rgba(34,34,34,.4)}.social-icons-style-solid:hover .sqs-svg-icon--wrapper .sqs-use--background{fill:rgba(34,34,34,0)}.social-icons-style-solid:hover .sqs-svg-icon--wrapper:hover .sqs-use--mask{fill:#fff}.social-icons-style-solid:hover .sqs-svg-icon--wrapper:hover .sqs-use--icon{fill:rgba(34,34,34,0)}.social-icons-style-solid:hover .sqs-svg-icon--wrapper:hover .sqs-use--background{fill:#222}.social-icons-style-solid.social-icons-color-white:hover .sqs-svg-icon--wrapper .sqs-use--mask,.social-icons-style-solid.social-icons-color-black:hover .sqs-svg-icon--wrapper .sqs-use--mask{fill:rgba(255,255,255,.4)}.social-icons-style-solid.social-icons-color-white:hover .sqs-svg-icon--wrapper .sqs-use--icon,.social-icons-style-solid.social-icons-color-black:hover .sqs-svg-icon--wrapper .sqs-use--icon{fill:rgba(34,34,34,.4)}.social-icons-style-solid.social-icons-color-white:hover .sqs-svg-icon--wrapper .sqs-use--background,.social-icons-style-solid.social-icons-color-black:hover .sqs-svg-icon--wrapper .sqs-use--background{fill:rgba(34,34,34,0)}.social-icons-style-solid.social-icons-color-white:hover .sqs-svg-icon--wrapper:hover .sqs-use--mask,.social-icons-style-solid.social-icons-color-black:hover .sqs-svg-icon--wrapper:hover .sqs-use--mask{fill:#222}.social-icons-style-solid.social-icons-color-white:hover .sqs-svg-icon--wrapper:hover .sqs-use--icon,.social-icons-style-solid.social-icons-color-black:hover .sqs-svg-icon--wrapper:hover .sqs-use--icon{fill:rgba(255,255,255,0)}.social-icons-style-solid.social-icons-color-white:hover .sqs-svg-icon--wrapper:hover .sqs-use--background,.social-icons-style-solid.social-icons-color-black:hover .sqs-svg-icon--wrapper:hover .sqs-use--background{fill:#fff}@font-face{font-family:'social-icon-font';src:url('//static.squarespace.com/universal/fonts/social-20141119/social-icon-font.eot');src:url('//static.squarespace.com/universal/fonts/social-20141119/social-icon-font.eot?#iefix') format('embedded-opentype'),url('//static.squarespace.com/universal/fonts/social-20141119/social-icon-font.woff') format('woff'),url('//static.squarespace.com/universal/fonts/social-20141119/social-icon-font.ttf') format('truetype'),url('//static.squarespace.com/universal/fonts/social-20141119/social-icon-font.svg#social-icon-font') format('svg');font-weight:normal;font-style:normal}.social-smugmug:before,.social-dribbble:before,.social-youtube:before,.social-vimeo:before,.social-twitter:before,.social-tumblr:before,.social-pinterest:before,.social-linkedin:before,.social-instagram:before,.social-google:before,.social-foursquare:before,.social-flickr:before,.social-facebook:before,.social-fivehundredpix:before,.social-fivehundredpx:before,.social-email:before,.social-github:before,.social-rss:before,.social-spotify:before,.social-soundcloud:before,.social-itunes:before,.social-googleplay:before,.social-dropbox:before,.social-bandsintown:before,.social-behance:before,.social-codepen:before,.social-medium:before,.social-rdio:before,.social-squarespace:before,.social-vine:before,.social-yelp:before,.social-vevo:before,.social-meetup:before,.social-twitch:before,.social-vsco:before,.social-smugmug-square:before,.social-dribbble-square:before,.social-youtube-square:before,.social-vimeo-square:before,.social-twitter-square:before,.social-tumblr-square:before,.social-pinterest-square:before,.social-linkedin-square:before,.social-instagram-square:before,.social-google-square:before,.social-foursquare-square:before,.social-flickr-square:before,.social-facebook-square:before,.social-fivehundredpix-square:before,.social-fivehundredpx-square:before,.social-email-square:before,.social-github-square:before,.social-rss-square:before,.social-spotify-square:before,.social-soundcloud-square:before,.social-itunes-square:before,.social-googleplay-square:before,.social-dropbox-square:before,.social-bandsintown-square:before,.social-behance-square:before,.social-codepen-square:before,.social-medium-square:before,.social-rdio-square:before,.social-squarespace-square:before,.social-vine-square:before,.social-yelp-square:before,.social-vevo-square:before,.social-meetup-square:before,.social-twitch-square:before,.social-vsco-square:before,.social-smugmug-round:before,.social-dribbble-round:before,.social-youtube-round:before,.social-vimeo-round:before,.social-twitter-round:before,.social-tumblr-round:before,.social-pinterest-round:before,.social-linkedin-round:before,.social-instagram-round:before,.social-google-round:before,.social-foursquare-round:before,.social-flickr-round:before,.social-facebook-round:before,.social-fivehundredpix-round:before,.social-fivehundredpx-round:before,.social-email-round:before,.social-github-round:before,.social-rss-round:before,.social-spotify-round:before,.social-soundcloud-round:before,.social-itunes-round:before,.social-googleplay-round:before,.social-dropbox-round:before,.social-bandsintown-round:before,.social-behance-round:before,.social-codepen-round:before,.social-medium-round:before,.social-rdio-round:before,.social-squarespace-round:before,.social-vine-round:before,.social-yelp-round:before,.social-vevo-round:before,.social-meetup-round:before,.social-twitch-round:before,.social-vsco-round:before{font-family:'social-icon-font';speak:none;font-style:normal;font-weight:normal;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.social-smugmug:before{content:\"\\e600\"}.social-icon-style-square .social-smugmug:before{content:\"\\e601\"}.social-icon-style-round .social-smugmug:before{content:\"\\e602\"}.social-dribbble:before{content:\"\\e603\"}.social-icon-style-square .social-dribbble:before{content:\"\\e604\"}.social-icon-style-round .social-dribbble:before{content:\"\\e605\"}.social-youtube:before{content:\"\\e606\"}.social-icon-style-square .social-youtube:before{content:\"\\e607\"}.social-icon-style-round .social-youtube:before{content:\"\\e608\"}.social-vimeo:before{content:\"\\e609\"}.social-icon-style-square .social-vimeo:before{content:\"\\e60a\"}.social-icon-style-round .social-vimeo:before{content:\"\\e60b\"}.social-twitter:before{content:\"\\e60c\"}.social-icon-style-square .social-twitter:before{content:\"\\e60d\"}.social-icon-style-round .social-twitter:before{content:\"\\e60e\"}.social-tumblr:before{content:\"\\e60f\"}.social-icon-style-square .social-tumblr:before{content:\"\\e610\"}.social-icon-style-round .social-tumblr:before{content:\"\\e611\"}.social-pinterest:before{content:\"\\e612\"}.social-icon-style-square .social-pinterest:before{content:\"\\e613\"}.social-icon-style-round .social-pinterest:before{content:\"\\e614\"}.social-linkedin:before{content:\"\\e615\"}.social-icon-style-square .social-linkedin:before{content:\"\\e616\"}.social-icon-style-round .social-linkedin:before{content:\"\\e617\"}.social-instagram:before{content:\"\\e618\"}.social-icon-style-square .social-instagram:before{content:\"\\e619\"}.social-icon-style-round .social-instagram:before{content:\"\\e61a\"}.social-google:before{content:\"\\e61b\"}.social-icon-style-square .social-google:before{content:\"\\e61c\"}.social-icon-style-round .social-google:before{content:\"\\e61d\"}.social-googleauth2:before{content:\"\\e61b\"}.social-foursquare:before{content:\"\\e61e\"}.social-icon-style-square .social-foursquare:before{content:\"\\e61f\"}.social-icon-style-round .social-foursquare:before{content:\"\\e620\"}.social-flickr:before{content:\"\\e621\"}.social-icon-style-square .social-flickr:before{content:\"\\e622\"}.social-icon-style-round .social-flickr:before{content:\"\\e623\"}.social-facebook:before{content:\"\\e624\"}.social-icon-style-square .social-facebook:before{content:\"\\e625\"}.social-icon-style-round .social-facebook:before{content:\"\\e626\"}.social-fivehundredpix:before{content:\"\\e627\"}.social-icon-style-square .social-fivehundredpix:before{content:\"\\e628\"}.social-icon-style-round .social-fivehundredpix:before{content:\"\\e629\"}.social-fivehundredpx:before{content:\"\\e627\"}.social-icon-style-square .social-fivehundredpx:before{content:\"\\e628\"}.social-icon-style-round .social-fivehundredpx:before{content:\"\\e629\"}.social-email:before{content:\"\\e62a\"}.social-icon-style-square .social-email:before{content:\"\\e62b\"}.social-icon-style-round .social-email:before{content:\"\\e62c\"}.social-github:before{content:\"\\e62d\"}.social-icon-style-square .social-github:before{content:\"\\e62e\"}.social-icon-style-round .social-github:before{content:\"\\e62f\"}.social-rss:before{content:\"\\e630\"}.social-icon-style-square .social-rss:before{content:\"\\e631\"}.social-icon-style-round .social-rss:before{content:\"\\e632\"}.social-spotify:before{content:\"\\e633\"}.social-icon-style-square .social-spotify:before{content:\"\\e634\"}.social-icon-style-round .social-spotify:before{content:\"\\e635\"}.social-soundcloud:before{content:\"\\e636\"}.social-icon-style-square .social-soundcloud:before{content:\"\\e637\"}.social-icon-style-round .social-soundcloud:before{content:\"\\e638\"}.social-itunes:before{content:\"\\e639\"}.social-icon-style-square .social-itunes:before{content:\"\\e63a\"}.social-icon-style-round .social-itunes:before{content:\"\\e63b\"}.social-googleplay:before{content:\"\\e63c\"}.social-icon-style-square .social-googleplay:before{content:\"\\e63d\"}.social-icon-style-round .social-googleplay:before{content:\"\\e63e\"}.social-dropbox:before{content:\"\\e63f\"}.social-icon-style-square .social-dropbox:before{content:\"\\e640\"}.social-icon-style-round .social-dropbox:before{content:\"\\e641\"}.social-bandsintown:before{content:\"\\e642\"}.social-icon-style-square .social-bandsintown:before{content:\"\\e643\"}.social-icon-style-round .social-bandsintown:before{content:\"\\e644\"}.social-behance:before{content:\"\\e645\"}.social-icon-style-square .social-behance:before{content:\"\\e646\"}.social-icon-style-round .social-behance:before{content:\"\\e647\"}.social-codepen:before{content:\"\\e648\"}.social-icon-style-square .social-codepen:before{content:\"\\e649\"}.social-icon-style-round .social-codepen:before{content:\"\\e64a\"}.social-medium:before{content:\"\\e64b\"}.social-icon-style-square .social-medium:before{content:\"\\e64c\"}.social-icon-style-round .social-medium:before{content:\"\\e64d\"}.social-rdio:before{content:\"\\e64e\"}.social-icon-style-square .social-rdio:before{content:\"\\e64f\"}.social-icon-style-round .social-rdio:before{content:\"\\e650\"}.social-squarespace:before{content:\"\\e651\"}.social-icon-style-square .social-squarespace:before{content:\"\\e652\"}.social-icon-style-round .social-squarespace:before{content:\"\\e653\"}.social-vine:before{content:\"\\e654\"}.social-icon-style-square .social-vine:before{content:\"\\e655\"}.social-icon-style-round .social-vine:before{content:\"\\e656\"}.social-yelp:before{content:\"\\e657\"}.social-icon-style-square .social-yelp:before{content:\"\\e658\"}.social-icon-style-round .social-yelp:before{content:\"\\e659\"}.social-meetup:before{content:\"\\e65a\"}.social-icon-style-square .social-meetup:before{content:\"\\e65b\"}.social-icon-style-round .social-meetup:before{content:\"\\e65c\"}.social-vevo:before{content:\"\\e65d\"}.social-icon-style-square .social-vevo:before{content:\"\\e65e\"}.social-icon-style-round .social-vevo:before{content:\"\\e65f\"}.social-twitch:before{content:\"\\e660\"}.social-icon-style-square .social-twitch:before{content:\"\\e661\"}.social-icon-style-round .social-twitch:before{content:\"\\e662\"}.social-vsco:before{content:\"\\e663\"}.social-icon-style-square .social-vsco:before{content:\"\\e664\"}.social-icon-style-round .social-vsco:before{content:\"\\e665\"}.sqs-block-socialaccountlinks .social-account-svg-list,.sqs-block-socialaccountlinks-v2 .social-account-svg-list{text-align:left}.sqs-block-socialaccountlinks .social-account-svg-list:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list:before,.sqs-block-socialaccountlinks .social-account-svg-list:after,.sqs-block-socialaccountlinks-v2 .social-account-svg-list:after{content:\"\";display:table}.sqs-block-socialaccountlinks .social-account-svg-list:after,.sqs-block-socialaccountlinks-v2 .social-account-svg-list:after{clear:both}.sqs-block-socialaccountlinks .social-account-svg-list a,.sqs-block-socialaccountlinks-v2 .social-account-svg-list a,.sqs-block-socialaccountlinks .social-account-svg-list a:link,.sqs-block-socialaccountlinks-v2 .social-account-svg-list a:link,.sqs-block-socialaccountlinks .social-account-svg-list a:visited,.sqs-block-socialaccountlinks-v2 .social-account-svg-list a:visited{display:inline-block;width:20px;height:20px;font-size:20px;color:#111;text-decoration:none !important;*zoom:1;*display:inline;font-weight:normal}.sqs-block-socialaccountlinks .social-account-svg-list a:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list a:before,.sqs-block-socialaccountlinks .social-account-svg-list a:link:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list a:link:before,.sqs-block-socialaccountlinks .social-account-svg-list a:visited:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list a:visited:before{font-size:20px;line-height:20px}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-alignment-left a,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-alignment-left a{margin-right:.75em}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-alignment-right a,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-alignment-right a{margin-left:.75em}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-alignment-center a,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-alignment-center a{margin:0 .375em}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-alignment-center,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-alignment-center{text-align:center}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-alignment-right,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-alignment-right{text-align:right}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-white a,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-white a,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-white a:visited,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-white a:visited{color:#fff}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-bandsintown,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-bandsintown,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-bandsintown,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-bandsintown,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-bandsintown,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-bandsintown{color:#00b4b3}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-behance,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-behance,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-behance,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-behance,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-behance,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-behance{color:#1769ff}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-codepen,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-codepen,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-codepen,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-codepen,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-codepen,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-codepen{color:#222}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-dribbble,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-dribbble,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-dribbble,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-dribbble,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-dribbble,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-dribbble{color:#ea4c89}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-dropbox,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-dropbox,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-dropbox,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-dropbox,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-dropbox,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-dropbox{color:#007ee5}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-email,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-email,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-email,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-email,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-email,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-email{color:#222}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-facebook,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-facebook,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-facebook,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-facebook,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-facebook,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-facebook{color:#3b5998}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-fivehundredpix,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-fivehundredpix,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-fivehundredpix,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-fivehundredpix,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-fivehundredpix,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-fivehundredpix,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-fivehundredpx,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-fivehundredpx,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-fivehundredpx,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-fivehundredpx,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-fivehundredpx,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-fivehundredpx{color:#00aeef}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-flickr,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-flickr,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-flickr,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-flickr,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-flickr,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-flickr{color:#0063dc}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-foursquare,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-foursquare,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-foursquare,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-foursquare,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-foursquare,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-foursquare{color:#f94877}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-github,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-github,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-github,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-github,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-github,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-github{color:#4183c4}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-google,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-google,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-google,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-google,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-google,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-google{color:#dd4b39}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-googleplay,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-googleplay,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-googleplay,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-googleplay,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-googleplay,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-googleplay{color:#5adfcb}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-instagram,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-instagram,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-instagram,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-instagram,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-instagram,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-instagram{color:#3f729b}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-itunes,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-itunes,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-itunes,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-itunes,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-itunes,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-itunes{color:#ff3241}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-linkedin,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-linkedin,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-linkedin,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-linkedin,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-linkedin,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-linkedin{color:#0976b4}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-medium,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-medium,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-medium,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-medium,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-medium,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-medium{color:#222}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-meetup,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-meetup,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-meetup,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-meetup,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-meetup,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-meetup{color:#e0393e}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-pinterest,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-pinterest,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-pinterest,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-pinterest,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-pinterest,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-pinterest{color:#cc2127}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-rdio,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-rdio,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-rdio,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-rdio,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-rdio,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-rdio{color:#006ed2}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-rss,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-rss,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-rss,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-rss,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-rss,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-rss{color:#222}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-smugmug,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-smugmug,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-smugmug,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-smugmug,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-smugmug,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-smugmug{color:#7dbb00}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-soundcloud,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-soundcloud,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-soundcloud,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-soundcloud,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-soundcloud,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-soundcloud{color:#f80}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-spotify,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-spotify,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-spotify,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-spotify,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-spotify,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-spotify{color:#84bd00}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-squarespace,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-squarespace,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-squarespace,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-squarespace,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-squarespace,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-squarespace{color:#222}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-tumblr,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-tumblr,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-tumblr,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-tumblr,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-tumblr,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-tumblr{color:#35465d}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-twitter,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-twitter,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-twitter,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-twitter,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-twitter,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-twitter{color:#55acee}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-vimeo,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-vimeo,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-vimeo,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-vimeo,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-vimeo,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-vimeo{color:#1ab7ea}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-vine,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-vine,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-vine,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-vine,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-vine,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-vine{color:#00b488}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-yelp,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-yelp,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-yelp,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-yelp,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-yelp,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-yelp{color:#c41200}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-youtube,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-youtube,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-youtube,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-youtube,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-youtube,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-youtube{color:#e52d27}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-meetup,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-meetup,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-meetup,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-meetup,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-meetup,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-meetup{color:#e0393e}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-vevo,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-vevo,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-vevo,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-vevo,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-vevo,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-vevo{color:#ff0031}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-twitch,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-twitch,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-twitch,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-twitch,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-twitch,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-twitch{color:#6441a5}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-vsco,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-vsco,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-vsco,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-vsco,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-vsco,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-vsco{color:#a9a849}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-large a,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-large a,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-large a:link,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-large a:link,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-large a:visited,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-large a:visited{width:24px;height:24px;font-size:24px}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-large a:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-large a:before,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-large a:link:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-large a:link:before,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-large a:visited:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-large a:visited:before{font-size:24px;line-height:24px}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-small a,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-small a,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-small a:link,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-small a:link,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-small a:visited,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-small a:visited{width:16px;height:16px;font-size:16px}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-small a:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-small a:before,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-small a:link:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-small a:link:before,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-small a:visited:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-small a:visited:before{font-size:16px;line-height:16px}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-style-round a,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-style-round a,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-style-square a,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-style-square a,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-style-round a:link,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-style-round a:link,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-style-square a:link,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-style-square a:link,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-style-round a:visited,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-style-round a:visited,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-style-square a:visited,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-style-square a:visited{width:30px;height:30px;font-size:30px}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-style-round a:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-style-round a:before,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-style-square a:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-style-square a:before,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-style-round a:link:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-style-round a:link:before,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-style-square a:link:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-style-square a:link:before,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-style-round a:visited:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-style-round a:visited:before,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-style-square a:visited:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-style-square a:visited:before{font-size:30px;line-height:30px}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-style-round.social-icon-alignment-left a,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-style-round.social-icon-alignment-left a,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-style-square.social-icon-alignment-left a,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-style-square.social-icon-alignment-left a{margin-right:.25em}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-style-round.social-icon-alignment-right a,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-style-round.social-icon-alignment-right a,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-style-square.social-icon-alignment-right a,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-style-square.social-icon-alignment-right a{margin-left:.25em}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-style-round.social-icon-alignment-center a,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-style-round.social-icon-alignment-center a,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-style-square.social-icon-alignment-center a,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-style-square.social-icon-alignment-center a{margin:0 .125em}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-large.social-icon-style-round a,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-large.social-icon-style-round a,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-large.social-icon-style-square a,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-large.social-icon-style-square a,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-large.social-icon-style-round a:link,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-large.social-icon-style-round a:link,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-large.social-icon-style-square a:link,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-large.social-icon-style-square a:link,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-large.social-icon-style-round a:visited,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-large.social-icon-style-round a:visited,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-large.social-icon-style-square a:visited,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-large.social-icon-style-square a:visited{width:36px;height:36px;font-size:36px}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-large.social-icon-style-round a:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-large.social-icon-style-round a:before,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-large.social-icon-style-square a:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-large.social-icon-style-square a:before,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-large.social-icon-style-round a:link:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-large.social-icon-style-round a:link:before,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-large.social-icon-style-square a:link:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-large.social-icon-style-square a:link:before,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-large.social-icon-style-round a:visited:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-large.social-icon-style-round a:visited:before,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-large.social-icon-style-square a:visited:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-large.social-icon-style-square a:visited:before{font-size:36px;line-height:36px}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-small.social-icon-style-round a,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-small.social-icon-style-round a,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-small.social-icon-style-square a,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-small.social-icon-style-square a,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-small.social-icon-style-round a:link,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-small.social-icon-style-round a:link,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-small.social-icon-style-square a:link,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-small.social-icon-style-square a:link,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-small.social-icon-style-round a:visited,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-small.social-icon-style-round a:visited,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-small.social-icon-style-square a:visited,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-small.social-icon-style-square a:visited{width:24px;height:24px;font-size:24px}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-small.social-icon-style-round a:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-small.social-icon-style-round a:before,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-small.social-icon-style-square a:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-small.social-icon-style-square a:before,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-small.social-icon-style-round a:link:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-small.social-icon-style-round a:link:before,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-small.social-icon-style-square a:link:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-small.social-icon-style-square a:link:before,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-small.social-icon-style-round a:visited:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-small.social-icon-style-round a:visited:before,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-small.social-icon-style-square a:visited:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-small.social-icon-style-square a:visited:before{font-size:24px;line-height:24px}.sqs-album-block{zoom:1;font-size:13px;color:#282828}.sqs-album-block .social-links{display:inline-block;line-height:3em}.sqs-album-block .social-links .social-account-svg-list{text-align:left}.sqs-album-block .social-links .social-account-svg-list:before,.sqs-album-block .social-links .social-account-svg-list:after{content:\"\";display:table}.sqs-album-block .social-links .social-account-svg-list:after{clear:both}.sqs-album-block .social-links .social-account-svg-list a,.sqs-album-block .social-links .social-account-svg-list a:link,.sqs-album-block .social-links .social-account-svg-list a:visited{display:inline-block;width:20px;height:20px;font-size:20px;color:#111;text-decoration:none !important;*zoom:1;*display:inline;font-weight:normal}.sqs-album-block .social-links .social-account-svg-list a:before,.sqs-album-block .social-links .social-account-svg-list a:link:before,.sqs-album-block .social-links .social-account-svg-list a:visited:before{font-size:20px;line-height:20px}.sqs-album-block .social-links .social-account-svg-list.social-icon-alignment-left a{margin-right:.75em}.sqs-album-block .social-links .social-account-svg-list.social-icon-alignment-right a{margin-left:.75em}.sqs-album-block .social-links .social-account-svg-list.social-icon-alignment-center a{margin:0 .375em}.sqs-album-block .social-links .social-account-svg-list.social-icon-alignment-center{text-align:center}.sqs-album-block .social-links .social-account-svg-list.social-icon-alignment-right{text-align:right}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-white a,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-white a:visited{color:#fff}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-bandsintown,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-bandsintown,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-bandsintown{color:#00b4b3}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-behance,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-behance,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-behance{color:#1769ff}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-codepen,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-codepen,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-codepen{color:#222}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-dribbble,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-dribbble,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-dribbble{color:#ea4c89}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-dropbox,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-dropbox,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-dropbox{color:#007ee5}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-email,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-email,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-email{color:#222}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-facebook,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-facebook,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-facebook{color:#3b5998}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-fivehundredpix,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-fivehundredpix,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-fivehundredpix,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-fivehundredpx,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-fivehundredpx,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-fivehundredpx{color:#00aeef}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-flickr,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-flickr,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-flickr{color:#0063dc}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-foursquare,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-foursquare,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-foursquare{color:#f94877}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-github,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-github,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-github{color:#4183c4}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-google,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-google,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-google{color:#dd4b39}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-googleplay,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-googleplay,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-googleplay{color:#5adfcb}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-instagram,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-instagram,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-instagram{color:#3f729b}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-itunes,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-itunes,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-itunes{color:#ff3241}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-linkedin,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-linkedin,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-linkedin{color:#0976b4}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-medium,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-medium,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-medium{color:#222}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-meetup,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-meetup,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-meetup{color:#e0393e}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-pinterest,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-pinterest,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-pinterest{color:#cc2127}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-rdio,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-rdio,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-rdio{color:#006ed2}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-rss,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-rss,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-rss{color:#222}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-smugmug,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-smugmug,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-smugmug{color:#7dbb00}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-soundcloud,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-soundcloud,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-soundcloud{color:#f80}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-spotify,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-spotify,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-spotify{color:#84bd00}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-squarespace,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-squarespace,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-squarespace{color:#222}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-tumblr,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-tumblr,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-tumblr{color:#35465d}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-twitter,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-twitter,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-twitter{color:#55acee}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-vimeo,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-vimeo,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-vimeo{color:#1ab7ea}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-vine,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-vine,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-vine{color:#00b488}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-yelp,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-yelp,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-yelp{color:#c41200}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-youtube,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-youtube,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-youtube{color:#e52d27}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-meetup,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-meetup,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-meetup{color:#e0393e}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-vevo,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-vevo,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-vevo{color:#ff0031}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-twitch,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-twitch,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-twitch{color:#6441a5}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-vsco,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-vsco,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-vsco{color:#a9a849}.sqs-album-block .social-links .social-account-svg-list.social-icon-size-large a,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-large a:link,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-large a:visited{width:24px;height:24px;font-size:24px}.sqs-album-block .social-links .social-account-svg-list.social-icon-size-large a:before,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-large a:link:before,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-large a:visited:before{font-size:24px;line-height:24px}.sqs-album-block .social-links .social-account-svg-list.social-icon-size-small a,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-small a:link,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-small a:visited{width:16px;height:16px;font-size:16px}.sqs-album-block .social-links .social-account-svg-list.social-icon-size-small a:before,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-small a:link:before,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-small a:visited:before{font-size:16px;line-height:16px}.sqs-album-block .social-links .social-account-svg-list.social-icon-style-round a,.sqs-album-block .social-links .social-account-svg-list.social-icon-style-square a,.sqs-album-block .social-links .social-account-svg-list.social-icon-style-round a:link,.sqs-album-block .social-links .social-account-svg-list.social-icon-style-square a:link,.sqs-album-block .social-links .social-account-svg-list.social-icon-style-round a:visited,.sqs-album-block .social-links .social-account-svg-list.social-icon-style-square a:visited{width:30px;height:30px;font-size:30px}.sqs-album-block .social-links .social-account-svg-list.social-icon-style-round a:before,.sqs-album-block .social-links .social-account-svg-list.social-icon-style-square a:before,.sqs-album-block .social-links .social-account-svg-list.social-icon-style-round a:link:before,.sqs-album-block .social-links .social-account-svg-list.social-icon-style-square a:link:before,.sqs-album-block .social-links .social-account-svg-list.social-icon-style-round a:visited:before,.sqs-album-block .social-links .social-account-svg-list.social-icon-style-square a:visited:before{font-size:30px;line-height:30px}.sqs-album-block .social-links .social-account-svg-list.social-icon-style-round.social-icon-alignment-left a,.sqs-album-block .social-links .social-account-svg-list.social-icon-style-square.social-icon-alignment-left a{margin-right:.25em}.sqs-album-block .social-links .social-account-svg-list.social-icon-style-round.social-icon-alignment-right a,.sqs-album-block .social-links .social-account-svg-list.social-icon-style-square.social-icon-alignment-right a{margin-left:.25em}.sqs-album-block .social-links .social-account-svg-list.social-icon-style-round.social-icon-alignment-center a,.sqs-album-block .social-links .social-account-svg-list.social-icon-style-square.social-icon-alignment-center a{margin:0 .125em}.sqs-album-block .social-links .social-account-svg-list.social-icon-size-large.social-icon-style-round a,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-large.social-icon-style-square a,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-large.social-icon-style-round a:link,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-large.social-icon-style-square a:link,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-large.social-icon-style-round a:visited,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-large.social-icon-style-square a:visited{width:36px;height:36px;font-size:36px}.sqs-album-block .social-links .social-account-svg-list.social-icon-size-large.social-icon-style-round a:before,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-large.social-icon-style-square a:before,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-large.social-icon-style-round a:link:before,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-large.social-icon-style-square a:link:before,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-large.social-icon-style-round a:visited:before,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-large.social-icon-style-square a:visited:before{font-size:36px;line-height:36px}.sqs-album-block .social-links .social-account-svg-list.social-icon-size-small.social-icon-style-round a,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-small.social-icon-style-square a,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-small.social-icon-style-round a:link,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-small.social-icon-style-square a:link,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-small.social-icon-style-round a:visited,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-small.social-icon-style-square a:visited{width:24px;height:24px;font-size:24px}.sqs-album-block .social-links .social-account-svg-list.social-icon-size-small.social-icon-style-round a:before,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-small.social-icon-style-square a:before,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-small.social-icon-style-round a:link:before,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-small.social-icon-style-square a:link:before,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-small.social-icon-style-round a:visited:before,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-small.social-icon-style-square a:visited:before{font-size:24px;line-height:24px}.sqs-album-block .social-links .social-account-svg-list{text-align:left}.sqs-album-block .social-links .social-account-svg-list:before,.sqs-album-block .social-links .social-account-svg-list:after{content:\"\";display:table}.sqs-album-block .social-links .social-account-svg-list:after{clear:both}.sqs-album-block .social-links .social-account-svg-list a,.sqs-album-block .social-links .social-account-svg-list a:link,.sqs-album-block .social-links .social-account-svg-list a:visited{display:inline-block;width:20px;height:20px;font-size:20px;color:#111;text-decoration:none !important;*zoom:1;*display:inline;font-weight:normal}.sqs-album-block .social-links .social-account-svg-list a:before,.sqs-album-block .social-links .social-account-svg-list a:link:before,.sqs-album-block .social-links .social-account-svg-list a:visited:before{font-size:20px;line-height:20px}.sqs-album-block .social-links .social-account-svg-list.social-icon-alignment-left a{margin-right:.75em}.sqs-album-block .social-links .social-account-svg-list.social-icon-alignment-right a{margin-left:.75em}.sqs-album-block .social-links .social-account-svg-list.social-icon-alignment-center a{margin:0 .375em}.sqs-album-block .social-links .social-account-svg-list.social-icon-alignment-center{text-align:center}.sqs-album-block .social-links .social-account-svg-list.social-icon-alignment-right{text-align:right}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-white a,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-white a:visited{color:#fff}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-bandsintown,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-bandsintown,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-bandsintown{color:#00b4b3}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-behance,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-behance,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-behance{color:#1769ff}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-codepen,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-codepen,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-codepen{color:#222}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-dribbble,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-dribbble,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-dribbble{color:#ea4c89}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-dropbox,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-dropbox,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-dropbox{color:#007ee5}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-email,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-email,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-email{color:#222}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-facebook,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-facebook,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-facebook{color:#3b5998}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-fivehundredpix,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-fivehundredpix,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-fivehundredpix,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-fivehundredpx,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-fivehundredpx,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-fivehundredpx{color:#00aeef}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-flickr,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-flickr,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-flickr{color:#0063dc}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-foursquare,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-foursquare,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-foursquare{color:#f94877}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-github,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-github,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-github{color:#4183c4}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-google,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-google,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-google{color:#dd4b39}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-googleplay,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-googleplay,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-googleplay{color:#5adfcb}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-instagram,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-instagram,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-instagram{color:#3f729b}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-itunes,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-itunes,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-itunes{color:#ff3241}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-linkedin,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-linkedin,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-linkedin{color:#0976b4}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-medium,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-medium,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-medium{color:#222}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-meetup,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-meetup,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-meetup{color:#e0393e}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-pinterest,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-pinterest,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-pinterest{color:#cc2127}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-rdio,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-rdio,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-rdio{color:#006ed2}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-rss,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-rss,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-rss{color:#222}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-smugmug,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-smugmug,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-smugmug{color:#7dbb00}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-soundcloud,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-soundcloud,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-soundcloud{color:#f80}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-spotify,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-spotify,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-spotify{color:#84bd00}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-squarespace,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-squarespace,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-squarespace{color:#222}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-tumblr,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-tumblr,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-tumblr{color:#35465d}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-twitter,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-twitter,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-twitter{color:#55acee}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-vimeo,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-vimeo,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-vimeo{color:#1ab7ea}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-vine,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-vine,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-vine{color:#00b488}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-yelp,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-yelp,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-yelp{color:#c41200}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-youtube,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-youtube,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-youtube{color:#e52d27}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-meetup,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-meetup,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-meetup{color:#e0393e}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-vevo,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-vevo,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-vevo{color:#ff0031}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-twitch,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-twitch,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-twitch{color:#6441a5}.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a.social-vsco,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:link.social-vsco,.sqs-album-block .social-links .social-account-svg-list.social-icon-color-standard a:visited.social-vsco{color:#a9a849}.sqs-album-block .social-links .social-account-svg-list.social-icon-size-large a,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-large a:link,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-large a:visited{width:24px;height:24px;font-size:24px}.sqs-album-block .social-links .social-account-svg-list.social-icon-size-large a:before,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-large a:link:before,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-large a:visited:before{font-size:24px;line-height:24px}.sqs-album-block .social-links .social-account-svg-list.social-icon-size-small a,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-small a:link,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-small a:visited{width:16px;height:16px;font-size:16px}.sqs-album-block .social-links .social-account-svg-list.social-icon-size-small a:before,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-small a:link:before,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-small a:visited:before{font-size:16px;line-height:16px}.sqs-album-block .social-links .social-account-svg-list.social-icon-style-round a,.sqs-album-block .social-links .social-account-svg-list.social-icon-style-square a,.sqs-album-block .social-links .social-account-svg-list.social-icon-style-round a:link,.sqs-album-block .social-links .social-account-svg-list.social-icon-style-square a:link,.sqs-album-block .social-links .social-account-svg-list.social-icon-style-round a:visited,.sqs-album-block .social-links .social-account-svg-list.social-icon-style-square a:visited{width:30px;height:30px;font-size:30px}.sqs-album-block .social-links .social-account-svg-list.social-icon-style-round a:before,.sqs-album-block .social-links .social-account-svg-list.social-icon-style-square a:before,.sqs-album-block .social-links .social-account-svg-list.social-icon-style-round a:link:before,.sqs-album-block .social-links .social-account-svg-list.social-icon-style-square a:link:before,.sqs-album-block .social-links .social-account-svg-list.social-icon-style-round a:visited:before,.sqs-album-block .social-links .social-account-svg-list.social-icon-style-square a:visited:before{font-size:30px;line-height:30px}.sqs-album-block .social-links .social-account-svg-list.social-icon-style-round.social-icon-alignment-left a,.sqs-album-block .social-links .social-account-svg-list.social-icon-style-square.social-icon-alignment-left a{margin-right:.25em}.sqs-album-block .social-links .social-account-svg-list.social-icon-style-round.social-icon-alignment-right a,.sqs-album-block .social-links .social-account-svg-list.social-icon-style-square.social-icon-alignment-right a{margin-left:.25em}.sqs-album-block .social-links .social-account-svg-list.social-icon-style-round.social-icon-alignment-center a,.sqs-album-block .social-links .social-account-svg-list.social-icon-style-square.social-icon-alignment-center a{margin:0 .125em}.sqs-album-block .social-links .social-account-svg-list.social-icon-size-large.social-icon-style-round a,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-large.social-icon-style-square a,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-large.social-icon-style-round a:link,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-large.social-icon-style-square a:link,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-large.social-icon-style-round a:visited,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-large.social-icon-style-square a:visited{width:36px;height:36px;font-size:36px}.sqs-album-block .social-links .social-account-svg-list.social-icon-size-large.social-icon-style-round a:before,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-large.social-icon-style-square a:before,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-large.social-icon-style-round a:link:before,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-large.social-icon-style-square a:link:before,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-large.social-icon-style-round a:visited:before,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-large.social-icon-style-square a:visited:before{font-size:36px;line-height:36px}.sqs-album-block .social-links .social-account-svg-list.social-icon-size-small.social-icon-style-round a,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-small.social-icon-style-square a,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-small.social-icon-style-round a:link,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-small.social-icon-style-square a:link,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-small.social-icon-style-round a:visited,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-small.social-icon-style-square a:visited{width:24px;height:24px;font-size:24px}.sqs-album-block .social-links .social-account-svg-list.social-icon-size-small.social-icon-style-round a:before,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-small.social-icon-style-square a:before,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-small.social-icon-style-round a:link:before,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-small.social-icon-style-square a:link:before,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-small.social-icon-style-round a:visited:before,.sqs-album-block .social-links .social-account-svg-list.social-icon-size-small.social-icon-style-square a:visited:before{font-size:24px;line-height:24px}.sqs-album-block:after{display:block;visibility:hidden;font-size:0;height:0;clear:both;content:\".\"}.sqs-album-block:after{display:block;visibility:hidden;font-size:0;height:0;clear:both;content:\".\"}.sqs-album-block .album-info{width:33%;float:left;zoom:1}.sqs-album-block .album-info:after{display:block;visibility:hidden;font-size:0;height:0;clear:both;content:\".\"}.sqs-album-block .album-info:after{display:block;visibility:hidden;font-size:0;height:0;clear:both;content:\".\"}.sqs-album-block .album-cover-wrapper{position:relative;width:90px;height:90px;margin-bottom:30px}.sqs-album-block .album-controls{position:absolute;top:0;right:0;bottom:0;left:0;cursor:pointer}.sqs-album-block .album-controls .button{-webkit-transition:.25s all linear;-moz-transition:.25s all linear;-ms-transition:.25s all linear;-o-transition:.25s all linear;transition:.25s all linear;-moz-border-radius:50%;border-radius:50%;position:absolute;bottom:50%;left:50%;display:block;width:90px;height:90px;margin-bottom:-45px;margin-left:-45px;background:#000}.sqs-album-block .album-controls .icon{display:block;position:relative;left:50%;top:50%;margin-top:-20px;margin-left:-10px;width:0px;height:0px;border-top:18px solid transparent;border-bottom:18px solid transparent;border-left:30px solid #fff;-webkit-transform:translatez();-ms-transform:translatez();transform:translatez()}.sqs-album-block .album-title{margin:0;margin-bottom:3px}.sqs-album-block.playing .album-controls .button .icon{border-width:0px;margin-top:-15px;margin-left:-13px}.sqs-album-block.playing .album-controls .button .icon,.sqs-album-block.playing .album-controls .button .icon:before{height:30px;width:10px;background-color:#fff}.sqs-album-block.playing .album-controls .button .icon:before{content:'';display:block;margin-left:16px}.sqs-album-block.has-album-cover .album-cover-wrapper{position:relative;width:100%;height:0;padding-bottom:100%;margin-bottom:20px}.sqs-album-block.has-album-cover .album-cover{position:absolute;top:0;right:0;bottom:0;left:0}.sqs-album-block.has-album-cover .button{background:rgba(0,0,0,.7);opacity:.9}.sqs-album-block.has-album-cover:hover .button{background:rgba(0,0,0,.9)}.sqs-album-block.has-album-cover.playing .album-controls .button{margin:-15px;bottom:0;left:0;-webkit-transform:scale(.4,.4);-ms-transform:scale(.4,.4);transform:scale(.4,.4)}.sqs-album-block.playing .track{opacity:.4}.sqs-album-block.playing .track:hover,.sqs-album-block.playing .track.selected,.sqs-album-block.playing .track.universal-track{opacity:1}.sqs-album-block .tracks{float:right;width:60%;margin:0;padding:0}.sqs-album-block .track{list-style-type:none;padding:0;margin:0 0 36px;cursor:pointer;zoom:1;font-style:normal;font-weight:normal;letter-spacing:0;text-transform:none;line-height:1.4em}.sqs-album-block .track:after{display:block;visibility:hidden;font-size:0;height:0;clear:both;content:\".\"}.sqs-album-block .track:after{display:block;visibility:hidden;font-size:0;height:0;clear:both;content:\".\"}.sqs-album-block .track-progress-bar{clear:both;height:2px;position:relative;cursor:pointer;padding-bottom:2.5%}.sqs-album-block .track-progress-bar .bar{position:absolute;top:0;left:0;height:4px;width:0%}.sqs-album-block .track-progress-bar .bar.bg{width:100%}.sqs-album-block .track-progress-bar .bar.play-bar{position:relative}.sqs-album-block .timers{float:right;text-align:right;margin-left:1em}.sqs-album-block .tracks .timers .elapsed{display:none}.sqs-album-block.playing .tracks .track.selected .timers .duration,.sqs-album-block.paused .tracks .track.selected .timers .duration{display:none}.sqs-album-block.playing .tracks .track.selected .timers .elapsed,.sqs-album-block.paused .tracks .track.selected .timers .elapsed{display:block}.sqs-album-block .universal-player{display:none;margin-top:25px;width:100%}.sqs-album-block .universal-player .universal-progress{padding-bottom:1em}.sqs-album-block .universal-controls{margin-bottom:1em}.sqs-album-block .universal-controls .play-pause-group{display:inline-block}.sqs-album-block .universal-controls .next-prev-group{display:inline-block;float:right}.sqs-album-block .universal-controls .pause{display:none}.sqs-album-block.playing .universal-controls .pause{display:block}.sqs-album-block.playing .universal-controls .play{display:none}.sqs-album-block.hide-track-artists .artist{display:none}.sqs-album-block.md .album-info,.sqs-album-block.md .tracks{width:100%;float:none}.sqs-album-block.md .album-info{padding-bottom:60px}.sqs-album-block.md .album-cover-wrapper{float:left;margin-right:30px}.sqs-album-block.md.has-album-cover .album-cover-wrapper{width:40%;padding-bottom:40%}.sqs-album-block.md.no-main-image .album-description{margin-left:120px}.sqs-album-block.sm .album-info,.sqs-album-block.sm .tracks{width:100%;float:none}.sqs-album-block.sm .tracks{margin-top:30px}.sqs-album-block.sm .tracks .track{margin-bottom:27px}.sqs-album-block.sm .album-info{padding-bottom:0}.sqs-album-block.sm .album-cover-wrapper{float:none;margin-right:0;margin-bottom:20px}.sqs-album-block.sm.has-album-cover .album-cover-wrapper{width:100%;padding-bottom:100%;float:none;margin-right:0}.sqs-album-block.sm.no-main-image .album-description{margin-left:0}.sqs-album-block.mini-player .album-description,.sqs-album-block.mini-player .album-title,.sqs-album-block.mini-player .album-artist-name,.sqs-album-block.mini-player .tracks,.sqs-album-block.mini-player .social-links{display:none}.sqs-album-block.mini-player.no-album-cover .album-info{display:none}.sqs-album-block.mini-player.lg.has-album-cover .album-info,.sqs-album-block.mini-player.md.has-album-cover .album-info{width:145px;display:inline-block;float:left;padding-bottom:0;margin-right:1em}.sqs-album-block.mini-player.lg .universal-player,.sqs-album-block.mini-player.md .universal-player{display:inline-block}.sqs-album-block.mini-player.lg.has-album-cover .universal-player,.sqs-album-block.mini-player.md.has-album-cover .universal-player{width:calc(100% - 145px - 1em)}.sqs-album-block.mini-player.lg.has-album-cover .play-pause-group,.sqs-album-block.mini-player.md.has-album-cover .play-pause-group{display:none}.sqs-album-block.mini-player.lg.has-album-cover .next-prev-group,.sqs-album-block.mini-player.md.has-album-cover .next-prev-group{float:none}.sqs-album-block.mini-player.lg.has-album-cover .album-cover-wrapper,.sqs-album-block.mini-player.md.has-album-cover .album-cover-wrapper{padding-bottom:100%;width:145px;margin:0;float:none}.sqs-album-block.mini-player.sm .universal-player{display:block}.sqs-album-block .album-title{font-weight:bold;font-size:20px}.sqs-album-block .album-description{line-height:21px;font-size:13px}.sqs-album-block.sm .album-description{font-size:11px}.sqs-album-block .title a{font-size:15px;text-decoration:none}.sqs-album-block .tracks .timers .elapsed:before{content:\"(\"}.sqs-album-block .tracks .timers .elapsed:after{content:\")\"}.sqs-album-block .universal-controls{text-transform:uppercase}.sqs-album-block .universal-controls a{text-decoration:none}.sqs-album-block .track-progress-bar{-webkit-tap-highlight-color:rgba(40,40,40,.17)}.sqs-album-block .track-progress-bar .bar{-webkit-tap-highlight-color:rgba(40,40,40,.17)}.sqs-album-block .track-progress-bar .bar.bg{background-color:rgba(40,40,40,.17)}.sqs-album-block .track-progress-bar .bar.load-bar{background-color:rgba(40,40,40,.07)}.sqs-album-block .track-progress-bar .bar.play-bar{background-color:#282828}.sqs-album-block .timers,.sqs-album-block .artist,.sqs-album-block .album-description{color:rgba(40,40,40,.5)}.sqs-album-block .track.selected .timers .elapsed{color:#282828}.sqs-album-block .title a{color:#282828}.sqs-album-block .universal-controls a{color:#282828}.sqs-album-block.inverted{color:#d7d7d7}.sqs-album-block.inverted .track-progress-bar{-webkit-tap-highlight-color:rgba(215,215,215,.17)}.sqs-album-block.inverted .track-progress-bar .bar{-webkit-tap-highlight-color:rgba(215,215,215,.17)}.sqs-album-block.inverted .track-progress-bar .bar.bg{background-color:rgba(215,215,215,.17)}.sqs-album-block.inverted .track-progress-bar .bar.load-bar{background-color:rgba(215,215,215,.07)}.sqs-album-block.inverted .track-progress-bar .bar.play-bar{background-color:#d7d7d7}.sqs-album-block.inverted .timers,.sqs-album-block.inverted .artist,.sqs-album-block.inverted .album-description{color:rgba(215,215,215,.5)}.sqs-album-block.inverted .track.selected .timers .elapsed{color:#d7d7d7}.sqs-album-block.inverted .title a{color:#d7d7d7}.sqs-album-block.inverted .universal-controls a{color:#d7d7d7}.social-summary-block .state-message.synchronizing{background-image:none;padding-left:15px}.social-summary-block .state-message.synchronizing .sync-text{float:left;margin-left:10px}.social-summary-block .state-message.synchronizing .spinner{float:left;background:transparent url('//static.squarespace.com/universal/images-v6/icons/icon-settings-16-light.png') center center no-repeat;height:19px;width:19px;-webkit-animation-duration:2s;-moz-animation-duration:2s;-o-animation-duration:2s;animation-duration:2s;-webkit-animation-iteration-count:infinite;-moz-animation-iteration-count:infinite;-o-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-name:spin-frames;-moz-animation-name:spin-frames;-o-animation-name:spin-frames;animation-name:spin-frames}.product-block .image-container{position:relative;display:block;width:100%}.product-block .image-container a{display:block;width:100%}.product-block .image-container img{width:100%;max-width:100%}.product-block .image-container .product-mark{position:absolute;top:15px;right:0;background:#222;padding:6px 8px;color:#fff;line-height:1em;text-transform:uppercase;-webkit-font-smoothing:antialiased}.product-block .productDetails.center{text-align:center}.product-block .productDetails.right{text-align:right}.product-block .productDetails .product-title{font-size:1.3em;line-height:1em;margin:1em 0 .2em 0;display:inline-block}.product-block .productDetails .product-price{font-size:1.1em;margin:0 0 1em 0}.product-block .productDetails .product-price input{width:130px;height:30px;padding-left:5px}.product-block .productDetails .product-price .minimum-price{margin-top:3px;margin-left:10px}.product-block .productDetails .product-price .original-price{text-decoration:line-through;opacity:.7;filter:alpha(opacity=70)}.product-block .productDetails .product-price .strikeout{text-decoration:line-through}.product-block .productDetails .product-variants .variant-option{margin:0 0 1em 0}.product-block .productDetails .product-variants .variant-out-of-stock{color:#c00;margin-top:8px}.product-block .buy-button,.product-block .sqs-add-to-cart-button-wrapper{margin:20px 0;display:block}.product-block .buy-button:hover,.product-block .sqs-add-to-cart-button-wrapper:hover{opacity:1}.product-block .sqs-add-to-cart-button{display:inline-block;width:auto;height:auto;padding:1em 2.5em;color:#fff;background-color:#272727;border-width:0;font-family:\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-size:12px;line-height:1em;font-weight:normal;font-style:normal;text-transform:uppercase;letter-spacing:0px;text-align:center;text-decoration:none;cursor:pointer;outline:none;-webkit-appearance:none;-moz-appearance:none;appearance:none}.product-block .sqs-amazon-button{display:inline-block;width:auto;height:auto;padding:1em 2.5em;color:#fff;background-color:#272727;border-width:0;font-family:\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-size:12px;line-height:1em;font-weight:normal;font-style:normal;text-transform:uppercase;letter-spacing:0px;text-align:center;text-decoration:none;cursor:pointer;outline:none;-webkit-appearance:none;-moz-appearance:none;appearance:none;text-transform:none}.product-block .center .sqs-amazon-button{text-align:center}.product-block .right .sqs-amazon-button{text-align:right}.sqs-block-quote figure{margin:1em 0}.sqs-block-quote blockquote{margin:0}.sqs-block-quote .source{text-align:right}.foursquare-block ul{list-style-type:none;margin:0;padding:0;line-height:1.4em}.foursquare-block ul .foursquare-checkin{margin-bottom:12px}.foursquare-block ul .foursquare-checkin a{border:0}.foursquare-block ul .foursquare-checkin .foursquare-icon-wrapper{float:left}.foursquare-block ul .foursquare-checkin .foursquare-text{margin-left:42px;font-size:12px}.foursquare-block ul .foursquare-checkin .foursquare-venue{font-weight:bold}.foursquare-block ul .foursquare-checkin .foursquare-location{display:inline-block;padding-left:4px}.foursquare-block ul .foursquare-checkin .foursquare-timestamp{font-size:10px}.tagcloud-block ul{list-style-type:none;margin:0;padding-left:0}.tagcloud-block ul li{display:inline-block}.sqs-block-postsbycategory ul,.sqs-block-postsbyauthor ul,.sqs-block-postsbytag ul,.sqs-block-postsbymonth ul{list-style-type:none;margin:0;padding:0}.sqs-block-postsbycategory ul li,.sqs-block-postsbyauthor ul li,.sqs-block-postsbytag ul li,.sqs-block-postsbymonth ul li{margin:0 0 .3em 0;padding:0}.sqs-block-postsbycategory .count,.sqs-block-postsbyauthor .count,.sqs-block-postsbytag .count,.sqs-block-postsbymonth .count{display:none}.sqs-block-image .sqs-image-caption p,.sqs-block-image .image-caption p{font-size:12px;line-height:1.68em}.sqs-block-image .sqs-image-caption p a,.sqs-block-image .image-caption p a{display:inline}body.squarespace-config .sqs-block-image .sqs-image-caption{color:#999}body.squarespace-config .sqs-block-image .sqs-image-caption p{margin-bottom:0}body.squarespace-config .sqs-block-image .sqs-image-caption.sqs-placeholder-show{margin-top:1em}body.squarespace-config .sqs-block-image .sqs-image-caption .sqs-html-content{min-height:23px}.sqs-block-image:not(.sqs-block-focused) .sqs-image-caption.sqs-placeholder-show{display:none}.sqs-block-image .sqs-placeholder p{margin:0;margin-top:.7em}.sqs-block-image .image-block-outer-wrapper .image-block-wrapper img{max-width:none}.sqs-block-image .image-block-lightbox{cursor:pointer;display:block}.sqs-block-image .lightbox img{cursor:pointer}.sqs-block-image.sized .image-block-wrapper{overflow:hidden;padding-bottom:inherit !important}.sqs-block-image.sized .image-block-wrapper img{text-align:inherit;max-width:none}.sqs-block-image img{display:block}.sqs-block-image .image-block-wrapper.sqs-default-image{text-align:center}.sqs-block-image .image-block-wrapper.sqs-default-image img{display:inline-block}.sqs-block-image .image-block-outer-wrapper.layout-caption-overlay .intrinsic,.sqs-block-image .image-block-outer-wrapper.layout-caption-overlay-hover .intrinsic{position:relative}.sqs-block-image .image-block-outer-wrapper.layout-caption-overlay .intrinsic .image-caption-wrapper,.sqs-block-image .image-block-outer-wrapper.layout-caption-overlay-hover .intrinsic .image-caption-wrapper{position:absolute;overflow:hidden;top:auto;bottom:0;left:0;right:0;padding:18px;background:rgba(0,0,0,.7)}.sqs-block-image .image-block-outer-wrapper.layout-caption-overlay .intrinsic .image-caption-wrapper h1,.sqs-block-image .image-block-outer-wrapper.layout-caption-overlay-hover .intrinsic .image-caption-wrapper h1,.sqs-block-image .image-block-outer-wrapper.layout-caption-overlay .intrinsic .image-caption-wrapper strong,.sqs-block-image .image-block-outer-wrapper.layout-caption-overlay-hover .intrinsic .image-caption-wrapper strong{color:#eee}.sqs-block-image .image-block-outer-wrapper.layout-caption-overlay .intrinsic .image-caption-wrapper p,.sqs-block-image .image-block-outer-wrapper.layout-caption-overlay-hover .intrinsic .image-caption-wrapper p{color:#bbb;line-height:1.68em}.sqs-block-image .image-block-outer-wrapper.layout-caption-overlay .intrinsic .image-caption-wrapper p a,.sqs-block-image .image-block-outer-wrapper.layout-caption-overlay-hover .intrinsic .image-caption-wrapper p a{color:#bbb;text-decoration:underline}.sqs-block-image .image-block-outer-wrapper.layout-caption-overlay .intrinsic .image-caption-wrapper p:first-child,.sqs-block-image .image-block-outer-wrapper.layout-caption-overlay-hover .intrinsic .image-caption-wrapper p:first-child{padding-top:0;margin-top:0}.sqs-block-image .image-block-outer-wrapper.layout-caption-overlay .intrinsic .image-caption-wrapper p:last-child,.sqs-block-image .image-block-outer-wrapper.layout-caption-overlay-hover .intrinsic .image-caption-wrapper p:last-child{padding-bottom:0;margin-bottom:0}.sqs-block-image .image-block-outer-wrapper.layout-caption-overlay-hover:hover .image-caption-wrapper{visibility:visible;opacity:1}.sqs-block-image .image-block-outer-wrapper.layout-caption-overlay-hover .image-caption-wrapper{visibility:hidden;opacity:0;-webkit-transition:opacity .1s ease-out;-moz-transition:opacity .1s ease-out;-ms-transition:opacity .1s ease-out;-o-transition:opacity .1s ease-out;transition:opacity .1s ease-out}.sqs-block-image .image-block-outer-wrapper.layout-caption-overlay-hover:hover .image-caption{margin-bottom:0}.sqs-block-image .image-block-outer-wrapper.layout-caption-overlay-hover .image-caption{-webkit-transition:margin-bottom .1s ease-out;-moz-transition:margin-bottom .1s ease-out;-ms-transition:margin-bottom .1s ease-out;-o-transition:margin-bottom .1s ease-out;transition:margin-bottom .1s ease-out;margin-bottom:-5px}.sqs-block-image .image-block-outer-wrapper.layout-image-left{zoom:1}.sqs-block-image .image-block-outer-wrapper.layout-image-left:after{display:block;visibility:hidden;font-size:0;height:0;clear:both;content:\".\"}.sqs-block-image .image-block-outer-wrapper.layout-image-left .image-block-wrapper{float:left}.sqs-block-image .image-block-outer-wrapper.layout-image-left .image-caption-wrapper{float:left}.sqs-block-image .image-block-outer-wrapper.layout-image-left .image-caption{padding-left:15px}.sqs-block-image .image-block-outer-wrapper.layout-image-left .image-caption h1{font-size:18px;line-height:24px}.sqs-block-image .image-block-outer-wrapper.layout-image-right{zoom:1}.sqs-block-image .image-block-outer-wrapper.layout-image-right:after{display:block;visibility:hidden;font-size:0;height:0;clear:both;content:\".\"}.sqs-block-image .image-block-outer-wrapper.layout-image-right .image-block-wrapper{float:right}.sqs-block-image .image-block-outer-wrapper.layout-image-right .image-caption-wrapper{float:right;text-align:right}.sqs-block-image .image-block-outer-wrapper.layout-image-right .image-caption{padding-right:15px}.sqs-block-image .image-block-outer-wrapper.layout-image-right .image-caption h1{font-size:18px;line-height:24px}.sqs-block-image .image-block-wrapper{line-height:0;text-align:center;position:relative;overflow:hidden}.sqs-block-image .image-block-wrapper img{max-width:100%}.sqs-block-image .image-block-wrapper img.block-stretch{width:100%}.sqs-block-image .image-block-wrapper.float-right .image-block-wrapper{text-align:right}.sqs-block-image .intrinsic{margin:auto}.sqs-block-image .intrinsic .image-block-wrapper img{position:absolute;top:0;left:0;width:100%}.sqs-block-image .sqs-action-overlay{z-index:1000}.sqs-block-image .processing{background:#ccc;text-align:center}.sqs-block-image .processing .progress-container{background:#ccc;top:15px}.sqs-block-image .processing-failed{background:#ccc;text-align:center;position:relative;height:100%}.sqs-block-image.vsize-1 .image-block-wrapper{height:34px}.sqs-block-image.vsize-2 .image-block-wrapper{height:68px}.sqs-block-image.vsize-3 .image-block-wrapper{height:102px}.sqs-block-image.vsize-4 .image-block-wrapper{height:136px}.sqs-block-image.vsize-5 .image-block-wrapper{height:170px}.sqs-block-image.vsize-6 .image-block-wrapper{height:204px}.sqs-block-image.vsize-7 .image-block-wrapper{height:238px}.sqs-block-image.vsize-8 .image-block-wrapper{height:272px}.sqs-block-image.vsize-9 .image-block-wrapper{height:306px}.sqs-block-image.vsize-10 .image-block-wrapper{height:340px}.sqs-block-image.vsize-11 .image-block-wrapper{height:374px}.sqs-block-image.vsize-12 .image-block-wrapper{height:408px}.sqs-block-image.vsize-13 .image-block-wrapper{height:442px}.sqs-block-image.vsize-14 .image-block-wrapper{height:476px}.sqs-block-image.vsize-15 .image-block-wrapper{height:510px}.sqs-block-image.vsize-16 .image-block-wrapper{height:544px}.sqs-block-image.vsize-17 .image-block-wrapper{height:578px}.sqs-block-image.vsize-18 .image-block-wrapper{height:612px}.sqs-block-image.vsize-19 .image-block-wrapper{height:646px}.sqs-block-image.vsize-20 .image-block-wrapper{height:680px}.sqs-block-image.vsize-21 .image-block-wrapper{height:714px}.sqs-block-image.vsize-22 .image-block-wrapper{height:748px}.sqs-block-image.vsize-23 .image-block-wrapper{height:782px}.sqs-block-image.vsize-24 .image-block-wrapper{height:816px}.sqs-block-image.vsize-25 .image-block-wrapper{height:850px}.sqs-block-image.vsize-26 .image-block-wrapper{height:884px}.sqs-block-image.vsize-27 .image-block-wrapper{height:918px}.sqs-block-image.vsize-28 .image-block-wrapper{height:952px}.sqs-block-image.vsize-29 .image-block-wrapper{height:986px}.sqs-block-image.vsize-30 .image-block-wrapper{height:1020px}.sqs-block-image.vsize-1 .sqs-block-content{height:auto;overflow:visible}.sqs-block-image.vsize-2 .sqs-block-content{height:auto;overflow:visible}.sqs-block-image.vsize-3 .sqs-block-content{height:auto;overflow:visible}.sqs-block-image.vsize-4 .sqs-block-content{height:auto;overflow:visible}.sqs-block-image.vsize-5 .sqs-block-content{height:auto;overflow:visible}.sqs-block-image.vsize-6 .sqs-block-content{height:auto;overflow:visible}.sqs-block-image.vsize-7 .sqs-block-content{height:auto;overflow:visible}.sqs-block-image.vsize-8 .sqs-block-content{height:auto;overflow:visible}.sqs-block-image.vsize-9 .sqs-block-content{height:auto;overflow:visible}.sqs-block-image.vsize-10 .sqs-block-content{height:auto;overflow:visible}.sqs-block-image.vsize-11 .sqs-block-content{height:auto;overflow:visible}.sqs-block-image.vsize-12 .sqs-block-content{height:auto;overflow:visible}.sqs-block-image.vsize-13 .sqs-block-content{height:auto;overflow:visible}.sqs-block-image.vsize-14 .sqs-block-content{height:auto;overflow:visible}.sqs-block-image.vsize-15 .sqs-block-content{height:auto;overflow:visible}.sqs-block-image.vsize-16 .sqs-block-content{height:auto;overflow:visible}.sqs-block-image.vsize-17 .sqs-block-content{height:auto;overflow:visible}.sqs-block-image.vsize-18 .sqs-block-content{height:auto;overflow:visible}.sqs-block-image.vsize-19 .sqs-block-content{height:auto;overflow:visible}.sqs-block-image.vsize-20 .sqs-block-content{height:auto;overflow:visible}.sqs-block-image.vsize-21 .sqs-block-content{height:auto;overflow:visible}.sqs-block-image.vsize-22 .sqs-block-content{height:auto;overflow:visible}.sqs-block-image.vsize-23 .sqs-block-content{height:auto;overflow:visible}.sqs-block-image.vsize-24 .sqs-block-content{height:auto;overflow:visible}.sqs-block-image.vsize-25 .sqs-block-content{height:auto;overflow:visible}.sqs-block-image.vsize-26 .sqs-block-content{height:auto;overflow:visible}.sqs-block-image.vsize-27 .sqs-block-content{height:auto;overflow:visible}.sqs-block-image.vsize-28 .sqs-block-content{height:auto;overflow:visible}.sqs-block-image.vsize-29 .sqs-block-content{height:auto;overflow:visible}.sqs-block-image.vsize-30 .sqs-block-content{height:auto;overflow:visible}.sqs-block-horizontalrule hr{border:none;color:#bbb;background-color:#bbb;height:1px}.sqs-block-html{clear:none}.sqs-block-html .sqs-block-content{outline:none}.sqs-block-html .sqs-block-content>*:first-child{margin-top:0}.sqs-block-html .sqs-block-content>*:last-child{margin-bottom:0}.sqs-html{position:relative;z-index:1}.sqs-html .sqs-html-content{outline:none;z-index:2}.sqs-html .sqs-html-hidden{display:none}.sqs-block-markdown{clear:none}.sqs-block-markdown .sqs-block-content{position:relative}.sqs-block-markdown .sqs-block-content *:first-child{margin-top:0}.sqs-block-markdown .sqs-block-content *:last-child{margin-bottom:0}.sqs-block-markdown .sqs-placeholder{color:#999}.sqs-block-markdown .sqs-editing-overlay{display:none}.sqs-block-markdown hr{border:none;border-bottom:1px solid #ccc;width:75%;margin-left:auto;margin-right:auto}.sqs-block-markdown textarea{position:absolute;top:17px;bottom:0;left:17px;right:17px;width:calc( 100% - 34px);padding:0;margin:0;border:none;background:transparent;outline:none;resize:none;overflow:hidden;color:#333}.sqs-block-markdown .textarea-clone{margin:0;min-height:18px;visibility:hidden}.sqs-block-markdown textarea,.sqs-block-markdown .textarea-clone{font:15px/18px 'Courier New',monospace !important;white-space:pre-wrap;word-wrap:break-word}.sqs-block-markdown img{max-width:100%;height:auto}.source-code{white-space:pre;overflow:auto}.cm-keyword{color:#708}.cm-atom{color:#219}.cm-number{color:#164}.cm-def{color:blue}.cm-variable-2{color:#05a}.cm-variable-3{color:#085}.cm-comment{color:#aaa}.cm-string{color:#1a1}.cm-string-2{color:#5f0}.cm-meta{color:#555}.cm-error{color:red}.cm-qualifier{color:#555}.cm-builtin{color:#30a}.cm-bracket{color:#cc7}.cm-tag{color:#170}.cm-attribute{color:#00c}.cm-header{color:#000}.cm-quote{color:#900}.cm-hr{color:#999}.cm-link{color:#00c}.dark .cm-comment{color:#75715e}.dark .cm-atom{color:#ae81ff}.dark .cm-number{color:#ae81ff}.dark .cm-property,.dark .cm-attribute{color:#a6e22e}.dark .cm-keyword{color:#f92672}.dark .cm-string{color:#e6db74}.dark .cm-variable-2{color:#9effff}.dark .cm-def{color:#fd971f}.dark .cm-error{background:#f92672;color:#f8f8f0}.dark .cm-bracket{color:#f8f8f2}.dark .cm-tag{color:#f92672}.dark .cm-link{color:#ae81ff}.code-block .state-message:not(:last-child){margin-bottom:10px}.embed-block .intrinsic,.video-block .intrinsic,.embed-block .sqs-block-content .intrinsic,.video-block .sqs-block-content .intrinsic{position:relative}.embed-block .intrinsic .embed-block-wrapper:not(.embed-block-provider-SoundCloud),.video-block .intrinsic .embed-block-wrapper:not(.embed-block-provider-SoundCloud),.embed-block .sqs-block-content .intrinsic .embed-block-wrapper:not(.embed-block-provider-SoundCloud),.video-block .sqs-block-content .intrinsic .embed-block-wrapper:not(.embed-block-provider-SoundCloud){position:relative}.embed-block .intrinsic .embed-block-wrapper:not(.embed-block-provider-SoundCloud) .sqs-video-wrapper,.video-block .intrinsic .embed-block-wrapper:not(.embed-block-provider-SoundCloud) .sqs-video-wrapper,.embed-block .sqs-block-content .intrinsic .embed-block-wrapper:not(.embed-block-provider-SoundCloud) .sqs-video-wrapper,.video-block .sqs-block-content .intrinsic .embed-block-wrapper:not(.embed-block-provider-SoundCloud) .sqs-video-wrapper{position:absolute;top:0;left:0;width:100%;height:100%}.embed-block .intrinsic .embed-block-wrapper:not(.embed-block-provider-SoundCloud) iframe,.video-block .intrinsic .embed-block-wrapper:not(.embed-block-provider-SoundCloud) iframe,.embed-block .sqs-block-content .intrinsic .embed-block-wrapper:not(.embed-block-provider-SoundCloud) iframe,.video-block .sqs-block-content .intrinsic .embed-block-wrapper:not(.embed-block-provider-SoundCloud) iframe{position:absolute;top:0;left:0;width:100%;height:100%}.embed-block .intrinsic .embed-block-wrapper:not(.embed-block-provider-SoundCloud) .flickr-oembed,.video-block .intrinsic .embed-block-wrapper:not(.embed-block-provider-SoundCloud) .flickr-oembed,.embed-block .sqs-block-content .intrinsic .embed-block-wrapper:not(.embed-block-provider-SoundCloud) .flickr-oembed,.video-block .sqs-block-content .intrinsic .embed-block-wrapper:not(.embed-block-provider-SoundCloud) .flickr-oembed{position:absolute;top:0;left:0;width:100%;height:100%}.embed-block .intrinsic .embed-block-provider-SoundCloud,.video-block .intrinsic .embed-block-provider-SoundCloud,.embed-block .sqs-block-content .intrinsic .embed-block-provider-SoundCloud,.video-block .sqs-block-content .intrinsic .embed-block-provider-SoundCloud{padding-bottom:0 !important}.embed-block .intrinsic .embed-block-provider-SoundCloud iframe,.video-block .intrinsic .embed-block-provider-SoundCloud iframe,.embed-block .sqs-block-content .intrinsic .embed-block-provider-SoundCloud iframe,.video-block .sqs-block-content .intrinsic .embed-block-provider-SoundCloud iframe{width:100%}.sqs-block-soundcloud .sqs-intrinsic iframe{position:absolute;top:0;left:0;width:100% !important;height:100% !important}.sqs-block-audio{min-height:34px}.sqs-block-tourdates .sqs-spin{position:absolute;top:50px;left:50%;margin-left:-15px}.sqs-block-tourdates .tour-list{list-style-type:none;margin:0;padding:0;min-height:150px}.sqs-block-tourdates .loaded .tour-list{min-height:0}.sqs-block-tourdates .tour-item{position:relative;margin:0;padding:17px 0;border-bottom:1px solid rgba(130,130,130,.15);overflow:hidden}.sqs-block-tourdates .tour-item.clone{display:none}.sqs-block-tourdates .tour-item:first-of-type{padding-top:0}.sqs-block-tourdates .tour-item:last-of-type{border:none}.sqs-block-tourdates .loaded .tour-item-no-results:after{content:'There are no upcoming tour dates.'}.sqs-block-tourdates .tour-timeframe,.sqs-block-tourdates .tour-venue,.sqs-block-tourdates .tour-location,.sqs-block-tourdates .tour-actions{float:left;box-sizing:border-box;font-size:16px;line-height:28px}.sqs-block-tourdates .tour-timeframe{width:120px;white-space:nowrap;padding:2px 0 0 0;font-size:13px !important;font-weight:bold;letter-spacing:.5px}.sqs-block-tourdates .tour-date,.sqs-block-tourdates .tour-weekday{box-sizing:border-box;display:inline-block;width:50%;text-transform:uppercase}.sqs-block-tourdates .tour-venue{width:calc(60% -  171px);width:-webkit-calc(60% -  171px);width:-moz-calc(60% -  171px);padding:1px 25.5px 0 0}.sqs-block-tourdates .tour-venue-link,.sqs-block-tourdates .tour-location-link{color:inherit !important;text-decoration:none !important}.sqs-block-tourdates .tour-venue-name,.sqs-block-tourdates .tour-lineup{display:block}.sqs-block-tourdates .tour-lineup{opacity:.6;margin-top:2px;font-size:14px;line-height:18px}.sqs-block-tourdates .tour-lineup:before{content:'w/ '}.sqs-block-tourdates .tour-lineup-item{display:inline}.sqs-block-tourdates .tour-lineup-item:after{content:', '}.sqs-block-tourdates .tour-lineup-item:last-of-type:after{content:none}.sqs-block-tourdates .tour-location{width:calc(40% -  114px);width:-webkit-calc(40% -  114px);width:-moz-calc(40% -  114px);padding:1px 25.5px 0 0}.sqs-block-tourdates .tour-actions{width:165px;white-space:nowrap;text-align:right}.sqs-block-tourdates .tour-button{width:auto;height:auto;padding:1em 2.5em;color:#fff;background-color:#272727;border-width:0;font-family:\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-size:12px;line-height:1em;font-weight:normal;font-style:normal;text-transform:uppercase;letter-spacing:0px;text-align:center;text-decoration:none;cursor:pointer;outline:none;-webkit-appearance:none;-moz-appearance:none;appearance:none;display:inline-block;padding:0 12px !important;font-size:11px !important;line-height:28px}.sqs-block-tourdates .tour-button--ticket:empty{display:none}.sqs-block-tourdates .tour-button--disabled.tour-button,.sqs-block-tourdates .tour-button--soldout.tour-button,.sqs-block-tourdates .tour-button--disabled.tour-button:hover,.sqs-block-tourdates .tour-button--soldout.tour-button:hover{opacity:.3;cursor:default;pointer-events:none}.sqs-block-tourdates .tour-button--rsvp:before{content:'RSVP'}.sqs-block-tourdates .tourblock-compact-mode .tour-item{display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;-webkit-flex-flow:column nowrap;-moz-flex-flow:column nowrap;-ms-flex-flow:column nowrap;flex-flow:column nowrap;-webkit-box-pack:flex-start;-ms-flex-pack:flex-start;-webkit-box-pack:start;-ms-flex-pack:start;-webkit-justify-content:flex-start;-moz-justify-content:flex-start;justify-content:flex-start;width:100%;position:relative;padding:20px 0;flex-direction:column;-webkit-box-orient:vertical;overflow:visible}.sqs-block-tourdates .tourblock-compact-mode .tour-item:first-of-type{padding-top:0}.sqs-block-tourdates .tourblock-compact-mode .tour-timeframe,.sqs-block-tourdates .tourblock-compact-mode .tour-venue,.sqs-block-tourdates .tourblock-compact-mode .tour-location,.sqs-block-tourdates .tourblock-compact-mode .tour-actions{float:none;display:block;width:auto;-webkit-flex-basis:auto;-moz-flex-basis:auto;-ms-flex-preferred-size:auto;flex-basis:auto;padding-top:0 !important}.sqs-block-tourdates .tourblock-compact-mode .tour-timeframe{-webkit-box-ordinal-group:1;-moz-box-ordinal-group:1;-ms-flex-order:1;-webkit-order:1;order:1}.sqs-block-tourdates .tourblock-compact-mode .tour-location{-webkit-box-ordinal-group:2;-moz-box-ordinal-group:2;-ms-flex-order:2;-webkit-order:2;order:2}.sqs-block-tourdates .tourblock-compact-mode .tour-venue{-webkit-box-ordinal-group:3;-moz-box-ordinal-group:3;-ms-flex-order:3;-webkit-order:3;order:3}.sqs-block-tourdates .tourblock-compact-mode .tour-timeframe{margin-bottom:14px;right:0}.sqs-block-tourdates .tourblock-compact-mode .tour-date,.sqs-block-tourdates .tourblock-compact-mode .tour-weekday{width:auto;margin-right:5px}.sqs-block-tourdates .tourblock-compact-mode .tour-lineup{margin-top:0}.sqs-block-tourdates .tourblock-compact-mode .tour-actions{position:absolute;top:18px;right:0}.sqs-block-tourdates .tourblock-compact-mode .tour-item:first-of-type .tour-actions{top:-2px}.sqs-block-tourdates .tourblock-has-small-container .tour-item{display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;-webkit-flex-flow:column nowrap;-moz-flex-flow:column nowrap;-ms-flex-flow:column nowrap;flex-flow:column nowrap;-webkit-box-pack:flex-start;-ms-flex-pack:flex-start;-webkit-box-pack:start;-ms-flex-pack:start;-webkit-justify-content:flex-start;-moz-justify-content:flex-start;justify-content:flex-start;width:100%;position:relative;padding:20px 0;flex-direction:column;-webkit-box-orient:vertical;overflow:visible}.sqs-block-tourdates .tourblock-has-small-container .tour-item:first-of-type{padding-top:0}.sqs-block-tourdates .tourblock-has-small-container .tour-timeframe,.sqs-block-tourdates .tourblock-has-small-container .tour-venue,.sqs-block-tourdates .tourblock-has-small-container .tour-location,.sqs-block-tourdates .tourblock-has-small-container .tour-actions{float:none;display:block;width:auto;-webkit-flex-basis:auto;-moz-flex-basis:auto;-ms-flex-preferred-size:auto;flex-basis:auto;padding-top:0 !important}.sqs-block-tourdates .tourblock-has-small-container .tour-timeframe{-webkit-box-ordinal-group:1;-moz-box-ordinal-group:1;-ms-flex-order:1;-webkit-order:1;order:1}.sqs-block-tourdates .tourblock-has-small-container .tour-location{-webkit-box-ordinal-group:2;-moz-box-ordinal-group:2;-ms-flex-order:2;-webkit-order:2;order:2}.sqs-block-tourdates .tourblock-has-small-container .tour-venue{-webkit-box-ordinal-group:3;-moz-box-ordinal-group:3;-ms-flex-order:3;-webkit-order:3;order:3}.sqs-block-tourdates .tourblock-has-small-container .tour-timeframe{margin-bottom:14px;right:0}.sqs-block-tourdates .tourblock-has-small-container .tour-date,.sqs-block-tourdates .tourblock-has-small-container .tour-weekday{width:auto;margin-right:5px}.sqs-block-tourdates .tourblock-has-small-container .tour-lineup{margin-top:0}.sqs-block-tourdates .tourblock-has-small-container .tour-actions{position:absolute;top:18px;right:0}.sqs-block-tourdates .tourblock-has-small-container .tour-item:first-of-type .tour-actions{top:-2px}@media screen and (max-width:450px){.sqs-block-tourdates .tour-item{display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;-webkit-flex-flow:column nowrap;-moz-flex-flow:column nowrap;-ms-flex-flow:column nowrap;flex-flow:column nowrap;-webkit-box-pack:flex-start;-ms-flex-pack:flex-start;-webkit-box-pack:start;-ms-flex-pack:start;-webkit-justify-content:flex-start;-moz-justify-content:flex-start;justify-content:flex-start;width:100%;position:relative;padding:20px 0;flex-direction:column;-webkit-box-orient:vertical;overflow:visible}.sqs-block-tourdates .tour-item:first-of-type{padding-top:0}.sqs-block-tourdates .tour-timeframe,.sqs-block-tourdates .tour-venue,.sqs-block-tourdates .tour-location,.sqs-block-tourdates .tour-actions{float:none;display:block;width:auto;-webkit-flex-basis:auto;-moz-flex-basis:auto;-ms-flex-preferred-size:auto;flex-basis:auto;padding-top:0 !important}.sqs-block-tourdates .tour-timeframe{-webkit-box-ordinal-group:1;-moz-box-ordinal-group:1;-ms-flex-order:1;-webkit-order:1;order:1}.sqs-block-tourdates .tour-location{-webkit-box-ordinal-group:2;-moz-box-ordinal-group:2;-ms-flex-order:2;-webkit-order:2;order:2}.sqs-block-tourdates .tour-venue{-webkit-box-ordinal-group:3;-moz-box-ordinal-group:3;-ms-flex-order:3;-webkit-order:3;order:3}.sqs-block-tourdates .tour-timeframe{margin-bottom:14px;right:0}.sqs-block-tourdates .tour-date,.sqs-block-tourdates .tour-weekday{width:auto;margin-right:5px}.sqs-block-tourdates .tour-lineup{margin-top:0}.sqs-block-tourdates .tour-actions{position:absolute;top:18px;right:0}.sqs-block-tourdates .tour-item:first-of-type .tour-actions{top:-2px}}.button-style-outline .sqs-block-tourdates .tour-timeframe{padding-top:3px}.button-style-outline .sqs-block-tourdates .tour-venue,.button-style-outline .sqs-block-tourdates .tour-location{padding-top:2px}.button-style-outline .sqs-block-tourdates .tour-button{margin-left:2px}.sqs-search-ui-button-wrapper{position:relative}.sqs-search-ui-button-wrapper.color-dark .search-input{background-image:url(/universal/images-v6/icons/icon-searchqueries-20-dark.png);border:1px solid #aaa}.sqs-search-ui-button-wrapper.color-dark::-webkit-input-placeholder{color:#666}.sqs-search-ui-button-wrapper.color-dark:-moz-placeholder{color:#666}.sqs-search-ui-button-wrapper.color-dark::-moz-placeholder{color:#666}.sqs-search-ui-button-wrapper.color-dark:-ms-input-placeholder{color:#666}.sqs-search-ui-button-wrapper.color-light .search-input{background-image:url(/universal/images-v6/icons/icon-searchqueries-20-light.png);color:#f7f7f7;border:1px solid #eee}.sqs-search-ui-button-wrapper.color-light::-webkit-input-placeholder{color:#ddd}.sqs-search-ui-button-wrapper.color-light:-moz-placeholder{color:#ddd}.sqs-search-ui-button-wrapper.color-light::-moz-placeholder{color:#ddd}.sqs-search-ui-button-wrapper.color-light:-ms-input-placeholder{color:#ddd}.sqs-search-ui-button-wrapper .search-input{opacity:.7;-webkit-transition:opacity .2s ease-out;-moz-transition:opacity .2s ease-out;-ms-transition:opacity .2s ease-out;-o-transition:opacity .2s ease-out;transition:opacity .2s ease-out;-webkit-transition:background-image .2s ease-out;-moz-transition:background-image .2s ease-out;-ms-transition:background-image .2s ease-out;-o-transition:background-image .2s ease-out;transition:background-image .2s ease-out;padding:12px 12px 12px 45px;background:no-repeat 15px 50%;width:100%;min-height:20px;display:block;outline:0;box-sizing:border-box}.sqs-search-ui-button-wrapper .search-input.loading{background-image:none}.sqs-search-ui-button-wrapper .search-input.disabled{cursor:pointer}.sqs-search-ui-button-wrapper .search-input.hover-effect:hover,.sqs-search-ui-button-wrapper .search-input.hover-effect:focus{opacity:1}.sqs-search-ui-button-wrapper .search-input:hover::-webkit-input-placeholder{font-style:normal}.sqs-search-ui-button-wrapper .search-input:hover:-moz-placeholder{font-style:normal}.sqs-search-ui-button-wrapper .search-input:hover::-moz-placeholder{font-style:normal}.sqs-search-ui-button-wrapper .search-input:hover:-ms-input-placeholder{font-style:normal}.sqs-search-ui-button-wrapper .spinner-wrapper{position:absolute;top:50%;-webkit-transform:translatey(-50%);-moz-transform:translatey(-50%);-ms-transform:translatey(-50%);-o-transform:translatey(-50%);transform:translatey(-50%);left:18px}.sqs-search-ui-button-wrapper .spinner-wrapper .sqs-spin{display:block;vertical-align:middle}.sqs-search-preview-ui{position:absolute;z-index:999999;background-color:#fff;width:100%}.sqs-search-preview-ui .sqs-search-ui-result{border-top:none;border:1px solid #ddd}.sqs-search-preview-ui .sqs-search-ui-result .search-result-notice{background-color:#fff;font-weight:200;font-size:12px;padding:6px 12px}.sqs-search-preview-ui .sqs-search-ui-result .search-result-notice.hide{display:none}.sqs-search-preview-ui .sqs-search-ui-result .sqs-search-ui-list{max-height:500px;overflow-x:hidden;overflow-y:scroll}.sqs-search-preview-ui .sqs-search-ui-result .sqs-search-ui-list .search-result{padding:16px;cursor:pointer;border-bottom:1px solid #ddd;-webkit-transition:background-color .2s ease-out;-moz-transition:background-color .2s ease-out;-ms-transition:background-color .2s ease-out;-o-transition:background-color .2s ease-out;transition:background-color .2s ease-out}.sqs-search-preview-ui .sqs-search-ui-result .sqs-search-ui-list .search-result:last-child{border-bottom:none}.sqs-search-preview-ui .sqs-search-ui-result .sqs-search-ui-list .search-result.selected,.sqs-search-preview-ui .sqs-search-ui-result .sqs-search-ui-list .search-result:hover{background-color:#f5f5f5}.sqs-search-preview-ui .sqs-search-ui-result .sqs-search-ui-list .search-result .sqs-search-ui-item{border-top:none}.sqs-search-preview-ui .sqs-search-ui-result .sqs-search-ui-list .search-result .sqs-search-ui-item em{color:#222;font-style:italic}.sqs-search-preview-ui .sqs-search-ui-result .sqs-search-ui-list .search-result .sqs-search-ui-item .sqs-main-image{position:absolute;top:0;left:0;right:0;bottom:0}.sqs-search-preview-ui .sqs-search-ui-result .sqs-search-ui-list .search-result .sqs-search-ui-item .sqs-main-image-container{width:50px;float:right;margin-left:5px;box-shadow:#ddd 1px -1px 5px}.sqs-search-preview-ui .sqs-search-ui-result .sqs-search-ui-list .search-result .sqs-search-ui-item .sqs-main-image-intrinsic{position:relative;width:100%;height:0;padding-bottom:100%}.sqs-search-preview-ui .sqs-search-ui-result .sqs-search-ui-list .search-result .sqs-search-ui-item .sqs-title{font-size:16px;line-height:1.2em;margin-bottom:.5em;color:#333}.sqs-search-preview-ui .sqs-search-ui-result .sqs-search-ui-list .search-result .sqs-search-ui-item .sqs-content{font-size:12px;line-height:1.4em}.sqs-search-preview-ui.no-image .sqs-main-image-container{display:none}.sqs-block-map .sqs-block-map-content{position:relative}.sqs-block-map .sqs-block-map-content .sqs-map-wrapper{position:absolute !important;top:0;left:0;height:100%;max-width:none;width:100%}.rss-block .social-rss:before{position:relative;top:-.05em;margin-right:.4em;font-size:.7em}.twitter-block .tweet-list{list-style-type:none;margin:0 0 2.2em 0;padding:0}.twitter-block .tweet{margin:0 0 2.2em 0}.twitter-block .tweet a{border:0}.twitter-block .tweet .tweet-avatar-wrapper{float:left}.twitter-block .tweet .tweet-avatar{border-radius:2px}.twitter-block .tweet .tweet-text-wrapper{margin-left:60px}.twitter-block .tweet.no-avatar .tweet-text-wrapper{margin-left:0px}.twitter-block .tweet .tweet-from{font-size:1.1em;margin:0 0 .5em 0;line-height:1em;font-weight:bold}.twitter-block .tweet .tweet-timestamp a{font-size:.8em}.form-wrapper .field-list{line-height:normal}.form-wrapper .field-list fieldset,.form-wrapper .field-list legend{margin:0;padding:0;border:0}.form-wrapper .field-list legend{display:none}.form-wrapper .field-list textarea{min-height:100px;resize:vertical}.form-wrapper .field-list textarea.medium{min-height:200px}.form-wrapper .field-list textarea.large{min-height:300px}.form-wrapper .field-list .section{margin:2em 0;padding-bottom:.3em;font-size:.9em;text-transform:uppercase}.form-wrapper .field-list .section.underline{border-bottom:1px solid #999}.form-wrapper .field-list .section:nth-child(1){margin:0 0 2em 0}.form-wrapper .field-list .title{display:block}.form-wrapper .field-list .description{padding:.5em 0 .5em;font-size:12px;-ms-filter:\"progid:DXImageTransform.Microsoft.Alpha(Opacity=70)\";filter:alpha(opacity=70);-moz-opacity:.7;-khtml-opacity:.7;opacity:.7;display:block}.form-wrapper .field-list .field{position:relative;margin:0 0 24px}.form-wrapper .field-list .field .caption{font-size:12px}.form-wrapper .field-list .field .caption .field-element{font-size:14px}.form-wrapper .field-list .field .field-element{width:100%;padding:12px;margin:6px 0 4px;border:1px solid #ccc;background:#fafafa;font-family:sans-serif;font-size:12px;line-height:normal;box-sizing:border-box;border-radius:2px}.form-wrapper .field-list .field .field-element:focus{background:#fff;-webkit-transition:background .1s ease-in;-moz-transition:background .1s ease-in;-ms-transition:background .1s ease-in;-o-transition:background .1s ease-in;transition:background .1s ease-in;outline:none}.form-wrapper .field-list .field select{margin:6px 0 4px;max-width:100%}.form-wrapper .field-list .field .prefix{position:absolute;bottom:16px;left:8px;color:#aaa;font-family:sans-serif;font-size:13px;line-height:16px}.form-wrapper .field-list .field.twitter .field-element{padding-left:22px}.form-wrapper .field-list .field.currency.hassymbol .field-element{padding-left:20px}.form-wrapper .field-list .field.website .field-element{padding-left:45px}.form-wrapper .field-list .field.checkbox label,.form-wrapper .field-list .field.radio label{cursor:pointer}.form-wrapper .field-list .field.checkbox input,.form-wrapper .field-list .field.radio input{margin-right:5px}.form-wrapper .field-list .field .option{margin:6px 0 4px;font-size:13px}.form-wrapper .field-list .field.likert .item{overflow:hidden;margin:1.6em 0 1.6em 0}.form-wrapper .field-list .field.likert .question{margin:0 0 .5em 0;font-size:.9em}.form-wrapper .field-list .field.likert .option{width:20%;float:left;text-align:left;border-top:1px solid #ddd}.form-wrapper .field-list .field.likert .option label{margin:0;padding:0 0 0 1px;font-size:.9em;display:block;cursor:pointer}.form-wrapper .field-list .field.likert .option input{margin:10px 0;display:block}.form-wrapper .field-list .field.likert .option:last-of-type{border-right:none}.form-wrapper .field-list .fields{margin:0 0 0 -2%}.form-wrapper .field-list .fields .title,.form-wrapper .field-list .fields .description,.form-wrapper .field-list .fields .field,.form-wrapper .field-list .fields .field-error{margin-left:2%}.form-wrapper .field-list .fields .field{float:left}.form-wrapper .field-list .fields .field.two-digits{width:3.5em}.form-wrapper .field-list .fields .field.three-digits{width:4.2em}.form-wrapper .field-list .fields .field.four-digits{width:4.8em}.form-wrapper .field-list .fields .field.ampm{width:4.5em}.form-wrapper .field-list .fields.name .field{width:48%}.form-wrapper .field-list .fields.address .field.address1,.form-wrapper .field-list .fields.address .field.address2{width:98%}.form-wrapper .field-list .fields.address .field.city{width:70%}.form-wrapper .field-list .fields.address .field.state-province{width:26%}.form-wrapper .field-list .fields.address .field.zip{width:36%}.form-wrapper .field-list .fields.address .field.country{width:98%}.form-wrapper .field-list .fields.payment .field.card-expiry-month{width:40%}.form-wrapper .field-list .fields.payment .field.card-expiry-year{width:40%}.form-wrapper .field-list .form-item.error,.form-wrapper .field-list .form-item.error .caption,.form-wrapper .field-list .form-item.error .title,.form-wrapper .field-list .form-item.error .description{color:#bd0000}.form-wrapper .field-list .form-item.error input,.form-wrapper .field-list .form-item.error textarea{border:1px solid #e99292}.form-wrapper .form-button-wrapper--align-left{text-align:left}.form-wrapper .form-button-wrapper--align-center{text-align:center}.form-wrapper .form-button-wrapper--align-right{text-align:right}.form-wrapper input[type=submit]{display:inline-block;width:auto;height:auto;padding:1em 2.5em;color:#fff;background-color:#272727;border-width:0;font-family:\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-size:12px;line-height:1em;font-weight:normal;font-style:normal;text-transform:uppercase;letter-spacing:0px;text-align:center;text-decoration:none;cursor:pointer;outline:none;-webkit-appearance:none;-moz-appearance:none;appearance:none;font-size:14px;text-transform:none}.form-wrapper .form-submission-text{margin-top:20px}.form-wrapper .field-error{color:#fff;background:#cc3b3b url('//static.squarespace.com/universal/images-v6/standard/icon_close_7_light.png') no-repeat 9px 50%;padding:5px 15px 3px 25px;font-size:13px;border-radius:2px;margin:12px 0;line-height:23px;display:inline-block}.form-wrapper .field .field-error{margin-bottom:.5em}.form-wrapper .submitting .field-list{opacity:.7}.form-wrapper .hidden,.form-wrapper.hidden{display:none}.form-block .lightbox-handle-wrapper--align-left{text-align:left}.form-block .lightbox-handle-wrapper--align-center{text-align:center}.form-block .lightbox-handle-wrapper--align-right{text-align:right}.form-block .lightbox-handle{display:inline-block;width:auto;height:auto;padding:1em 2.5em;color:#fff;background-color:#272727;border-width:0;font-family:\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-size:12px;line-height:1em;font-weight:normal;font-style:normal;text-transform:uppercase;letter-spacing:0px;text-align:center;text-decoration:none;cursor:pointer;outline:none;-webkit-appearance:none;-moz-appearance:none;appearance:none;font-size:14px;text-transform:none}.sqs-modal-lightbox{width:100%;font-size:14px;text-transform:none;font-style:normal;text-decoration:none}.sqs-modal-lightbox-content{z-index:100000000;position:absolute;height:100%;width:100%;top:0}.sqs-modal-lightbox-content .lightbox-background{position:absolute;top:0;width:100%;height:100%;background:#000;opacity:.4}.sqs-modal-lightbox-content .lightbox-inner{position:absolute;overflow:auto;-webkit-overflow-scrolling:touch;width:100%;height:100%;top:0}.sqs-modal-lightbox-content .lightbox-inner .lightbox-content{max-width:600px;margin:0 auto;position:relative;padding:40px;background:#fff}.sqs-modal-lightbox-content .lightbox-inner .lightbox-content .form-wrapper{color:#222;font-family:inherit}.sqs-modal-lightbox-content .lightbox-inner .lightbox-content .form-wrapper .form-title{font-size:22px;line-height:1.2em;margin-right:22px;color:#333}.sqs-modal-lightbox-content .lightbox-inner .lightbox-content .form-wrapper .form-inner-wrapper form{margin-top:55px}.sqs-modal-lightbox-content .lightbox-inner .lightbox-content .form-wrapper .form-inner-wrapper form .radio .option{margin-left:1px}.sqs-modal-lightbox-content .lightbox-inner .lightbox-content .lightbox-close{position:absolute;color:#333;font-size:22px;width:22px;line-height:22px;top:40px;right:40px;text-align:center;cursor:pointer}@media only screen and (max-width:600px){.sqs-modal-lightbox .lightbox-inner{background:#fff}.sqs-modal-lightbox .lightbox-inner .lightbox-content{margin-top:0 !important}}html.sqs-modal-lightbox-open,html.sqs-modal-lightbox-open body{overflow:hidden}.menu-block .menu-selector{margin-bottom:3em}.menu-block .menu-selector label{display:inline-block;padding:0 .5em;font-size:1.1em}.menu-block .menu-select-button{display:none}.menu-block .menu-header{margin-bottom:3em}.menu-block .menu-section{margin-top:1em}.menu-block .menu-section+.menu-section{margin-top:5em}.menu-block .menu-section-header{margin-bottom:2em;padding-bottom:1em}.menu-block .menu-section-title{font-size:1.5em}.menu-block .menu-section-description{font-size:.85em;line-height:1.4em}.menu-block .menu-item{margin-bottom:2em;margin-top:0;line-height:1.2em}.menu-block .menu-item-title{font-size:1.1em;font-weight:700;line-height:1.2em}.menu-block .menu-item-description{line-height:1.3em;margin-top:5px}.menu-block .menu-item-price-bottom{margin:.5em 0}.menu-block .menu-item-option{font-size:.8em;font-style:italic}.menu-block .menu-style-classic .menu-selector,.menu-block .menu-style-classic .menu-header,.menu-block .menu-style-classic .menu-section-title,.menu-block .menu-style-classic .menu-section-description{text-align:center}.menu-block .menu-style-classic .menu-items{-webkit-column-width:18em;-webkit-column-gap:3em;-moz-column-width:18em;-moz-column-gap:3em;-ms-column-width:18em;-ms-column-gap:3em;-o-column-width:18em;-o-column-gap:3em;column-width:18em;column-gap:3em}.menu-block .menu-style-classic .menu-item{page-break-inside:avoid;-webkit-column-break-inside:avoid;-moz-column-break-inside:avoid;-ms-column-break-inside:avoid;-o-column-break-inside:avoid;break-inside:avoid;width:100%}.menu-block .menu-style-classic .menu-item-description{margin-right:3em}.menu-block .menu-style-classic .menu-item-price-top{float:right;padding-left:20px}.menu-block .menu-style-classic .menu-item-price-bottom{display:none}.menu-block .menu-style-simple .menu-selector,.menu-block .menu-style-simple .menu{text-align:center}.menu-block .menu-style-simple .menu-item-price-top{display:none}.donation-block .sqs-donate-button-wrapper{display:block}.donation-block .sqs-donate-button-wrapper--align-left{text-align:left}.donation-block .sqs-donate-button-wrapper--align-center{text-align:center}.donation-block .sqs-donate-button-wrapper--align-right{text-align:right}.donation-block .sqs-donate-button{display:inline-block;width:auto;height:auto;padding:1em 2.5em;color:#fff;background-color:#272727;border-width:0;font-family:\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-size:12px;line-height:1em;font-weight:normal;font-style:normal;text-transform:uppercase;letter-spacing:0px;text-align:center;text-decoration:none;cursor:pointer;outline:none;-webkit-appearance:none;-moz-appearance:none;appearance:none}.sqs-block-opentable-hidden{display:none !important}.sqs-block-opentable iframe{visibility:hidden;position:absolute}.sqs-block-opentable *{box-sizing:border-box;-webkit-box-sizing:border-box;-moz-box-sizing:border-box}.sqs-block-opentable #OT_form{padding:0;margin:0;width:165px;width:auto}.sqs-block-opentable .OT_wrapper{border:none;border-radius:0;background-color:rgba(0,0,0,.05);font-family:inherit;width:100%;margin:0;padding:34px 17px 40px;background:rgba(0,0,0,.05);color:#272727;font-size:15px;line-height:1em;text-align:center;position:relative}.sqs-block-opentable .OT_header{width:85%;margin:0 auto;position:relative}.sqs-block-opentable .OT_title{position:relative;width:100%;margin:0 0 17px 0;padding:0;font-size:30px;color:#272727;color:rgba(0,0,0,.95);font-weight:normal;text-align:center;line-height:1em}.sqs-block-opentable .OT_subtitle{margin:0;padding:0;font-size:10px;letter-spacing:.15em;color:#272727;color:rgba(0,0,0,.8);text-transform:uppercase;font-weight:normal;white-space:nowrap;width:auto;line-height:1em}.sqs-block-opentable .OT_list{list-style:none;margin:28px 0 0 0;padding:0;width:auto;display:inline-block;line-height:1em}.sqs-block-opentable .OT_day,.sqs-block-opentable .OT_time,.sqs-block-opentable .OT_party{margin:0 12px;padding:6px 0 6px 35px;height:auto;background-image:url('/universal/images-v6/icons/opentable-icons.svg');background-repeat:no-repeat;background-position:0 0;width:33%;min-width:150px;max-width:180px;position:relative;border:none !important;list-style:none;display:inline-block;line-height:1em}.sqs-block-opentable.sqs-block-opentable-hide-fields .OT_day,.sqs-block-opentable.sqs-block-opentable-hide-fields .OT_time,.sqs-block-opentable.sqs-block-opentable-hide-fields .OT_party{display:none}.sqs-block-opentable.sqs-block-opentable-hide-fields .OT_submit{margin:0}.sqs-block-opentable .OT_day{margin:0 12px;padding:6px 0 6px 35px;background-position:-18px -7px;border:none;list-style:none;background-size:123px}.sqs-block-opentable .OT_time{background-position:-18px -55px;border:none}.sqs-block-opentable .OT_party{background-position:-18px -102px;border:none}.sqs-block-opentable .OT_searchTimeField,.sqs-block-opentable .OT_searchDateField,.sqs-block-opentable .OT_searchPartyField{font-family:inherit;background:#fff url('/universal/images-v6/icons/opentable-icons.svg') no-repeat;color:#272727;font-weight:normal;margin:0;border:1px solid rgba(0,0,0,.12);width:100%;height:auto;font-size:13px;font-style:normal;padding:.7em 1.1em;border-radius:0px;cursor:pointer;line-height:normal;outline:none;background-position:right -14px top -75px;background-size:43px;-moz-background-clip:padding;-webkit-background-clip:padding;background-clip:padding-box}.sqs-block-opentable #OT_timeList,.sqs-block-opentable #OT_partyList{max-height:195px;overflow:auto;border:1px solid rgba(0,0,0,.12);position:absolute;width:auto;top:100%;left:35px;right:0;display:none;margin-top:-7px;text-align:left;-moz-background-clip:padding;-webkit-background-clip:padding;background-clip:padding-box}.sqs-block-opentable .OT_navList{list-style:none;padding:0;margin:-6px 0 0 0;float:none;position:absolute;background-color:#fff;z-index:200;width:auto;top:100%;left:35px;right:0}.sqs-block-opentable .OT_navListItem{padding:0;margin:0;position:relative;float:none;line-height:1em;width:auto;list-style:none}.sqs-block-opentable #OT_timeList .OT_navListItem,.sqs-block-opentable #OT_partyList .OT_navListItem{width:auto}.sqs-block-opentable #OT_timeList li a.OT_navLink,.sqs-block-opentable #OT_partyList li a.OT_navLink{border:0;width:auto}.sqs-block-opentable a.OT_navLink:link,.sqs-block-opentable a.OT_navLink:visited,.sqs-block-opentable a.OT_navLink:hover,.sqs-block-opentable a.OT_navLink:active{font-family:inherit;color:#272727;text-decoration:none;font-size:13px;line-height:1em;width:auto;display:block;padding:.7em 1.1em;border:none}.sqs-block-opentable a.OT_navLink:hover,.sqs-block-opentable a.OT_navLink.selected,.sqs-block-opentable a.OT_navLink:active{background-color:rgba(0,0,0,.05);color:#272727;opacity:1}.sqs-block-opentable a.OT_navLink.selected,.sqs-block-opentable a.OT_navLink:active{background-color:rgba(0,0,0,.12)}.sqs-block-opentable .OT_submit{margin:24px 0 0 0;padding:0;width:auto;height:auto;list-style:none;display:block}.sqs-block-opentable .OTButton,.sqs-block-opentable #OTButton{width:auto;text-align:center;margin:0;padding:0}.sqs-block-opentable a.OT_Find_a_Table:link,.sqs-block-opentable a.OT_Find_a_Table:visited,.sqs-block-opentable a.OT_Find_a_Table:hover,.sqs-block-opentable a.OT_Find_a_Table:active{background-image:none;background-repeat:repeat;background-position:0 0;background-color:#272727;background-color:rgba(0,0,0,.95);font-family:inherit;font-size:13px;font-weight:normal;text-decoration:none;color:#fff;text-align:center;height:auto;display:inline-block;padding:1.1em 2.3em;line-height:normal;text-shadow:none;opacity:.8;position:relative;width:auto;border:none;text-transform:uppercase;white-space:nowrap;cursor:pointer;outline:none;-webkit-appearance:none;-moz-appearance:none;-webkit-transition:opacity .3s ease-out,background .3s ease-out;-moz-transition:opacity .3s ease-out,background .3s ease-out;-ms-transition:opacity .3s ease-out,background .3s ease-out;-o-transition:opacity .3s ease-out,background .3s ease-out;transition:opacity .3s ease-out,background .3s ease-out}.opentable-style-light .OT_wrapper{color:#fff}.opentable-style-light .OT_title{color:#fff}.opentable-style-light .OT_subtitle{color:#fff}.opentable-style-light a.OT_Find_a_Table:link,.opentable-style-light a.OT_Find_a_Table:visited,.opentable-style-light a.OT_Find_a_Table:hover,.opentable-style-light a.OT_Find_a_Table:active{background-color:#272727;background-color:rgba(0,0,0,.3);background:rgba(0,0,0,.05)}.opentable-style-light a.OT_Find_a_Table:link:hover,.opentable-style-light a.OT_Find_a_Table:visited:hover,.opentable-style-light a.OT_Find_a_Table:hover:hover,.opentable-style-light a.OT_Find_a_Table:active:hover{background-color:#272727;background-color:rgba(0,0,0,.8);background:rgba(0,0,0,.05)}.opentable-style-light .OT_day{background-position:-80px -7px}.opentable-style-light .OT_time{background-position:-80px -55px}.opentable-style-light .OT_party{background-position:-80px -102px}.hide-opentable-icons .OT_day,.hide-opentable-icons .OT_time,.hide-opentable-icons .OT_party{margin:0;padding:6px;background:none}.hide-opentable-icons #OT_timeList,.hide-opentable-icons #OT_partyList,.hide-opentable-icons .OT_navList{left:6px;right:6px}.no-svg .OT_day,.no-svg .OT_time,.no-svg .OT_party,.no-svg .OT_searchTimeField,.no-svg .OT_searchDateField,.no-svg .OT_searchPartyField{background-image:url('/universal/images-v6/icons/opentable-icons.png')}.sqs-svg-icon--list.social-icon-alignment-left{text-align:left}.sqs-svg-icon--list.social-icon-alignment-right{text-align:right}.sqs-svg-icon--list.social-icon-alignment-center{text-align:center}.rss-block .social-rss:before,.rss-block .social-rss-square:before,.rss-block .social-rss-round:before{font-family:'social-icon-font';speak:none;font-style:normal;font-weight:normal;line-height:1;-webkit-font-smoothing:antialiased;position:relative;top:0;margin-right:10px;font-size:.7em}.rss-block .social-rss:before{content:\"\\e630\"}.social-icons-style-regular .sqs-use--icon{fill:#fff}.social-icons-style-regular .sqs-use--background,.social-icons-style-regular .sqs-use--mask{fill:transparent}.social-icons-style-regular.social-icons-color-white .sqs-use--icon{fill:#fff}.social-icons-style-regular.social-icons-color-white .sqs-use--background,.social-icons-style-regular.social-icons-color-white .sqs-use--mask{fill:transparent}.social-icons-style-regular.social-icons-color-black .sqs-use--icon{fill:#222}.social-icons-style-regular.social-icons-color-black .sqs-use--background,.social-icons-style-regular.social-icons-color-black .sqs-use--mask{fill:transparent}.social-icons-style-border .sqs-svg-icon--wrapper{border-color:#fff}.social-icons-style-border .sqs-use--icon{fill:#fff}.social-icons-style-border .sqs-use--background,.social-icons-style-border .sqs-use--mask{fill:transparent}.social-icons-style-border.social-icons-color-white .sqs-svg-icon--wrapper{border-color:#fff}.social-icons-style-border.social-icons-color-white .sqs-use--icon{fill:#fff}.social-icons-style-border.social-icons-color-white .sqs-use--background,.social-icons-style-border.social-icons-color-white .sqs-use--mask{fill:transparent}.social-icons-style-border.social-icons-color-black .sqs-svg-icon--wrapper{border-color:#222}.social-icons-style-border.social-icons-color-black .sqs-use--icon{fill:#222}.social-icons-style-border.social-icons-color-black .sqs-use--background,.social-icons-style-border.social-icons-color-black .sqs-use--mask{fill:transparent}.social-icons-style-knockout .sqs-use--mask{fill:#fff}.social-icons-style-knockout .sqs-use--background,.social-icons-style-knockout .sqs-use--icon{fill:transparent}.social-icons-style-knockout.social-icons-color-white .sqs-use--mask{fill:#fff}.social-icons-style-knockout.social-icons-color-white .sqs-use--background,.social-icons-style-knockout.social-icons-color-white .sqs-use--icon{fill:transparent}.social-icons-style-knockout.social-icons-color-black .sqs-use--mask{fill:#222}.social-icons-style-knockout.social-icons-color-black .sqs-use--background,.social-icons-style-knockout.social-icons-color-black .sqs-use--icon{fill:transparent}.social-icons-style-solid .sqs-use--icon{fill:#222}.social-icons-style-solid .sqs-use--background{fill:#222}.social-icons-style-solid .sqs-use--mask{fill:#fff}.social-icons-style-solid.social-icons-color-white .sqs-use--icon{fill:#222}.social-icons-style-solid.social-icons-color-white .sqs-use--background{fill:#fff}.social-icons-style-solid.social-icons-color-white .sqs-use--mask{fill:#fff}.social-icons-style-solid.social-icons-color-black .sqs-use--icon{fill:#fff}.social-icons-style-solid.social-icons-color-black .sqs-use--background{fill:#222}.social-icons-style-solid.social-icons-color-black .sqs-use--mask{fill:#222}.social-icons-style-border .sqs-svg-icon--wrapper:hover{background-color:#fff}.social-icons-style-border .sqs-svg-icon--wrapper:hover .sqs-use--icon{fill:#222}.social-icons-style-border.social-icons-color-white .sqs-svg-icon--wrapper:hover{background-color:#fff}.social-icons-style-border.social-icons-color-white .sqs-svg-icon--wrapper:hover .sqs-use--icon{fill:#222}.social-icons-style-border.social-icons-color-black .sqs-svg-icon--wrapper:hover{background-color:#222}.social-icons-style-border.social-icons-color-black .sqs-svg-icon--wrapper:hover .sqs-use--icon{fill:#fff}.social-icons-style-regular:hover .sqs-svg-icon--wrapper .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-style-regular:hover .sqs-svg-icon--wrapper:hover .sqs-use--icon{fill:#fff}.social-icons-style-regular.social-icons-color-white:hover .sqs-svg-icon--wrapper .sqs-use--icon{fill:rgba(255,255,255,.4)}.social-icons-style-regular.social-icons-color-white:hover .sqs-svg-icon--wrapper:hover .sqs-use--icon{fill:#fff}.social-icons-style-regular.social-icons-color-black:hover .sqs-svg-icon--wrapper .sqs-use--icon{fill:rgba(34,34,34,.4)}.social-icons-style-regular.social-icons-color-black:hover .sqs-svg-icon--wrapper:hover .sqs-use--icon{fill:#222}.social-icons-style-knockout:hover .sqs-svg-icon--wrapper .sqs-use--mask{fill:rgba(255,255,255,.4)}.social-icons-style-knockout:hover .sqs-svg-icon--wrapper:hover .sqs-use--mask{fill:#fff}.social-icons-style-knockout.social-icons-color-white:hover .sqs-svg-icon--wrapper .sqs-use--mask{fill:rgba(255,255,255,.4)}.social-icons-style-knockout.social-icons-color-white:hover .sqs-svg-icon--wrapper:hover .sqs-use--mask{fill:#fff}.social-icons-style-knockout.social-icons-color-black:hover .sqs-svg-icon--wrapper .sqs-use--mask{fill:rgba(34,34,34,.4)}.social-icons-style-knockout.social-icons-color-black:hover .sqs-svg-icon--wrapper:hover .sqs-use--mask{fill:#222}.social-icons-style-solid:hover .sqs-svg-icon--wrapper .sqs-use--mask{fill:rgba(255,255,255,.4)}.social-icons-style-solid:hover .sqs-svg-icon--wrapper .sqs-use--icon{fill:rgba(34,34,34,.4)}.social-icons-style-solid:hover .sqs-svg-icon--wrapper .sqs-use--background{fill:rgba(34,34,34,0)}.social-icons-style-solid:hover .sqs-svg-icon--wrapper:hover .sqs-use--mask{fill:#fff}.social-icons-style-solid:hover .sqs-svg-icon--wrapper:hover .sqs-use--icon{fill:rgba(34,34,34,0)}.social-icons-style-solid:hover .sqs-svg-icon--wrapper:hover .sqs-use--background{fill:#222}.social-icons-style-solid.social-icons-color-white:hover .sqs-svg-icon--wrapper .sqs-use--mask,.social-icons-style-solid.social-icons-color-black:hover .sqs-svg-icon--wrapper .sqs-use--mask{fill:rgba(255,255,255,.4)}.social-icons-style-solid.social-icons-color-white:hover .sqs-svg-icon--wrapper .sqs-use--icon,.social-icons-style-solid.social-icons-color-black:hover .sqs-svg-icon--wrapper .sqs-use--icon{fill:rgba(34,34,34,.4)}.social-icons-style-solid.social-icons-color-white:hover .sqs-svg-icon--wrapper .sqs-use--background,.social-icons-style-solid.social-icons-color-black:hover .sqs-svg-icon--wrapper .sqs-use--background{fill:rgba(34,34,34,0)}.social-icons-style-solid.social-icons-color-white:hover .sqs-svg-icon--wrapper:hover .sqs-use--mask,.social-icons-style-solid.social-icons-color-black:hover .sqs-svg-icon--wrapper:hover .sqs-use--mask{fill:#222}.social-icons-style-solid.social-icons-color-white:hover .sqs-svg-icon--wrapper:hover .sqs-use--icon,.social-icons-style-solid.social-icons-color-black:hover .sqs-svg-icon--wrapper:hover .sqs-use--icon{fill:rgba(255,255,255,0)}.social-icons-style-solid.social-icons-color-white:hover .sqs-svg-icon--wrapper:hover .sqs-use--background,.social-icons-style-solid.social-icons-color-black:hover .sqs-svg-icon--wrapper:hover .sqs-use--background{fill:#fff}.sqs-block-socialaccountlinks .social-account-svg-list,.sqs-block-socialaccountlinks-v2 .social-account-svg-list{text-align:left}.sqs-block-socialaccountlinks .social-account-svg-list:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list:before,.sqs-block-socialaccountlinks .social-account-svg-list:after,.sqs-block-socialaccountlinks-v2 .social-account-svg-list:after{content:\"\";display:table}.sqs-block-socialaccountlinks .social-account-svg-list:after,.sqs-block-socialaccountlinks-v2 .social-account-svg-list:after{clear:both}.sqs-block-socialaccountlinks .social-account-svg-list a,.sqs-block-socialaccountlinks-v2 .social-account-svg-list a,.sqs-block-socialaccountlinks .social-account-svg-list a:link,.sqs-block-socialaccountlinks-v2 .social-account-svg-list a:link,.sqs-block-socialaccountlinks .social-account-svg-list a:visited,.sqs-block-socialaccountlinks-v2 .social-account-svg-list a:visited{display:inline-block;width:20px;height:20px;font-size:20px;color:#111;text-decoration:none !important;*zoom:1;*display:inline;font-weight:normal}.sqs-block-socialaccountlinks .social-account-svg-list a:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list a:before,.sqs-block-socialaccountlinks .social-account-svg-list a:link:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list a:link:before,.sqs-block-socialaccountlinks .social-account-svg-list a:visited:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list a:visited:before{font-size:20px;line-height:20px}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-alignment-left a,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-alignment-left a{margin-right:.75em}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-alignment-right a,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-alignment-right a{margin-left:.75em}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-alignment-center a,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-alignment-center a{margin:0 .375em}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-alignment-center,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-alignment-center{text-align:center}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-alignment-right,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-alignment-right{text-align:right}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-white a,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-white a,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-white a:visited,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-white a:visited{color:#fff}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-bandsintown,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-bandsintown,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-bandsintown,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-bandsintown,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-bandsintown,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-bandsintown{color:#00b4b3}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-behance,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-behance,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-behance,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-behance,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-behance,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-behance{color:#1769ff}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-codepen,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-codepen,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-codepen,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-codepen,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-codepen,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-codepen{color:#222}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-dribbble,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-dribbble,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-dribbble,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-dribbble,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-dribbble,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-dribbble{color:#ea4c89}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-dropbox,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-dropbox,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-dropbox,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-dropbox,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-dropbox,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-dropbox{color:#007ee5}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-email,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-email,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-email,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-email,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-email,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-email{color:#222}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-facebook,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-facebook,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-facebook,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-facebook,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-facebook,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-facebook{color:#3b5998}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-fivehundredpix,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-fivehundredpix,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-fivehundredpix,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-fivehundredpix,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-fivehundredpix,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-fivehundredpix,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-fivehundredpx,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-fivehundredpx,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-fivehundredpx,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-fivehundredpx,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-fivehundredpx,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-fivehundredpx{color:#00aeef}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-flickr,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-flickr,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-flickr,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-flickr,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-flickr,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-flickr{color:#0063dc}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-foursquare,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-foursquare,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-foursquare,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-foursquare,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-foursquare,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-foursquare{color:#f94877}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-github,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-github,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-github,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-github,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-github,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-github{color:#4183c4}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-google,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-google,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-google,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-google,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-google,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-google{color:#dd4b39}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-googleplay,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-googleplay,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-googleplay,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-googleplay,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-googleplay,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-googleplay{color:#5adfcb}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-instagram,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-instagram,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-instagram,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-instagram,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-instagram,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-instagram{color:#3f729b}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-itunes,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-itunes,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-itunes,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-itunes,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-itunes,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-itunes{color:#ff3241}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-linkedin,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-linkedin,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-linkedin,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-linkedin,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-linkedin,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-linkedin{color:#0976b4}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-medium,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-medium,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-medium,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-medium,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-medium,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-medium{color:#222}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-meetup,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-meetup,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-meetup,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-meetup,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-meetup,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-meetup{color:#e0393e}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-pinterest,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-pinterest,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-pinterest,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-pinterest,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-pinterest,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-pinterest{color:#cc2127}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-rdio,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-rdio,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-rdio,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-rdio,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-rdio,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-rdio{color:#006ed2}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-rss,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-rss,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-rss,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-rss,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-rss,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-rss{color:#222}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-smugmug,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-smugmug,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-smugmug,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-smugmug,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-smugmug,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-smugmug{color:#7dbb00}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-soundcloud,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-soundcloud,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-soundcloud,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-soundcloud,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-soundcloud,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-soundcloud{color:#f80}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-spotify,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-spotify,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-spotify,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-spotify,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-spotify,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-spotify{color:#84bd00}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-squarespace,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-squarespace,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-squarespace,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-squarespace,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-squarespace,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-squarespace{color:#222}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-tumblr,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-tumblr,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-tumblr,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-tumblr,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-tumblr,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-tumblr{color:#35465d}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-twitter,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-twitter,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-twitter,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-twitter,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-twitter,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-twitter{color:#55acee}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-vimeo,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-vimeo,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-vimeo,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-vimeo,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-vimeo,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-vimeo{color:#1ab7ea}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-vine,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-vine,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-vine,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-vine,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-vine,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-vine{color:#00b488}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-yelp,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-yelp,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-yelp,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-yelp,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-yelp,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-yelp{color:#c41200}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-youtube,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-youtube,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-youtube,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-youtube,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-youtube,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-youtube{color:#e52d27}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-meetup,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-meetup,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-meetup,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-meetup,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-meetup,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-meetup{color:#e0393e}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-vevo,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-vevo,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-vevo,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-vevo,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-vevo,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-vevo{color:#ff0031}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-twitch,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-twitch,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-twitch,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-twitch,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-twitch,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-twitch{color:#6441a5}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a.social-vsco,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a.social-vsco,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:link.social-vsco,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:link.social-vsco,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-color-standard a:visited.social-vsco,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-color-standard a:visited.social-vsco{color:#a9a849}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-large a,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-large a,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-large a:link,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-large a:link,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-large a:visited,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-large a:visited{width:24px;height:24px;font-size:24px}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-large a:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-large a:before,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-large a:link:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-large a:link:before,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-large a:visited:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-large a:visited:before{font-size:24px;line-height:24px}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-small a,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-small a,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-small a:link,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-small a:link,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-small a:visited,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-small a:visited{width:16px;height:16px;font-size:16px}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-small a:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-small a:before,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-small a:link:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-small a:link:before,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-small a:visited:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-small a:visited:before{font-size:16px;line-height:16px}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-style-round a,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-style-round a,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-style-square a,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-style-square a,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-style-round a:link,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-style-round a:link,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-style-square a:link,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-style-square a:link,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-style-round a:visited,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-style-round a:visited,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-style-square a:visited,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-style-square a:visited{width:30px;height:30px;font-size:30px}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-style-round a:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-style-round a:before,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-style-square a:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-style-square a:before,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-style-round a:link:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-style-round a:link:before,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-style-square a:link:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-style-square a:link:before,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-style-round a:visited:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-style-round a:visited:before,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-style-square a:visited:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-style-square a:visited:before{font-size:30px;line-height:30px}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-style-round.social-icon-alignment-left a,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-style-round.social-icon-alignment-left a,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-style-square.social-icon-alignment-left a,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-style-square.social-icon-alignment-left a{margin-right:.25em}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-style-round.social-icon-alignment-right a,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-style-round.social-icon-alignment-right a,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-style-square.social-icon-alignment-right a,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-style-square.social-icon-alignment-right a{margin-left:.25em}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-style-round.social-icon-alignment-center a,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-style-round.social-icon-alignment-center a,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-style-square.social-icon-alignment-center a,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-style-square.social-icon-alignment-center a{margin:0 .125em}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-large.social-icon-style-round a,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-large.social-icon-style-round a,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-large.social-icon-style-square a,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-large.social-icon-style-square a,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-large.social-icon-style-round a:link,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-large.social-icon-style-round a:link,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-large.social-icon-style-square a:link,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-large.social-icon-style-square a:link,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-large.social-icon-style-round a:visited,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-large.social-icon-style-round a:visited,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-large.social-icon-style-square a:visited,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-large.social-icon-style-square a:visited{width:36px;height:36px;font-size:36px}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-large.social-icon-style-round a:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-large.social-icon-style-round a:before,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-large.social-icon-style-square a:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-large.social-icon-style-square a:before,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-large.social-icon-style-round a:link:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-large.social-icon-style-round a:link:before,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-large.social-icon-style-square a:link:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-large.social-icon-style-square a:link:before,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-large.social-icon-style-round a:visited:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-large.social-icon-style-round a:visited:before,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-large.social-icon-style-square a:visited:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-large.social-icon-style-square a:visited:before{font-size:36px;line-height:36px}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-small.social-icon-style-round a,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-small.social-icon-style-round a,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-small.social-icon-style-square a,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-small.social-icon-style-square a,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-small.social-icon-style-round a:link,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-small.social-icon-style-round a:link,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-small.social-icon-style-square a:link,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-small.social-icon-style-square a:link,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-small.social-icon-style-round a:visited,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-small.social-icon-style-round a:visited,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-small.social-icon-style-square a:visited,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-small.social-icon-style-square a:visited{width:24px;height:24px;font-size:24px}.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-small.social-icon-style-round a:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-small.social-icon-style-round a:before,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-small.social-icon-style-square a:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-small.social-icon-style-square a:before,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-small.social-icon-style-round a:link:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-small.social-icon-style-round a:link:before,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-small.social-icon-style-square a:link:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-small.social-icon-style-square a:link:before,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-small.social-icon-style-round a:visited:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-small.social-icon-style-round a:visited:before,.sqs-block-socialaccountlinks .social-account-svg-list.social-icon-size-small.social-icon-style-square a:visited:before,.sqs-block-socialaccountlinks-v2 .social-account-svg-list.social-icon-size-small.social-icon-style-square a:visited:before{font-size:24px;line-height:24px}.newsletter-block *{box-sizing:border-box;-webkit-box-sizing:border-box;-moz-box-sizing:border-box}.newsletter-block .newsletter-form-wrapper{width:100%;padding:34px 17px;background:rgba(0,199,166,0);color:#272727;font-size:15px}.newsletter-block .newsletter-form-wrapper.hidden,.newsletter-block .newsletter-form-wrapper .hidden{display:none}.newsletter-block .newsletter-form{text-align:center;overflow:hidden}.newsletter-block .newsletter-form-header{width:85%;margin:0 auto}.newsletter-block .newsletter-form-header-title{margin:0 0 17px 0;padding:0;color:#272727;font-size:30px;line-height:1.2em;text-align:center}.newsletter-block .newsletter-form-header-title a{color:#272727 !important;text-decoration:underline}.newsletter-block .newsletter-form-header-description p{margin:17px 0;padding:0;color:#272727;font-size:15px;line-height:1.6em;text-align:center}.newsletter-block .newsletter-form-header-description a{color:#272727 !important;text-decoration:underline}.newsletter-block .newsletter-form-body{padding:0 0 12px 0}.newsletter-block .newsletter-form-fields-wrapper{display:inline-block;width:auto;margin:12px 0 0 0}.newsletter-block .newsletter-form-name-fieldset{display:inline-block;width:auto;margin:0;padding:0;border:none}.newsletter-block .newsletter-form-field-wrapper{display:inline-block;width:auto;padding:6px 3px}.newsletter-block .newsletter-form-field-label{display:none}.newsletter-block .newsletter-form-field-element{width:100%;padding:1em;background:#fff;border:1px solid rgba(0,0,0,.12);font-family:inherit;font-size:15px;line-height:normal;outline:none;-moz-background-clip:padding;-webkit-background-clip:padding;background-clip:padding-box;-webkit-transition:background .3s ease-out,border .3s ease-out;-moz-transition:background .3s ease-out,border .3s ease-out;-ms-transition:background .3s ease-out,border .3s ease-out;-o-transition:background .3s ease-out,border .3s ease-out;transition:background .3s ease-out,border .3s ease-out}.newsletter-block .newsletter-form-field-element:focus{background:#fff;-moz-background-clip:padding;-webkit-background-clip:padding;background-clip:padding-box}.newsletter-block .newsletter-form-field-element::-webkit-input-placeholder{color:rgba(0,0,0,.3)}.newsletter-block .newsletter-form-field-element:-moz-placeholder{color:rgba(0,0,0,.3)}.newsletter-block .newsletter-form-field-element::-moz-placeholder{color:rgba(0,0,0,.3)}.newsletter-block .newsletter-form-field-element:-ms-input-placeholder{color:rgba(0,0,0,.3)}.newsletter-block .field-error{display:none}.newsletter-block .newsletter-form-field-wrapper .field-error{display:block;margin-bottom:12px;padding:6px;background:#fed9db;color:#f23d3d;font-size:12px;line-height:normal}.newsletter-block .newsletter-form-button-wrapper{display:inline-block;width:auto;margin:12px 0 0 0;padding:6px 3px}.newsletter-block .newsletter-form-button{position:relative;width:auto;padding:1em 2.25em;color:#fff;background-color:#23c890;border:1px solid #23c890 !important;font-family:inherit;font-size:15px;line-height:normal;font-weight:normal;text-align:center;text-transform:uppercase;white-space:nowrap;cursor:pointer;outline:none;-webkit-appearance:none;-moz-appearance:none}.newsletter-block .newsletter-form-spinner.sqs-spin.light.large{visibility:hidden;position:absolute;top:50%;left:50%;height:22px;width:22px;margin-top:-11px;margin-left:-11px}.newsletter-block .newsletter-form:not(.submitting) .newsletter-form-spinner.sqs-spin.light.large{-webkit-animation:none;-moz-animation:none;-ms-animation:none;-o-animation:none;animation:none}.newsletter-block .newsletter-form.submitting .newsletter-form-spinner.sqs-spin.light.large{visibility:visible}.newsletter-block .newsletter-form.submitting .newsletter-form-button-label{visibility:hidden}.newsletter-block .newsletter-form-footnote p{opacity:.8;margin:17px 0;padding:0;color:#272727;font-size:12px !important;line-height:normal}.newsletter-block .newsletter-form-footnote p:last-child{margin-bottom:0}.newsletter-block .newsletter-form-footnote a{color:#272727 !important;text-decoration:underline}.newsletter-block .form-submission-text p{margin:17px 0;padding:0;color:#272727;font-size:15px;line-height:1.6em}.newsletter-block .form-submission-text p:first-child{margin-top:0}.newsletter-block .form-submission-text p:last-child{margin-bottom:0}.newsletter-block .form-submission-text a{color:#272727 !important;text-decoration:underline}.newsletter-style-light .newsletter-block .newsletter-form-wrapper,.newsletter-style-light .newsletter-block .newsletter-form-header-title,.newsletter-style-light .newsletter-block .newsletter-form-header-description p,.newsletter-style-light .newsletter-block .newsletter-form-footnote p,.newsletter-style-light .newsletter-block .form-submission-text p{color:#fff}.newsletter-style-light .newsletter-block .newsletter-form-header-title a,.newsletter-style-light .newsletter-block .newsletter-form-header-description a,.newsletter-style-light .newsletter-block .newsletter-form-footnote a,.newsletter-style-light .newsletter-block .form-submission-text a{color:#fff !important}.newsletter-form-small-mode .newsletter-form-wrapper{padding:22px 17px}.newsletter-form-small-mode .newsletter-form-header{width:100%}.newsletter-form-small-mode .newsletter-form-header-title{font-size:22.5px !important;margin:0 0 14px 0}.newsletter-form-small-mode .newsletter-form-header-description p{margin:0 0 14px 0;line-height:normal}.newsletter-form-small-mode .newsletter-form-body{padding:0 0 6px 0}.newsletter-form-small-mode .newsletter-form-fields-wrapper{display:block}.newsletter-form-small-mode .newsletter-form-name-fieldset{width:100%}.newsletter-form-small-mode .newsletter-form-field-wrapper,.newsletter-form-small-mode .newsletter-form-button-wrapper{display:block;width:100%;min-width:0;padding:5px 0}.newsletter-form-small-mode .newsletter-form-button-wrapper{margin:6px 0 0 0}.newsletter-form-small-mode .newsletter-form-footnote p{margin:14px 0}.newsletter-form-small-mode .newsletter-form-footnote p:last-child{margin-bottom:0}.newsletter-form-small-mode .form-submission-text p{margin:14px 0;line-height:normal}.newsletter-form-small-mode .form-submission-text p:first-child{margin-top:0}.newsletter-form-small-mode .form-submission-text p:last-child{margin-bottom:0}@media screen and (max-width:320px){.newsletter-block .newsletter-form-wrapper{padding:22px 17px}.newsletter-block .newsletter-form-header{width:100%}.newsletter-block .newsletter-form-header-title{font-size:22.5px !important;margin:0 0 14px 0}.newsletter-block .newsletter-form-header-description p{margin:0 0 14px 0;line-height:normal}.newsletter-block .newsletter-form-body{padding:0 0 6px 0}.newsletter-block .newsletter-form-fields-wrapper{display:block}.newsletter-block .newsletter-form-name-fieldset{width:100%}.newsletter-block .newsletter-form-field-wrapper,.newsletter-block .newsletter-form-button-wrapper{display:block;width:100%;min-width:0;padding:5px 0}.newsletter-block .newsletter-form-button-wrapper{margin:6px 0 0 0}.newsletter-block .newsletter-form-footnote p{margin:14px 0}.newsletter-block .newsletter-form-footnote p:last-child{margin-bottom:0}.newsletter-block .form-submission-text p{margin:14px 0;line-height:normal}.newsletter-block .form-submission-text p:first-child{margin-top:0}.newsletter-block .form-submission-text p:last-child{margin-bottom:0}}.newsletter-block.newsletter-form-has-small-container .newsletter-form-wrapper{padding:22px 17px}.newsletter-block.newsletter-form-has-small-container .newsletter-form-header{width:100%}.newsletter-block.newsletter-form-has-small-container .newsletter-form-header-title{font-size:22.5px !important;margin:0 0 14px 0}.newsletter-block.newsletter-form-has-small-container .newsletter-form-header-description p{margin:0 0 14px 0;line-height:normal}.newsletter-block.newsletter-form-has-small-container .newsletter-form-body{padding:0 0 6px 0}.newsletter-block.newsletter-form-has-small-container .newsletter-form-fields-wrapper{display:block}.newsletter-block.newsletter-form-has-small-container .newsletter-form-name-fieldset{width:100%}.newsletter-block.newsletter-form-has-small-container .newsletter-form-field-wrapper,.newsletter-block.newsletter-form-has-small-container .newsletter-form-button-wrapper{display:block;width:100%;min-width:0;padding:5px 0}.newsletter-block.newsletter-form-has-small-container .newsletter-form-button-wrapper{margin:6px 0 0 0}.newsletter-block.newsletter-form-has-small-container .newsletter-form-footnote p{margin:14px 0}.newsletter-block.newsletter-form-has-small-container .newsletter-form-footnote p:last-child{margin-bottom:0}.newsletter-block.newsletter-form-has-small-container .form-submission-text p{margin:14px 0;line-height:normal}.newsletter-block.newsletter-form-has-small-container .form-submission-text p:first-child{margin-top:0}.newsletter-block.newsletter-form-has-small-container .form-submission-text p:last-child{margin-bottom:0}.newsletter-block.newsletter-form-has-regular-container .newsletter-form-field-wrapper{min-width:250px}.small-button-block-font{font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:600;font-style:normal;text-transform:uppercase;letter-spacing:1px}.medium-button-block-font{font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:600;font-style:normal;text-transform:uppercase;letter-spacing:1px}.large-button-block-font{font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:600;font-style:normal;text-transform:uppercase;letter-spacing:1px}.sqs-block-button .sqs-block-button-container--left{text-align:left}.sqs-block-button .sqs-block-button-container--center{text-align:center}.sqs-block-button .sqs-block-button-container--right{text-align:right}.sqs-block-button .sqs-block-button-element{display:inline-block;width:auto;height:auto;padding:1em 2.5em;color:#fff;background-color:#272727;border-width:0;font-family:\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-size:12px;line-height:1em;font-weight:normal;font-style:normal;text-transform:uppercase;letter-spacing:0px;text-align:center;text-decoration:none;cursor:pointer;outline:none;-webkit-appearance:none;-moz-appearance:none;appearance:none;line-height:normal}.sqs-block-button .sqs-block-button-element:hover{opacity:1}.sqs-block-button .sqs-block-button-element--small{padding:13px 26px;color:#23c890;background-color:#29292d;border-color:#29292d;font-size:12px;font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-family:\"proxima-nova\";text-transform:uppercase;letter-spacing:1px;font-weight:600;font-style:normal}.sqs-block-button .sqs-block-button-element--medium{padding:21px 34px;color:#fff;background-color:#23c890;border-color:#23c890;font-size:15px;font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;text-transform:uppercase;letter-spacing:1px;font-weight:600;font-style:normal}.sqs-block-button .sqs-block-button-element--large{padding:25px 46px;color:#fff;background-color:#23c890;border-color:#23c890;font-size:20px;font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-family:\"proxima-nova\";text-transform:uppercase;letter-spacing:1px;font-weight:600;font-style:normal}.small-button-style-solid .sqs-block-button .sqs-block-button-element--small,.medium-button-style-solid .sqs-block-button .sqs-block-button-element--medium,.large-button-style-solid .sqs-block-button .sqs-block-button-element--large{-webkit-transition:.1s opacity linear;-moz-transition:.1s opacity linear;-o-transition:.1s opacity linear;transition:.1s opacity linear;-webkit-backface-visibility:hidden}.small-button-style-solid .sqs-block-button .sqs-block-button-element--small:hover,.medium-button-style-solid .sqs-block-button .sqs-block-button-element--medium:hover,.large-button-style-solid .sqs-block-button .sqs-block-button-element--large:hover{opacity:.8}.small-button-style-outline .sqs-block-button .sqs-block-button-element--small,.medium-button-style-outline .sqs-block-button .sqs-block-button-element--medium,.large-button-style-outline .sqs-block-button .sqs-block-button-element--large{border-width:2px;border-style:solid;background-color:transparent;-webkit-transition:0.1s background-color linear, 0.1s color linear;-moz-transition:0.1s background-color linear, 0.1s color linear;-o-transition:0.1s background-color linear, 0.1s color linear;transition:0.1s background-color linear, 0.1s color linear}.small-button-style-outline .sqs-block-button .sqs-block-button-element--small:hover,.medium-button-style-outline .sqs-block-button .sqs-block-button-element--medium:hover,.large-button-style-outline .sqs-block-button .sqs-block-button-element--large:hover{color:#fff}.small-button-style-outline .sqs-block-button .sqs-block-button-element--small{color:#29292d}.small-button-style-outline .sqs-block-button .sqs-block-button-element--small:hover{background-color:#29292d;color:#fff}.medium-button-style-outline .sqs-block-button .sqs-block-button-element--medium{color:#23c890}.medium-button-style-outline .sqs-block-button .sqs-block-button-element--medium:hover{background-color:#23c890;color:#1d1d1d;color:#fff}.large-button-style-outline .sqs-block-button .sqs-block-button-element--large{color:#23c890}.large-button-style-outline .sqs-block-button .sqs-block-button-element--large:hover{background-color:#23c890;color:#1d1d1d;color:#fff}.small-button-style-raised .sqs-block-button .sqs-block-button-element--small,.medium-button-style-raised .sqs-block-button .sqs-block-button-element--medium,.large-button-style-raised .sqs-block-button .sqs-block-button-element--large{position:relative;-webkit-transition:.1s background-color linear;-moz-transition:.1s background-color linear;-o-transition:.1s background-color linear;transition:.1s background-color linear}.small-button-style-raised .sqs-block-button .sqs-block-button-element--small:active,.medium-button-style-raised .sqs-block-button .sqs-block-button-element--medium:active,.large-button-style-raised .sqs-block-button .sqs-block-button-element--large:active{top:1px}.small-button-style-raised .sqs-block-button .sqs-block-button-element--small{-webkit-box-shadow:0 2px 0 0 #161618;-moz-box-shadow:0 2px 0 0 #161618;box-shadow:0 2px 0 0 #161618}.small-button-style-raised .sqs-block-button .sqs-block-button-element--small:hover{background-color:#303035}.small-button-style-raised .sqs-block-button .sqs-block-button-element--small:active{-webkit-box-shadow:0 1px 0 0 #161618;-moz-box-shadow:0 1px 0 0 #161618;box-shadow:0 1px 0 0 #161618}.medium-button-style-raised .sqs-block-button .sqs-block-button-element--medium{-webkit-box-shadow:0 2px 0 0 #1da577;-moz-box-shadow:0 2px 0 0 #1da577;box-shadow:0 2px 0 0 #1da577}.medium-button-style-raised .sqs-block-button .sqs-block-button-element--medium:hover{background-color:#25d599}.medium-button-style-raised .sqs-block-button .sqs-block-button-element--medium:active{-webkit-box-shadow:0 1px 0 0 #1da577;-moz-box-shadow:0 1px 0 0 #1da577;box-shadow:0 1px 0 0 #1da577}.large-button-style-raised .sqs-block-button .sqs-block-button-element--large{-webkit-box-shadow:0 3px 0 0 #1da577;-moz-box-shadow:0 3px 0 0 #1da577;box-shadow:0 3px 0 0 #1da577}.large-button-style-raised .sqs-block-button .sqs-block-button-element--large:hover{background-color:#25d599}.large-button-style-raised .sqs-block-button .sqs-block-button-element--large:active{top:2px;-webkit-box-shadow:0 1px 0 0 #1da577;-moz-box-shadow:0 1px 0 0 #1da577;box-shadow:0 1px 0 0 #1da577}.small-button-shape-rounded .sqs-block-button .sqs-block-button-element--small,.medium-button-shape-rounded .sqs-block-button .sqs-block-button-element--medium,.large-button-shape-rounded .sqs-block-button .sqs-block-button-element--large{border-radius:3px}.small-button-shape-pill .sqs-block-button .sqs-block-button-element--small,.medium-button-shape-pill .sqs-block-button .sqs-block-button-element--medium,.large-button-shape-pill .sqs-block-button .sqs-block-button-element--large{border-radius:300px}@media screen and (max-width:640px){.sqs-block-button .sqs-block-button-element--large{padding:21px 34px;font-size:15px}}.sqs-block-summary-v2 *{box-sizing:border-box;-webkit-box-sizing:border-box;-moz-box-sizing:border-box}.sqs-block-summary-v2 .summary-heading{display:none;margin:0 0 15px 0;padding-right:10px;font-size:14px;line-height:normal;text-transform:uppercase}.sqs-block-summary-v2 .summary-carousel-pager{display:none}.sqs-block-summary-v2 .summary-item-list{list-style-type:none;margin:0;padding:0}.sqs-block-summary-v2 .summary-item{visibility:hidden;overflow:hidden}.sqs-block-summary-v2 .summary-item.positioned{visibility:visible}.sqs-block-summary-v2 .summary-thumbnail-container{display:block}.sqs-block-summary-v2 .summary-thumbnail-container:hover{opacity:1 !important}.sqs-block-summary-v2 .img-wrapper,.sqs-block-summary-v2 .sqs-video-wrapper{position:relative;width:100%;height:auto}.sqs-block-summary-v2 .img-wrapper img,.sqs-block-summary-v2 .sqs-video-wrapper img{opacity:0;display:block;width:100%;height:auto;font-size:13px;line-height:normal;-webkit-transition:.6s opacity;-moz-transition:.6s opacity;-ms-transition:.6s opacity;-o-transition:.6s opacity;transition:.6s opacity}.sqs-block-summary-v2 .img-wrapper img.loaded,.sqs-block-summary-v2 .sqs-video-wrapper img.loaded{opacity:1}.sqs-block-summary-v2 .summary-product-status .product-mark{position:absolute;top:15px;right:0;padding:6px 8px;background:#222;color:#fff;font-size:14px;line-height:14px;text-transform:uppercase;-webkit-font-smoothing:antialiased;box-sizing:content-box;-webkit-box-sizing:content-box;-moz-box-sizing:content-box}.sqs-block-summary-v2 .summary-thumbnail-event-date{display:none;position:absolute;top:10px;right:10px;height:50px;width:50px;padding:3px;background:#fff;text-align:center;box-sizing:content-box;-webkit-box-sizing:content-box;-moz-box-sizing:content-box}.sqs-block-summary-v2 .summary-thumbnail-event-date-inner{display:table-cell;vertical-align:middle}.sqs-block-summary-v2 .summary-thumbnail-event-date-month{display:block;color:#333;font-size:14px;line-height:14px;text-transform:uppercase}.sqs-block-summary-v2 .summary-thumbnail-event-date-day{display:block;color:#333;font-size:26px;line-height:26px}.sqs-block-summary-v2 .summary-content{text-align:left}.sqs-block-summary-v2 .summary-title{margin:0 0 10px 0;font-size:20px;line-height:1.2em;text-align:left}.sqs-block-summary-v2 .summary-price{margin:0 0 10px 0}.sqs-block-summary-v2 .summary-price .product-price{font-size:14px;line-height:20px;text-align:left}.sqs-block-summary-v2 .summary-price .product-price .original-price{opacity:.7;filter:alpha(opacity=70);text-decoration:line-through}.sqs-block-summary-v2 .summary-excerpt{margin:0 0 10px 0}.sqs-block-summary-v2 .summary-excerpt p,.sqs-block-summary-v2 .summary-excerpt ul,.sqs-block-summary-v2 .summary-excerpt li{font-size:14px;line-height:1.4em;margin:0 0 10px 0;text-align:left}.sqs-block-summary-v2 .summary-excerpt p:first-of-type,.sqs-block-summary-v2 .summary-excerpt ul:first-of-type,.sqs-block-summary-v2 .summary-excerpt li:first-of-type{margin-top:0 !important}.sqs-block-summary-v2 .summary-excerpt p:last-of-type,.sqs-block-summary-v2 .summary-excerpt ul:last-of-type,.sqs-block-summary-v2 .summary-excerpt li:last-of-type{margin-bottom:0 !important}.sqs-block-summary-v2 .summary-read-more-link{display:none;margin:0 0 10px 0;font-size:14px;line-height:20px;text-align:left}.sqs-block-summary-v2 .summary-read-more-link:after{content:'Read More \\2192'}.sqs-block-summary-v2 .summary-metadata-container{display:none;font-size:13px;line-height:normal}.sqs-block-summary-v2 .summary-block-setting-metadata-position-above-title .summary-metadata-container--above-title,.sqs-block-summary-v2 .summary-block-setting-metadata-position-below-title .summary-metadata-container--below-title,.sqs-block-summary-v2 .summary-block-setting-metadata-position-below-content .summary-metadata-container--below-content{display:block}.sqs-block-summary-v2 .summary-block-setting-metadata-position-above-title.summary-block-setting-primary-metadata-date .summary-metadata-container,.sqs-block-summary-v2 .summary-block-setting-metadata-position-below-title.summary-block-setting-primary-metadata-date .summary-metadata-container,.sqs-block-summary-v2 .summary-block-setting-metadata-position-above-title.summary-block-setting-secondary-metadata-date .summary-metadata-container,.sqs-block-summary-v2 .summary-block-setting-metadata-position-below-title.summary-block-setting-secondary-metadata-date .summary-metadata-container,.sqs-block-summary-v2 .summary-block-setting-metadata-position-above-title.summary-block-setting-primary-metadata-event-time .summary-metadata-container,.sqs-block-summary-v2 .summary-block-setting-metadata-position-below-title.summary-block-setting-primary-metadata-event-time .summary-metadata-container,.sqs-block-summary-v2 .summary-block-setting-metadata-position-above-title.summary-block-setting-secondary-metadata-event-time .summary-metadata-container,.sqs-block-summary-v2 .summary-block-setting-metadata-position-below-title.summary-block-setting-secondary-metadata-event-time .summary-metadata-container,.sqs-block-summary-v2 .summary-block-setting-metadata-position-above-title.summary-block-setting-primary-metadata-cats .summary-item-has-cats .summary-metadata-container,.sqs-block-summary-v2 .summary-block-setting-metadata-position-below-title.summary-block-setting-primary-metadata-cats .summary-item-has-cats .summary-metadata-container,.sqs-block-summary-v2 .summary-block-setting-metadata-position-above-title.summary-block-setting-secondary-metadata-cats .summary-item-has-cats .summary-metadata-container,.sqs-block-summary-v2 .summary-block-setting-metadata-position-below-title.summary-block-setting-secondary-metadata-cats .summary-item-has-cats .summary-metadata-container,.sqs-block-summary-v2 .summary-block-setting-metadata-position-above-title.summary-block-setting-primary-metadata-tags .summary-item-has-tags .summary-metadata-container,.sqs-block-summary-v2 .summary-block-setting-metadata-position-below-title.summary-block-setting-primary-metadata-tags .summary-item-has-tags .summary-metadata-container,.sqs-block-summary-v2 .summary-block-setting-metadata-position-above-title.summary-block-setting-secondary-metadata-tags .summary-item-has-tags .summary-metadata-container,.sqs-block-summary-v2 .summary-block-setting-metadata-position-below-title.summary-block-setting-secondary-metadata-tags .summary-item-has-tags .summary-metadata-container,.sqs-block-summary-v2 .summary-block-setting-metadata-position-above-title.summary-block-setting-primary-metadata-author .summary-item-has-author .summary-metadata-container,.sqs-block-summary-v2 .summary-block-setting-metadata-position-below-title.summary-block-setting-primary-metadata-author .summary-item-has-author .summary-metadata-container,.sqs-block-summary-v2 .summary-block-setting-metadata-position-above-title.summary-block-setting-secondary-metadata-author .summary-item-has-author .summary-metadata-container,.sqs-block-summary-v2 .summary-block-setting-metadata-position-below-title.summary-block-setting-secondary-metadata-author .summary-item-has-author .summary-metadata-container,.sqs-block-summary-v2 .summary-block-setting-metadata-position-above-title.summary-block-setting-primary-metadata-comments .summary-item-has-comments-enabled .summary-metadata-container,.sqs-block-summary-v2 .summary-block-setting-metadata-position-below-title.summary-block-setting-primary-metadata-comments .summary-item-has-comments-enabled .summary-metadata-container,.sqs-block-summary-v2 .summary-block-setting-metadata-position-above-title.summary-block-setting-secondary-metadata-comments .summary-item-has-comments-enabled .summary-metadata-container,.sqs-block-summary-v2 .summary-block-setting-metadata-position-below-title.summary-block-setting-secondary-metadata-comments .summary-item-has-comments-enabled .summary-metadata-container,.sqs-block-summary-v2 .summary-block-setting-metadata-position-above-title.summary-block-setting-primary-metadata-location .summary-item-has-location .summary-metadata-container,.sqs-block-summary-v2 .summary-block-setting-metadata-position-below-title.summary-block-setting-primary-metadata-location .summary-item-has-location .summary-metadata-container,.sqs-block-summary-v2 .summary-block-setting-metadata-position-above-title.summary-block-setting-secondary-metadata-location .summary-item-has-location .summary-metadata-container,.sqs-block-summary-v2 .summary-block-setting-metadata-position-below-title.summary-block-setting-secondary-metadata-location .summary-item-has-location .summary-metadata-container{margin:0 0 10px 0}.sqs-block-summary-v2 .summary-metadata{display:none}.sqs-block-summary-v2 .summary-block-setting-primary-metadata-date .summary-metadata--primary,.sqs-block-summary-v2 .summary-block-setting-primary-metadata-event-time .summary-metadata--primary,.sqs-block-summary-v2 .summary-block-setting-primary-metadata-cats .summary-item-has-cats .summary-metadata--primary,.sqs-block-summary-v2 .summary-block-setting-primary-metadata-tags .summary-item-has-tags .summary-metadata--primary,.sqs-block-summary-v2 .summary-block-setting-primary-metadata-author .summary-item-has-author .summary-metadata--primary,.sqs-block-summary-v2 .summary-block-setting-primary-metadata-comments .summary-item-has-comments-enabled .summary-metadata--primary,.sqs-block-summary-v2 .summary-block-setting-primary-metadata-location .summary-item-has-location .summary-metadata--primary{display:inline-block}.sqs-block-summary-v2 .summary-block-setting-secondary-metadata-date .summary-metadata--secondary,.sqs-block-summary-v2 .summary-block-setting-secondary-metadata-event-time .summary-metadata--secondary,.sqs-block-summary-v2 .summary-block-setting-secondary-metadata-cats .summary-item-has-cats .summary-metadata--secondary,.sqs-block-summary-v2 .summary-block-setting-secondary-metadata-tags .summary-item-has-tags .summary-metadata--secondary,.sqs-block-summary-v2 .summary-block-setting-secondary-metadata-author .summary-item-has-author .summary-metadata--secondary,.sqs-block-summary-v2 .summary-block-setting-secondary-metadata-comments .summary-item-has-comments-enabled .summary-metadata--secondary,.sqs-block-summary-v2 .summary-block-setting-secondary-metadata-location .summary-item-has-location .summary-metadata--secondary{display:inline-block}.sqs-block-summary-v2 .summary-metadata-item{display:none;opacity:.7;margin:0;font-size:13px;line-height:1.4em;text-transform:none}.sqs-block-summary-v2 .summary-metadata-item a,.sqs-block-summary-v2 .summary-metadata-item a:hover{opacity:1;text-decoration:none}.sqs-block-summary-v2 .summary-block-setting-primary-metadata-date .summary-metadata--primary .summary-metadata-item--date,.sqs-block-summary-v2 .summary-block-setting-primary-metadata-event-time .summary-metadata--primary .summary-metadata-item--event-time,.sqs-block-summary-v2 .summary-block-setting-primary-metadata-cats .summary-item-has-cats .summary-metadata--primary .summary-metadata-item--cats,.sqs-block-summary-v2 .summary-block-setting-primary-metadata-tags .summary-item-has-tags .summary-metadata--primary .summary-metadata-item--tags,.sqs-block-summary-v2 .summary-block-setting-primary-metadata-author .summary-item-has-author .summary-metadata--primary .summary-metadata-item--author,.sqs-block-summary-v2 .summary-block-setting-primary-metadata-comments .summary-item-has-comments-enabled .summary-metadata--primary .summary-metadata-item--comments,.sqs-block-summary-v2 .summary-block-setting-primary-metadata-location .summary-item-has-location .summary-metadata--primary .summary-metadata-item--location{display:inline-block}.sqs-block-summary-v2 .summary-block-setting-secondary-metadata-date .summary-metadata--secondary .summary-metadata-item--date,.sqs-block-summary-v2 .summary-block-setting-secondary-metadata-event-time .summary-metadata--secondary .summary-metadata-item--event-time,.sqs-block-summary-v2 .summary-block-setting-secondary-metadata-cats .summary-item-has-cats .summary-metadata--secondary .summary-metadata-item--cats,.sqs-block-summary-v2 .summary-block-setting-secondary-metadata-tags .summary-item-has-tags .summary-metadata--secondary .summary-metadata-item--tags,.sqs-block-summary-v2 .summary-block-setting-secondary-metadata-author .summary-item-has-author .summary-metadata--secondary .summary-metadata-item--author,.sqs-block-summary-v2 .summary-block-setting-secondary-metadata-comments .summary-item-has-comments-enabled .summary-metadata--secondary .summary-metadata-item--comments,.sqs-block-summary-v2 .summary-block-setting-secondary-metadata-location .summary-item-has-location .summary-metadata--secondary .summary-metadata-item--location{display:inline-block}.sqs-block-summary-v2 .summary-block-setting-secondary-metadata-date .summary-metadata--primary .summary-metadata-item:after,.sqs-block-summary-v2 .summary-block-setting-secondary-metadata-event-time .summary-metadata--primary .summary-metadata-item:after,.sqs-block-summary-v2 .summary-block-setting-secondary-metadata-cats .summary-item-has-cats .summary-metadata--primary .summary-metadata-item:after,.sqs-block-summary-v2 .summary-block-setting-secondary-metadata-tags .summary-item-has-tags .summary-metadata--primary .summary-metadata-item:after,.sqs-block-summary-v2 .summary-block-setting-secondary-metadata-author .summary-item-has-author .summary-metadata--primary .summary-metadata-item:after,.sqs-block-summary-v2 .summary-block-setting-secondary-metadata-comments .summary-item-has-comments-enabled .summary-metadata--primary .summary-metadata-item:after,.sqs-block-summary-v2 .summary-block-setting-secondary-metadata-location .summary-item-has-location .summary-metadata--primary .summary-metadata-item:after{content:\" ·\";margin:0 .3em}.sqs-block-summary-v2 .summary-block-setting-text-size-extralarge .summary-title{font-size:54px}.sqs-block-summary-v2 .summary-block-setting-text-size-extralarge .summary-excerpt p{font-size:16px}.sqs-block-summary-v2 .summary-block-setting-text-size-large .summary-title{font-size:30px}.sqs-block-summary-v2 .summary-block-setting-text-size-medium .summary-title{font-size:20px}.sqs-block-summary-v2 .summary-block-setting-text-size-small .summary-title{font-size:14px}.sqs-block-summary-v2 .summary-block-setting-text-align-center .summary-title,.sqs-block-summary-v2 .summary-block-setting-text-align-center .summary-price .product-price,.sqs-block-summary-v2 .summary-block-setting-text-align-center .summary-excerpt p,.sqs-block-summary-v2 .summary-block-setting-text-align-center .summary-read-more-link,.sqs-block-summary-v2 .summary-block-setting-text-align-center .summary-content{text-align:center}.sqs-block-summary-v2 .summary-block-setting-text-align-right .summary-title,.sqs-block-summary-v2 .summary-block-setting-text-align-right .summary-price .product-price,.sqs-block-summary-v2 .summary-block-setting-text-align-right .summary-excerpt p,.sqs-block-summary-v2 .summary-block-setting-text-align-right .summary-read-more-link,.sqs-block-summary-v2 .summary-block-setting-text-align-right .summary-content{text-align:right}.sqs-block-summary-v2 .summary-item-record-type-text .summary-read-more-link{display:block}.sqs-block-summary-v2 .summary-item-record-type-event .summary-thumbnail-event-date{display:table}.sqs-block-summary-v2 .summary-thumbnail-container{margin:0}.sqs-block-summary-v2 .summary-block-setting-show-title .summary-thumbnail-container,.sqs-block-summary-v2 .summary-block-setting-show-price .summary-item-record-type-store-item .summary-thumbnail-container,.sqs-block-summary-v2 .summary-block-setting-show-excerpt .summary-thumbnail-container,.sqs-block-summary-v2 .summary-block-wrapper:not(.summary-block-setting-primary-metadata-none) .summary-thumbnail-container,.sqs-block-summary-v2 .summary-block-wrapper:not(.summary-block-setting-secondary-metadata-none) .summary-thumbnail-container{margin:0 0 15px 0}.sqs-block-summary-v2 .summary-block-wrapper:not(.summary-block-setting-show-excerpt) .summary-title,.sqs-block-summary-v2 .summary-block-wrapper:not(.summary-block-setting-show-excerpt) .summary-price{margin:0 0 2px 0}.sqs-block-summary-v2 .summary-block-wrapper:not(.summary-block-setting-show-excerpt).summary-block-setting-metadata-position-above-title.summary-block-setting-primary-metadata-date .summary-metadata-container,.sqs-block-summary-v2 .summary-block-wrapper:not(.summary-block-setting-show-excerpt).summary-block-setting-metadata-position-below-title.summary-block-setting-primary-metadata-date .summary-metadata-container,.sqs-block-summary-v2 .summary-block-wrapper:not(.summary-block-setting-show-excerpt).summary-block-setting-metadata-position-above-title.summary-block-setting-secondary-metadata-date .summary-metadata-container,.sqs-block-summary-v2 .summary-block-wrapper:not(.summary-block-setting-show-excerpt).summary-block-setting-metadata-position-below-title.summary-block-setting-secondary-metadata-date .summary-metadata-container,.sqs-block-summary-v2 .summary-block-wrapper:not(.summary-block-setting-show-excerpt).summary-block-setting-metadata-position-above-title.summary-block-setting-primary-metadata-event-time .summary-metadata-container,.sqs-block-summary-v2 .summary-block-wrapper:not(.summary-block-setting-show-excerpt).summary-block-setting-metadata-position-below-title.summary-block-setting-primary-metadata-event-time .summary-metadata-container,.sqs-block-summary-v2 .summary-block-wrapper:not(.summary-block-setting-show-excerpt).summary-block-setting-metadata-position-above-title.summary-block-setting-secondary-metadata-event-time .summary-metadata-container,.sqs-block-summary-v2 .summary-block-wrapper:not(.summary-block-setting-show-excerpt).summary-block-setting-metadata-position-below-title.summary-block-setting-secondary-metadata-event-time .summary-metadata-container,.sqs-block-summary-v2 .summary-block-wrapper:not(.summary-block-setting-show-excerpt).summary-block-setting-metadata-position-above-title.summary-block-setting-primary-metadata-cats .summary-item-has-cats .summary-metadata-container,.sqs-block-summary-v2 .summary-block-wrapper:not(.summary-block-setting-show-excerpt).summary-block-setting-metadata-position-below-title.summary-block-setting-primary-metadata-cats .summary-item-has-cats .summary-metadata-container,.sqs-block-summary-v2 .summary-block-wrapper:not(.summary-block-setting-show-excerpt).summary-block-setting-metadata-position-above-title.summary-block-setting-secondary-metadata-cats .summary-item-has-cats .summary-metadata-container,.sqs-block-summary-v2 .summary-block-wrapper:not(.summary-block-setting-show-excerpt).summary-block-setting-metadata-position-below-title.summary-block-setting-secondary-metadata-cats .summary-item-has-cats .summary-metadata-container,.sqs-block-summary-v2 .summary-block-wrapper:not(.summary-block-setting-show-excerpt).summary-block-setting-metadata-position-above-title.summary-block-setting-primary-metadata-tags .summary-item-has-tags .summary-metadata-container,.sqs-block-summary-v2 .summary-block-wrapper:not(.summary-block-setting-show-excerpt).summary-block-setting-metadata-position-below-title.summary-block-setting-primary-metadata-tags .summary-item-has-tags .summary-metadata-container,.sqs-block-summary-v2 .summary-block-wrapper:not(.summary-block-setting-show-excerpt).summary-block-setting-metadata-position-above-title.summary-block-setting-secondary-metadata-tags .summary-item-has-tags .summary-metadata-container,.sqs-block-summary-v2 .summary-block-wrapper:not(.summary-block-setting-show-excerpt).summary-block-setting-metadata-position-below-title.summary-block-setting-secondary-metadata-tags .summary-item-has-tags .summary-metadata-container,.sqs-block-summary-v2 .summary-block-wrapper:not(.summary-block-setting-show-excerpt).summary-block-setting-metadata-position-above-title.summary-block-setting-primary-metadata-author .summary-item-has-author .summary-metadata-container,.sqs-block-summary-v2 .summary-block-wrapper:not(.summary-block-setting-show-excerpt).summary-block-setting-metadata-position-below-title.summary-block-setting-primary-metadata-author .summary-item-has-author .summary-metadata-container,.sqs-block-summary-v2 .summary-block-wrapper:not(.summary-block-setting-show-excerpt).summary-block-setting-metadata-position-above-title.summary-block-setting-secondary-metadata-author .summary-item-has-author .summary-metadata-container,.sqs-block-summary-v2 .summary-block-wrapper:not(.summary-block-setting-show-excerpt).summary-block-setting-metadata-position-below-title.summary-block-setting-secondary-metadata-author .summary-item-has-author .summary-metadata-container,.sqs-block-summary-v2 .summary-block-wrapper:not(.summary-block-setting-show-excerpt).summary-block-setting-metadata-position-above-title.summary-block-setting-primary-metadata-comments .summary-item-has-comments-enabled .summary-metadata-container,.sqs-block-summary-v2 .summary-block-wrapper:not(.summary-block-setting-show-excerpt).summary-block-setting-metadata-position-below-title.summary-block-setting-primary-metadata-comments .summary-item-has-comments-enabled .summary-metadata-container,.sqs-block-summary-v2 .summary-block-wrapper:not(.summary-block-setting-show-excerpt).summary-block-setting-metadata-position-above-title.summary-block-setting-secondary-metadata-comments .summary-item-has-comments-enabled .summary-metadata-container,.sqs-block-summary-v2 .summary-block-wrapper:not(.summary-block-setting-show-excerpt).summary-block-setting-metadata-position-below-title.summary-block-setting-secondary-metadata-comments .summary-item-has-comments-enabled .summary-metadata-container,.sqs-block-summary-v2 .summary-block-wrapper:not(.summary-block-setting-show-excerpt).summary-block-setting-metadata-position-above-title.summary-block-setting-primary-metadata-location .summary-item-has-location .summary-metadata-container,.sqs-block-summary-v2 .summary-block-wrapper:not(.summary-block-setting-show-excerpt).summary-block-setting-metadata-position-below-title.summary-block-setting-primary-metadata-location .summary-item-has-location .summary-metadata-container,.sqs-block-summary-v2 .summary-block-wrapper:not(.summary-block-setting-show-excerpt).summary-block-setting-metadata-position-above-title.summary-block-setting-secondary-metadata-location .summary-item-has-location .summary-metadata-container,.sqs-block-summary-v2 .summary-block-wrapper:not(.summary-block-setting-show-excerpt).summary-block-setting-metadata-position-below-title.summary-block-setting-secondary-metadata-location .summary-item-has-location .summary-metadata-container{margin:0 0 2px 0}.sqs-block-summary-v2 .summary-block-setting-design-list .summary-item{visibility:visible !important;margin-bottom:17px !important;padding-bottom:17px !important}.sqs-block-summary-v2 .summary-block-setting-design-list .summary-item.summary-item-show-thumbnail{margin-bottom:17px !important;padding-bottom:17px !important}.sqs-block-summary-v2 .summary-block-setting-design-list .summary-thumbnail-container{margin:0 !important}.sqs-block-summary-v2 .summary-block-setting-design-list.summary-block-setting-design-list-thumbnail-right .summary-thumbnail-container{float:right;padding:0 0 0 20px}.sqs-block-summary-v2 .summary-block-setting-design-list.summary-block-setting-design-list-thumbnail-right .summary-item-record-type-store-item .product-mark{right:0;left:auto}.sqs-block-summary-v2 .summary-block-setting-design-list .summary-item-record-type-store-item .product-mark{left:0;right:auto}.sqs-block-summary-v2 .summary-block-setting-design-list .summary-item-record-type-event .summary-thumbnail-event-date{display:none}.sqs-block-summary-v2 .summary-block-setting-design-carousel .summary-carousel-pager{display:block}.sqs-block-summary-v2 .summary-block-setting-design-carousel .summary-block-header{overflow:hidden}.sqs-block-summary-v2 .summary-block-setting-design-carousel .summary-heading{display:block;float:left;width:calc(100% -  50px);width:-webkit-calc(100% -  50px);width:-moz-calc(100% -  50px)}.sqs-block-summary-v2 .summary-block-setting-design-carousel .summary-collection-title{display:none}.sqs-block-summary-v2 .summary-block-setting-design-carousel .summary-carousel-pager{float:right;width:50px}.sqs-block-archive .archive-group-list,.sqs-block-archive .archive-item-list{list-style-type:none;margin:0;padding:0}.sqs-block-archive .archive-group-count::before{content:\"(\"}.sqs-block-archive .archive-group-count::after{content:\")\"}.sqs-block-archive .archive-block-setting-layout-list.archive-block-setting-multicolumns .archive-group-list{columns:140px;column-gap:60px;-moz-columns:140px;-moz-column-gap:60px;-webkit-columns:140px;-webkit-column-gap:60px}.sqs-block-archive .archive-block-setting-layout-index .archive-group-name-link{font-size:1.4em;line-height:1.4em;text-decoration:none}.sqs-block-archive .archive-block-setting-layout-index .archive-item-list{display:block;margin:1.4em 0 2.8em 0;font-size:1em;line-height:1.4em}.sqs-block-archive .archive-block-setting-layout-index .archive-item{margin:0 0 .7em 0}.sqs-block-archive .archive-block-setting-layout-index .archive-item.archive-item--show-date{margin:0 0 1.4em 0}.sqs-block-archive .archive-block-setting-layout-index .archive-item-date-before{display:none;opacity:.7;margin-right:5px}.sqs-block-archive .archive-block-setting-layout-index .archive-item-link{display:block;margin-right:5px;color:inherit !important}.sqs-block-archive .archive-block-setting-layout-index .archive-item-link--untitled::before{content:\"Untitled\"}.sqs-block-archive .archive-block-setting-layout-index .archive-item-date-after{display:block;opacity:.7}.sqs-block-archive .archive-block-setting-layout-index.archive-block-setting-multicolumns .archive-group-list{columns:225px;column-gap:60px;-moz-columns:225px;-moz-column-gap:60px;-webkit-columns:225px;-webkit-column-gap:60px}.sqs-block-archive .archive-block-setting-layout-index.archive-block-setting-multicolumns .archive-group{display:inline-block;column-break-inside:avoid;-moz-column-break-inside:avoid;-webkit-column-break-inside:avoid}.sqs-block-archive .archive-block-setting-layout-index.archive-block-setting-multicolumns .archive-group-name-link,.sqs-block-archive .archive-block-setting-layout-index.archive-block-setting-multicolumns .archive-item-list{display:inline-block;min-width:225px}.sqs-block-archive .archive-block-setting-layout-dropdown.archive-block-wrapper{max-width:300px;background:rgba(0,0,0,.025);border-radius:1px}.sqs-block-archive .archive-block-setting-layout-dropdown .archive-dropdown-toggle-checkbox{position:absolute;left:-9999px}.sqs-block-archive .archive-block-setting-layout-dropdown .archive-dropdown-toggle-checkbox:checked~.archive-group-list{display:block}.sqs-block-archive .archive-block-setting-layout-dropdown .archive-dropdown-toggle-checkbox:checked~.archive-dropdown-toggle-label .archive-dropdown-toggle-icon:before{font-family:'squarespace-ui-font';font-style:normal;speak:none;font-weight:normal;-webkit-font-smoothing:antialiased;content:\"\\e006\";text-align:center;display:inline-block;vertical-align:middle}.sqs-block-archive .archive-block-setting-layout-dropdown .archive-dropdown-toggle-checkbox:checked~.archive-dropdown-toggle-label .archive-dropdown-toggle-icon:before{font-size:16px;width:16px;height:16px;line-height:16px}.sqs-block-archive .archive-block-setting-layout-dropdown .archive-dropdown-toggle-checkbox:checked~.archive-dropdown-toggle-label .archive-dropdown-toggle-icon:before{font-size:1em;width:1em;height:1em;line-height:1em}.sqs-block-archive .archive-block-setting-layout-dropdown .archive-dropdown-toggle-label{display:block;padding:12px 18px;font-size:1em;line-height:1.6em;cursor:pointer;overflow:hidden;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none}.sqs-block-archive .archive-block-setting-layout-dropdown .archive-dropdown-toggle-title{float:left;width:90%;padding-right:5px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;box-sizing:border-box}.sqs-block-archive .archive-block-setting-layout-dropdown .archive-dropdown-toggle-icon{position:relative;bottom:1px;float:right;width:10%;text-align:right;box-sizing:border-box}.sqs-block-archive .archive-block-setting-layout-dropdown .archive-dropdown-toggle-icon:before{font-family:'squarespace-ui-font';font-style:normal;speak:none;font-weight:normal;-webkit-font-smoothing:antialiased;content:\"\\e009\";text-align:center;display:inline-block;vertical-align:middle}.sqs-block-archive .archive-block-setting-layout-dropdown .archive-dropdown-toggle-icon:before{font-size:16px;width:16px;height:16px;line-height:16px}.sqs-block-archive .archive-block-setting-layout-dropdown .archive-dropdown-toggle-icon:before{font-size:1em;width:1em;height:1em;line-height:1em}.sqs-block-archive .archive-block-setting-layout-dropdown .archive-group-list{display:none;padding:0 18px 12px}.sqs-block-archive .archive-block-setting-layout-dropdown .archive-group-name-link{text-decoration:none}@media only screen and (max-width:400px){.sqs-block-archive .archive-block-setting-layout-dropdown.archive-block-wrapper{max-width:none}}.sqs-block-archive.sqs-edit-dialog-open .sqs-editing-overlay,.sqs-block-archive.sqs-edit-dialog-open .sqs-block-editor-button-container{z-index:1}.sqs-block-archive.sqs-edit-dialog-open .sqs-block-archive-content{position:relative;z-index:1000}.sqs-block-archive.sqs-edit-dialog-open .archive-group-list{pointer-events:none}.sqs-block-spacer .sqs-block-content{visibility:hidden}.sqs-layout .html-block.sqs-block img{max-width:100%;height:auto}.sqs-layout .html-block.sqs-block img[align=left]{margin-right:34px}.sqs-layout .html-block.sqs-block img[align=right]{margin-left:34px}.sqs-layout .html-block.sqs-block img[align=top]{vertical-align:top}.sqs-layout .html-block.sqs-block img[align=middle]{vertical-align:middle}.sqs-layout .html-block.sqs-block img[align=bottom]{vertical-align:bottom}.sqs-layout .html-block.sqs-block .full-image-float-left,.sqs-layout .html-block.sqs-block .thumbnail-image-float-left{float:left;margin-right:34px}.sqs-layout .html-block.sqs-block .full-image-float-right,.sqs-layout .html-block.sqs-block .thumbnail-image-float-right{float:right;margin-left:34px}.sqs-layout .html-block.sqs-block .full-image-block{display:block;margin-bottom:34px}.sqs-layout .html-block.sqs-block div[data-src=\"v5\"] img{max-width:100%}.sqs-layout .html-block.sqs-block .thumbnail-caption{display:block}.sqs-layout .html-block.sqs-block .entry-content img{margin:0 0 34px 0}.sqs-layout .html-block.sqs-block .alignleft,.sqs-layout .html-block.sqs-block img.alignleft{margin-right:34px;display:inline;float:left;width:auto}.sqs-layout .html-block.sqs-block .alignright,.sqs-layout .html-block.sqs-block img.alignright{margin-left:34px;display:inline;float:right;width:auto}.sqs-layout .html-block.sqs-block .aligncenter,.sqs-layout .html-block.sqs-block img.aligncenter{margin-right:auto;margin-left:auto;display:block;clear:both;width:auto}.sqs-layout .html-block.sqs-block blockquote.left{margin-right:34px;text-align:right;margin-left:0;width:33%;float:left}.sqs-layout .html-block.sqs-block blockquote.right{margin-left:34px;text-align:left;margin-right:0;width:33%;float:right}.system-button-font{font-family:\"proxima-nova\",sans-serif;font-weight:600;font-style:normal;text-transform:uppercase;letter-spacing:1px}body:not(.button-style-default) .sqs-editable-button,body:not(.button-style-default) .sqs-editable-button-layout{display:inline-block;width:auto;height:auto;padding:1em 2.5em;border-width:0;text-align:center;cursor:pointer;outline:none;-webkit-appearance:none;-moz-appearance:none;appearance:none}body:not(.button-style-default) .sqs-editable-button:hover,body:not(.button-style-default) .sqs-editable-button-layout:hover{opacity:1}body:not(.button-style-default) .sqs-editable-button,body:not(.button-style-default) .sqs-editable-button-color{color:#fff;background-color:#23c890;border-color:#23c890}body:not(.button-style-default) .sqs-editable-button,body:not(.button-style-default) .sqs-editable-button-font{font-family:\"proxima-nova\",sans-serif;letter-spacing:1px;font-family:\"proxima-nova\";text-transform:uppercase;letter-spacing:.6px;font-weight:600;font-style:normal}body:not(.button-style-default).button-style-solid .sqs-editable-button,body:not(.button-style-default).button-style-solid .sqs-editable-button-style{-webkit-transition:.1s opacity linear;-moz-transition:.1s opacity linear;-o-transition:.1s opacity linear;transition:.1s opacity linear;-webkit-backface-visibility:hidden}body:not(.button-style-default).button-style-solid .sqs-editable-button:hover,body:not(.button-style-default).button-style-solid .sqs-editable-button-style:hover{opacity:.8}body:not(.button-style-default).button-style-outline .sqs-editable-button,body:not(.button-style-default).button-style-outline .sqs-editable-button-style{border-width:2px;border-style:solid;-webkit-transition:0.1s background-color linear, 0.1s color linear;-moz-transition:0.1s background-color linear, 0.1s color linear;-o-transition:0.1s background-color linear, 0.1s color linear;transition:0.1s background-color linear, 0.1s color linear}body:not(.button-style-default).button-style-outline .sqs-editable-button,body:not(.button-style-default).button-style-outline .sqs-editable-button-color{background-color:transparent;color:#23c890}body:not(.button-style-default).button-style-outline .sqs-editable-button:hover,body:not(.button-style-default).button-style-outline .sqs-editable-button-color:hover{background-color:#23c890;color:#1d1d1d;color:#fff}body:not(.button-style-default).button-style-raised .sqs-editable-button,body:not(.button-style-default).button-style-raised .sqs-editable-button-style{position:relative;-webkit-transition:.1s background-color linear;-moz-transition:.1s background-color linear;-o-transition:.1s background-color linear;transition:.1s background-color linear}body:not(.button-style-default).button-style-raised .sqs-editable-button:active,body:not(.button-style-default).button-style-raised .sqs-editable-button-style:active{top:1px}body:not(.button-style-default).button-style-raised .sqs-editable-button,body:not(.button-style-default).button-style-raised .sqs-editable-button-color{-webkit-box-shadow:0 2px 0 0 #1da577;-moz-box-shadow:0 2px 0 0 #1da577;box-shadow:0 2px 0 0 #1da577}body:not(.button-style-default).button-style-raised .sqs-editable-button:hover,body:not(.button-style-default).button-style-raised .sqs-editable-button-color:hover{background-color:#25d599}body:not(.button-style-default).button-style-raised .sqs-editable-button:active,body:not(.button-style-default).button-style-raised .sqs-editable-button-color:active{-webkit-box-shadow:0 1px 0 0 #1da577;-moz-box-shadow:0 1px 0 0 #1da577;box-shadow:0 1px 0 0 #1da577}body:not(.button-style-default).button-corner-style-square .sqs-editable-button,body:not(.button-style-default).button-corner-style-square .sqs-editable-button-shape{border-radius:0}body:not(.button-style-default).button-corner-style-rounded .sqs-editable-button,body:not(.button-style-default).button-corner-style-rounded .sqs-editable-button-shape{border-radius:3px}body:not(.button-style-default).button-corner-style-pill .sqs-editable-button,body:not(.button-style-default).button-corner-style-pill .sqs-editable-button-shape{border-radius:300px}body:not(.button-style-default).button-style-outline .newsletter-block .newsletter-form-button{border-width:1px;-webkit-box-shadow:inset 0px 0px 0px 1px #23c890;-moz-box-shadow:inset 0px 0px 0px 1px #23c890;box-shadow:inset 0px 0px 0px 1px #23c890;background:transparent;color:#23c890}body:not(.button-style-default).button-style-outline .newsletter-block .newsletter-form-button:hover{background-color:#23c890;color:#1d1d1d;color:#fff}body:not(.button-style-default).button-style-raised .newsletter-block .newsletter-form-button{border-width:0 !important;top:-1px;-webkit-box-shadow:0 2px 0 0 #1da577;-moz-box-shadow:0 2px 0 0 #1da577;box-shadow:0 2px 0 0 #1da577}body:not(.button-style-default).button-style-raised .newsletter-block .newsletter-form-button:hover{background-color:#25d599}body:not(.button-style-default).button-style-raised .newsletter-block .newsletter-form-button:active{top:0px;-webkit-box-shadow:0 1px 0 0 #1da577;-moz-box-shadow:0 1px 0 0 #1da577;box-shadow:0 1px 0 0 #1da577}body:not(.button-style-default) .opentable-block .OT_Find_a_Table{font-family:\"proxima-nova\",sans-serif;letter-spacing:1px;font-family:\"proxima-nova\";text-transform:uppercase;letter-spacing:.6px;font-weight:600;font-style:normal}body:not(.button-style-default).button-corner-style-rounded .opentable-block .OT_Find_a_Table{border-radius:3px}body:not(.button-style-default).button-corner-style-pill .opentable-block .OT_Find_a_Table{border-radius:300px}.announcement-bar-font{font-family:'proxima-nova',arial,sans-serif;font-size:13px;font-weight:300;font-style:normal;letter-spacing:1px;text-transform:none}.sqs-announcement-bar{overflow:hidden;position:relative;top:0;left:0;z-index:10000;background:#f5dc00;text-align:center;-webkit-transition:height .3s cubic-bezier(.23,1,.32,1);-moz-transition:height .3s cubic-bezier(.23,1,.32,1);-ms-transition:height .3s cubic-bezier(.23,1,.32,1);-o-transition:height .3s cubic-bezier(.23,1,.32,1);transition:height .3s cubic-bezier(.23,1,.32,1)}.sqs-announcement-bar-url{position:absolute;top:0;left:0;width:100%;height:100%}.sqs-announcement-bar-text{padding:.8em 3em;font-family:'proxima-nova',arial,sans-serif;font-size:13px;font-weight:300;font-family:\"proxima-nova\";font-size:19px;text-transform:none;letter-spacing:1px;font-weight:700;font-style:normal;line-height:1.2em}.sqs-announcement-bar-text p{color:#000;margin:0;font-family:'proxima-nova',arial,sans-serif;font-size:13px;font-weight:300;font-family:\"proxima-nova\";font-size:19px;text-transform:none;letter-spacing:1px;font-weight:700;font-style:normal;line-height:inherit}.sqs-announcement-bar-text a{position:relative;color:#000 !important;text-decoration:underline !important}.sqs-announcement-bar-close{cursor:pointer;position:absolute;top:0;right:0;width:2.8em;height:2.78em;background:rgba(0,0,0,.15);color:#000}.sqs-announcement-bar-close:after{content:'×';display:block;font-family:helvetica,arial,sans-serif;font-size:1em;font-weight:100;line-height:2.7em;letter-spacing:normal;padding:0}.sqs-announcement-bar-hidden{height:0 !important}@media screen and (max-width:1024px){.sqs-announcement-bar-text,.sqs-announcement-bar-text p{font-size:13px}}@font-face{font-family:'social-icon-font';src:url('//static.squarespace.com/universal/fonts/social-20141119/social-icon-font.eot');src:url('//static.squarespace.com/universal/fonts/social-20141119/social-icon-font.eot?#iefix') format('embedded-opentype'),url('//static.squarespace.com/universal/fonts/social-20141119/social-icon-font.woff') format('woff'),url('//static.squarespace.com/universal/fonts/social-20141119/social-icon-font.ttf') format('truetype'),url('//static.squarespace.com/universal/fonts/social-20141119/social-icon-font.svg#social-icon-font') format('svg');font-weight:normal;font-style:normal}.social-smugmug:before,.social-dribbble:before,.social-youtube:before,.social-vimeo:before,.social-twitter:before,.social-tumblr:before,.social-pinterest:before,.social-linkedin:before,.social-instagram:before,.social-google:before,.social-foursquare:before,.social-flickr:before,.social-facebook:before,.social-fivehundredpix:before,.social-fivehundredpx:before,.social-email:before,.social-github:before,.social-rss:before,.social-spotify:before,.social-soundcloud:before,.social-itunes:before,.social-googleplay:before,.social-dropbox:before,.social-bandsintown:before,.social-behance:before,.social-codepen:before,.social-medium:before,.social-rdio:before,.social-squarespace:before,.social-vine:before,.social-yelp:before,.social-vevo:before,.social-meetup:before,.social-twitch:before,.social-vsco:before,.social-smugmug-square:before,.social-dribbble-square:before,.social-youtube-square:before,.social-vimeo-square:before,.social-twitter-square:before,.social-tumblr-square:before,.social-pinterest-square:before,.social-linkedin-square:before,.social-instagram-square:before,.social-google-square:before,.social-foursquare-square:before,.social-flickr-square:before,.social-facebook-square:before,.social-fivehundredpix-square:before,.social-fivehundredpx-square:before,.social-email-square:before,.social-github-square:before,.social-rss-square:before,.social-spotify-square:before,.social-soundcloud-square:before,.social-itunes-square:before,.social-googleplay-square:before,.social-dropbox-square:before,.social-bandsintown-square:before,.social-behance-square:before,.social-codepen-square:before,.social-medium-square:before,.social-rdio-square:before,.social-squarespace-square:before,.social-vine-square:before,.social-yelp-square:before,.social-vevo-square:before,.social-meetup-square:before,.social-twitch-square:before,.social-vsco-square:before,.social-smugmug-round:before,.social-dribbble-round:before,.social-youtube-round:before,.social-vimeo-round:before,.social-twitter-round:before,.social-tumblr-round:before,.social-pinterest-round:before,.social-linkedin-round:before,.social-instagram-round:before,.social-google-round:before,.social-foursquare-round:before,.social-flickr-round:before,.social-facebook-round:before,.social-fivehundredpix-round:before,.social-fivehundredpx-round:before,.social-email-round:before,.social-github-round:before,.social-rss-round:before,.social-spotify-round:before,.social-soundcloud-round:before,.social-itunes-round:before,.social-googleplay-round:before,.social-dropbox-round:before,.social-bandsintown-round:before,.social-behance-round:before,.social-codepen-round:before,.social-medium-round:before,.social-rdio-round:before,.social-squarespace-round:before,.social-vine-round:before,.social-yelp-round:before,.social-vevo-round:before,.social-meetup-round:before,.social-twitch-round:before,.social-vsco-round:before{font-family:'social-icon-font';speak:none;font-style:normal;font-weight:normal;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.social-smugmug:before{content:\"\\e600\"}.social-icon-style-square .social-smugmug:before{content:\"\\e601\"}.social-icon-style-round .social-smugmug:before{content:\"\\e602\"}.social-dribbble:before{content:\"\\e603\"}.social-icon-style-square .social-dribbble:before{content:\"\\e604\"}.social-icon-style-round .social-dribbble:before{content:\"\\e605\"}.social-youtube:before{content:\"\\e606\"}.social-icon-style-square .social-youtube:before{content:\"\\e607\"}.social-icon-style-round .social-youtube:before{content:\"\\e608\"}.social-vimeo:before{content:\"\\e609\"}.social-icon-style-square .social-vimeo:before{content:\"\\e60a\"}.social-icon-style-round .social-vimeo:before{content:\"\\e60b\"}.social-twitter:before{content:\"\\e60c\"}.social-icon-style-square .social-twitter:before{content:\"\\e60d\"}.social-icon-style-round .social-twitter:before{content:\"\\e60e\"}.social-tumblr:before{content:\"\\e60f\"}.social-icon-style-square .social-tumblr:before{content:\"\\e610\"}.social-icon-style-round .social-tumblr:before{content:\"\\e611\"}.social-pinterest:before{content:\"\\e612\"}.social-icon-style-square .social-pinterest:before{content:\"\\e613\"}.social-icon-style-round .social-pinterest:before{content:\"\\e614\"}.social-linkedin:before{content:\"\\e615\"}.social-icon-style-square .social-linkedin:before{content:\"\\e616\"}.social-icon-style-round .social-linkedin:before{content:\"\\e617\"}.social-instagram:before{content:\"\\e618\"}.social-icon-style-square .social-instagram:before{content:\"\\e619\"}.social-icon-style-round .social-instagram:before{content:\"\\e61a\"}.social-google:before{content:\"\\e61b\"}.social-icon-style-square .social-google:before{content:\"\\e61c\"}.social-icon-style-round .social-google:before{content:\"\\e61d\"}.social-googleauth2:before{content:\"\\e61b\"}.social-foursquare:before{content:\"\\e61e\"}.social-icon-style-square .social-foursquare:before{content:\"\\e61f\"}.social-icon-style-round .social-foursquare:before{content:\"\\e620\"}.social-flickr:before{content:\"\\e621\"}.social-icon-style-square .social-flickr:before{content:\"\\e622\"}.social-icon-style-round .social-flickr:before{content:\"\\e623\"}.social-facebook:before{content:\"\\e624\"}.social-icon-style-square .social-facebook:before{content:\"\\e625\"}.social-icon-style-round .social-facebook:before{content:\"\\e626\"}.social-fivehundredpix:before{content:\"\\e627\"}.social-icon-style-square .social-fivehundredpix:before{content:\"\\e628\"}.social-icon-style-round .social-fivehundredpix:before{content:\"\\e629\"}.social-fivehundredpx:before{content:\"\\e627\"}.social-icon-style-square .social-fivehundredpx:before{content:\"\\e628\"}.social-icon-style-round .social-fivehundredpx:before{content:\"\\e629\"}.social-email:before{content:\"\\e62a\"}.social-icon-style-square .social-email:before{content:\"\\e62b\"}.social-icon-style-round .social-email:before{content:\"\\e62c\"}.social-github:before{content:\"\\e62d\"}.social-icon-style-square .social-github:before{content:\"\\e62e\"}.social-icon-style-round .social-github:before{content:\"\\e62f\"}.social-rss:before{content:\"\\e630\"}.social-icon-style-square .social-rss:before{content:\"\\e631\"}.social-icon-style-round .social-rss:before{content:\"\\e632\"}.social-spotify:before{content:\"\\e633\"}.social-icon-style-square .social-spotify:before{content:\"\\e634\"}.social-icon-style-round .social-spotify:before{content:\"\\e635\"}.social-soundcloud:before{content:\"\\e636\"}.social-icon-style-square .social-soundcloud:before{content:\"\\e637\"}.social-icon-style-round .social-soundcloud:before{content:\"\\e638\"}.social-itunes:before{content:\"\\e639\"}.social-icon-style-square .social-itunes:before{content:\"\\e63a\"}.social-icon-style-round .social-itunes:before{content:\"\\e63b\"}.social-googleplay:before{content:\"\\e63c\"}.social-icon-style-square .social-googleplay:before{content:\"\\e63d\"}.social-icon-style-round .social-googleplay:before{content:\"\\e63e\"}.social-dropbox:before{content:\"\\e63f\"}.social-icon-style-square .social-dropbox:before{content:\"\\e640\"}.social-icon-style-round .social-dropbox:before{content:\"\\e641\"}.social-bandsintown:before{content:\"\\e642\"}.social-icon-style-square .social-bandsintown:before{content:\"\\e643\"}.social-icon-style-round .social-bandsintown:before{content:\"\\e644\"}.social-behance:before{content:\"\\e645\"}.social-icon-style-square .social-behance:before{content:\"\\e646\"}.social-icon-style-round .social-behance:before{content:\"\\e647\"}.social-codepen:before{content:\"\\e648\"}.social-icon-style-square .social-codepen:before{content:\"\\e649\"}.social-icon-style-round .social-codepen:before{content:\"\\e64a\"}.social-medium:before{content:\"\\e64b\"}.social-icon-style-square .social-medium:before{content:\"\\e64c\"}.social-icon-style-round .social-medium:before{content:\"\\e64d\"}.social-rdio:before{content:\"\\e64e\"}.social-icon-style-square .social-rdio:before{content:\"\\e64f\"}.social-icon-style-round .social-rdio:before{content:\"\\e650\"}.social-squarespace:before{content:\"\\e651\"}.social-icon-style-square .social-squarespace:before{content:\"\\e652\"}.social-icon-style-round .social-squarespace:before{content:\"\\e653\"}.social-vine:before{content:\"\\e654\"}.social-icon-style-square .social-vine:before{content:\"\\e655\"}.social-icon-style-round .social-vine:before{content:\"\\e656\"}.social-yelp:before{content:\"\\e657\"}.social-icon-style-square .social-yelp:before{content:\"\\e658\"}.social-icon-style-round .social-yelp:before{content:\"\\e659\"}.social-meetup:before{content:\"\\e65a\"}.social-icon-style-square .social-meetup:before{content:\"\\e65b\"}.social-icon-style-round .social-meetup:before{content:\"\\e65c\"}.social-vevo:before{content:\"\\e65d\"}.social-icon-style-square .social-vevo:before{content:\"\\e65e\"}.social-icon-style-round .social-vevo:before{content:\"\\e65f\"}.social-twitch:before{content:\"\\e660\"}.social-icon-style-square .social-twitch:before{content:\"\\e661\"}.social-icon-style-round .social-twitch:before{content:\"\\e662\"}.social-vsco:before{content:\"\\e663\"}.social-icon-style-square .social-vsco:before{content:\"\\e664\"}.social-icon-style-round .social-vsco:before{content:\"\\e665\"}.site-title-font{font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:600;font-style:normal;font-size:20px;letter-spacing:2px;text-transform:uppercase}.nav-font{font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:600;font-style:normal;font-size:14px;letter-spacing:1px;text-transform:uppercase;text-decoration:none}.nav-button-font{font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:700;font-style:normal;letter-spacing:1px;text-transform:uppercase;text-decoration:none}.banner-heading-font{font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:400;font-style:normal;font-size:48px;letter-spacing:0px;text-transform:none;line-height:1em}.banner-text-font{font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:400;font-style:normal;font-size:18px;letter-spacing:0px;text-transform:none;line-height:1.5em}.banner-button-font{font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:700;font-style:normal;font-size:16px;letter-spacing:1px;text-transform:uppercase;text-decoration:none}.body-font{font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:400;font-style:normal;font-size:16px;letter-spacing:0px;line-height:1.6em}.heading1-font{font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:400;font-style:normal;font-size:48px;letter-spacing:0px;text-transform:none;line-height:1.2em}.heading2-font{font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:400;font-style:normal;font-size:32px;letter-spacing:0px;text-transform:none;line-height:1.2em}.heading3-font{font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:400;font-style:normal;font-size:21px;letter-spacing:0px;text-transform:none;line-height:1.2em}.quote-font{font-family:Georgia,\"Times New Roman\",serif;font-weight:400;font-style:italic;font-size:27px;letter-spacing:0px;line-height:1.65em}.summary-heading-font{font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;letter-spacing:1px;text-transform:uppercase}.subnav-title-font{font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:700;font-style:normal;font-size:14px;letter-spacing:1px;text-transform:uppercase;text-decoration:none}.subnav-link-font{font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:400;font-style:normal;font-size:14px;letter-spacing:1px;text-transform:uppercase;text-decoration:none}.footer-nav-font{font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:400;font-style:normal;font-size:14px;letter-spacing:1px;text-transform:uppercase;text-decoration:none}.site-info-font{font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:600;font-style:normal;font-size:14px;letter-spacing:1px;text-transform:uppercase;text-decoration:none}html:not(.js) body[class^=collection-] img{max-width:100%}html:not(.js) body[class^=collection-] [href=\"#\"]{display:none !important;visibility:hidden}.hidden{display:none !important;visibility:hidden}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}img[data-src]:not([src]){visibility:hidden}.full-image-float-left,.thumbnail-image-float-left{float:left;margin-right:1.5em}.full-image-float-right,.thumbnail-image-float-right{float:right;margin-left:1.5em}.full-image-block{display:block;margin-bottom:1.5em}.thumbnail-caption{display:block}.clearfix:before,.clearfix:after{content:\" \";display:table}.clearfix:after{clear:both}@media print{*{background:transparent !important;color:#000 !important;box-shadow:none !important;text-shadow:none !important}a,a:visited{text-decoration:underline}a[href]:after{content:\" (\" attr(href) \")\"}abbr[title]:after{content:\" (\" attr(title) \")\"}a[href^=\"javascript:\"]:after,a[href^=\"#\"]:after{content:\"\"}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}@page {margin:.5cm}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}.border-box,.border-box:before,.border-box:after{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}.header-anim 0%{opacity:0}.header-anim 72%{opacity:0}.header-anim 100%{opacity:1}@-webkit-keyframes header-anim{0%{opacity:0}72%{opacity:0}100%{opacity:1}}@keyframes header-anim{0%{opacity:0}72%{opacity:0}100%{opacity:1}}.feature-bg-anim 0%{opacity:0}.feature-bg-anim 50%{opacity:0}.feature-bg-anim 100%{opacity:1}@-webkit-keyframes feature-bg-anim{0%{opacity:0}50%{opacity:0}100%{opacity:1}}@keyframes feature-bg-anim{0%{opacity:0}50%{opacity:0}100%{opacity:1}}.feature-text-anim 0%{opacity:0;-webkit-transform:translate3d(0,10px,0);-moz-transform:translate3d(0,10px,0);-ms-transform:translate3d(0,10px,0);-o-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}.feature-text-anim 75%{opacity:0;-webkit-transform:translate3d(0,10px,0);-moz-transform:translate3d(0,10px,0);-ms-transform:translate3d(0,10px,0);-o-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}.feature-text-anim 100%{opacity:1;-webkit-transform:translate3d(0,0,0);-moz-transform:translate3d(0,0,0);-ms-transform:translate3d(0,0,0);-o-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}@-webkit-keyframes feature-text-anim{0%{opacity:0;-webkit-transform:translate3d(0,10px,0);-moz-transform:translate3d(0,10px,0);-ms-transform:translate3d(0,10px,0);-o-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}75%{opacity:0;-webkit-transform:translate3d(0,10px,0);-moz-transform:translate3d(0,10px,0);-ms-transform:translate3d(0,10px,0);-o-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}100%{opacity:1;-webkit-transform:translate3d(0,0,0);-moz-transform:translate3d(0,0,0);-ms-transform:translate3d(0,0,0);-o-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}@keyframes feature-text-anim{0%{opacity:0;-webkit-transform:translate3d(0,10px,0);-moz-transform:translate3d(0,10px,0);-ms-transform:translate3d(0,10px,0);-o-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}75%{opacity:0;-webkit-transform:translate3d(0,10px,0);-moz-transform:translate3d(0,10px,0);-ms-transform:translate3d(0,10px,0);-o-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}100%{opacity:1;-webkit-transform:translate3d(0,0,0);-moz-transform:translate3d(0,0,0);-ms-transform:translate3d(0,0,0);-o-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.feature-text-anim-alt 0%{opacity:0;-webkit-transform:translate3d(-50%,-45%,0);-moz-transform:translate3d(-50%,-45%,0);-ms-transform:translate3d(-50%,-45%,0);-o-transform:translate3d(-50%,-45%,0);transform:translate3d(-50%,-45%,0)}.feature-text-anim-alt 67%{opacity:0;-webkit-transform:translate3d(-50%,-45%,0);-moz-transform:translate3d(-50%,-45%,0);-ms-transform:translate3d(-50%,-45%,0);-o-transform:translate3d(-50%,-45%,0);transform:translate3d(-50%,-45%,0)}.feature-text-anim-alt 100%{opacity:1;-webkit-transform:translate3d(-50%,-50%,0);-moz-transform:translate3d(-50%,-50%,0);-ms-transform:translate3d(-50%,-50%,0);-o-transform:translate3d(-50%,-50%,0);transform:translate3d(-50%,-50%,0)}@-webkit-keyframes feature-text-anim-alt{0%{opacity:0;-webkit-transform:translate3d(-50%,-45%,0);-moz-transform:translate3d(-50%,-45%,0);-ms-transform:translate3d(-50%,-45%,0);-o-transform:translate3d(-50%,-45%,0);transform:translate3d(-50%,-45%,0)}67%{opacity:0;-webkit-transform:translate3d(-50%,-45%,0);-moz-transform:translate3d(-50%,-45%,0);-ms-transform:translate3d(-50%,-45%,0);-o-transform:translate3d(-50%,-45%,0);transform:translate3d(-50%,-45%,0)}100%{opacity:1;-webkit-transform:translate3d(-50%,-50%,0);-moz-transform:translate3d(-50%,-50%,0);-ms-transform:translate3d(-50%,-50%,0);-o-transform:translate3d(-50%,-50%,0);transform:translate3d(-50%,-50%,0)}}@keyframes feature-text-anim-alt{0%{opacity:0;-webkit-transform:translate3d(-50%,-45%,0);-moz-transform:translate3d(-50%,-45%,0);-ms-transform:translate3d(-50%,-45%,0);-o-transform:translate3d(-50%,-45%,0);transform:translate3d(-50%,-45%,0)}67%{opacity:0;-webkit-transform:translate3d(-50%,-45%,0);-moz-transform:translate3d(-50%,-45%,0);-ms-transform:translate3d(-50%,-45%,0);-o-transform:translate3d(-50%,-45%,0);transform:translate3d(-50%,-45%,0)}100%{opacity:1;-webkit-transform:translate3d(-50%,-50%,0);-moz-transform:translate3d(-50%,-50%,0);-ms-transform:translate3d(-50%,-50%,0);-o-transform:translate3d(-50%,-50%,0);transform:translate3d(-50%,-50%,0)}}*::selection{background-color:#000;color:#fff}body{background-color:#29292d}#siteWrapper{font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;line-height:1.6em;font-family:\"adelle-sans\";font-size:16px;line-height:1.7em;letter-spacing:0px;font-weight:400;font-style:normal;color:rgba(13,13,13,.7)}.sqs-modal-lightbox-content .lightbox-inner .lightbox-content .form-wrapper{font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;line-height:1.6em;font-family:\"adelle-sans\";font-size:16px;line-height:1.7em;letter-spacing:0px;font-weight:400;font-style:normal}.html-block a:not(.sqs-block-button-element),.markdown-block a:not(.sqs-block-button-element),.entry-more-link a:not(.sqs-block-button-element),.twitter-block a:not(.sqs-block-button-element),.foursquare-block a:not(.sqs-block-button-element),.rss-block a:not(.sqs-block-button-element),.layout-caption-below .image-caption a:not(.sqs-block-button-element),.summary-excerpt a:not(.sqs-block-button-element),.album-description a:not(.sqs-block-button-element),.product-excerpt a:not(.sqs-block-button-element),.product-description a:not(.sqs-block-button-element),.eventlist-excerpt a:not(.sqs-block-button-element),.html-block a:not(.sqs-block-button-element):visited,.markdown-block a:not(.sqs-block-button-element):visited,.entry-more-link a:not(.sqs-block-button-element):visited,.twitter-block a:not(.sqs-block-button-element):visited,.foursquare-block a:not(.sqs-block-button-element):visited,.rss-block a:not(.sqs-block-button-element):visited,.layout-caption-below .image-caption a:not(.sqs-block-button-element):visited,.summary-excerpt a:not(.sqs-block-button-element):visited,.album-description a:not(.sqs-block-button-element):visited,.product-excerpt a:not(.sqs-block-button-element):visited,.product-description a:not(.sqs-block-button-element):visited,.eventlist-excerpt a:not(.sqs-block-button-element):visited{color:#3d9991;text-decoration:none}a{text-decoration:none;color:rgba(13,13,13,.7)}.sqs-lightbox-meta a{color:inherit;text-decoration:underline}h1,h2,h3{text-rendering:optimizeLegibility}article header h1 a{color:rgba(26,26,26,.9)}h1,.entry-title{font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-size:48px;font-family:\"adelle-sans\";font-size:32px;line-height:1.2em;text-transform:none;letter-spacing:0px;font-weight:400;font-style:normal}h1,.entry-title{color:rgba(26,26,26,.9)}h2{font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-size:32px;letter-spacing:0px;text-transform:none;font-family:\"proxima-nova\";font-size:22px;line-height:1.2em;text-transform:uppercase;letter-spacing:2px;font-weight:400;font-style:normal}h2,.summary-title a{color:#4a4a4a}h3{font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:400;font-size:21px;letter-spacing:0px;text-transform:none;line-height:1.2em;font-family:\"proxima-nova\";font-size:16px;line-height:1em;text-transform:uppercase;letter-spacing:1px;font-weight:600;font-style:normal}h3{color:rgba(26,26,26,.9)}h1,h2,h3,.entry-title{margin:1em 0 .5em}h1:first-child,h2:first-child,h3:first-child,.entry-title:first-child{margin-top:0}blockquote{margin:0;padding:.5em 2.5em;font-style:italic}.entry-actions,.entry-comments,.eventitem-addtocallinks,.album-info .engagement,.entry-dateline,.entry-byline,.entry-morefrom,.entry-tags,.entry-source,.eventitem-backlink,.sqs-audio-playlist .tracks .track-info .artist,.summary-info-item{color:rgba(26,26,26,.4)}.entry-actions a,.entry-comments a,.eventitem-addtocallinks a,.album-info .engagement a,.entry-dateline a,.entry-byline a,.entry-morefrom a,.entry-tags a,.entry-source a,.eventitem-backlink a,.sqs-audio-playlist .tracks .track-info .artist a,.summary-info-item a{color:rgba(26,26,26,.4)}.sqs-block-summary-v2 .summary-info-item{opacity:1}.comment-count{font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;line-height:1.6em;font-family:\"adelle-sans\";font-size:16px;line-height:1.7em;letter-spacing:0px;font-weight:400;font-style:normal;color:rgba(13,13,13,.7)}.quote-block figure{font-family:Georgia,\"Times New Roman\",serif;font-style:italic;font-size:27px;font-family:\"adobe-garamond-pro\";font-size:20px;line-height:1.65em;letter-spacing:0px;font-weight:400;font-style:normal;color:rgba(26,26,26,.9);padding:32px 32px 0;text-align:center;margin:0}.quote-block blockquote{padding:0;border-left-width:0;font-style:inherit}.quote-block blockquote>span:first-child{font-size:4em;display:block;opacity:.3}.quote-block blockquote>span:last-child{display:none}.quote-block .source{font-size:.875em;padding-top:1em;opacity:.5;text-align:center}.sqs-block-horizontalrule hr{border-style:none;border-width:0;margin:32px 0;color:rgba(13,13,13,.15);background-color:rgba(13,13,13,.15)}#preFooter .sqs-block-horizontalrule hr{color:rgba(255,255,255,.15);background-color:rgba(255,255,255,.15)}#footer .sqs-block-horizontalrule hr{color:rgba(255,255,255,.15);background-color:rgba(255,255,255,.15)}#siteWrapper{position:relative;padding:0;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}#siteWrapper{background-color:#fff}.sqs-cart-dropzone .sqs-pill-shopping-cart{z-index:9999}@media screen and (min-width:641px){.sqs-cart-dropzone{position:absolute;top:100px;right:20px;width:auto;max-width:282px;z-index:999}}.category-nav .nav-section-label,.folder-nav .nav-section-label{font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:700;font-size:14px;letter-spacing:1px;text-transform:uppercase;font-family:\"adobe-garamond-pro\";font-size:22px;text-transform:none;text-decoration:none;letter-spacing:0px;font-weight:400;font-style:normal;line-height:1.2em;color:#00746b;margin-bottom:.5em}.category-nav a,.folder-nav a,.category-nav a:visited,.folder-nav a:visited{font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:400;letter-spacing:1px;font-family:\"proxima-nova\";font-size:14px;text-transform:uppercase;text-decoration:none;letter-spacing:2px;font-weight:600;font-style:normal;color:rgba(26,26,26,.4);line-height:1.25em;display:block;padding:0 0 .75em}.category-nav a:hover,.folder-nav a:hover,.category-nav a:visited:hover,.folder-nav a:visited:hover{color:#1a1a1a}.category-nav li.active-link:not(.all) a,.folder-nav li.active-link:not(.all) a,.category-nav li.active-link:not(.all) a:visited,.folder-nav li.active-link:not(.all) a:visited{color:#1a1a1a}.view-list #categoryNav ul li.active-link.all a,.view-list #categoryNav ul li.active-link.all a:visited{color:#1a1a1a}.header-inner,.footer-inner,.pre-footer-inner{width:auto;margin:auto;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}.footer-inner,.pre-footer-inner{max-width:1020px}#header{padding:0 20px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;z-index:1000;top:0;left:0;width:100%;line-height:1em;background-color:#212121;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;position:relative}#header a{text-decoration:none}#header #siteTitle{position:relative;z-index:1000}.header-inner,.footer-inner,.pre-footer-inner .sqs-layout{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}.header-inner{padding:20px 0;display:table;width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-animation:header-anim 1s ease-in-out;animation:header-anim 1s ease-in-out}.footer-inner{padding:64px 32px}.pre-footer-inner .sqs-layout{padding:32px}.pre-footer-inner .sqs-layout.empty{padding:0 32px}body:not(.sqs-edit-mode) .pre-footer-inner .sqs-layout.empty{max-height:0}.transparent-header #header{background-color:transparent;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;position:absolute}body:not(.has-banner-image).transparent-header #header,.collection-type-gallery.has-banner-image.transparent-header #header{background-color:#212121;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;position:relative}#preFooter{background-color:#fff;-moz-osx-font-smoothing:auto;-webkit-font-smoothing:subpixel-antialiased}#preFooter{color:rgba(255,255,255,.7)}#preFooter h1,#preFooter h2,#preFooter h3{color:rgba(255,255,255,.7)}.pre-footer-inner{-webkit-transition:all .25s ease-in-out .1s;-moz-transition:all .25s ease-in-out .1s;-ms-transition:all .25s ease-in-out .1s;-o-transition:all .25s ease-in-out .1s;transition:all .25s ease-in-out .1s}.pre-footer-inner a:not(.sqs-block-button-element):not(.sqs-svg-icon--wrapper),.pre-footer-inner a:visited:not(.sqs-block-button-element):not(.sqs-svg-icon--wrapper){color:rgba(255,255,255,.7);border-bottom:1px solid rgba(255,255,255,.7)}.pre-footer-inner .social-account-list a,.pre-footer-inner .social-account-list a:visited{text-decoration:none;border:none}.unscrolled .pre-footer-inner{opacity:0;-webkit-transform:translate3d(0,12px,0);-moz-transform:translate3d(0,12px,0);-ms-transform:translate3d(0,12px,0);-o-transform:translate3d(0,12px,0);transform:translate3d(0,12px,0)}#footer{background-color:#29292d;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased}#footer .html-block a,#footer .html-block a:visited{color:rgba(255,255,255,.8);border-bottom:1px solid rgba(255,255,255,.8)}#footer nav:not(.social-account-list){font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-size:14px;letter-spacing:1px;font-family:\"proxima-nova\";font-size:13px;text-transform:uppercase;text-decoration:none;letter-spacing:2px;font-weight:400;font-style:normal}#footer nav:not(.social-account-list) a,#footer nav:not(.social-account-list) a:visited,#footer nav:not(.social-account-list) label{text-decoration:none;line-height:1.25em;color:#fff;border:none}#footer nav:not(.social-account-list) a.active,#footer nav:not(.social-account-list) a:visited.active,#footer nav:not(.social-account-list) label.active,#footer nav:not(.social-account-list) a:hover,#footer nav:not(.social-account-list) a:visited:hover,#footer nav:not(.social-account-list) label:hover{color:#fff}#footer .folder .subnav{background-color:#29292d}#footer{color:rgba(255,255,255,.8)}#footer h1,#footer h2,#footer h3{color:rgba(255,255,255,.8)}#page{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;width:auto;margin:auto;max-width:1020px;padding:96px 32px;-moz-osx-font-smoothing:auto;-webkit-font-smoothing:subpixel-antialiased}#content{width:100%;display:block}.collection-type-page #content{margin:auto}#folderNav,#categoryNav,#rightSidebar{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;width:26.5625%;width:calc(255px - 0%);display:inline-block;vertical-align:top}#folderNav,#categoryNav{padding-right:64px}li.filter{display:none;visibility:hidden}#rightSidebar{padding-left:64px;font-size:.85em}.collection-type-blog #content{width:73.4375%;width:calc(100% - 255px);display:inline-block;vertical-align:top}.collection-type-page:not(.hide-page-sidebar) #folderNav+#content,.collection-type-products:not(.hide-products-sidebar) #folderNav+#content,.collection-type-page:not(.hide-page-sidebar) #categoryNav+#content,.collection-type-products:not(.hide-products-sidebar) #categoryNav+#content{width:73.4375%;width:calc(100% - 255px);display:inline-block;vertical-align:top}@media only screen and (min-width:641px){#header{width:100%}#header #logoWrapper,#header #siteTitleWrapper,#header #headerNav{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;display:table-cell;vertical-align:middle}#header #mainNavWrapper{position:relative;z-index:1000}#header #headerNav{text-align:right}#header #logoWrapper,#header #logoImage{width:140px}#header #siteTitleWrapper,#header #siteTitle{width:165px}#header.tweaking #logoWrapper,#header.tweaking #siteTitleWrapper,#header.tweaking #mainNavWrapper{border:1px solid #14aaff}}#logoImage{margin:0;font-size:0;max-width:100%}#logoImage a{display:block}#logoImage img{height:auto;max-height:100px;width:auto;max-width:100%}#siteTitle,#siteTitle a{font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:600;font-size:20px;letter-spacing:2px;text-transform:uppercase;font-family:\"proxima-nova\";font-size:27px;text-transform:none;letter-spacing:0px;font-weight:300;font-style:normal;color:#1fd699;margin:0;padding-top:0;padding-bottom:0;line-height:1em}.hide-page-sidebar #folderNav{display:none}.hide-page-sidebar #folderNav+#content{display:block}.hide-products-sidebar #categoryNav{display:none}.hide-products-sidebar #categoryNav+#content{display:block}.hide-sidebar-title .category-nav .nav-section-label,.hide-sidebar-title .folder-nav .nav-section-label{display:none}.collection-type-page.has-promoted-gallery #promotedGalleryWrapper .sqs-block,.collection-type-index.has-promoted-gallery #promotedGalleryWrapper .sqs-block,.collection-type-page.has-promoted-gallery .promoted-gallery-wrapper .sqs-block,.collection-type-index.has-promoted-gallery .promoted-gallery-wrapper .sqs-block{padding:0 !important}.collection-type-page.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow,.collection-type-index.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow,.collection-type-page.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow,.collection-type-index.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow{width:100%;height:600px !important;max-width:100%;margin:0;padding:0;opacity:1}.collection-type-page.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow .sqs-gallery,.collection-type-index.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow .sqs-gallery,.collection-type-page.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow .sqs-gallery,.collection-type-index.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow .sqs-gallery{width:100%;height:600px !important;max-width:100%}.collection-type-page.has-promoted-gallery.transparent-header #promotedGalleryWrapper .sqs-gallery-block-slideshow,.collection-type-index.has-promoted-gallery.transparent-header #promotedGalleryWrapper .sqs-gallery-block-slideshow,.collection-type-page.has-promoted-gallery.transparent-header .promoted-gallery-wrapper .sqs-gallery-block-slideshow,.collection-type-index.has-promoted-gallery.transparent-header .promoted-gallery-wrapper .sqs-gallery-block-slideshow{height:700px !important}.collection-type-page.has-promoted-gallery.transparent-header #promotedGalleryWrapper .sqs-gallery-block-slideshow .sqs-gallery,.collection-type-index.has-promoted-gallery.transparent-header #promotedGalleryWrapper .sqs-gallery-block-slideshow .sqs-gallery,.collection-type-page.has-promoted-gallery.transparent-header .promoted-gallery-wrapper .sqs-gallery-block-slideshow .sqs-gallery,.collection-type-index.has-promoted-gallery.transparent-header .promoted-gallery-wrapper .sqs-gallery-block-slideshow .sqs-gallery{height:700px !important}.collection-type-page.has-promoted-gallery.collection-type-index.transparent-header .index-section:not(:first-of-type) #promotedGalleryWrapper .sqs-gallery-block-slideshow,.collection-type-index.has-promoted-gallery.collection-type-index.transparent-header .index-section:not(:first-of-type) #promotedGalleryWrapper .sqs-gallery-block-slideshow,.collection-type-page.has-promoted-gallery.collection-type-index.transparent-header .index-section:not(:first-of-type) .promoted-gallery-wrapper .sqs-gallery-block-slideshow,.collection-type-index.has-promoted-gallery.collection-type-index.transparent-header .index-section:not(:first-of-type) .promoted-gallery-wrapper .sqs-gallery-block-slideshow{height:600px !important}.collection-type-page.has-promoted-gallery.collection-type-index.transparent-header .index-section:not(:first-of-type) #promotedGalleryWrapper .sqs-gallery-block-slideshow .sqs-gallery,.collection-type-index.has-promoted-gallery.collection-type-index.transparent-header .index-section:not(:first-of-type) #promotedGalleryWrapper .sqs-gallery-block-slideshow .sqs-gallery,.collection-type-page.has-promoted-gallery.collection-type-index.transparent-header .index-section:not(:first-of-type) .promoted-gallery-wrapper .sqs-gallery-block-slideshow .sqs-gallery,.collection-type-index.has-promoted-gallery.collection-type-index.transparent-header .index-section:not(:first-of-type) .promoted-gallery-wrapper .sqs-gallery-block-slideshow .sqs-gallery{height:600px !important}.collection-type-page.has-promoted-gallery.loading #promotedGalleryWrapper .sqs-gallery-block-slideshow,.collection-type-index.has-promoted-gallery.loading #promotedGalleryWrapper .sqs-gallery-block-slideshow,.collection-type-page.has-promoted-gallery.loading .promoted-gallery-wrapper .sqs-gallery-block-slideshow,.collection-type-index.has-promoted-gallery.loading .promoted-gallery-wrapper .sqs-gallery-block-slideshow{opacity:0}.collection-type-page.has-promoted-gallery .banner-thumbnail-wrapper,.collection-type-index.has-promoted-gallery .promoted-full~.banner-thumbnail-wrapper{display:none}.collection-type-page.has-promoted-gallery .sqs-gallery-meta-container .sqs-gallery-controls .previous,.collection-type-index.has-promoted-gallery .sqs-gallery-meta-container .sqs-gallery-controls .previous,.collection-type-page.has-promoted-gallery .sqs-gallery-meta-container .sqs-gallery-controls .next,.collection-type-index.has-promoted-gallery .sqs-gallery-meta-container .sqs-gallery-controls .next{background-color:transparent}.collection-type-page.has-promoted-gallery .sqs-gallery-meta-container .sqs-gallery-controls .previous:hover,.collection-type-index.has-promoted-gallery .sqs-gallery-meta-container .sqs-gallery-controls .previous:hover,.collection-type-page.has-promoted-gallery .sqs-gallery-meta-container .sqs-gallery-controls .next:hover,.collection-type-index.has-promoted-gallery .sqs-gallery-meta-container .sqs-gallery-controls .next:hover{background-color:transparent}.banner-slideshow-controls-none .sqs-gallery-meta-container .sqs-gallery-controls .previous,.banner-slideshow-controls-none .sqs-gallery-meta-container .sqs-gallery-controls .next{display:none}.collection-type-page.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-thumbnails,.collection-type-index.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-thumbnails,.collection-type-page.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-thumbnails,.collection-type-index.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-thumbnails{display:none;height:0;padding:0;margin:0}.has-promoted-gallery .promoted-gallery-wrapper [data-type=\"video\"] .meta{display:none}.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-bottom .meta,.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-bottom .meta,.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-top .meta,.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-top .meta,.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-top-left .meta,.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-top-left .meta,.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-center .meta,.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-center .meta,.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-bottom-left .meta,.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-bottom-left .meta,.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-bottom-right .meta,.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-bottom-right .meta,.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-top-right .meta,.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-top-right .meta{left:50% !important;top:50% !important;-webkit-transform:translate(-50%,-45%) !important;-moz-transform:translate(-50%,-45%) !important;-ms-transform:translate(-50%,-45%) !important;-o-transform:translate(-50%,-45%) !important;transform:translate(-50%,-45%) !important;-webkit-transition:all .25s ease-in-out .3s;-moz-transition:all .25s ease-in-out .3s;-ms-transition:all .25s ease-in-out .3s;-o-transition:all .25s ease-in-out .3s;transition:all .25s ease-in-out .3s;opacity:0;margin:0 !important}.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-bottom .sqs-active-slide.loaded .meta,.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-bottom .sqs-active-slide.loaded .meta,.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-top .sqs-active-slide.loaded .meta,.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-top .sqs-active-slide.loaded .meta,.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-top-left .sqs-active-slide.loaded .meta,.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-top-left .sqs-active-slide.loaded .meta,.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-center .sqs-active-slide.loaded .meta,.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-center .sqs-active-slide.loaded .meta,.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-bottom-left .sqs-active-slide.loaded .meta,.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-bottom-left .sqs-active-slide.loaded .meta,.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-bottom-right .sqs-active-slide.loaded .meta,.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-bottom-right .sqs-active-slide.loaded .meta,.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-top-right .sqs-active-slide.loaded .meta,.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow.sqs-gallery-block-meta-position-top-right .sqs-active-slide.loaded .meta{-webkit-transform:translate(-50%,-50%) !important;-moz-transform:translate(-50%,-50%) !important;-ms-transform:translate(-50%,-50%) !important;-o-transform:translate(-50%,-50%) !important;transform:translate(-50%,-50%) !important;opacity:1}.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow .hide-body-text .meta .meta-description p,.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow .hide-body-text .meta .meta-description p{width:100%;visibility:hidden;line-height:0 !important;margin:0 auto}.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow .hide-body-text .meta .meta-description p:first-child>strong,.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow .hide-body-text .meta .meta-description p:first-child>strong{margin-top:0 !important}.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow .hide-body-text .meta .meta-description p>strong,.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow .hide-body-text .meta .meta-description p>strong,.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow .hide-body-text .meta .meta-description p>em>strong,.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow .hide-body-text .meta .meta-description p>em>strong{visibility:visible;line-height:1em !important;margin:20px auto}.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow .hide-body-text .meta .meta-description p>em>strong,.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow .hide-body-text .meta .meta-description p>em>strong{font-style:italic}.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow .hide-body-text .meta .meta-description p:last-child>a,.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow .hide-body-text .meta .meta-description p:last-child>a{visibility:visible;line-height:1em !important}.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow .meta,.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow .meta{text-align:center;background-color:transparent !important;background:none !important;margin:0;z-index:100;height:auto !important;overflow-y:visible !important;text-rendering:optimizeLegibility}.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow .meta .meta-inside,.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow .meta .meta-inside{padding:0 32px}.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow .meta .meta-title,.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow .meta .meta-title{display:none;margin:0;padding:0}.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow .meta .meta-description p,.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow .meta .meta-description p{font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-size:18px;letter-spacing:0px;line-height:1.5em;font-family:\"camingodos-web\";font-size:49px;line-height:1em;text-transform:none;letter-spacing:1px;font-weight:400;font-style:normal;color:#fff;margin:20px auto;width:700px !important}.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow .meta .meta-description p>strong,.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow .meta .meta-description p>strong,.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow .meta .meta-description p>em>strong,.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow .meta .meta-description p>em>strong{display:block;font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:400;font-size:48px;letter-spacing:0px;font-family:\"adelle-sans\";font-size:41px;line-height:1em;text-transform:none;letter-spacing:4px;font-weight:100;font-style:normal;color:#fff}.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow .meta .meta-description p>em>strong,.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow .meta .meta-description p>em>strong{font-style:italic}.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow .meta .meta-description p:last-child>a,.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow .meta .meta-description p:last-child>a{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-size:16px;letter-spacing:1px;font-family:\"proxima-nova\";font-size:20px;text-transform:uppercase;letter-spacing:2px;font-weight:700;font-style:normal;text-decoration:none;padding:1em 1.75em;display:inline-block;line-height:1em;margin:10px auto;color:#29292d}.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow .meta .meta-description p:last-child>a,.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow .meta .meta-description p:last-child>a{background-color:#23c890;-webkit-transition:background-color .1s 0s ease-in-out,color .1s 0s ease-in-out;-moz-transition:background-color .1s 0s ease-in-out,color .1s 0s ease-in-out;-ms-transition:background-color .1s 0s ease-in-out,color .1s 0s ease-in-out;-o-transition:background-color .1s 0s ease-in-out,color .1s 0s ease-in-out;transition:background-color .1s 0s ease-in-out,color .1s 0s ease-in-out}.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow .meta .meta-description p:last-child>a:hover,.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow .meta .meta-description p:last-child>a:hover{background-color:rgba(35,200,144,.8)}.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow .meta .meta-description p:empty,.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow .meta .meta-description p:empty{display:none}.transparent-header.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow .meta,.transparent-header.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow .meta{padding-top:25px}.collection-type-index.transparent-header.has-promoted-gallery .index-section:not(:first-of-type) #promotedGalleryWrapper .sqs-gallery-block-slideshow .meta,.collection-type-index.transparent-header.has-promoted-gallery .index-section:not(:first-of-type) .promoted-gallery-wrapper .sqs-gallery-block-slideshow .meta{padding-top:0}.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow .meta,.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow .meta{max-width:1084px !important;width:1084px !important;bottom:auto}.sqs-featured-posts-gallery .title-desc-wrapper{max-width:1084px !important;width:1084px !important;text-align:center}#promotedGalleryWrapper,.promoted-gallery-wrapper,.banner-thumbnail-wrapper,.sqs-featured-posts-gallery{background-color:#001a16;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased}#promotedGalleryWrapper .color-overlay,.promoted-gallery-wrapper .color-overlay,.banner-thumbnail-wrapper .color-overlay,.sqs-featured-posts-gallery .color-overlay{position:absolute;top:0;right:0;bottom:0;left:0;background-color:rgba(0,26,22,.5);z-index:99}.collection-type-blog .no-main-image .color-overlay{background-color:#001a16}.banner-thumbnail-wrapper{position:relative;overflow:hidden;min-height:320px;width:100%}.view-list .banner-thumbnail-wrapper,.collection-type-page .banner-thumbnail-wrapper,.collection-type-index .banner-thumbnail-wrapper{min-height:0;padding:130px 0}.transparent-header.view-list .banner-thumbnail-wrapper,.transparent-header.collection-type-page .banner-thumbnail-wrapper{padding:180px 0 155px}.collection-type-index.transparent-header.view-list .index-section:not(:first-of-type) .banner-thumbnail-wrapper,.collection-type-index.transparent-header.collection-type-page .index-section:not(:first-of-type) .banner-thumbnail-wrapper{padding:130px 0}#thumbnail{position:absolute;top:0;left:0;bottom:0;right:0;-webkit-animation:feature-bg-anim .6s ease-in-out;animation:feature-bg-anim .6s ease-in-out}.desc-wrapper{-webkit-animation:feature-text-anim .75s ease-in-out;animation:feature-text-anim .75s ease-in-out;z-index:100;position:relative;width:100%;max-width:956px;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;margin:0 auto;padding:32px;text-align:center;text-rendering:optimizeLegibility}.desc-wrapper p{font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-size:18px;letter-spacing:0px;line-height:1.5em;font-family:\"camingodos-web\";font-size:49px;line-height:1em;text-transform:none;letter-spacing:1px;font-weight:400;font-style:normal;color:#fff;margin:20px auto;-webkit-transform:translatez(0)}.desc-wrapper p a{color:#fff;border-bottom:1px solid #fff}.desc-wrapper p>strong,.desc-wrapper p>em>strong{display:block;font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:400;font-size:48px;letter-spacing:0px;font-family:\"adelle-sans\";font-size:41px;line-height:1em;text-transform:none;letter-spacing:4px;font-weight:100;font-style:normal;color:#fff}.desc-wrapper p>em>strong{font-style:italic}.desc-wrapper p:last-child>a{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-size:16px;letter-spacing:1px;font-family:\"proxima-nova\";font-size:20px;text-transform:uppercase;letter-spacing:2px;font-weight:700;font-style:normal;text-decoration:none;padding:1em 1.75em;background-color:#23c890;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;line-height:1em;margin:10px 0;color:#29292d;border:none;-webkit-transition:background-color .1s 0s ease-in-out,color .1s 0s ease-in-out;-moz-transition:background-color .1s 0s ease-in-out,color .1s 0s ease-in-out;-ms-transition:background-color .1s 0s ease-in-out,color .1s 0s ease-in-out;-o-transition:background-color .1s 0s ease-in-out,color .1s 0s ease-in-out;transition:background-color .1s 0s ease-in-out,color .1s 0s ease-in-out}.desc-wrapper p:last-child>a:hover{background-color:rgba(35,200,144,.8)}.desc-wrapper p:last-child>a+a{margin-left:1em}.desc-wrapper p:empty{display:none}body:not(.collection-type-gallery).banner-button-style-outline .desc-wrapper p:last-child>a,body:not(.collection-type-gallery).banner-button-style-outline.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow .meta-description p:last-child>a,body:not(.collection-type-gallery).banner-button-style-outline.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow .meta-description p:last-child>a{background-color:transparent;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;border:2px solid #23c890 !important;color:#23c890}body:not(.collection-type-gallery).banner-button-style-outline .desc-wrapper p:last-child>a:hover,body:not(.collection-type-gallery).banner-button-style-outline.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow .meta-description p:last-child>a:hover,body:not(.collection-type-gallery).banner-button-style-outline.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow .meta-description p:last-child>a:hover{background-color:#23c890;color:#181818;color:#efefef}body:not(.collection-type-gallery).banner-button-style-raised .desc-wrapper p:last-child>a,body:not(.collection-type-gallery).banner-button-style-raised.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow .meta-description p:last-child>a,body:not(.collection-type-gallery).banner-button-style-raised.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow .meta-description p:last-child>a{-webkit-box-shadow:0px .2em 0px 0px #1da577;-moz-box-shadow:0px .2em 0px 0px #1da577;box-shadow:0px .2em 0px 0px #1da577}body:not(.collection-type-gallery).banner-button-style-raised .desc-wrapper p:last-child>a:hover,body:not(.collection-type-gallery).banner-button-style-raised.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow .meta-description p:last-child>a:hover,body:not(.collection-type-gallery).banner-button-style-raised.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow .meta-description p:last-child>a:hover{background-color:#25d599}body:not(.collection-type-gallery).banner-button-corner-style-rounded .desc-wrapper p:last-child>a,body:not(.collection-type-gallery).banner-button-corner-style-rounded.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow .meta-description p:last-child>a,body:not(.collection-type-gallery).banner-button-corner-style-rounded.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow .meta-description p:last-child>a{-webkit-border-radius:3px;border-radius:3px}body:not(.collection-type-gallery).banner-button-corner-style-pill .desc-wrapper p:last-child>a,body:not(.collection-type-gallery).banner-button-corner-style-pill.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow .meta-description p:last-child>a,body:not(.collection-type-gallery).banner-button-corner-style-pill.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow .meta-description p:last-child>a{-webkit-border-radius:300px;border-radius:300px}#headerNav nav a,#sidecarNav nav a,#headerNav nav a:visited,#sidecarNav nav a:visited,#headerNav nav label,#sidecarNav nav label{font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:600;letter-spacing:1px;font-family:\"proxima-nova\";font-size:14px;text-transform:uppercase;text-decoration:none;letter-spacing:2px;font-weight:700;font-style:normal;line-height:1em;color:#fff}#headerNav nav a:hover,#sidecarNav nav a:hover,#headerNav nav a:visited:hover,#sidecarNav nav a:visited:hover,#headerNav nav label:hover,#sidecarNav nav label:hover{color:#1fd699}#headerNav nav .active>a,#sidecarNav nav .active>a,#headerNav nav .active>a:visited,#sidecarNav nav .active>a:visited,#headerNav nav .active>label,#sidecarNav nav .active>label{color:#1fd699}#headerNav nav .subnav,#sidecarNav nav .subnav{background-color:#212121}@media only screen and (min-width:641px){.show-on-scroll-wrapper #header{position:fixed !important;top:-20px;left:0;width:100%;visibility:hidden;opacity:0;background-color:#212121;-webkit-transform:translate3d(0,0,0);-moz-transform:translate3d(0,0,0);-ms-transform:translate3d(0,0,0);-o-transform:translate3d(0,0,0);transform:translate3d(0,0,0);-webkit-transition:opacity .14s ease-in-out,visibility 0s .14s linear,top .14s ease-in-out;-moz-transition:opacity .14s ease-in-out,visibility 0s .14s linear,top .14s ease-in-out;-ms-transition:opacity .14s ease-in-out,visibility 0s .14s linear,top .14s ease-in-out;-o-transition:opacity .14s ease-in-out,visibility 0s .14s linear,top .14s ease-in-out;transition:opacity .14s ease-in-out,visibility 0s .14s linear,top .14s ease-in-out}.show-on-scroll-wrapper.show #header{top:0;visibility:visible;opacity:1;-webkit-transition:opacity .14s ease-in-out,visibility 0s 0s linear,top .14s ease-in-out;-moz-transition:opacity .14s ease-in-out,visibility 0s 0s linear,top .14s ease-in-out;-ms-transition:opacity .14s ease-in-out,visibility 0s 0s linear,top .14s ease-in-out;-o-transition:opacity .14s ease-in-out,visibility 0s 0s linear,top .14s ease-in-out;transition:opacity .14s ease-in-out,visibility 0s 0s linear,top .14s ease-in-out}body:not(.force-mobile-nav) #headerNav{white-space:nowrap}body:not(.force-mobile-nav) .nav-wrapper{position:relative}body:not(.force-mobile-nav) .nav-wrapper nav>div{display:inline-block;vertical-align:middle;margin:0}body:not(.force-mobile-nav) .nav-wrapper nav>div a,body:not(.force-mobile-nav) .nav-wrapper nav>div label{-webkit-transition:color .1s 0s ease-in-out;-moz-transition:color .1s 0s ease-in-out;-ms-transition:color .1s 0s ease-in-out;-o-transition:color .1s 0s ease-in-out;transition:color .1s 0s ease-in-out}body:not(.force-mobile-nav) .nav-wrapper nav>div>a,body:not(.force-mobile-nav) .nav-wrapper nav>div label{display:block;padding:.75em 1em}body:not(.force-mobile-nav) .nav-wrapper nav>div:last-child>a,body:not(.force-mobile-nav) .nav-wrapper nav>div:last-child label{padding-right:0}body:not(.force-mobile-nav) .nav-wrapper#headerNav{text-align:right}body:not(.force-mobile-nav) #secondaryNavWrapper.nav-wrapper .folder .subnav{top:auto;bottom:100%;-webkit-transform-origin:0 100%;-moz-transform-origin:0 100%;-ms-transform-origin:0 100%;-o-transform-origin:0 100%;transform-origin:0 100%}html:not(.touch-styles) body:not(.force-mobile-nav) .nav-wrapper .folder .subnav{text-align:left;padding:1em 0;display:inline-block;position:absolute;top:100%;left:-.5em;z-index:1000;font-size:14px;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;white-space:nowrap;-webkit-transform:scale(1,0);-moz-transform:scale(1,0);-ms-transform:scale(1,0);-o-transform:scale(1,0);transform:scale(1,0);-webkit-transform-origin:0 0;-moz-transform-origin:0 0;-ms-transform-origin:0 0;-o-transform-origin:0 0;transform-origin:0 0;-webkit-transition:-webkit-transform .14s 0s ease-in-out;-moz-transition:-moz-transform .14s 0s ease-in-out;-ms-transition:-ms-transform .14s 0s ease-in-out;-o-transition:-o-transform .14s 0s ease-in-out;transition:transform .14s 0s ease-in-out}html:not(.touch-styles) body:not(.force-mobile-nav) .nav-wrapper .folder .subnav>div{opacity:0;padding:0;-webkit-transition:opacity .05s 0s ease-in-out;-moz-transition:opacity .05s 0s ease-in-out;-ms-transition:opacity .05s 0s ease-in-out;-o-transition:opacity .05s 0s ease-in-out;transition:opacity .05s 0s ease-in-out}html:not(.touch-styles) body:not(.force-mobile-nav) .nav-wrapper .folder .subnav>div a{display:block;padding:.5em 1.5em;-webkit-transform:translatez(0);-moz-transform:translatez(0);-ms-transform:translatez(0);-o-transform:translatez(0);transform:translatez(0)}html:not(.touch-styles) body:not(.force-mobile-nav) .nav-wrapper .folder .subnav.right{left:auto;right:-.5em}html:not(.touch-styles) body:not(.force-mobile-nav) .nav-wrapper .folder:hover .subnav{-webkit-transform:scale(1,1);-moz-transform:scale(1,1);-ms-transform:scale(1,1);-o-transform:scale(1,1);transform:scale(1,1)}html:not(.touch-styles) body:not(.force-mobile-nav) .nav-wrapper .folder:hover .subnav>div{opacity:1;-webkit-transition:opacity .14s .14s ease-in-out;-moz-transition:opacity .14s .14s ease-in-out;-ms-transition:opacity .14s .14s ease-in-out;-o-transition:opacity .14s .14s ease-in-out;transition:opacity .14s .14s ease-in-out}html:not(.touch-styles) body:not(.force-mobile-nav) #siteWrapper .nav-wrapper .folder:last-child .subnav{text-align:right;right:-1.5em;left:auto}}#sidecarNav .folder label:before{content:'+';padding-right:.25em;width:.75em;display:inline-block}#sidecarNav .folder .folder-toggle-box:checked~label:before{content:'–'}.touch-styles .folder{position:relative}.touch-styles .folder-toggle-label~.subnav{height:0;max-height:0;overflow:hidden;padding:0 1.5em 0;font-size:14px;text-align:left}.touch-styles .folder-toggle-label~.subnav>div{padding:1em 0 0}.touch-styles .folder-toggle-box:checked~.subnav{height:auto;max-height:999px;padding:0 1em 1em}.touch-styles #header .folder-toggle-label~.subnav{position:absolute;left:0}.touch-styles #header .folder:last-child .subnav{text-align:right;right:-1em;left:auto}.force-mobile-nav #sidecarNav .folder-toggle-label~.subnav{height:0;max-height:0;overflow:hidden;padding:0 1.5em;font-size:14px}.force-mobile-nav #sidecarNav .folder-toggle-label~.subnav>div{padding:.5em 0}.force-mobile-nav #sidecarNav .folder-toggle-box:checked~.subnav{height:auto;max-height:999px;padding:0 1em 1em}.force-mobile-nav #secondaryNavWrapper.nav-wrapper{position:relative}.force-mobile-nav #secondaryNavWrapper.nav-wrapper nav>div{display:inline-block;vertical-align:middle;margin:0}.force-mobile-nav #secondaryNavWrapper.nav-wrapper nav>div>a,.force-mobile-nav #secondaryNavWrapper.nav-wrapper nav>div label{display:block;padding:.75em 1em}.force-mobile-nav #secondaryNavWrapper.nav-wrapper nav>div:first-child>a,.force-mobile-nav #secondaryNavWrapper.nav-wrapper nav>div:first-child label{padding-left:0}.force-mobile-nav #secondaryNavWrapper.nav-wrapper .folder .subnav{display:inline-block;position:absolute;top:auto;bottom:100%;left:-.5em;z-index:1000;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;white-space:nowrap;-webkit-transform-origin:0 100%;-moz-transform-origin:0 100%;-ms-transform-origin:0 100%;-o-transform-origin:0 100%;transform-origin:0 100%;text-align:left;padding:1em 0;-webkit-transform:scale(1,0);-moz-transform:scale(1,0);-ms-transform:scale(1,0);-o-transform:scale(1,0);transform:scale(1,0);-webkit-transition:-webkit-transform .14s 0s ease-in-out;-moz-transition:-moz-transform .14s 0s ease-in-out;-ms-transition:-ms-transform .14s 0s ease-in-out;-o-transition:-o-transform .14s 0s ease-in-out;transition:transform .14s 0s ease-in-out}.force-mobile-nav #secondaryNavWrapper.nav-wrapper .folder .subnav>div{opacity:0;padding:0;-webkit-transition:opacity .05s 0s ease-in-out;-moz-transition:opacity .05s 0s ease-in-out;-ms-transition:opacity .05s 0s ease-in-out;-o-transition:opacity .05s 0s ease-in-out;transition:opacity .05s 0s ease-in-out}.force-mobile-nav #secondaryNavWrapper.nav-wrapper .folder .subnav>div a{display:block;padding:.5em 1.5em;-webkit-transform:translatez(0);-moz-transform:translatez(0);-ms-transform:translatez(0);-o-transform:translatez(0);transform:translatez(0)}.force-mobile-nav #secondaryNavWrapper.nav-wrapper .folder:last-child .subnav{text-align:right;right:-.5em;left:auto}.force-mobile-nav #secondaryNavWrapper.nav-wrapper .folder:hover .subnav{-webkit-transform:scale(1,1);-moz-transform:scale(1,1);-ms-transform:scale(1,1);-o-transform:scale(1,1);transform:scale(1,1)}.force-mobile-nav #secondaryNavWrapper.nav-wrapper .folder:hover .subnav>div{opacity:1;-webkit-transition:opacity .14s .14s ease-in-out;-moz-transition:opacity .14s .14s ease-in-out;-ms-transition:opacity .14s .14s ease-in-out;-o-transition:opacity .14s .14s ease-in-out;transition:opacity .14s .14s ease-in-out}.folder{position:relative}.folder-toggle-label{cursor:pointer}body{-webkit-animation:bugfix infinite 1s}@-webkit-keyframes bugfix{from{padding:0}to{padding:0}}#mobileNavToggle:checked~.body-overlay{position:absolute;top:0;bottom:0;left:0;right:0;z-index:9999;cursor:e-resize;-webkit-transform:translatex(-260px) translatez(0);-moz-transform:translatex(-260px) translatez(0);-ms-transform:translatex(-260px) translatez(0);-o-transform:translatex(-260px) translatez(0);transform:translatex(-260px) translatez(0)}#sidecarNav{position:fixed;width:260px;z-index:-1;top:0;right:0;bottom:0;height:100%;line-height:1em;text-align:left;overflow:auto;visibility:hidden;background-color:#212121;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;-webkit-transition:height 0s .14s linear,visibility 0s .14s linear;-moz-transition:height 0s .14s linear,visibility 0s .14s linear;-ms-transition:height 0s .14s linear,visibility 0s .14s linear;-o-transition:height 0s .14s linear,visibility 0s .14s linear;transition:height 0s .14s linear,visibility 0s .14s linear}#sidecarNav nav{padding:24px 36px 72px}#sidecarNav nav div{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}#sidecarNav nav div a,#sidecarNav nav div label{display:block;padding:.75em 0}#sidecarNav nav div .subnav>div a{padding:0 0 .5em}#sidecarNav nav div .subnav>div:last-child a{padding-bottom:1em}.force-mobile-nav #header #headerNav{display:none}.force-mobile-nav #sidecarNav .site-title{vertical-align:middle}.force-mobile-nav .mobile-nav-toggle-label{display:none;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;z-index:100;width:10%;position:absolute;z-index:1002;top:50%;right:20px;margin-top:-8px;padding:0;vertical-align:middle;line-height:16px;text-align:right;cursor:pointer;user-select:none;color:#fff;width:22px;height:22px}.force-mobile-nav .mobile-nav-toggle-label .top-bar,.force-mobile-nav .mobile-nav-toggle-label .middle-bar,.force-mobile-nav .mobile-nav-toggle-label .bottom-bar{width:22px;height:2px;background-color:#fff;-webkit-transition:-webkit-transform .1s 0s ease-in-out,top .1s .1s ease-in-out;-moz-transition:-moz-transform .1s 0s ease-in-out,top .1s .1s ease-in-out;-ms-transition:-ms-transform .1s 0s ease-in-out,top .1s .1s ease-in-out;-o-transition:-o-transform .1s 0s ease-in-out,top .1s .1s ease-in-out;transition:transform .1s 0s ease-in-out,top .1s .1s ease-in-out;-webkit-transform-origin:50% 50%;-moz-transform-origin:50% 50%;-ms-transform-origin:50% 50%;-o-transform-origin:50% 50%;transform-origin:50% 50%;position:absolute;top:0;right:0}.force-mobile-nav .mobile-nav-toggle-label .middle-bar{-webkit-transition:opacity 0s .15s linear;-moz-transition:opacity 0s .15s linear;-ms-transition:opacity 0s .15s linear;-o-transition:opacity 0s .15s linear;transition:opacity 0s .15s linear;top:7px}.force-mobile-nav .mobile-nav-toggle-label .bottom-bar{top:14px}.force-mobile-nav #mainNavWrapper{display:block;position:fixed;width:260px;z-index:-1;top:0;right:0;bottom:0;height:100%;line-height:1em;text-align:left;overflow:auto;background-color:#212121;-webkit-overflow-scrolling:touch;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;-webkit-transform:translate3d(260px,0,0);-moz-transform:translate3d(260px,0,0);-ms-transform:translate3d(260px,0,0);-o-transform:translate3d(260px,0,0);transform:translate3d(260px,0,0);-webkit-transition:-webkit-transform 0s 0s ease-in-out;-moz-transition:-moz-transform 0s 0s ease-in-out;-ms-transition:-ms-transform 0s 0s ease-in-out;-o-transition:-o-transform 0s 0s ease-in-out;transition:transform 0s 0s ease-in-out}.force-mobile-nav #mainNavWrapper nav{padding:32px 40px;display:none}.force-mobile-nav #mainNavWrapper nav div{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}.force-mobile-nav #mainNavWrapper nav div a,.force-mobile-nav #mainNavWrapper nav div label{display:block;padding:.75em 0}.force-mobile-nav #mainNavWrapper nav div .subnav>div a{padding:0 0 .5em}.force-mobile-nav #mainNavWrapper nav div .subnav>div:last-child a{padding-bottom:1em}.force-mobile-nav #siteWrapper{width:100%;-webkit-transition:-webkit-transform .2s ease-in-out;-moz-transition:-moz-transform .2s ease-in-out;-ms-transition:-ms-transform .2s ease-in-out;-o-transition:-o-transform .2s ease-in-out;transition:transform .2s ease-in-out}.force-mobile-nav #mobileNavToggle:checked~#siteWrapper{position:fixed;-webkit-transform:translatex(-260px);-webkit-transform:translate3d(-260px,0,0);-moz-transform:translatex(-260px) translatez(0);-moz-transform:translate3d(-260px,0,0);-ms-transform:translatex(-260px) translatez(0);-ms-transform:translate3d(-260px,0,0);-o-transform:translatex(-260px) translatez(0);-o-transform:translate3d(-260px,0,0);transform:translatex(-260px) translatez(0);transform:translate3d(-260px,0,0)}.force-mobile-nav #mobileNavToggle:checked~#siteWrapper .mobile-nav-toggle-label .top-bar,.force-mobile-nav #mobileNavToggle:checked~#siteWrapper .mobile-nav-toggle-label .bottom-bar{-webkit-transition:top .1s .1s ease-in-out,-webkit-transform .1s .2s ease-in-out;-moz-transition:top .1s .1s ease-in-out,-moz-transform .1s .2s ease-in-out;-ms-transition:top .1s .1s ease-in-out,-ms-transform .1s .2s ease-in-out;-o-transition:top .1s .1s ease-in-out,-o-transform .1s .2s ease-in-out;transition:top .1s .1s ease-in-out,transform .1s .2s ease-in-out}.force-mobile-nav #mobileNavToggle:checked~#siteWrapper .mobile-nav-toggle-label .top-bar{-webkit-transform:rotate(45deg);-moz-transform:rotate(45deg);-ms-transform:rotate(45deg);-o-transform:rotate(45deg);transform:rotate(45deg);top:7px}.force-mobile-nav #mobileNavToggle:checked~#siteWrapper .mobile-nav-toggle-label .middle-bar{opacity:0}.force-mobile-nav #mobileNavToggle:checked~#siteWrapper .mobile-nav-toggle-label .bottom-bar{-webkit-transform:rotate(-45deg);-moz-transform:rotate(-45deg);-ms-transform:rotate(-45deg);-o-transform:rotate(-45deg);transform:rotate(-45deg);top:7px}.force-mobile-nav #mobileNavToggle:checked~#sidecarNav{height:100%;visibility:visible;-webkit-transition:height 0s .14s linear,visibility 0s 0s linear;-moz-transition:height 0s .14s linear,visibility 0s 0s linear;-ms-transition:height 0s .14s linear,visibility 0s 0s linear;-o-transition:height 0s .14s linear,visibility 0s 0s linear;transition:height 0s .14s linear,visibility 0s 0s linear}.force-mobile-nav.enable-nav-button #sidecarNav nav>div:not(.folder):last-child a{display:inline-block;margin:1em 0 0 0;line-height:1;padding:1em 1.5em}.force-mobile-nav .folder-toggle-box:checked~.subnav{padding:.25em 0 .5em}@media only screen and (min-width:641px){.sqs-style-mode.dialog-open.force-mobile-nav #mobileNavToggle:checked~#siteWrapper{-webkit-transform:translate3d(-480px,0,0);-moz-transform:translate3d(-480px,0,0);-ms-transform:translate3d(-480px,0,0);-o-transform:translate3d(-480px,0,0);transform:translate3d(-480px,0,0)}}.mobile-nav-toggle-label{display:none}.enable-nav-button #headerNav nav>div:not(.folder):last-child a,.enable-nav-button #sidecarNav nav>div:not(.folder):last-child a{font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-family:\"proxima-nova\";text-transform:uppercase;text-decoration:none;letter-spacing:1px;font-weight:700;font-style:normal;margin-left:1em;padding:1em 1.5em !important;display:block;background-color:#23c890;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;color:#fff;-webkit-transition:background-color .1s 0s ease-in-out,color .1s 0s ease-in-out;-moz-transition:background-color .1s 0s ease-in-out,color .1s 0s ease-in-out;-ms-transition:background-color .1s 0s ease-in-out,color .1s 0s ease-in-out;-o-transition:background-color .1s 0s ease-in-out,color .1s 0s ease-in-out;transition:background-color .1s 0s ease-in-out,color .1s 0s ease-in-out}.enable-nav-button #headerNav nav>div:not(.folder):last-child a:hover,.enable-nav-button #sidecarNav nav>div:not(.folder):last-child a:hover{background-color:rgba(35,200,144,.8)}.nav-button-style-outline.enable-nav-button #headerNav nav>div:not(.folder):last-child a,.nav-button-style-outline.enable-nav-button #sidecarNav nav>div:not(.folder):last-child a{background-color:transparent;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;border:2px solid #23c890;color:#23c890}.nav-button-style-outline.enable-nav-button #headerNav nav>div:not(.folder):last-child a:hover,.nav-button-style-outline.enable-nav-button #sidecarNav nav>div:not(.folder):last-child a:hover{background-color:#23c890;color:#181818;color:#efefef}.nav-button-style-raised.enable-nav-button #headerNav nav>div:not(.folder):last-child a,.nav-button-style-raised.enable-nav-button #sidecarNav nav>div:not(.folder):last-child a{-webkit-box-shadow:0px .2em 0px 0px #1da577;-moz-box-shadow:0px .2em 0px 0px #1da577;box-shadow:0px .2em 0px 0px #1da577}.nav-button-style-raised.enable-nav-button #headerNav nav>div:not(.folder):last-child a:hover,.nav-button-style-raised.enable-nav-button #sidecarNav nav>div:not(.folder):last-child a:hover{background-color:#25d599}.nav-button-corner-style-rounded.enable-nav-button #headerNav nav>div:not(.folder):last-child a,.nav-button-corner-style-rounded.enable-nav-button #sidecarNav nav>div:not(.folder):last-child a{-webkit-border-radius:3px;border-radius:3px}.nav-button-corner-style-pill.enable-nav-button #headerNav nav>div:not(.folder):last-child a,.nav-button-corner-style-pill.enable-nav-button #sidecarNav nav>div:not(.folder):last-child a{-webkit-border-radius:300px;border-radius:300px}.back-to-top-nav{display:none}#secondaryNavWrapper{padding:0 0 1.5em;z-index:3;position:relative;line-height:1.25em;right:auto}#secondaryNavWrapper nav>div:first-child>a,#secondaryNavWrapper nav>div:first-child label{padding-left:0}#siteInfo{font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:600;letter-spacing:1px;font-family:\"proxima-nova\";font-size:14px;text-transform:uppercase;text-decoration:none;letter-spacing:2px;font-weight:400;font-style:normal;color:rgba(255,255,255,.8)}#siteInfo a,#siteInfo a:visited{color:rgba(255,255,255,.8)}.site-phone,.site-email{white-space:nowrap}.site-address+.site-phone,.site-address+.site-email,.site-phone+.site-email{margin-left:1em}.center-navigation--info #secondaryNavWrapper{text-align:center;left:auto}.center-navigation--info #siteInfo{text-align:center}.hide-site-info #siteInfo{display:none}#footerBlocks:not(.empty){margin-top:1.5em}.folder-nav-toggle-label,.category-nav-toggle-label{display:none}.sqs-simple-like{line-height:inherit}.sqs-simple-like .like-count{margin-right:1.2em}.sqs-simple-like .like-count:before{font-family:'squarespace-ui-font';font-style:normal;speak:none;font-weight:normal;-webkit-font-smoothing:antialiased;content:\"\\e012\";text-align:center;display:inline-block;vertical-align:middle}.sqs-simple-like .like-count:before{font-size:16px;width:16px;height:16px;line-height:16px}.sqs-simple-like .like-count:before{margin-right:.2em;position:relative;top:.13em;font-size:1.2em;width:auto;height:auto;line-height:inherit;text-align:left;vertical-align:initial}.sqs-simple-like .like-icon{display:none}.ss-social-button:before{font-family:'squarespace-ui-font';font-style:normal;speak:none;font-weight:normal;-webkit-font-smoothing:antialiased;content:\"\\e02b\";text-align:center;display:inline-block;vertical-align:middle}.ss-social-button:before{font-size:16px;width:16px;height:16px;line-height:16px}.ss-social-button:before{margin-right:.4em;font-size:.85em;width:auto;height:auto;line-height:inherit;text-align:left;vertical-align:initial}.ss-social-button div{display:inline-block}.ss-social-button-icon{display:none !important}#indexList figure{width:100%}#indexList figure a{display:block}#indexList figure img{max-width:100%}.embed-block iframe,.embed-block img{max-width:100%}.sqs-block.image-block .image-caption-wrapper p{font-size:.875em;line-height:1.25em}html.touch .squarespace-comments .comments-content .comment-list .comment .comment-header .controls .squarespace-comment-buttons .comment-control{opacity:1}#productList .product .product-title,.no-touch .product-list-titles-overlay #productList .product .product-title{font-size:1.25em;line-height:1.25em;margin-bottom:.75em}#productList .product .product-price,.no-touch .product-list-titles-overlay #productList .product .product-price{font-size:1em;line-height:1.25em;margin-top:.5em;margin-bottom:.75em}.collection-type-gallery.full-width-gallery #page{max-width:100%;padding:32px}.banner-thumbnail-wrapper.sqs-frontend-edit-wrapper.sqs-frontend-outline{outline-offset:0px}.banner-thumbnail-wrapper .sqs-frontend-edit{top:auto !important;bottom:1px !important;right:1px}.sqs-layout:not(.sqs-editing)>.sqs-row:last-child>[class*=sqs-col]>.sqs-block:last-child{padding-bottom:0}.sqs-layout:not(.sqs-editing)>.sqs-row:last-child>[class*=sqs-col]:first-child>.sqs-block:last-child{padding-bottom:0}.sqs-layout:not(.sqs-editing)>.sqs-row:last-child>[class*=sqs-col]:last-child>.sqs-block:last-child{padding-bottom:0}.has-promoted-gallery #promotedGalleryWrapper .reduce-text-size .meta .meta-description p,.has-promoted-gallery .promoted-gallery-wrapper .reduce-text-size .meta .meta-description p{font-size:14px;letter-spacing:2px}.has-promoted-gallery #promotedGalleryWrapper .reduce-text-size .meta .meta-description p>strong,.has-promoted-gallery .promoted-gallery-wrapper .reduce-text-size .meta .meta-description p>strong,.has-promoted-gallery #promotedGalleryWrapper .reduce-text-size .meta .meta-description p>em>strong,.has-promoted-gallery .promoted-gallery-wrapper .reduce-text-size .meta .meta-description p>em>strong{font-size:22px;letter-spacing:2px}.has-promoted-gallery #promotedGalleryWrapper .reduce-text-size .meta .meta-description p:last-child>a,.has-promoted-gallery .promoted-gallery-wrapper .reduce-text-size .meta .meta-description p:last-child>a{font-size:13px;letter-spacing:2px}.sqs-block-summary-v2 .summary-title,.sqs-block-summary-v2 .summary-heading{font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;text-transform:uppercase;letter-spacing:1px;font-weight:400;font-style:normal;color:#666}.sqs-block-summary-v2 .summary-title a,.sqs-block-summary-v2 .summary-heading a,.sqs-block-summary-v2 .summary-title a:link,.sqs-block-summary-v2 .summary-heading a:link,.sqs-block-summary-v2 .summary-title a:visited,.sqs-block-summary-v2 .summary-heading a:visited{color:#666}.sqs-block-summary-v2 .summary-title a:hover,.sqs-block-summary-v2 .summary-heading a:hover,.sqs-block-summary-v2 .summary-title a:link:hover,.sqs-block-summary-v2 .summary-heading a:link:hover,.sqs-block-summary-v2 .summary-title a:visited:hover,.sqs-block-summary-v2 .summary-heading a:visited:hover{color:#3d9991}.sqs-block-summary-v2 a,.sqs-block-summary-v2 a:link,.sqs-block-summary-v2 a:visited{color:#3d9991}.sqs-block-summary-v2 .summary-metadata-item{color:rgba(26,26,26,.4)}.sqs-block-summary-v2 .summary-metadata-item a,.sqs-block-summary-v2 .summary-metadata-item a:link,.sqs-block-summary-v2 .summary-metadata-item a:visited{color:rgba(26,26,26,.4)}.sqs-block-summary-v2 .summary-metadata-item a:hover,.sqs-block-summary-v2 .summary-metadata-item a:link:hover,.sqs-block-summary-v2 .summary-metadata-item a:visited:hover{color:#3d9991}#preFooter nav:not(.social-icons-style-border) a,#footer nav:not(.social-icons-style-border) a,#preFooter nav:not(.social-icons-style-border) a:visited,#footer nav:not(.social-icons-style-border) a:visited{border-bottom:none}#preFooter nav.sqs-svg-icon--list,#footer nav.sqs-svg-icon--list{text-decoration:none !important}#rightSidebar hr{margin:initial}.view-list .filter-heading{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;max-width:1020px;margin:0 auto -32px;padding:32px 32px 0}.view-list .filter-heading span:after{content:'\\00D7';padding-left:1em}.view-list .filter-heading a{font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:600;letter-spacing:1px;font-family:\"proxima-nova\";font-size:14px;text-transform:uppercase;letter-spacing:2px;font-weight:700;font-style:normal;color:#3d9991;text-decoration:none;padding:.5em 0;border-bottom:2px solid #3d9991}.view-list:not(.collection-type-blog) .filter-heading{display:none}.view-list .entry+.entry{margin-top:128px}.view-list .excerpt-thumb{display:none;height:12em;width:12em;margin:0 1em 2em 0;float:left;overflow:hidden}.view-list .excerpt-thumb a{display:block;height:100%}.view-list .p-summary p:first-child{margin-top:0}.view-item .blog-item article.has-main-image .meta-above-title,.view-item .blog-item article.has-main-image .entry-title,.view-item .blog-item article.has-main-image .entry-title-passthrough{display:none}.view-item .blog-item .entry-footer{margin-top:2em}.entry-header{margin-bottom:36px}.entry-dateline,.entry-byline,.entry-morefrom{display:inline}.entry-title{margin:12px 0}.entry-title-passthrough:after{content:\" \\279D\";font:normal .9em sans-serif}.entry-more-link{margin-bottom:0}.entry-more-link a{display:block;min-width:2em;min-height:1em}.entry-more-link a:before{content:\"Read More\"}.entry-more-link a:after{content:\" \\279D\";font:normal .9em sans-serif}.entry-footer{margin-top:1em;line-height:1.25em}.entry-tags,.entry-source{max-width:30em}.entry-source{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.entry-actions .entry-comments,.entry-actions .sqs-disqus-comment-link{display:inline-block;margin-right:1em;text-decoration:none}.entry-actions .entry-comments:before,.entry-actions .sqs-disqus-comment-link:before{font-family:'squarespace-ui-font';font-style:normal;speak:none;font-weight:normal;-webkit-font-smoothing:antialiased;content:\"\\e010\";text-align:center;display:inline-block;vertical-align:middle}.entry-actions .entry-comments:before,.entry-actions .sqs-disqus-comment-link:before{font-size:16px;width:16px;height:16px;line-height:16px}.entry-actions .entry-comments:before,.entry-actions .sqs-disqus-comment-link:before{margin-right:.2em;position:relative;top:.12em;font-size:1.2em;width:auto;height:auto;line-height:inherit;text-align:left;vertical-align:initial}.pagination{margin-top:6em}.pagination>div{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;display:inline-block;vertical-align:top;width:50%}.pagination>div a{display:block;color:#3d9991}.pagination .newer{padding-right:1em}.pagination .older{padding-left:1em;text-align:right}.center-entry-title--meta.hide-blog-sidebar .filter-heading,.center-entry-title--meta.hide-blog-sidebar .entry-header,.center-entry-title--meta.hide-blog-sidebar .entry-footer{text-align:center}.center-entry-title--meta.hide-blog-sidebar .entry-tags{margin-left:auto;margin-right:auto}.hide-entry-author:not(.meta-priority-author) .entry-author{display:none}.hide-blog-sidebar.collection-type-blog #page #rightSidebar{display:none}.hide-blog-sidebar.collection-type-blog #page #content{width:100%;max-width:700px;display:block;margin:0 auto}.hide-blog-sidebar.collection-type-blog.view-list .filter-heading{max-width:700px;padding-left:0}.hide-blog-recents #rightSidebar .recent-posts{display:none}.hide-blog-categories #rightSidebar .blog-categories{display:none}.hide-list-entry-footer.view-list .entry-footer{display:none}.meta-priority-date .meta-above-title .entry-morefrom{display:none}.meta-priority-date .meta-above-title .entry-author{display:none}.meta-priority-date .meta-below-title .entry-dateline{display:none}.meta-priority-date.hide-entry-author .no-categories{margin:0}.meta-priority-date:not(.hide-entry-author) .meta-below-title .entry-morefrom:before{content:'\\00B7';padding:0 .5em}.meta-priority-date .sqs-featured-posts-gallery .title-desc-wrapper .post-author,.meta-priority-date .blog-item-wrapper .post-author,.meta-priority-date .recent-posts .post-author{display:none}.meta-priority-date .sqs-featured-posts-gallery .title-desc-wrapper .post-category,.meta-priority-date .blog-item-wrapper .post-category,.meta-priority-date .recent-posts .post-category{display:none}.meta-priority-category .meta-above-title .entry-dateline{display:none}.meta-priority-category .meta-above-title .entry-author{display:none}.meta-priority-category .meta-below-title .entry-morefrom{display:none}.meta-priority-category:not(.hide-entry-author) .meta-below-title .entry-dateline:before{content:'\\00B7';padding:0 .5em}.meta-priority-category .sqs-featured-posts-gallery .title-desc-wrapper .post-author,.meta-priority-category .blog-item-wrapper .post-author,.meta-priority-category .recent-posts .post-author{display:none}.meta-priority-category .sqs-featured-posts-gallery .title-desc-wrapper .post-date,.meta-priority-category .blog-item-wrapper .post-date,.meta-priority-category .recent-posts .post-date{display:none}.meta-priority-author .meta-above-title .entry-dateline{display:none}.meta-priority-author .meta-above-title .entry-morefrom{display:none}.meta-priority-author .meta-below-title .entry-author{display:none}.meta-priority-author .meta-below-title .entry-morefrom:before{content:'\\00B7';padding:0 .5em}.meta-priority-author .sqs-featured-posts-gallery .title-desc-wrapper .post-date,.meta-priority-author .blog-item-wrapper .post-date,.meta-priority-author .recent-posts .post-date{display:none}.meta-priority-author .sqs-featured-posts-gallery .title-desc-wrapper .post-category,.meta-priority-author .blog-item-wrapper .post-category,.meta-priority-author .recent-posts .post-category{display:none}.meta-priority-none .meta-above-title .entry-dateline{display:none}.meta-priority-none .meta-above-title .entry-morefrom{display:none}.meta-priority-none .meta-above-title .entry-author{display:none}.meta-priority-none .entry-morefrom:before{content:'\\00B7';padding:0 .5em}.meta-priority-none:not(.hide-entry-author) .entry-dateline:before{content:'\\00B7';padding:0 .5em}.meta-priority-none .sqs-featured-posts-gallery .title-desc-wrapper .post-date,.meta-priority-none .blog-item-wrapper .post-date,.meta-priority-none .recent-posts .post-date{display:none}.meta-priority-none .sqs-featured-posts-gallery .title-desc-wrapper .post-category,.meta-priority-none .blog-item-wrapper .post-category,.meta-priority-none .recent-posts .post-category{display:none}.meta-priority-none .sqs-featured-posts-gallery .title-desc-wrapper .post-author,.meta-priority-none .blog-item-wrapper .post-author,.meta-priority-none .recent-posts .post-author{display:none}.collection-type-blog.view-item .banner-thumbnail-wrapper{min-height:0;padding:130px 0}.collection-type-blog.view-item.transparent-header .banner-thumbnail-wrapper{padding:180px 0 155px}.blog-item-wrapper{display:block;z-index:100;position:relative;width:100%;max-width:1084px;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;margin:0 auto;padding:32px;text-align:center;text-rendering:optimizeLegibility}.blog-item-wrapper .title-desc-wrapper{-webkit-animation:feature-text-anim .75s ease-in-out;animation:feature-text-anim .75s ease-in-out}.blog-item-wrapper .post-title{font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:400;font-size:48px;letter-spacing:0px;font-family:\"adelle-sans\";font-size:41px;line-height:1em;text-transform:none;letter-spacing:4px;font-weight:100;font-style:normal;-webkit-transform:translatez(0);text-decoration:none;color:#fff}.blog-item-wrapper .post-date,.blog-item-wrapper .post-author,.blog-item-wrapper .post-category{display:block;font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-size:18px;letter-spacing:0px;line-height:1.5em;font-family:\"camingodos-web\";font-size:49px;line-height:1em;text-transform:none;letter-spacing:1px;font-weight:400;font-style:normal;color:#fff;line-height:1.125em;margin-bottom:.75em;-webkit-transform:translatez(0)}.blog-item-wrapper .post-date a,.blog-item-wrapper .post-author a,.blog-item-wrapper .post-category a{color:#fff}.sqs-featured-posts-gallery .arrow.previous-slide:before{font-family:'squarespace-ui-font';font-style:normal;speak:none;font-weight:normal;-webkit-font-smoothing:antialiased;content:\"\\e02c\";text-align:center;display:inline-block;vertical-align:middle}.sqs-featured-posts-gallery .arrow.previous-slide:before{font-size:32px;width:32px;height:32px;line-height:32px}.sqs-featured-posts-gallery .arrow.previous-slide:before{font-size:24px;width:24px;height:24px;line-height:24px}.sqs-featured-posts-gallery .arrow.next-slide:before{font-family:'squarespace-ui-font';font-style:normal;speak:none;font-weight:normal;-webkit-font-smoothing:antialiased;content:\"\\e02d\";text-align:center;display:inline-block;vertical-align:middle}.sqs-featured-posts-gallery .arrow.next-slide:before{font-size:32px;width:32px;height:32px;line-height:32px}.sqs-featured-posts-gallery .arrow.next-slide:before{font-size:24px;width:24px;height:24px;line-height:24px}.sqs-featured-posts-gallery .arrow,.sqs-featured-posts-gallery .icons span{display:none;-moz-user-select:-moz-none;-webkit-user-select:none;-ms-user-select:none;user-select:none}.sqs-featured-posts-gallery .gallery-wrapper{position:relative;width:100%}.sqs-featured-posts-gallery .gallery-wrapper .posts{display:block;width:100%;height:600px !important}.sqs-featured-posts-gallery .gallery-wrapper .posts .post{height:600px !important;width:100%;background-color:#001a16;-webkit-transform:translatez(0);-moz-transform:translatez(0);-ms-transform:translatez(0);-o-transform:translatez(0);transform:translatez(0)}.sqs-featured-posts-gallery .gallery-wrapper .posts .post:not(:first-of-type){opacity:0}.sqs-featured-posts-gallery .gallery-wrapper .posts .post:first-of-type img{-webkit-animation:feature-bg-anim .6s ease-in-out;animation:feature-bg-anim .6s ease-in-out}.sqs-featured-posts-gallery .gallery-wrapper .posts .post a{display:block}.sqs-featured-posts-gallery.loaded .gallery-wrapper .posts .post{opacity:1}.sqs-featured-posts-gallery .slides-controls{position:relative;z-index:991;overflow:hidden;cursor:pointer}.sqs-featured-posts-gallery .circles{display:none;margin:20px 0;cursor:pointer}.sqs-featured-posts-gallery .circles.sqs-gallery-controls-disabled{display:none}.sqs-featured-posts-gallery.sqs-featured-posts-gallery-interaction .arrow{opacity:0}.sqs-featured-posts-gallery.sqs-featured-posts-gallery-interaction.sqs-featured-posts-gallery-hover-slides-left .arrow.previous-slide:not(.sqs-disabled),.sqs-featured-posts-gallery.sqs-featured-posts-gallery-video-iframe .arrow.previous-slide:not(.sqs-disabled){opacity:1}.sqs-featured-posts-gallery.sqs-featured-posts-gallery-interaction.sqs-featured-posts-gallery-hover-slides-right .arrow.next-slide:not(.sqs-disabled),.sqs-featured-posts-gallery.sqs-featured-posts-gallery-video-iframe .arrow.next-slide:not(.sqs-disabled){opacity:1}.sqs-featured-posts-gallery .title-desc-wrapper{position:absolute;left:50%;top:50%;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;padding:32px 0;-webkit-transform:translate(-50%,-45%) !important;-moz-transform:translate(-50%,-45%) !important;-ms-transform:translate(-50%,-45%) !important;-o-transform:translate(-50%,-45%) !important;transform:translate(-50%,-45%) !important;z-index:1000;opacity:0;-webkit-transition:all .25s ease-in-out .3s;-moz-transition:all .25s ease-in-out .3s;-ms-transition:all .25s ease-in-out .3s;-o-transition:all .25s ease-in-out .3s;transition:all .25s ease-in-out .3s;text-rendering:optimizeLegibility}.sqs-featured-posts-gallery .title-desc-wrapper .post-title{margin-bottom:.75em;-webkit-transform:translatez(0)}.sqs-featured-posts-gallery .title-desc-wrapper .post-title a{font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:400;font-size:48px;letter-spacing:0px;font-family:\"adelle-sans\";font-size:41px;line-height:1em;text-transform:none;letter-spacing:4px;font-weight:100;font-style:normal;text-decoration:none;color:#fff;padding-left:4px}.sqs-featured-posts-gallery .title-desc-wrapper .post-date,.sqs-featured-posts-gallery .title-desc-wrapper .post-author,.sqs-featured-posts-gallery .title-desc-wrapper .post-category{display:block;font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-size:18px;letter-spacing:0px;line-height:1.5em;font-family:\"camingodos-web\";font-size:49px;line-height:1em;text-transform:none;letter-spacing:1px;font-weight:400;font-style:normal;color:#fff;line-height:1.125em;margin-bottom:.75em;-webkit-transform:translatez(0)}.sqs-featured-posts-gallery .title-desc-wrapper .post-date a,.sqs-featured-posts-gallery .title-desc-wrapper .post-author a,.sqs-featured-posts-gallery .title-desc-wrapper .post-category a{color:#fff}.sqs-featured-posts-gallery .title-desc-wrapper .post-excerpt{font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-size:18px;letter-spacing:0px;line-height:1.5em;font-family:\"camingodos-web\";font-size:49px;line-height:1em;text-transform:none;letter-spacing:1px;font-weight:400;font-style:normal;color:#fff;margin-bottom:.75em;display:none}.sqs-featured-posts-gallery .title-desc-wrapper .post-excerpt p{margin:0}.sqs-featured-posts-gallery .title-desc-wrapper .post-excerpt p~p{margin-top:.75em}.sqs-featured-posts-gallery .title-desc-wrapper .view-post{font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:600;letter-spacing:1px;font-family:\"proxima-nova\";font-size:14px;text-transform:uppercase;letter-spacing:2px;font-weight:700;font-style:normal;text-decoration:none;display:block;-webkit-transform:translatez(0);line-height:1em;margin-top:1.4em}.sqs-featured-posts-gallery .title-desc-wrapper .view-post:before{content:'View Post';display:inline-block;font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-size:18px;letter-spacing:0px;line-height:1.5em;font-family:\"camingodos-web\";font-size:49px;line-height:1em;text-transform:none;letter-spacing:1px;font-weight:400;font-style:normal;color:#fff;line-height:1.125em;vertical-align:middle}.sqs-featured-posts-gallery .title-desc-wrapper .view-post:after{display:inline-block;content:'\\279D';color:#fff;font:normal .9em sans-serif;margin-left:6px;vertical-align:middle}.sqs-featured-posts-gallery .loaded .title-desc-wrapper{-webkit-transform:translate(-50%,-50%) !important;-moz-transform:translate(-50%,-50%) !important;-ms-transform:translate(-50%,-50%) !important;-o-transform:translate(-50%,-50%) !important;transform:translate(-50%,-50%) !important;opacity:1}.transparent-header .sqs-featured-posts-gallery .gallery-wrapper .posts{height:700px !important}.transparent-header .sqs-featured-posts-gallery .gallery-wrapper .posts .post{height:100% !important}.transparent-header .sqs-featured-posts-gallery .title-desc-wrapper{padding:57px 0 32px}.banner-slideshow-controls-both .sqs-featured-posts-gallery .arrow,.banner-slideshow-controls-arrows .sqs-featured-posts-gallery .arrow{display:block;position:absolute;top:50%;outline:none;color:#fff !important;z-index:999;font-size:14px;line-height:40px;margin-top:-30px;display:inline-block;padding:10px;cursor:pointer}.banner-slideshow-controls-both .sqs-featured-posts-gallery .arrow.previous-slide,.banner-slideshow-controls-arrows .sqs-featured-posts-gallery .arrow.previous-slide{left:0}.banner-slideshow-controls-both .sqs-featured-posts-gallery .arrow.next-slide,.banner-slideshow-controls-arrows .sqs-featured-posts-gallery .arrow.next-slide{right:0;float:right}.banner-slideshow-controls-both .sqs-featured-posts-gallery .arrow.sqs-disabled,.banner-slideshow-controls-arrows .sqs-featured-posts-gallery .arrow.sqs-disabled{opacity:0}.collection-type-index #page{max-width:100%;padding:0}.collection-type-index .page-content{max-width:1020px;margin:0 auto;padding:96px 32px}.collection-type-index .promoted-gallery-wrapper .sqs-block{padding-top:0;padding-bottom:0}.index-section.empty .page-content{display:none}@media only screen and (max-width:1024px){.touch-styles a,.touch-styles label{-webkit-tap-highlight-color:rgba(0,0,0,0) !important;-moz-tap-highlight-color:rgba(0,0,0,0) !important;tap-highlight-color:rgba(0,0,0,0) !important}.sqs-block-horizontalrule hr{margin:32px 0}.sqs-featured-posts-gallery .title-desc-wrapper{max-width:90% !important;width:90% !important;text-align:center;padding:0 20px}.sqs-featured-posts-gallery .title-desc-wrapper .post-excerpt{max-width:90%}}@media only screen and (max-device-height:768px){.collection-type-page.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow,.collection-type-index.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow,.collection-type-page.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow,.collection-type-index.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow{height:600px !important}.collection-type-page.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow .sqs-gallery,.collection-type-index.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow .sqs-gallery,.collection-type-page.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow .sqs-gallery,.collection-type-index.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow .sqs-gallery{height:600px !important}.collection-type-page.has-promoted-gallery.transparent-header #promotedGalleryWrapper .sqs-gallery-block-slideshow,.collection-type-index.has-promoted-gallery.transparent-header #promotedGalleryWrapper .sqs-gallery-block-slideshow,.collection-type-page.has-promoted-gallery.transparent-header .promoted-gallery-wrapper .sqs-gallery-block-slideshow,.collection-type-index.has-promoted-gallery.transparent-header .promoted-gallery-wrapper .sqs-gallery-block-slideshow{height:640px !important}.collection-type-page.has-promoted-gallery.transparent-header #promotedGalleryWrapper .sqs-gallery-block-slideshow .sqs-gallery,.collection-type-index.has-promoted-gallery.transparent-header #promotedGalleryWrapper .sqs-gallery-block-slideshow .sqs-gallery,.collection-type-page.has-promoted-gallery.transparent-header .promoted-gallery-wrapper .sqs-gallery-block-slideshow .sqs-gallery,.collection-type-index.has-promoted-gallery.transparent-header .promoted-gallery-wrapper .sqs-gallery-block-slideshow .sqs-gallery{height:640px !important}.collection-type-page.has-promoted-gallery.transparent-header #promotedGalleryWrapper .sqs-gallery-block-slideshow .meta .meta-inside,.collection-type-index.has-promoted-gallery.transparent-header #promotedGalleryWrapper .sqs-gallery-block-slideshow .meta .meta-inside,.collection-type-page.has-promoted-gallery.transparent-header .promoted-gallery-wrapper .sqs-gallery-block-slideshow .meta .meta-inside,.collection-type-index.has-promoted-gallery.transparent-header .promoted-gallery-wrapper .sqs-gallery-block-slideshow .meta .meta-inside{padding-top:40px}.collection-type-page.has-promoted-gallery.transparent-header.collection-type-index .index-section:not(:first-of-type) #promotedGalleryWrapper .sqs-gallery-block-slideshow,.collection-type-index.has-promoted-gallery.transparent-header.collection-type-index .index-section:not(:first-of-type) #promotedGalleryWrapper .sqs-gallery-block-slideshow,.collection-type-page.has-promoted-gallery.transparent-header.collection-type-index .index-section:not(:first-of-type) .promoted-gallery-wrapper .sqs-gallery-block-slideshow,.collection-type-index.has-promoted-gallery.transparent-header.collection-type-index .index-section:not(:first-of-type) .promoted-gallery-wrapper .sqs-gallery-block-slideshow{height:600px !important}.collection-type-page.has-promoted-gallery.transparent-header.collection-type-index .index-section:not(:first-of-type) #promotedGalleryWrapper .sqs-gallery-block-slideshow .sqs-gallery,.collection-type-index.has-promoted-gallery.transparent-header.collection-type-index .index-section:not(:first-of-type) #promotedGalleryWrapper .sqs-gallery-block-slideshow .sqs-gallery,.collection-type-page.has-promoted-gallery.transparent-header.collection-type-index .index-section:not(:first-of-type) .promoted-gallery-wrapper .sqs-gallery-block-slideshow .sqs-gallery,.collection-type-index.has-promoted-gallery.transparent-header.collection-type-index .index-section:not(:first-of-type) .promoted-gallery-wrapper .sqs-gallery-block-slideshow .sqs-gallery{height:600px !important}.collection-type-page.has-promoted-gallery.transparent-header.collection-type-index .index-section:not(:first-of-type) #promotedGalleryWrapper .sqs-gallery-block-slideshow .meta .meta-inside,.collection-type-index.has-promoted-gallery.transparent-header.collection-type-index .index-section:not(:first-of-type) #promotedGalleryWrapper .sqs-gallery-block-slideshow .meta .meta-inside,.collection-type-page.has-promoted-gallery.transparent-header.collection-type-index .index-section:not(:first-of-type) .promoted-gallery-wrapper .sqs-gallery-block-slideshow .meta .meta-inside,.collection-type-index.has-promoted-gallery.transparent-header.collection-type-index .index-section:not(:first-of-type) .promoted-gallery-wrapper .sqs-gallery-block-slideshow .meta .meta-inside{padding-top:0}.sqs-featured-posts-gallery .gallery-wrapper .posts{height:600px !important}.sqs-featured-posts-gallery .gallery-wrapper .posts .post{height:600px !important}.transparent-header .sqs-featured-posts-gallery .gallery-wrapper .posts{height:640px !important}.transparent-header .sqs-featured-posts-gallery .gallery-wrapper .posts .post{height:640px !important}.transparent-header .sqs-featured-posts-gallery .title-desc-wrapper{padding:60px 20px 20px}.view-list .banner-thumbnail-wrapper,.collection-type-page .banner-thumbnail-wrapper,.collection-type-blog.view-item .banner-thumbnail-wrapper{padding-top:0;padding-bottom:0}.view-list .banner-thumbnail-wrapper:not(.has-description),.collection-type-page .banner-thumbnail-wrapper:not(.has-description),.collection-type-blog.view-item .banner-thumbnail-wrapper:not(.has-description){min-height:120px}.view-list.transparent-header .banner-thumbnail-wrapper,.collection-type-page.transparent-header .banner-thumbnail-wrapper,.collection-type-blog.view-item.transparent-header .banner-thumbnail-wrapper{padding:60px 0 20px}.collection-type-index.view-list.transparent-header .index-section:not(:first-of-type) .banner-thumbnail-wrapper{padding-top:0;padding-bottom:0}}@media only screen and (max-device-height:640px){.collection-type-page.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow,.collection-type-index.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow,.collection-type-page.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow,.collection-type-index.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow{height:300px !important}.collection-type-page.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow .sqs-gallery,.collection-type-index.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow .sqs-gallery,.collection-type-page.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow .sqs-gallery,.collection-type-index.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow .sqs-gallery{height:300px !important}.collection-type-page.has-promoted-gallery.transparent-header #promotedGalleryWrapper .sqs-gallery-block-slideshow,.collection-type-index.has-promoted-gallery.transparent-header #promotedGalleryWrapper .sqs-gallery-block-slideshow,.collection-type-page.has-promoted-gallery.transparent-header .promoted-gallery-wrapper .sqs-gallery-block-slideshow,.collection-type-index.has-promoted-gallery.transparent-header .promoted-gallery-wrapper .sqs-gallery-block-slideshow{height:340px !important}.collection-type-page.has-promoted-gallery.transparent-header #promotedGalleryWrapper .sqs-gallery-block-slideshow .sqs-gallery,.collection-type-index.has-promoted-gallery.transparent-header #promotedGalleryWrapper .sqs-gallery-block-slideshow .sqs-gallery,.collection-type-page.has-promoted-gallery.transparent-header .promoted-gallery-wrapper .sqs-gallery-block-slideshow .sqs-gallery,.collection-type-index.has-promoted-gallery.transparent-header .promoted-gallery-wrapper .sqs-gallery-block-slideshow .sqs-gallery{height:340px !important}.collection-type-page.has-promoted-gallery.transparent-header.collection-type-index .index-section:not(:first-of-type) #promotedGalleryWrapper .sqs-gallery-block-slideshow,.collection-type-index.has-promoted-gallery.transparent-header.collection-type-index .index-section:not(:first-of-type) #promotedGalleryWrapper .sqs-gallery-block-slideshow,.collection-type-page.has-promoted-gallery.transparent-header.collection-type-index .index-section:not(:first-of-type) .promoted-gallery-wrapper .sqs-gallery-block-slideshow,.collection-type-index.has-promoted-gallery.transparent-header.collection-type-index .index-section:not(:first-of-type) .promoted-gallery-wrapper .sqs-gallery-block-slideshow{height:300px !important}.collection-type-page.has-promoted-gallery.transparent-header.collection-type-index .index-section:not(:first-of-type) #promotedGalleryWrapper .sqs-gallery-block-slideshow .sqs-gallery,.collection-type-index.has-promoted-gallery.transparent-header.collection-type-index .index-section:not(:first-of-type) #promotedGalleryWrapper .sqs-gallery-block-slideshow .sqs-gallery,.collection-type-page.has-promoted-gallery.transparent-header.collection-type-index .index-section:not(:first-of-type) .promoted-gallery-wrapper .sqs-gallery-block-slideshow .sqs-gallery,.collection-type-index.has-promoted-gallery.transparent-header.collection-type-index .index-section:not(:first-of-type) .promoted-gallery-wrapper .sqs-gallery-block-slideshow .sqs-gallery{height:300px !important}.sqs-featured-posts-gallery .gallery-wrapper .posts{height:300px !important}.sqs-featured-posts-gallery .gallery-wrapper .posts .post{height:300px !important}.transparent-header .sqs-featured-posts-gallery .gallery-wrapper .posts{height:340px !important}.transparent-header .sqs-featured-posts-gallery .gallery-wrapper .posts .post{height:340px !important}.view-list .banner-thumbnail-wrapper:not(.has-description),.collection-type-page .banner-thumbnail-wrapper:not(.has-description),.collection-type-blog.view-item .banner-thumbnail-wrapper:not(.has-description){min-height:80px}}@media only screen and (max-width:767px){#page{padding:32px}.collection-type-blog:not(.hide-sidebar) #content,.collection-type-blog:not(.hide-sidebar) #rightSidebar{display:block;width:100%}.collection-type-blog:not(.hide-sidebar) #rightSidebar{padding-top:20px;padding-left:0}.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow .meta,.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow .meta{max-width:90% !important;width:90% !important}.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow .meta .meta-inside,.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow .meta .meta-inside{padding:0 20px}.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow .meta .meta-inside p,.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow .meta .meta-inside p{width:90% !important}.sqs-featured-posts-gallery .title-desc-wrapper{max-width:90% !important;width:90% !important;text-align:center;padding:0 20px}.sqs-featured-posts-gallery .title-desc-wrapper .post-excerpt{max-width:90%}}@media only screen and (max-device-width:667px){.back-to-top-nav{display:block}.back-to-top{display:inline-block}.back-to-top a{display:block;padding:.75em 1em}}@media only screen and (max-width:640px){.sqs-layout [class*=sqs-col]{float:none !important;width:auto !important}.sqs-layout .spacer-block{display:none}.sqs-layout .sqs-row .sqs-block:first-child{padding-top:17px !important}.sqs-layout .sqs-row .sqs-block:last-child{padding-bottom:17px !important}.sqs-layout .sqs-row+.sqs-row,.sqs-layout .sqs-row+.sqs-block{margin-top:0 !important}.sqs-gallery-design-grid-slide{width:50% !important;margin:0 0 10px 0 !important}#page{padding:40px 20px}#header{padding:0 20px}h1,.entry-title{font-size:30px}.sqs-block-horizontalrule hr{margin:initial}blockquote{padding:.5em 20px}.quote-block figure{padding:20px}.entry-header{margin-bottom:12px}.view-list .filter-heading{margin:0 auto;padding:1em 1em 0}.view-list .entry+.entry{margin-top:80px}body:not(.collection-type-gallery) .desc-wrapper p,body:not(.collection-type-gallery).has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow .meta-description p,body:not(.collection-type-gallery).has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow .meta-description p{font-size:18px;margin:10px auto}body:not(.collection-type-gallery) .desc-wrapper p>strong,body:not(.collection-type-gallery).has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow .meta-description p>strong,body:not(.collection-type-gallery).has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow .meta-description p>strong,body:not(.collection-type-gallery) .desc-wrapper p>em>strong,body:not(.collection-type-gallery).has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow .meta-description p>em>strong,body:not(.collection-type-gallery).has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow .meta-description p>em>strong{font-size:30px;letter-spacing:2px}body:not(.collection-type-gallery) .desc-wrapper p:last-child>a,body:not(.collection-type-gallery).has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow .meta-description p:last-child>a,body:not(.collection-type-gallery).has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow .meta-description p:last-child>a{font-size:13px;margin:5px auto}.collection-type-page.has-promoted-gallery .main-content .sqs-layout>.sqs-row:first-child>.sqs-col-12:first-child{width:100% !important}.collection-type-page.has-promoted-gallery .main-content .sqs-layout>.sqs-row:first-child>.sqs-col-12:first-child>.gallery-block:first-child{padding-top:0 !important;padding-bottom:0 !important}.collection-type-page.has-promoted-gallery .main-content .sqs-layout>.sqs-row:first-child>.sqs-col-12:first-child>.gallery-block:first-child .sqs-gallery{height:300px !important}.collection-type-page.has-promoted-gallery #promotedGalleryWrapper .sqs-gallery-block-slideshow .meta .meta-inside,.collection-type-index.has-promoted-gallery .promoted-gallery-wrapper .sqs-gallery-block-slideshow .meta .meta-inside{padding:20px}.collection-type-page.has-promoted-gallery #promotedGalleryWrapper .sqs-video-wrapper+.meta,.collection-type-index.has-promoted-gallery .promoted-gallery-wrapper .sqs-video-wrapper+.meta{display:none}.sqs-featured-posts-gallery .title-desc-wrapper{padding:20px 20px}.sqs-featured-posts-gallery .title-desc-wrapper .post-title,.sqs-featured-posts-gallery .title-desc-wrapper .post-title a{font-size:30px;letter-spacing:2px}.sqs-featured-posts-gallery .title-desc-wrapper .post-excerpt{display:none}.blog-item-wrapper .post-title,.blog-item-wrapper .post-title a{font-size:30px;letter-spacing:2px}.blog-item-wrapper .post-date,.blog-item-wrapper .post-author,.blog-item-wrapper .post-category,.blog-item-wrapper .post-date a,.blog-item-wrapper .post-author a,.blog-item-wrapper .post-category a{font-size:18px}.header-inner{padding:20px 0;display:block}.footer-inner,.pre-footer-inner .sqs-layout{padding:20px}#logoWrapper,#siteTitleWrapper{display:inline-block;vertical-align:middle;max-width:240px;padding:0;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}#logoWrapper #logoImage img,#siteTitleWrapper #logoImage img{max-height:50px;max-width:100%;width:auto;height:auto}#logoWrapper{width:140px}#productList .product{margin-bottom:40px}#productList .product .product-title{margin-top:.5em}.sqs-featured-posts-gallery .title-desc-wrapper .view-post{display:none}.sqs-featured-posts-gallery .title-desc-wrapper .view-post:before,.sqs-featured-posts-gallery .title-desc-wrapper .view-post:after{display:none}.sqs-featured-posts-gallery .title-desc-wrapper .post-date,.sqs-featured-posts-gallery .title-desc-wrapper .post-category,.sqs-featured-posts-gallery .title-desc-wrapper .post-author{font-size:16px}.index-section-wrapper.page-content{padding:20px}body{-webkit-animation:bugfix infinite 1s}@-webkit-keyframes bugfix{from{padding:0}to{padding:0}}#headerNav{display:none}#siteTitle,#siteTitle a{font-size:16px;line-height:1}#showOnScrollWrapper{display:none}.mobile-nav-toggle-label{display:none;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;z-index:100;width:10%;position:absolute;z-index:1002;top:50%;right:20px;margin-top:-8px;padding:0;vertical-align:middle;line-height:16px;text-align:right;cursor:pointer;user-select:none;color:#fff;width:22px;height:22px}.mobile-nav-toggle-label .top-bar,.mobile-nav-toggle-label .middle-bar,.mobile-nav-toggle-label .bottom-bar{width:22px;height:2px;background-color:#fff;-webkit-transition:-webkit-transform .15s 0s ease-in-out,top .15s .15s ease-in-out;-moz-transition:-moz-transform .15s 0s ease-in-out,top .15s .15s ease-in-out;-ms-transition:-ms-transform .15s 0s ease-in-out,top .15s .15s ease-in-out;-o-transition:-o-transform .15s 0s ease-in-out,top .15s .15s ease-in-out;transition:transform .15s 0s ease-in-out,top .15s .15s ease-in-out;-webkit-transform-origin:50% 50%;-moz-transform-origin:50% 50%;-ms-transform-origin:50% 50%;-o-transform-origin:50% 50%;transform-origin:50% 50%;position:absolute;top:0;right:0}.mobile-nav-toggle-label .middle-bar{-webkit-transition:opacity 0s .15s linear;-moz-transition:opacity 0s .15s linear;-ms-transition:opacity 0s .15s linear;-o-transition:opacity 0s .15s linear;transition:opacity 0s .15s linear;top:7px}.mobile-nav-toggle-label .bottom-bar{top:14px}.mobile-nav-toggle-label.fixed-nav-toggle-label{position:fixed;top:5px;right:5px;z-index:1001;visibility:hidden;opacity:0;padding:20px;margin-top:0;background-color:#212121;width:42px;height:36px;-webkit-transition:opacity .17s ease-in-out;-moz-transition:opacity .17s ease-in-out;-ms-transition:opacity .17s ease-in-out;-o-transition:opacity .17s ease-in-out;transition:opacity .17s ease-in-out}.mobile-nav-toggle-label.fixed-nav-toggle-label .top-bar,.mobile-nav-toggle-label.fixed-nav-toggle-label .middle-bar,.mobile-nav-toggle-label.fixed-nav-toggle-label .bottom-bar{margin-top:12px;margin-right:10px}.fix-header-nav .mobile-nav-toggle-label.fixed-nav-toggle-label{visibility:visible;opacity:1}#sidecarNav .folder-toggle-label~.subnav,#secondaryNavWrapper .folder-toggle-label~.subnav{height:0;max-height:0;overflow:hidden;padding:0 1.5em;font-size:14px}#sidecarNav .folder-toggle-label~.subnav>div,#secondaryNavWrapper .folder-toggle-label~.subnav>div{padding:.5em 0}#sidecarNav .folder-toggle-box:checked~.subnav,#secondaryNavWrapper .folder-toggle-box:checked~.subnav{height:auto;max-height:999px;padding:0 1em 1em}#siteWrapper{height:99.9%;width:100%;-webkit-transition:-webkit-transform .14s ease-in-out;-moz-transition:-moz-transform .14s ease-in-out;-ms-transition:-ms-transform .14s ease-in-out;-o-transition:-o-transform .14s ease-in-out;transition:transform .14s ease-in-out}#mobileNavToggle:checked~#sidecarNav{-webkit-overflow-scrolling:touch;visibility:visible;-webkit-transition:height 0s .14s linear,visibility 0s 0s linear;-moz-transition:height 0s .14s linear,visibility 0s 0s linear;-ms-transition:height 0s .14s linear,visibility 0s 0s linear;-o-transition:height 0s .14s linear,visibility 0s 0s linear;transition:height 0s .14s linear,visibility 0s 0s linear}#mobileNavToggle:checked~.sqs-announcement-bar-dropzone{display:none}#mobileNavToggle:checked~#siteWrapper{position:fixed;height:100%;-webkit-transform:translate3d(-260px,0,0);-moz-transform:translate3d(-260px,0,0);-ms-transform:translate3d(-260px,0,0);-o-transform:translate3d(-260px,0,0);transform:translate3d(-260px,0,0)}#mobileNavToggle:checked~#siteWrapper .mobile-nav-toggle-label .top-bar,#mobileNavToggle:checked~#siteWrapper .mobile-nav-toggle-label .bottom-bar{-webkit-transition:top .15s .15s ease-in-out,-webkit-transform .15s .3s ease-in-out;-moz-transition:top .15s .15s ease-in-out,-moz-transform .15s .3s ease-in-out;-ms-transition:top .15s .15s ease-in-out,-ms-transform .15s .3s ease-in-out;-o-transition:top .15s .15s ease-in-out,-o-transform .15s .3 ease-in-out;transition:top .15s .15s ease-in-out,transform .15s .3s ease-in-out}#mobileNavToggle:checked~#siteWrapper .mobile-nav-toggle-label .top-bar{-webkit-transform:rotate(45deg);-moz-transform:rotate(45deg);-ms-transform:rotate(45deg);-o-transform:rotate(45deg);transform:rotate(45deg);top:7px}#mobileNavToggle:checked~#siteWrapper .mobile-nav-toggle-label .middle-bar{opacity:0}#mobileNavToggle:checked~#siteWrapper .mobile-nav-toggle-label .bottom-bar{-webkit-transform:rotate(-45deg);-moz-transform:rotate(-45deg);-ms-transform:rotate(-45deg);-o-transform:rotate(-45deg);transform:rotate(-45deg);top:7px}.enable-nav-button #sidecarNav nav>div:not(.folder):last-child a{display:inline-block;margin:.75em 0 0 0;line-height:1;padding:1em 1.5em}.folder-toggle-box:checked~.subnav{padding:.25em 0 .5em}.pre-footer-inner,.footer-inner{text-align:center}.pre-footer-inner .socialaccountlinks-block .social-account-list,.footer-inner .socialaccountlinks-block .social-account-list,.pre-footer-inner .back-to-top,.footer-inner .back-to-top{text-align:center;margin:24px 0}.pre-footer-inner .sqs-block-button-container--right,.footer-inner .sqs-block-button-container--right,.pre-footer-inner .sqs-block-button-container--center,.footer-inner .sqs-block-button-container--center,.pre-footer-inner .sqs-block-button-container--left,.footer-inner .sqs-block-button-container--left{text-align:center}#secondaryNavWrapper #secondaryNavigation div{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;display:block}#secondaryNavWrapper #secondaryNavigation div a,#secondaryNavWrapper #secondaryNavigation div label{display:block;padding:.7em 0}#secondaryNavWrapper #secondaryNavigation>div{margin-right:0%}.site-phone,.site-email{display:block;margin-left:0 !important}.site-email>span{display:none}.site-email:before{content:'Email'}.folder-nav-toggle-label,.category-nav-toggle-label{display:block;width:100%;z-index:2;position:absolute;top:0;right:0;padding-top:12px;cursor:pointer;font-size:21px;line-height:14px;text-align:right}.folder-nav-toggle-label:after,.category-nav-toggle-label:after{content:\"+\";display:block;text-align:right}#folderNav,#categoryNav{display:block;width:100%;position:relative;padding-bottom:1.5em}#folderNav+#content,#categoryNav+#content{display:block;width:100%}#folderNav .folder-nav,#categoryNav .folder-nav{position:relative;z-index:1}#folderNav .category-nav,#categoryNav .category-nav{margin-bottom:1em;position:relative;z-index:1}#folderNav li,#categoryNav li{padding-top:.75em;padding-bottom:.75em}#folderNav li.nav-section-label,#categoryNav li.nav-section-label{display:none}#folderNav li.filter,#categoryNav li.filter{display:block;visibility:visible}#folderNav li a,#categoryNav li a,#folderNav li.nav-section-label,#categoryNav li.nav-section-label{font-size:14px;line-height:1}#folderNav li:not(.filter),#categoryNav li:not(.filter){display:none}.collection-type-page:not(.hide-page-sidebar) #folderNav+#content,.collection-type-products:not(.hide-products-sidebar) #folderNav+#content,.collection-type-page:not(.hide-page-sidebar) #categoryNav+#content,.collection-type-products:not(.hide-products-sidebar) #categoryNav+#content{display:block;width:100%}#folderNav #folderNavToggle:checked+.folder-nav-toggle-label{z-index:0}#folderNav #folderNavToggle:checked+.folder-nav-toggle-label:after{content:'–'}#folderNav #folderNavToggle:checked~.folder-nav li:not(.active-link){display:block}#folderNav #folderNavToggle:checked~.folder-nav li:not(.active-link).nav-section-label{display:none}#categoryNav #categoryNavToggle:checked+.category-nav-toggle-label{z-index:0}#categoryNav #categoryNavToggle:checked+.category-nav-toggle-label:after{content:'–'}#categoryNav #categoryNavToggle:checked~.category-nav li:not(.filter){display:block}#categoryNav #categoryNavToggle:checked~.category-nav li:not(.filter).nav-section-label{display:none}}@media only screen and (max-width:480px){#promotedGalleryWrapper .sqs-gallery-block-slideshow .meta,.promoted-gallery-wrapper .sqs-gallery-block-slideshow .meta{display:block !important}}.site-title-font{font-family:\"proxima-nova\";font-size:27px;text-transform:none;letter-spacing:0px;font-weight:300;font-style:normal}.nav-font{font-family:\"proxima-nova\";font-size:14px;text-transform:uppercase;text-decoration:none;letter-spacing:2px;font-weight:700;font-style:normal}.nav-button-font{font-family:\"proxima-nova\";text-transform:uppercase;text-decoration:none;letter-spacing:1px;font-weight:700;font-style:normal}.banner-heading-font{font-family:\"adelle-sans\";font-size:41px;line-height:1em;text-transform:none;letter-spacing:4px;font-weight:100;font-style:normal}.banner-text-font{font-family:\"camingodos-web\";font-size:49px;line-height:1em;text-transform:none;letter-spacing:1px;font-weight:400;font-style:normal}.banner-button-font{font-family:\"proxima-nova\";font-size:20px;text-transform:uppercase;text-decoration:none;letter-spacing:2px;font-weight:700;font-style:normal}.body-font{font-family:\"adelle-sans\";font-size:16px;line-height:1.7em;letter-spacing:0px;font-weight:400;font-style:normal}.heading1-font{font-family:\"adelle-sans\";font-size:32px;line-height:1.2em;text-transform:none;letter-spacing:0px;font-weight:400;font-style:normal}.heading2-font{font-family:\"proxima-nova\";font-size:22px;line-height:1.2em;text-transform:uppercase;letter-spacing:2px;font-weight:400;font-style:normal}.heading3-font{font-family:\"proxima-nova\";font-size:16px;line-height:1em;text-transform:uppercase;letter-spacing:1px;font-weight:600;font-style:normal}.quote-font{font-family:\"adobe-garamond-pro\";font-size:20px;line-height:1.65em;letter-spacing:0px;font-weight:400;font-style:normal}.summary-heading-font{font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;text-transform:uppercase;letter-spacing:1px;font-weight:400;font-style:normal}.subnav-title-font{font-family:\"adobe-garamond-pro\";font-size:22px;text-transform:none;text-decoration:none;letter-spacing:0px;font-weight:400;font-style:normal}.subnav-link-font{font-family:\"proxima-nova\";font-size:14px;text-transform:uppercase;text-decoration:none;letter-spacing:2px;font-weight:600;font-style:normal}.footer-nav-font{font-family:\"proxima-nova\";font-size:13px;text-transform:uppercase;text-decoration:none;letter-spacing:2px;font-weight:400;font-style:normal}.site-info-font{font-family:\"proxima-nova\";font-size:14px;text-transform:uppercase;text-decoration:none;letter-spacing:2px;font-weight:400;font-style:normal}.product-list-price-font{font-family:\"Helvetica Neue\",Helvetica,Arial,sans-serif;letter-spacing:0px;text-transform:none}.product-item-price-font{font-family:\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:600;font-size:16px;font-style:normal;letter-spacing:2px;text-transform:uppercase}.small-button-block-font{font-family:\"proxima-nova\";text-transform:uppercase;letter-spacing:1px;font-weight:600;font-style:normal}.medium-button-block-font{font-family:\"proxima-nova\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;text-transform:uppercase;letter-spacing:1px;font-weight:600;font-style:normal}.large-button-block-font{font-family:\"proxima-nova\";text-transform:uppercase;letter-spacing:1px;font-weight:600;font-style:normal}.system-button-font{font-family:\"proxima-nova\";text-transform:uppercase;letter-spacing:.6px;font-weight:600;font-style:normal}.announcement-bar-font{font-family:\"proxima-nova\";font-size:19px;text-transform:none;letter-spacing:1px;font-weight:700;font-style:normal}\n/*! Squarespace LESS Compiler  (less.js language v1.3.3)  */\n#header{position:fixed !important;padding:5;height:5;background-color:rgba(41,41,45,.9) !important}#preFooter{display:none}"
  },
  {
    "path": "server/http-api/src/test/java/cc/blynk/server/api/http/logic/HttpSignatureTest.java",
    "content": "package cc.blynk.server.api.http.logic;\n\nimport org.junit.Test;\n\nimport java.nio.ByteBuffer;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 23.01.18.\n */\npublic class HttpSignatureTest {\n\n    @Test\n    public void printSignatures() {\n        System.out.println(getLongVal(\"GET \")); //1195725856\n        System.out.println(getLongVal(\"POST\")); //1347375956\n        System.out.println(getLongVal(\"PUT \")); //1347769376\n        System.out.println(getLongVal(\"HEAD\")); //1212498244\n        System.out.println(getLongVal(\"OPTI\")); //1330664521\n        System.out.println(getLongVal(\"PATC\")); //1346458691\n        System.out.println(getLongVal(\"DELE\")); //1145392197\n        System.out.println(getLongVal(\"TRAC\")); //1414676803\n        System.out.println(getLongVal(\"CONN\")); //1129270862\n    }\n\n    private static long getLongVal(String requestStart) {\n        ByteBuffer bb = ByteBuffer.allocate(4);\n        for (char c : requestStart.toCharArray()) {\n            bb.put((byte) c);\n        }\n        bb.flip();\n        return Integer.toUnsignedLong(bb.getInt());\n    }\n\n}\n"
  },
  {
    "path": "server/http-api/src/test/java/cc/blynk/server/api/http/logic/TestHideSecureInfoForCloning.java",
    "content": "package cc.blynk.server.api.http.logic;\n\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.notifications.Notification;\nimport cc.blynk.server.core.model.widgets.notifications.Twitter;\nimport org.junit.Test;\n\nimport java.util.concurrent.ConcurrentHashMap;\n\nimport static org.junit.Assert.assertEquals;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 08.07.16.\n */\npublic class TestHideSecureInfoForCloning {\n\n    @Test\n    public void testHideInfo() throws Exception {\n        DashBoard dashBoard = new DashBoard();\n        dashBoard.name = \"123\";\n        dashBoard.widgets = new Widget[2];\n        Twitter twitter = new Twitter();\n        twitter.secret = \"secret\";\n        twitter.token = \"token\";\n        twitter.username = \"username\";\n        dashBoard.widgets[0] = twitter;\n        Notification notification = new Notification();\n        notification.iOSTokens = new ConcurrentHashMap<>();\n        notification.iOSTokens.put(\"uid\", \"token\");\n        notification.androidTokens = new ConcurrentHashMap<>();\n        notification.androidTokens.put(\"uid2\", \"token2\");\n        dashBoard.widgets[1] = notification;\n\n        assertEquals(\"{\\\"id\\\":0,\\\"parentId\\\":-1,\\\"isPreview\\\":false,\\\"name\\\":\\\"123\\\",\\\"createdAt\\\":0,\\\"updatedAt\\\":0,\\\"widgets\\\":[{\\\"type\\\":\\\"TWITTER\\\",\\\"id\\\":0,\\\"x\\\":0,\\\"y\\\":0,\\\"color\\\":0,\\\"width\\\":0,\\\"height\\\":0,\\\"tabId\\\":0,\\\"isDefaultColor\\\":false},{\\\"type\\\":\\\"NOTIFICATION\\\",\\\"id\\\":0,\\\"x\\\":0,\\\"y\\\":0,\\\"color\\\":0,\\\"width\\\":0,\\\"height\\\":0,\\\"tabId\\\":0,\\\"isDefaultColor\\\":false,\\\"notifyWhenOffline\\\":false,\\\"notifyWhenOfflineIgnorePeriod\\\":0,\\\"priority\\\":\\\"normal\\\"}],\\\"theme\\\":\\\"Blynk\\\",\\\"keepScreenOn\\\":false,\\\"isAppConnectedOn\\\":false,\\\"isNotificationsOff\\\":false,\\\"isShared\\\":false,\\\"isActive\\\":false,\\\"widgetBackgroundOn\\\":false,\\\"color\\\":-1,\\\"isDefaultColor\\\":true}\", JsonParser.restrictiveDashWriter.writeValueAsString(dashBoard));\n    }\n\n}\n"
  },
  {
    "path": "server/http-api/src/test/java/cc/blynk/server/api/http/pojo/TestDataStreamDataJson.java",
    "content": "package cc.blynk.server.api.http.pojo;\n\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport org.junit.Test;\n\nimport static org.junit.Assert.assertNotNull;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 31.08.16.\n */\npublic class TestDataStreamDataJson {\n\n    @Test\n    public void testParseString() throws Exception {\n        String pinDataString = \"[{\\\"timestamp\\\" : 123, \\\"value\\\":\\\"100\\\"}]\";\n        PinData[] pinData = JsonParser.MAPPER.readValue(pinDataString, PinData[].class);\n        assertNotNull(pinData);\n    }\n\n}\n"
  },
  {
    "path": "server/http-api/src/test/java/cc/blynk/server/reset/TokensPoolTest.java",
    "content": "package cc.blynk.server.reset;\n\nimport cc.blynk.server.internal.token.ResetPassToken;\nimport cc.blynk.server.internal.token.TokensPool;\nimport cc.blynk.utils.AppNameUtil;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNull;\n\n@RunWith(MockitoJUnitRunner.class)\npublic class TokensPoolTest {\n\n    @Test\n    public void addTokenTest() {\n        final ResetPassToken user = new ResetPassToken(\"test.gmail.com\", AppNameUtil.BLYNK);\n        final String token = \"123\";\n        final TokensPool tokensPool = new TokensPool(\"\");\n        tokensPool.addToken(token, user);\n        assertEquals(user, tokensPool.getBaseToken(token));\n    }\n\n    @Test\n    public void addTokenTwiceTest() {\n        final ResetPassToken user = new ResetPassToken(\"test.gmail.com\", AppNameUtil.BLYNK);\n        final String token = \"123\";\n        final TokensPool tokensPool = new TokensPool(\"\");\n        tokensPool.addToken(token, user);\n        tokensPool.addToken(token, user);\n        assertEquals(1, tokensPool.size());\n    }\n\n    @Test\n    public void remoteTokenTest() {\n        final ResetPassToken user = new ResetPassToken(\"test.gmail.com\", AppNameUtil.BLYNK);\n        final String token = \"123\";\n        final TokensPool tokensPool = new TokensPool(\"\");\n        tokensPool.addToken(token, user);\n        assertEquals(user, tokensPool.getBaseToken(token));\n        tokensPool.removeToken(token);\n        assertEquals(0, tokensPool.size());\n        assertNull(tokensPool.getBaseToken(token));\n    }\n}\n"
  },
  {
    "path": "server/http-core/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <artifactId>server</artifactId>\n        <groupId>cc.blynk.server</groupId>\n        <version>0.41.17-SNAPSHOT</version>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n\n    <groupId>cc.blynk.server.api.core</groupId>\n    <artifactId>http-core</artifactId>\n\n    <dependencies>\n        <dependency>\n            <groupId>cc.blynk.server</groupId>\n            <artifactId>core</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n    </dependencies>\n\n</project>"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/AnnotationsProcessor.java",
    "content": "package cc.blynk.core.http;\n\nimport cc.blynk.core.http.annotation.Consumes;\nimport cc.blynk.core.http.annotation.Context;\nimport cc.blynk.core.http.annotation.Path;\nimport cc.blynk.core.http.rest.HandlerWrapper;\nimport cc.blynk.core.http.rest.params.BodyParam;\nimport cc.blynk.core.http.rest.params.ContextParam;\nimport cc.blynk.core.http.rest.params.EnumQueryParam;\nimport cc.blynk.core.http.rest.params.FormParam;\nimport cc.blynk.core.http.rest.params.Param;\nimport cc.blynk.core.http.rest.params.PathParam;\nimport cc.blynk.core.http.rest.params.QueryParam;\nimport cc.blynk.server.core.stats.GlobalStats;\nimport cc.blynk.utils.http.MediaType;\nimport io.netty.channel.ChannelHandlerContext;\n\nimport java.lang.annotation.Annotation;\nimport java.lang.reflect.Method;\nimport java.lang.reflect.Parameter;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 09.12.15.\n */\npublic final class AnnotationsProcessor {\n\n    private AnnotationsProcessor() {\n    }\n\n    public static HandlerWrapper[] register(String rootPath, Object o, GlobalStats globalStats) {\n        return registerHandler(rootPath, o, globalStats);\n    }\n\n    private static HandlerWrapper[] registerHandler(String rootPath, Object handler, GlobalStats globalStats) {\n        Class<?> handlerClass = handler.getClass();\n        Annotation pathAnnotation = handlerClass.getAnnotation(Path.class);\n        String handlerMainPath = ((Path) pathAnnotation).value();\n\n        List<HandlerWrapper> processors = new ArrayList<>();\n\n        for (Method method : handlerClass.getMethods()) {\n            Annotation consumes = method.getAnnotation(Consumes.class);\n            String contentType = MediaType.APPLICATION_JSON;\n            if (consumes != null) {\n                contentType = ((Consumes) consumes).value()[0];\n            }\n\n            Annotation path = method.getAnnotation(Path.class);\n            if (path != null) {\n                String fullPath = rootPath + handlerMainPath + ((Path) path).value();\n                UriTemplate uriTemplate = new UriTemplate(fullPath);\n\n                HandlerWrapper handlerHolder = new HandlerWrapper(uriTemplate, method, handler, globalStats);\n\n                for (int i = 0; i < method.getParameterCount(); i++) {\n                    Parameter parameter = method.getParameters()[i];\n                    handlerHolder.params[i] = resolveParam(parameter, contentType);\n                }\n\n                processors.add(handlerHolder);\n            }\n        }\n\n        return processors.toArray(new HandlerWrapper[0]);\n    }\n\n    private static Param resolveParam(Parameter parameter, String contentType) {\n        cc.blynk.core.http.annotation.QueryParam queryParamAnnotation =\n                parameter.getAnnotation(cc.blynk.core.http.annotation.QueryParam.class);\n        if (queryParamAnnotation != null) {\n            return new QueryParam(queryParamAnnotation.value(), parameter.getType());\n        }\n\n        cc.blynk.core.http.annotation.EnumQueryParam enumQueryParamAnnotation =\n                parameter.getAnnotation(cc.blynk.core.http.annotation.EnumQueryParam.class);\n        if (enumQueryParamAnnotation != null) {\n            return new EnumQueryParam(enumQueryParamAnnotation.value());\n        }\n\n        cc.blynk.core.http.annotation.PathParam pathParamAnnotation =\n                parameter.getAnnotation(cc.blynk.core.http.annotation.PathParam.class);\n        if (pathParamAnnotation != null) {\n            return new PathParam(pathParamAnnotation.value(), parameter.getType());\n        }\n\n        cc.blynk.core.http.annotation.FormParam formParamAnnotation =\n                parameter.getAnnotation(cc.blynk.core.http.annotation.FormParam.class);\n        if (formParamAnnotation != null) {\n            return new FormParam(formParamAnnotation.value(), parameter.getType());\n        }\n\n        Annotation contextAnnotation = parameter.getAnnotation(Context.class);\n        if (contextAnnotation != null) {\n            return new ContextParam(ChannelHandlerContext.class);\n        }\n\n        return new BodyParam(parameter.getName(), parameter.getType(), contentType);\n    }\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/AuthHeadersBaseHttpHandler.java",
    "content": "package cc.blynk.core.http;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.core.dao.UserDao;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.utils.SHA256Util;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.handler.codec.http.HttpHeaderNames;\nimport io.netty.handler.codec.http.HttpRequest;\nimport io.netty.util.AttributeKey;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 24.12.15.\n */\npublic abstract class AuthHeadersBaseHttpHandler extends BaseHttpHandler {\n\n    public static final AttributeKey<User> USER = AttributeKey.newInstance(\"USER\");\n\n    private final UserDao userDao;\n\n    public AuthHeadersBaseHttpHandler(Holder holder, String rootPath) {\n        super(holder, rootPath);\n        this.userDao = holder.userDao;\n    }\n\n    @Override\n    public boolean process(ChannelHandlerContext ctx, HttpRequest req) {\n        try {\n            User superAdmin = validateAuth(userDao, req);\n            if (superAdmin != null) {\n                ctx.channel().attr(USER).set(superAdmin);\n                return super.process(ctx, req);\n            }\n        } catch (IllegalAccessException e) {\n            //return 403 and stop processing.\n            ctx.writeAndFlush(Response.forbidden(e.getMessage()));\n            return true;\n        }\n\n        return false;\n    }\n\n    public static User validateAuth(UserDao userDao, HttpRequest req) throws IllegalAccessException {\n        String auth = req.headers().get(HttpHeaderNames.AUTHORIZATION);\n        if (auth != null) {\n            try {\n                String encodedAuth = auth.substring(\"Basic \".length());\n                String decoded = new String(java.util.Base64.getDecoder().decode(encodedAuth));\n                String[] userAndPass = decoded.split(\":\");\n                String user = userAndPass[0].toLowerCase();\n                String pass = userAndPass[1];\n\n                User superUser = userDao.getSuperAdmin();\n                String passHash = SHA256Util.makeHash(pass, user);\n\n                log.info(\"Header auth attempt. User: {}, pass: {}\", user, pass);\n                if (superUser != null && superUser.email.equals(user) && superUser.pass.equals(passHash)) {\n                    return superUser;\n                } else {\n                    throw new IllegalAccessException(\"Authentication failed.\");\n                }\n            } catch (IllegalAccessException iae) {\n                log.error(\"Error invoking OTA handler. {}\", iae.getMessage());\n                throw iae;\n            } catch (Exception e) {\n                log.error(\"Error invoking OTA handler.\");\n            }\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/BaseHttpHandler.java",
    "content": "package cc.blynk.core.http;\n\nimport cc.blynk.core.http.rest.HandlerHolder;\nimport cc.blynk.core.http.rest.HandlerWrapper;\nimport cc.blynk.core.http.rest.URIDecoder;\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.core.dao.SessionDao;\nimport cc.blynk.server.core.dao.TokenManager;\nimport cc.blynk.server.core.stats.GlobalStats;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.ChannelInboundHandlerAdapter;\nimport io.netty.handler.codec.http.FullHttpResponse;\nimport io.netty.handler.codec.http.HttpRequest;\nimport io.netty.util.ReferenceCountUtil;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.util.Map;\nimport java.util.regex.Matcher;\n\nimport static cc.blynk.core.http.Response.serverError;\nimport static cc.blynk.server.core.protocol.handlers.DefaultExceptionHandler.handleUnexpectedException;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 24.12.15.\n */\npublic abstract class BaseHttpHandler extends ChannelInboundHandlerAdapter {\n\n    protected static final Logger log = LogManager.getLogger(BaseHttpHandler.class);\n\n    protected final TokenManager tokenManager;\n    protected final SessionDao sessionDao;\n    protected final HandlerWrapper[] handlers;\n    protected final String rootPath;\n\n    public BaseHttpHandler(Holder holder, String rootPath) {\n        this(holder.tokenManager, holder.sessionDao, holder.stats, rootPath);\n    }\n\n    BaseHttpHandler(TokenManager tokenManager, SessionDao sessionDao,\n                           GlobalStats globalStats, String rootPath) {\n        this.tokenManager = tokenManager;\n        this.sessionDao = sessionDao;\n        this.rootPath = rootPath;\n        this.handlers = AnnotationsProcessor.register(rootPath, this, globalStats);\n    }\n\n    @Override\n    public void channelRead(ChannelHandlerContext ctx, Object msg) {\n        if (msg instanceof HttpRequest) {\n            HttpRequest req = (HttpRequest) msg;\n\n            if (!process(ctx, req)) {\n                ctx.fireChannelRead(req);\n            }\n        }\n    }\n\n    public boolean process(ChannelHandlerContext ctx, HttpRequest req) {\n        HandlerHolder handlerHolder = lookupHandler(req);\n\n        if (handlerHolder != null) {\n            try {\n                invokeHandler(ctx, req, handlerHolder.handler, handlerHolder.extractedParams);\n            } catch (Exception e) {\n                log.debug(\"Error processing http request.\", e);\n                ctx.writeAndFlush(serverError(e.getMessage()), ctx.voidPromise());\n            } finally {\n                ReferenceCountUtil.release(req);\n            }\n            return true;\n        }\n\n        return false;\n    }\n\n    private void invokeHandler(ChannelHandlerContext ctx, HttpRequest req,\n                               HandlerWrapper handler, Map<String, String> extractedParams) {\n        log.debug(\"{} : {}\", req.method().name(), req.uri());\n        try (URIDecoder uriDecoder = new URIDecoder(req, extractedParams)) {\n            Object[] params = handler.fetchParams(ctx, uriDecoder);\n            finishHttp(ctx, uriDecoder, handler, params);\n        }\n    }\n\n    public void finishHttp(ChannelHandlerContext ctx, URIDecoder uriDecoder,\n                           HandlerWrapper handler, Object[] params) {\n        FullHttpResponse response = handler.invoke(params);\n        if (response != Response.NO_RESPONSE) {\n            ctx.writeAndFlush(response);\n        }\n    }\n\n    private HandlerHolder lookupHandler(HttpRequest req) {\n        for (HandlerWrapper handler : handlers) {\n            if (handler.httpMethod == req.method()) {\n                Matcher matcher = handler.uriTemplate.matcher(req.uri());\n                if (matcher.matches()) {\n                    Map<String, String> extractedParams = handler.uriTemplate.extractParameters(matcher);\n                    return new HandlerHolder(handler, extractedParams);\n                }\n            }\n        }\n        return null;\n    }\n\n    @Override\n    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {\n        handleUnexpectedException(ctx, cause);\n    }\n\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/CookiesBaseHttpHandler.java",
    "content": "package cc.blynk.core.http;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.core.dao.SessionDao;\nimport cc.blynk.server.core.dao.TokenManager;\nimport cc.blynk.server.core.stats.GlobalStats;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.handler.codec.http.HttpRequest;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 24.12.15.\n */\npublic abstract class CookiesBaseHttpHandler extends BaseHttpHandler {\n\n    public CookiesBaseHttpHandler(Holder holder, String rootPath) {\n        super(holder, rootPath);\n    }\n\n    public CookiesBaseHttpHandler(TokenManager tokenManager, SessionDao sessionDao,\n                                  GlobalStats globalStats, String rootPath) {\n        super(tokenManager, sessionDao, globalStats, rootPath);\n    }\n\n    @Override\n    public boolean process(ChannelHandlerContext ctx, HttpRequest req) {\n        if (ctx.channel().attr(SessionDao.userAttributeKey).get() != null) {\n            return super.process(ctx, req);\n        }\n        return false;\n    }\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/Response.java",
    "content": "package cc.blynk.core.http;\n\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport io.netty.buffer.Unpooled;\nimport io.netty.handler.codec.http.DefaultFullHttpResponse;\nimport io.netty.handler.codec.http.HttpHeaderValues;\nimport io.netty.handler.codec.http.HttpResponseStatus;\nimport io.netty.handler.codec.http.HttpVersion;\n\nimport java.nio.charset.StandardCharsets;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map;\n\nimport static cc.blynk.core.http.utils.ListUtils.subList;\nimport static cc.blynk.utils.http.MediaType.APPLICATION_JSON;\nimport static cc.blynk.utils.http.MediaType.TEXT_PLAIN;\nimport static io.netty.handler.codec.http.HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN;\nimport static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION;\nimport static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;\nimport static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE;\nimport static io.netty.handler.codec.http.HttpHeaderNames.LOCATION;\nimport static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;\nimport static io.netty.handler.codec.http.HttpResponseStatus.FORBIDDEN;\nimport static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR;\nimport static io.netty.handler.codec.http.HttpResponseStatus.MOVED_PERMANENTLY;\nimport static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;\nimport static io.netty.handler.codec.http.HttpResponseStatus.OK;\nimport static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.12.15.\n */\npublic final class Response extends DefaultFullHttpResponse {\n\n    private static final String JSON = APPLICATION_JSON + \";charset=utf-8\";\n    private static final String PLAIN_TEXT = TEXT_PLAIN + \";charset=utf-8\";\n\n    final static Response NO_RESPONSE = null;\n\n    private Response(HttpVersion version, HttpResponseStatus status, String content, String contentType) {\n        super(version, status, (\n                content == null\n                        ? Unpooled.EMPTY_BUFFER\n                        : Unpooled.copiedBuffer(content, StandardCharsets.UTF_8))\n        );\n        fillHeaders(contentType);\n    }\n\n    private Response(HttpVersion version, HttpResponseStatus status, byte[] content, String contentType) {\n        super(version, status, (content == null ? Unpooled.EMPTY_BUFFER : Unpooled.copiedBuffer(content)));\n        fillHeaders(contentType);\n    }\n\n    private Response(HttpVersion version, HttpResponseStatus status) {\n        super(version, status);\n        headers().set(CONNECTION, HttpHeaderValues.KEEP_ALIVE)\n                 .set(ACCESS_CONTROL_ALLOW_ORIGIN, \"*\")\n                 .set(CONTENT_LENGTH, 0);\n    }\n\n    private void fillHeaders(String contentType) {\n        headers().set(CONNECTION, HttpHeaderValues.KEEP_ALIVE)\n                 .set(CONTENT_TYPE, contentType)\n                 .set(ACCESS_CONTROL_ALLOW_ORIGIN, \"*\")\n                 .set(CONTENT_LENGTH, content().readableBytes());\n    }\n\n    public static Response noResponse() {\n        return NO_RESPONSE;\n    }\n\n    public static Response ok() {\n        return new Response(HTTP_1_1, OK);\n    }\n\n    public static Response notFound() {\n        return new Response(HTTP_1_1, NOT_FOUND);\n    }\n\n    public static Response forbidden() {\n        return new Response(HTTP_1_1, FORBIDDEN);\n    }\n\n    public static Response forbidden(String error) {\n        return new Response(HTTP_1_1, FORBIDDEN, error, PLAIN_TEXT);\n    }\n\n    public static Response badRequest() {\n        return new Response(HTTP_1_1, BAD_REQUEST);\n    }\n\n    public static Response redirect(String url) {\n        Response response = new Response(HTTP_1_1, MOVED_PERMANENTLY);\n        response.headers()\n                .set(LOCATION, url)\n                .set(ACCESS_CONTROL_ALLOW_ORIGIN, \"*\");\n        return response;\n    }\n\n    public static Response badRequest(String message) {\n        return new Response(HTTP_1_1, BAD_REQUEST, message, PLAIN_TEXT);\n    }\n\n    public static Response serverError() {\n        return new Response(HTTP_1_1, INTERNAL_SERVER_ERROR);\n    }\n\n    public static Response serverError(String message) {\n        return new Response(HTTP_1_1, INTERNAL_SERVER_ERROR, message, PLAIN_TEXT);\n    }\n\n    public static Response ok(String data) {\n        return new Response(HTTP_1_1, OK, data, JSON);\n    }\n\n    public static Response ok(String data, String contentType) {\n        return new Response(HTTP_1_1, OK, data, contentType);\n    }\n\n    public static Response ok(byte[] data, String contentType) {\n        return new Response(HTTP_1_1, OK, data, contentType);\n    }\n\n\n    public static Response ok(boolean bool) {\n        return new Response(HTTP_1_1, OK, String.valueOf(bool), JSON);\n    }\n\n    public static Response ok(User user) {\n        return ok(JsonParser.toJson(user));\n    }\n\n    public static Response ok(DashBoard dashBoard) {\n        return ok(JsonParser.toJson(dashBoard));\n    }\n\n    public static Response ok(List<?> list, int page, int size) {\n        String data = JsonParser.toJson(subList(list, page, size));\n        return ok(data == null ? \"[]\" : data);\n    }\n\n    public static Response ok(Map<?, ?> map) {\n        String data = JsonParser.toJson(map);\n        return ok(data == null ? \"{}\" : data);\n    }\n\n    public static Response ok(Collection<?> list) {\n        String data = JsonParser.toJson(list);\n        return ok(data == null ? \"[]\" : data);\n    }\n\n    public static Response appendTotalCountHeader(Response response, int count) {\n        response.headers().set(\"X-Total-Count\", count);\n        return response;\n    }\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/TokenBaseHttpHandler.java",
    "content": "package cc.blynk.core.http;\n\nimport cc.blynk.core.http.rest.HandlerWrapper;\nimport cc.blynk.core.http.rest.URIDecoder;\nimport cc.blynk.server.core.dao.SessionDao;\nimport cc.blynk.server.core.dao.TokenManager;\nimport cc.blynk.server.core.dao.TokenValue;\nimport cc.blynk.server.core.dao.UserKey;\nimport cc.blynk.server.core.model.auth.Session;\nimport cc.blynk.server.core.stats.GlobalStats;\nimport cc.blynk.server.internal.ReregisterChannelUtil;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.handler.codec.http.FullHttpResponse;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 24.12.15.\n */\npublic abstract class TokenBaseHttpHandler extends BaseHttpHandler {\n\n    public TokenBaseHttpHandler(TokenManager tokenManager, SessionDao sessionDao,\n                                GlobalStats globalStats, String rootPath) {\n        super(tokenManager, sessionDao, globalStats, rootPath);\n    }\n\n    @Override\n    public void finishHttp(ChannelHandlerContext ctx, URIDecoder uriDecoder,\n                           HandlerWrapper handler, Object[] params) {\n        String tokenPathParam = uriDecoder.pathData.get(\"token\");\n        if (tokenPathParam == null) {\n            ctx.writeAndFlush(Response.badRequest(\"No token provided.\"));\n            return;\n        }\n\n        //reregister logic\n        TokenValue tokenValue = tokenManager.getTokenValueByToken(tokenPathParam);\n        if (tokenValue == null) {\n            log.debug(\"Requested token {} not found.\", tokenPathParam);\n            ctx.writeAndFlush(Response.badRequest(\"Invalid token.\"), ctx.voidPromise());\n            return;\n        }\n\n        Session session = sessionDao.getOrCreateSessionByUser(new UserKey(tokenValue.user), ctx.channel().eventLoop());\n        if (session.isSameEventLoop(ctx)) {\n            completeLogin(ctx.channel(), handler.invoke(params));\n        } else {\n            log.trace(\"Re registering http channel. {}\", ctx.channel());\n            ReregisterChannelUtil.reRegisterChannel(ctx, session, channelFuture -> completeLogin(\n                    channelFuture.channel(), handler.invoke(params)));\n        }\n    }\n\n    private void completeLogin(Channel channel, FullHttpResponse response) {\n        channel.writeAndFlush(response);\n        log.trace(\"Re registering http channel finished.\");\n    }\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/UriTemplate.java",
    "content": "package cc.blynk.core.http;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\npublic class UriTemplate {\n\n    // Finds parameters in the URL pattern string.\n    private static final String URL_PARAM_REGEX = \"\\\\{(\\\\w*?)\\\\}\";\n\n    // Replaces parameter names in the URL pattern string to match parameters in URLs.\n    private static final String URL_PARAM_MATCH_REGEX =\n            \"\\\\([%\\\\\\\\w-.\\\\\\\\~!\\\\$&'\\\\\\\\(\\\\\\\\)\\\\\\\\*\\\\\\\\+,;=:\\\\\\\\[\\\\\\\\]@]+?\\\\)\";\n\n    // Pattern to match URL pattern parameter names.\n    private static final Pattern URL_PARAM_PATTERN = Pattern.compile(URL_PARAM_REGEX);\n\n    // Finds the 'format' portion of the URL pattern string.\n    private static final String URL_FORMAT_REGEX = \"(?:\\\\.\\\\{format\\\\})$\";\n\n    // Replaces the format parameter name in the URL pattern string to\n    // match the format specifier in URLs. Appended to the end of the regex string\n    // when a URL pattern contains a format parameter.\n    private static final String URL_FORMAT_MATCH_REGEX = \"(?:\\\\\\\\.\\\\([\\\\\\\\w%]+?\\\\))?\";\n\n    // Finds the query string portion withi    private Matcher matcher;n a URL.\n    // Appended to the end of the built-up regex string.\n    private static final String URL_QUERY_STRING_REGEX = \"(?:\\\\?.*?)?$\";\n\n    private final String urlPattern;\n    private Matcher matcher;\n    private Pattern compiledUrl;\n\n    private final List<String> parameterNames = new ArrayList<>();\n\n    public UriTemplate(String pattern) {\n        this.urlPattern = pattern;\n        compile();\n    }\n\n    public Matcher matcher(String url) {\n        return compiledUrl.matcher(url);\n    }\n\n    public void compile() {\n        acquireParameterNames();\n        String parsedPattern = urlPattern.replaceFirst(URL_FORMAT_REGEX, URL_FORMAT_MATCH_REGEX);\n        parsedPattern = parsedPattern.replaceAll(URL_PARAM_REGEX, URL_PARAM_MATCH_REGEX);\n        this.compiledUrl = Pattern.compile(parsedPattern + URL_QUERY_STRING_REGEX);\n    }\n\n    private void acquireParameterNames() {\n        Matcher m = URL_PARAM_PATTERN.matcher(urlPattern);\n\n        while (m.find()) {\n            parameterNames.add(m.group(1));\n        }\n    }\n\n    public Map<String, String> extractParameters(Matcher matcher) {\n        Map<String, String> values = new HashMap<>();\n\n        for (int i = 0; i < matcher.groupCount(); i++) {\n            String value = matcher.group(i + 1);\n\n            if (value != null) {\n                values.put(parameterNames.get(i), value);\n            }\n        }\n\n        return values;\n    }\n\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/annotation/Consumes.java",
    "content": "package cc.blynk.core.http.annotation;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 23.09.16.\n */\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Inherited;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n@Inherited\n@Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD})\n@Retention(RetentionPolicy.RUNTIME)\n@Documented\npublic @interface Consumes {\n    String[] value() default {\"*/*\"};\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/annotation/Context.java",
    "content": "package cc.blynk.core.http.annotation;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * This annotation is used to inject information into a class\n * field, bean property or classMethod parameter.\n *\n * @author Paul Sandoz\n * @author Marc Hadley\n * @since 1.0\n */\n@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})\n@Retention(RetentionPolicy.RUNTIME)\n@Documented\npublic @interface Context {\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/annotation/DELETE.java",
    "content": "package cc.blynk.core.http.annotation;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 23.09.16.\n */\n@Target({java.lang.annotation.ElementType.METHOD})\n@Retention(RetentionPolicy.RUNTIME)\n@HttpMethod(\"DELETE\")\n@Documented\npublic @interface DELETE {\n\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/annotation/EnumQueryParam.java",
    "content": "package cc.blynk.core.http.annotation;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 05.06.18.\n */\n@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})\n@Retention(RetentionPolicy.RUNTIME)\n@Documented\npublic @interface EnumQueryParam {\n\n    Class<?> value();\n\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/annotation/FormParam.java",
    "content": "package cc.blynk.core.http.annotation;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * Binds the value(s) of a form parameter contained within a request entity body\n * to a resource classMethod parameter. Values are URL decoded unless this is\n * disabled using the  annotation. A default value can be\n * specified using the  annotation.\n * If the request entity body is absent or is an unsupported media type, the\n * default value is used.\n *\n * The type {@code T} of the annotated parameter must either:\n * <ol>\n * <li>Be a primitive type</li>\n * <li>Have a constructor that accepts a single {@code String} argument</li>\n * <li>Have a static classMethod named {@code valueOf} or {@code fromString}\n * that accepts a single</li>\n * <li>Have a registered implementation of {@link javax.ws.rs.ext.ParamConverterProvider}\n * JAX-RS extension SPI that returns a {@link javax.ws.rs.ext.ParamConverter}\n * instance capable of a \"from string\" conversion for the type.</li>\n * {@code String} argument (see, for example, {@link Integer#valueOf(String)})</li>\n * <li>Be {@code List<T>}, {@code Set<T>} or\n * {@code SortedSet<T>}, where {@code T} satisfies 2, 3 or 4 above.\n * The resulting collection is read-only.</li>\n * </ol>\n *\n * <p>If the type is not one of the collection types listed in 5 above and the\n * form parameter is represented by multiple values then the first value (lexically)\n * of the parameter is used.</p>\n *\n * <p>Note that, whilst the annotation target permits use on fields and methods,\n * this annotation is only required to be supported on resource classMethod\n * parameters.</p>\n *\n * @author Paul Sandoz\n * @author Marc Hadley\n * @since 1.0\n */\n@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})\n@Retention(RetentionPolicy.RUNTIME)\n@Documented\npublic @interface FormParam {\n\n    /**\n     * Defines the name of the form parameter whose value will be used\n     * to initialize the value of the annotated classMethod argument. The name is\n     * specified in decoded form, any percent encoded literals within the value\n     * will not be decoded and will instead be treated as literal text. E.g. if\n     * the parameter name is \"a b\" then the value of the annotation is \"a b\",\n     * <i>not</i> \"a+b\" or \"a%20b\".\n     */\n    String value();\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/annotation/GET.java",
    "content": "package cc.blynk.core.http.annotation;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 23.09.16.\n */\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n@Target({java.lang.annotation.ElementType.METHOD})\n@Retention(RetentionPolicy.RUNTIME)\n@HttpMethod(\"GET\")\n@Documented\npublic @interface GET {\n\n}\n\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/annotation/HttpMethod.java",
    "content": "package cc.blynk.core.http.annotation;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 23.09.16.\n */\n@Target({java.lang.annotation.ElementType.ANNOTATION_TYPE})\n@Retention(RetentionPolicy.RUNTIME)\n@Documented\npublic @interface HttpMethod {\n    String GET = \"GET\";\n    String POST = \"POST\";\n    String PUT = \"PUT\";\n    String DELETE = \"DELETE\";\n    String HEAD = \"HEAD\";\n    String OPTIONS = \"OPTIONS\";\n\n    String value();\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/annotation/Metric.java",
    "content": "package cc.blynk.core.http.annotation;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})\n@Retention(RetentionPolicy.RUNTIME)\n@Documented\npublic @interface Metric {\n\n    short value();\n\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/annotation/POST.java",
    "content": "package cc.blynk.core.http.annotation;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 23.09.16.\n */\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n@Target({java.lang.annotation.ElementType.METHOD})\n@Retention(RetentionPolicy.RUNTIME)\n@HttpMethod(\"POST\")\n@Documented\npublic @interface POST {\n\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/annotation/PUT.java",
    "content": "package cc.blynk.core.http.annotation;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 23.09.16.\n */\n@Target({java.lang.annotation.ElementType.METHOD})\n@Retention(RetentionPolicy.RUNTIME)\n@HttpMethod(\"PUT\")\n@Documented\npublic @interface PUT {\n\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/annotation/Path.java",
    "content": "package cc.blynk.core.http.annotation;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * Identifies the URI path that a resource class or class classMethod will serve\n * requests for.\n *\n * <p>Paths are relative. For an annotated class the base URI is the\n * application path. For an annotated\n * classMethod the base URI is the\n * effective URI of the containing class. For the purposes of absolutizing a\n * path against the base URI , a leading '/' in a path is\n * ignored and base URIs are treated as if they ended in '/'. E.g.:</p>\n *\n * <pre>&#64;Path(\"widgets\")\n * public class WidgetsResource {\n *  &#64;GET\n *  String getList() {...}\n *\n *  &#64;GET &#64;Path(\"{id}\")\n *  String getWidget(&#64;PathParam(\"id\") String id) {...}\n * }</pre>\n *\n * <p>In the above, if the application path is\n * {@code catalogue} and the application is deployed at\n * {@code http://example.com/}, then {@code GET} requests for\n * {@code http://example.com/catalogue/widgets} will be handled by the\n * {@code getList} classMethod while requests for\n * <code>http://example.com/catalogue/widgets/<i>nnn</i></code> (where\n * <code><i>nnn</i></code> is some value) will be handled by the\n * {@code getWidget} classMethod. The same would apply if the value of either\n * {@code @Path} annotation started with '/'.</p>\n *\n * <p>Classes and methods may also be annotated with javax.ws.rs.Consumes and\n * to filter the requests they will receive.</p>\n *\n * @author Paul Sandoz\n * @author Marc Hadley\n * @since 1.0\n */\n@Target({ElementType.TYPE, ElementType.METHOD})\n@Retention(RetentionPolicy.RUNTIME)\n@Documented\npublic @interface Path {\n\n    /**\n     * Defines a URI template for the resource class or classMethod, must not\n     * include matrix parameters.\n     *\n     * <p>Embedded template parameters are allowed and are of the form:</p>\n     *\n     * <pre> param = \"{\" *WSP name *WSP [ \":\" *WSP regex *WSP ] \"}\"\n     * name = (ALPHA / DIGIT / \"_\")*(ALPHA / DIGIT / \".\" / \"_\" / \"-\" ) ; \\w[\\w\\.-]*\n     * regex = *( nonbrace / \"{\" *nonbrace \"}\" ) ; where nonbrace is any char other than \"{\" and \"}\"</pre>\n     *\n     * <p>See {@link <a href=\"http://tools.ietf.org/html/rfc5234\">RFC 5234</a>}\n     * for a description of the syntax used above and the expansions of\n     * {@code WSP}, {@code ALPHA} and {@code DIGIT}. In the above {@code name}\n     * is the template parameter name and the optional {@code regex} specifies\n     * the contents of the capturing group for the parameter. If {@code regex}\n     * is not supplied then a default value of {@code [^/]+} which terminates at\n     * a path segment boundary, is used. Matching of request URIs to URI\n     * templates is performed against encoded path values and implementations\n     * will not escape literal characters in regex automatically, therefore any\n     * literals in {@code regex} should be escaped by the author according to\n     * the rules of\n     * {@link <a href=\"http://tools.ietf.org/html/rfc3986#section-3.3\">RFC 3986 section 3.3</a>}.\n     * Caution is recommended in the use of {@code regex}, incorrect use can\n     * lead to a template parameter matching unexpected URI paths. See\n     * {@link <a href=\"http://download.oracle.com/javase/6/docs/api/java/util/regex/Pattern.html\">Pattern</a>}\n     * for further information on the syntax of regular expressions.\n     * Values of template parameters may be extracted using.</p>\n     *\n     * <p>The literal part of the supplied value (those characters\n     * that are not part of a template parameter) is automatically percent\n     * encoded to conform to the {@code path} production of\n     * {@link <a href=\"http://tools.ietf.org/html/rfc3986#section-3.3\">RFC 3986 section 3.3</a>}.\n     * Note that percent encoded values are allowed in the literal part of the\n     * value, an implementation will recognize such values and will not double\n     * encode the '%' character.</p>\n     */\n    String value();\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/annotation/PathParam.java",
    "content": "package cc.blynk.core.http.annotation;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * Binds the value of a URI template parameter or a path segment\n * containing the template parameter to a resource classMethod parameter, resource\n * class field, or resource class\n * bean property. The value is URL decoded unless this\n * is disabled using the annotation.\n * A default value can be specified using the\n * annotation.\n *\n * The type of the annotated parameter, field or property must either:\n * <ul>\n * <li>Be { javax.ws.rs.core.PathSegment}, the value will be the final\n * segment of the matching part of the path.\n * See { javax.ws.rs.core.UriInfo} for a means of retrieving all request\n * path segments.</li>\n * <li>Be {@code List<javax.ws.rs.core.PathSegment>}, the\n * value will be a list of {@code PathSegment} corresponding to the path\n * segment(s) that matched the named template parameter.\n * See {javax.ws.rs.core.UriInfo} for a means of retrieving all request\n * path segments.</li>\n * <li>Be a primitive type.</li>\n * <li>Have a constructor that accepts a single String argument.</li>\n * <li>Have a static classMethod named {@code valueOf} or {@code fromString}\n * that accepts a single\n * String argument (see, for example, {@link Integer#valueOf(String)}).</li>\n * <li>Have a registered implementation of { javax.ws.rs.ext.ParamConverterProvider}\n * JAX-RS extension SPI that returns a { javax.ws.rs.ext.ParamConverter}\n * instance capable of a \"from string\" conversion for the type.</li>\n * </ul>\n *\n * <p>The injected value corresponds to the latest use (in terms of scope) of\n * the path parameter. E.g. if a class and a sub-resource classMethod are both\n * annotated with a { javax.ws.rs.Path &#64;Path} containing the same URI template\n * parameter, use of {@code @PathParam} on a sub-resource classMethod parameter\n * will bind the value matching URI template parameter in the classMethod's\n * {@code @Path} annotation.</p>\n *\n * <p>Because injection occurs at object creation time, use of this annotation\n * on resource class fields and bean properties is only supported for the\n * default per-request resource class lifecycle. Resource classes using\n * other lifecycles should only use this annotation on resource classMethod\n * parameters.</p>\n *\n * @author Paul Sandoz\n * @author Marc Hadley\n * @since 1.0\n */\n@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})\n@Retention(RetentionPolicy.RUNTIME)\n@Documented\npublic @interface PathParam {\n\n    /**\n     * Defines the name of the URI template parameter whose value will be used\n     * to initialize the value of the annotated classMethod parameter, class field or\n     * property. See { javax.ws.rs.Path#value()} for a description of the syntax of\n     * template parameters.\n     *\n     * <p>E.g. a class annotated with: {@code @Path(\"widgets/{id}\")}\n     * can have methods annotated whose arguments are annotated\n     * with {@code @PathParam(\"id\")}.\n     */\n    String value();\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/annotation/QueryParam.java",
    "content": "package cc.blynk.core.http.annotation;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * Binds the value(s) of a HTTP query parameter to a resource classMethod parameter,\n * resource class field, or resource class bean property.\n * Values are URL decoded unless this is disabled using the\n * annotation. A default value can be specified using the\n * annotation.\n *\n * The type {@code T} of the annotated parameter, field or property must\n * either:\n * <ol>\n * <li>Be a primitive type</li>\n * <li>Have a constructor that accepts a single {@code String} argument</li>\n * <li>Have a static classMethod named {@code valueOf} or {@code fromString}\n * that accepts a single\n * {@code String} argument (see, for example, {@link Integer#valueOf(String)})</li>\n * <li>Have a registered implementation of { javax.ws.rs.ext.ParamConverterProvider}\n * JAX-RS extension SPI that returns a { javax.ws.rs.ext.ParamConverter}\n * instance capable of a \"from string\" conversion for the type.</li>\n * <li>Be {@code List<T>}, {@code Set<T>} or\n * {@code SortedSet<T>}, where {@code T} satisfies 2, 3 or 4 above.\n * The resulting collection is read-only.</li>\n * </ol>\n *\n * <p>If the type is not one of the collection types listed in 5 above and the\n * query parameter is represented by multiple values then the first value (lexically)\n * of the parameter is used.</p>\n *\n * <p>Because injection occurs at object creation time, use of this annotation\n * on resource class fields and bean properties is only supported for the\n * default per-request resource class lifecycle. Resource classes using\n * other lifecycles should only use this annotation on resource classMethod\n * parameters.</p>\n *\n * @author Paul Sandoz\n * @author Marc Hadley\n * @since 1.0\n */\n@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})\n@Retention(RetentionPolicy.RUNTIME)\n@Documented\npublic @interface QueryParam {\n\n    /**\n     * Defines the name of the HTTP query parameter whose value will be used\n     * to initialize the value of the annotated classMethod argument, class field or\n     * bean property. The name is specified in decoded form, any percent encoded\n     * literals within the value will not be decoded and will instead be\n     * treated as literal text. E.g. if the parameter name is \"a b\" then the\n     * value of the annotation is \"a b\", <i>not</i> \"a+b\" or \"a%20b\".\n     */\n    String value();\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/handlers/BlynkHttpPostMultipartRequestDecoder.java",
    "content": "package cc.blynk.core.http.handlers;\n\nimport io.netty.handler.codec.http.HttpContent;\nimport io.netty.handler.codec.http.HttpRequest;\nimport io.netty.handler.codec.http.multipart.HttpDataFactory;\nimport io.netty.handler.codec.http.multipart.HttpPostMultipartRequestDecoder;\n\nimport java.nio.charset.Charset;\n\n/**\n * Full copy of HttpPostRequestDecoder to fix\n * https://github.com/netty/netty/issues/10281\n */\npublic class BlynkHttpPostMultipartRequestDecoder extends HttpPostMultipartRequestDecoder {\n\n    private final boolean state;\n\n    public BlynkHttpPostMultipartRequestDecoder(HttpDataFactory factory, HttpRequest request, Charset charset) {\n        super(factory, request, charset);\n        this.state = true;\n    }\n\n    @Override\n    public HttpPostMultipartRequestDecoder offer(HttpContent content) {\n        //this is very dirty hack\n        //we skip the first invocation of the offer\n        //it wont work for every case, but we have Aggregate handler in the pipeline\n        //so we are covered here :)\n        if (state) {\n            return super.offer(content);\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/handlers/BlynkHttpPostRequestDecoder.java",
    "content": "package cc.blynk.core.http.handlers;\n\nimport io.netty.handler.codec.http.HttpConstants;\nimport io.netty.handler.codec.http.HttpContent;\nimport io.netty.handler.codec.http.HttpHeaderNames;\nimport io.netty.handler.codec.http.HttpHeaderValues;\nimport io.netty.handler.codec.http.HttpRequest;\nimport io.netty.handler.codec.http.multipart.DefaultHttpDataFactory;\nimport io.netty.handler.codec.http.multipart.HttpDataFactory;\nimport io.netty.handler.codec.http.multipart.HttpPostRequestDecoder;\nimport io.netty.handler.codec.http.multipart.HttpPostStandardRequestDecoder;\nimport io.netty.handler.codec.http.multipart.InterfaceHttpData;\nimport io.netty.handler.codec.http.multipart.InterfaceHttpPostRequestDecoder;\nimport io.netty.util.internal.ObjectUtil;\nimport io.netty.util.internal.StringUtil;\n\nimport java.nio.charset.Charset;\nimport java.util.List;\n\n/**\n * Full copy of HttpPostRequestDecoder to fix\n * https://github.com/netty/netty/issues/10281\n */\npublic class BlynkHttpPostRequestDecoder implements InterfaceHttpPostRequestDecoder {\n\n    private final InterfaceHttpPostRequestDecoder decoder;\n\n    /**\n     *\n     * @param request\n     *            the request to decode\n     * @throws NullPointerException\n     *             for request\n     * @throws HttpPostRequestDecoder.ErrorDataDecoderException\n     *             if the default charset was wrong when decoding or other\n     *             errors\n     */\n    public BlynkHttpPostRequestDecoder(HttpRequest request) {\n        this(new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE), request, HttpConstants.DEFAULT_CHARSET);\n    }\n\n    /**\n     *\n     * @param factory\n     *            the factory used to create InterfaceHttpData\n     * @param request\n     *            the request to decode\n     * @throws NullPointerException\n     *             for request or factory\n     * @throws HttpPostRequestDecoder.ErrorDataDecoderException\n     *             if the default charset was wrong when decoding or other\n     *             errors\n     */\n    public BlynkHttpPostRequestDecoder(HttpDataFactory factory, HttpRequest request) {\n        this(factory, request, HttpConstants.DEFAULT_CHARSET);\n    }\n\n    /**\n     *\n     * @param factory\n     *            the factory used to create InterfaceHttpData\n     * @param request\n     *            the request to decode\n     * @param charset\n     *            the charset to use as default\n     * @throws NullPointerException\n     *             for request or charset or factory\n     * @throws HttpPostRequestDecoder.ErrorDataDecoderException\n     *             if the default charset was wrong when decoding or other\n     *             errors\n     */\n    public BlynkHttpPostRequestDecoder(HttpDataFactory factory, HttpRequest request, Charset charset) {\n        ObjectUtil.checkNotNull(factory, \"factory\");\n        ObjectUtil.checkNotNull(request, \"request\");\n        ObjectUtil.checkNotNull(charset, \"charset\");\n\n        // Fill default values\n        if (isMultipart(request)) {\n            decoder = new BlynkHttpPostMultipartRequestDecoder(factory, request, charset);\n        } else {\n            decoder = new HttpPostStandardRequestDecoder(factory, request, charset);\n        }\n    }\n\n    /**\n     * Check if the given request is a multipart request\n     * @return True if the request is a Multipart request\n     */\n    public static boolean isMultipart(HttpRequest request) {\n        String mimeType = request.headers().get(HttpHeaderNames.CONTENT_TYPE);\n        if (mimeType != null && mimeType.startsWith(HttpHeaderValues.MULTIPART_FORM_DATA.toString())) {\n            return getMultipartDataBoundary(mimeType) != null;\n        }\n        return false;\n    }\n\n    /**\n     * Check from the request ContentType if this request is a Multipart request.\n     * @return an array of String if multipartDataBoundary exists with the multipartDataBoundary\n     * as first element, charset if any as second (missing if not set), else null\n     */\n    protected static String[] getMultipartDataBoundary(String contentType) {\n        // Check if Post using \"multipart/form-data; boundary=--89421926422648 [; charset=xxx]\"\n        String[] headerContentType = splitHeaderContentType(contentType);\n        final String multiPartHeader = HttpHeaderValues.MULTIPART_FORM_DATA.toString();\n        if (headerContentType[0].regionMatches(true, 0, multiPartHeader, 0, multiPartHeader.length())) {\n            int mrank;\n            int crank;\n            final String boundaryHeader = HttpHeaderValues.BOUNDARY.toString();\n            if (headerContentType[1].regionMatches(true, 0, boundaryHeader, 0, boundaryHeader.length())) {\n                mrank = 1;\n                crank = 2;\n            } else if (headerContentType[2].regionMatches(true, 0, boundaryHeader, 0, boundaryHeader.length())) {\n                mrank = 2;\n                crank = 1;\n            } else {\n                return null;\n            }\n            String boundary = StringUtil.substringAfter(headerContentType[mrank], '=');\n            if (boundary == null) {\n                throw new HttpPostRequestDecoder.ErrorDataDecoderException(\"Needs a boundary value\");\n            }\n            if (boundary.charAt(0) == '\"') {\n                String bound = boundary.trim();\n                int index = bound.length() - 1;\n                if (bound.charAt(index) == '\"') {\n                    boundary = bound.substring(1, index);\n                }\n            }\n            final String charsetHeader = HttpHeaderValues.CHARSET.toString();\n            if (headerContentType[crank].regionMatches(true, 0, charsetHeader, 0, charsetHeader.length())) {\n                String charset = StringUtil.substringAfter(headerContentType[crank], '=');\n                if (charset != null) {\n                    return new String[] {\"--\" + boundary, charset};\n                }\n            }\n            return new String[] {\"--\" + boundary};\n        }\n        return null;\n    }\n\n    @Override\n    public boolean isMultipart() {\n        return decoder.isMultipart();\n    }\n\n    @Override\n    public void setDiscardThreshold(int discardThreshold) {\n        decoder.setDiscardThreshold(discardThreshold);\n    }\n\n    @Override\n    public int getDiscardThreshold() {\n        return decoder.getDiscardThreshold();\n    }\n\n    @Override\n    public List<InterfaceHttpData> getBodyHttpDatas() {\n        return decoder.getBodyHttpDatas();\n    }\n\n    @Override\n    public List<InterfaceHttpData> getBodyHttpDatas(String name) {\n        return decoder.getBodyHttpDatas(name);\n    }\n\n    @Override\n    public InterfaceHttpData getBodyHttpData(String name) {\n        return decoder.getBodyHttpData(name);\n    }\n\n    @Override\n    public InterfaceHttpPostRequestDecoder offer(HttpContent content) {\n        return decoder.offer(content);\n    }\n\n    @Override\n    public boolean hasNext() {\n        return decoder.hasNext();\n    }\n\n    @Override\n    public InterfaceHttpData next() {\n        return decoder.next();\n    }\n\n    @Override\n    public InterfaceHttpData currentPartialHttpData() {\n        return decoder.currentPartialHttpData();\n    }\n\n    @Override\n    public void destroy() {\n        decoder.destroy();\n    }\n\n    @Override\n    public void cleanFiles() {\n        decoder.cleanFiles();\n    }\n\n    @Override\n    public void removeHttpDataFromClean(InterfaceHttpData data) {\n        decoder.removeHttpDataFromClean(data);\n    }\n\n    /**\n     * Split the very first line (Content-Type value) in 3 Strings\n     *\n     * @return the array of 3 Strings\n     */\n    private static String[] splitHeaderContentType(String sb) {\n        int aStart;\n        int aEnd;\n        int bStart;\n        int bEnd;\n        int cStart;\n        int cEnd;\n        aStart = HttpPostBodyUtil.findNonWhitespace(sb, 0);\n        aEnd =  sb.indexOf(';');\n        if (aEnd == -1) {\n            return new String[] {sb, \"\", \"\"};\n        }\n        bStart = HttpPostBodyUtil.findNonWhitespace(sb, aEnd + 1);\n        if (sb.charAt(aEnd - 1) == ' ') {\n            aEnd--;\n        }\n        bEnd =  sb.indexOf(';', bStart);\n        if (bEnd == -1) {\n            bEnd = HttpPostBodyUtil.findEndOfString(sb);\n            return new String[] {sb.substring(aStart, aEnd), sb.substring(bStart, bEnd), \"\"};\n        }\n        cStart = HttpPostBodyUtil.findNonWhitespace(sb, bEnd + 1);\n        if (sb.charAt(bEnd - 1) == ' ') {\n            bEnd--;\n        }\n        cEnd = HttpPostBodyUtil.findEndOfString(sb);\n        return new String[] {sb.substring(aStart, aEnd), sb.substring(bStart, bEnd), sb.substring(cStart, cEnd)};\n    }\n\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/handlers/CookieBasedUrlReWriterHandler.java",
    "content": "package cc.blynk.core.http.handlers;\n\nimport cc.blynk.server.core.dao.SessionDao;\nimport io.netty.channel.ChannelHandler;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.ChannelInboundHandlerAdapter;\nimport io.netty.handler.codec.http.FullHttpRequest;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 13.05.16.\n */\n@ChannelHandler.Sharable\npublic class CookieBasedUrlReWriterHandler extends ChannelInboundHandlerAdapter {\n\n    private final String initUrl;\n    private final String mapToUrlWithCookie;\n    private final String mapToUrlWithoutCookie;\n\n    public CookieBasedUrlReWriterHandler(String initUrl, String mapToUrlWithCookie, String mapToUrlWithoutCookie) {\n        this.initUrl = initUrl;\n        this.mapToUrlWithCookie = mapToUrlWithCookie;\n        this.mapToUrlWithoutCookie = mapToUrlWithoutCookie;\n    }\n\n    @Override\n    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {\n        if (msg instanceof FullHttpRequest) {\n            FullHttpRequest request = (FullHttpRequest) msg;\n            if (request.uri().equals(initUrl)) {\n                if (ctx.channel().attr(SessionDao.userAttributeKey).get() == null) {\n                    request.setUri(mapToUrlWithoutCookie);\n                } else {\n                    request.setUri(mapToUrlWithCookie);\n                }\n            }\n        }\n\n        super.channelRead(ctx, msg);\n    }\n\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/handlers/HttpPostBodyUtil.java",
    "content": "/*\n * Copyright 2012 The Netty Project\n *\n * The Netty Project licenses this file to you under the Apache License,\n * version 2.0 (the \"License\"); you may not use this file except in compliance\n * with the License. You may obtain a copy of the License at:\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n */\npackage cc.blynk.core.http.handlers;\n\n/**\n * Shared Static object between HttpMessageDecoder, HttpPostRequestDecoder and HttpPostRequestEncoder\n */\nfinal class HttpPostBodyUtil {\n\n    public static final int chunkSize = 8096;\n\n    /**\n     * Allowed mechanism for multipart\n     * mechanism := \"7bit\"\n                  / \"8bit\"\n                  / \"binary\"\n       Not allowed: \"quoted-printable\"\n                  / \"base64\"\n     */\n    public enum TransferEncodingMechanism {\n        /**\n         * Default encoding\n         */\n        BIT7(\"7bit\"),\n        /**\n         * Short lines but not in ASCII - no encoding\n         */\n        BIT8(\"8bit\"),\n        /**\n         * Could be long text not in ASCII - no encoding\n         */\n        BINARY(\"binary\");\n\n        private final String value;\n\n        TransferEncodingMechanism(String value) {\n            this.value = value;\n        }\n\n        public String value() {\n            return value;\n        }\n\n        @Override\n        public String toString() {\n            return value;\n        }\n    }\n\n    private HttpPostBodyUtil() {\n    }\n\n    /**\n     * Find the first non whitespace\n     * @return the rank of the first non whitespace\n     */\n    static int findNonWhitespace(String sb, int offset) {\n        int result;\n        for (result = offset; result < sb.length(); result++) {\n            if (!Character.isWhitespace(sb.charAt(result))) {\n                break;\n            }\n        }\n        return result;\n    }\n\n    /**\n     * Find the end of String\n     * @return the rank of the end of string\n     */\n    static int findEndOfString(String sb) {\n        int result;\n        for (result = sb.length(); result > 0; result--) {\n            if (!Character.isWhitespace(sb.charAt(result - 1))) {\n                break;\n            }\n        }\n        return result;\n    }\n\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/handlers/NoCacheStaticFile.java",
    "content": "package cc.blynk.core.http.handlers;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 11.07.16.\n */\npublic class NoCacheStaticFile extends StaticFile {\n\n    public NoCacheStaticFile(String path) {\n        super(path);\n    }\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/handlers/NoMatchHandler.java",
    "content": "package cc.blynk.core.http.handlers;\n\nimport cc.blynk.core.http.Response;\nimport io.netty.channel.ChannelHandler;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.ChannelInboundHandlerAdapter;\nimport io.netty.handler.codec.http.HttpRequest;\nimport io.netty.util.ReferenceCountUtil;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 12.03.17.\n */\n@ChannelHandler.Sharable\npublic class NoMatchHandler extends ChannelInboundHandlerAdapter {\n\n    private static final Logger log = LogManager.getLogger(NoMatchHandler.class);\n\n    @Override\n    public void channelRead(ChannelHandlerContext ctx, Object msg) {\n        try {\n            if (msg instanceof HttpRequest) {\n                HttpRequest req = (HttpRequest) msg;\n                log.debug(\"Error resolving url. No path found. {} : {}\", req.method().name(), req.uri());\n                if (ctx.channel().isWritable()) {\n                    ctx.writeAndFlush(Response.notFound(), ctx.voidPromise());\n                }\n            }\n        } finally {\n            ReferenceCountUtil.release(msg);\n        }\n    }\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/handlers/OTAHandler.java",
    "content": "/*\n * Copyright 2012 The Netty Project\n *\n * The Netty Project licenses this file to you under the Apache License,\n * version 2.0 (the \"License\"); you may not use this file except in compliance\n * with the License. You may obtain a copy of the License at:\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n */\npackage cc.blynk.core.http.handlers;\n\nimport cc.blynk.core.http.AuthHeadersBaseHttpHandler;\nimport cc.blynk.core.http.Response;\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.core.dao.SessionDao;\nimport cc.blynk.server.core.dao.TokenManager;\nimport cc.blynk.server.core.dao.TokenValue;\nimport cc.blynk.server.core.dao.UserDao;\nimport cc.blynk.server.core.dao.UserKey;\nimport cc.blynk.server.core.dao.ota.OTAInfo;\nimport cc.blynk.server.core.dao.ota.OTAManager;\nimport cc.blynk.server.core.model.auth.Session;\nimport cc.blynk.server.core.model.auth.User;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.handler.codec.http.HttpMethod;\nimport io.netty.handler.codec.http.HttpRequest;\nimport io.netty.handler.codec.http.QueryStringDecoder;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.util.List;\n\nimport static cc.blynk.core.http.Response.badRequest;\nimport static cc.blynk.core.http.Response.ok;\nimport static cc.blynk.server.core.protocol.enums.Command.BLYNK_INTERNAL;\n\npublic class OTAHandler extends UploadHandler {\n\n    private static final Logger log = LogManager.getLogger(OTAHandler.class);\n\n    private final TokenManager tokenManager;\n    private final SessionDao sessionDao;\n    private final UserDao userDao;\n    private QueryStringDecoder queryStringDecoder;\n    private final OTAManager otaManager;\n\n    public OTAHandler(Holder holder, String handlerUri, String uploadFolder) {\n        super(holder.props.jarPath, handlerUri, uploadFolder);\n        this.tokenManager = holder.tokenManager;\n        this.sessionDao = holder.sessionDao;\n        this.userDao = holder.userDao;\n        this.otaManager = holder.otaManager;\n    }\n\n    @Override\n    public boolean accept(ChannelHandlerContext ctx, HttpRequest req) {\n        if (req.method() == HttpMethod.POST && req.uri().startsWith(handlerUri)) {\n            try {\n                User superAdmin = AuthHeadersBaseHttpHandler.validateAuth(userDao, req);\n                if (superAdmin != null) {\n                    ctx.channel().attr(AuthHeadersBaseHttpHandler.USER).set(superAdmin);\n                    queryStringDecoder = new QueryStringDecoder(req.uri());\n                    return true;\n                }\n            } catch (IllegalAccessException e) {\n                //return 403 and stop processing.\n                ctx.writeAndFlush(Response.forbidden(e.getMessage()));\n                return true;\n            }\n        }\n        return false;\n    }\n\n    @Override\n    public Response afterUpload(ChannelHandlerContext ctx, String pathToFirmware) {\n        String token = getParam(\"token\");\n        if (token != null) {\n            log.info(\"Requested OTA for single device {}.\", token);\n            return singleDeviceOTA(ctx, token, pathToFirmware);\n        }\n\n        String user = getParam(\"user\");\n        if (user != null) {\n            String appName = getParam(\"appName\");\n            UserKey userKey = new UserKey(user, appName);\n            String project = getParam(\"project\");\n            log.info(\"Requested OTA for single user {}. Project {}.\", user, project);\n            return singleUserOTA(ctx, userKey, project, pathToFirmware);\n        }\n\n        log.info(\"Requested OTA for all devices...\");\n        return allDevicesOTA(ctx, pathToFirmware);\n    }\n\n    private String getParam(String paramString) {\n        List<String> param = queryStringDecoder.parameters().get(paramString);\n        if (param == null) {\n            return null;\n        }\n        return param.get(0);\n    }\n\n    private Response allDevicesOTA(ChannelHandlerContext ctx, String pathToFirmware) {\n        User initiator = ctx.channel().attr(AuthHeadersBaseHttpHandler.USER).get();\n        otaManager.initiateForAll(initiator, pathToFirmware);\n\n        return ok(pathToFirmware);\n    }\n\n    private Response singleUserOTA(ChannelHandlerContext ctx, UserKey userKey,\n                                   String projectName, String pathToFirmware) {\n        User initiator = ctx.channel().attr(AuthHeadersBaseHttpHandler.USER).get();\n        User user = userDao.users.get(userKey);\n\n        if (user == null) {\n            log.info(\"Requested user {} not found.\", userKey);\n            return badRequest(\"Requested user not found.\");\n        }\n\n        otaManager.initiate(initiator, userKey, projectName, pathToFirmware);\n\n        return ok(pathToFirmware);\n    }\n\n    private Response singleDeviceOTA(ChannelHandlerContext ctx, String token, String pathToFirmware) {\n        TokenValue tokenValue = tokenManager.getTokenValueByToken(token);\n\n        if (tokenValue == null) {\n            log.debug(\"Requested token {} not found.\", token);\n            return badRequest(\"Invalid token.\");\n        }\n\n        User user = tokenValue.user;\n        int dashId = tokenValue.dash.id;\n        int deviceId = tokenValue.device.id;\n\n        Session session = sessionDao.get(new UserKey(user));\n        if (session == null) {\n            log.debug(\"No session for user {}.\", user.email);\n            return badRequest(\"Device wasn't connected yet.\");\n        }\n\n        String body = OTAInfo.makeHardwareBody(otaManager.serverHostUrl, pathToFirmware);\n        if (session.sendMessageToHardware(dashId, BLYNK_INTERNAL, 7777, body, deviceId)) {\n            log.debug(\"No device in session.\");\n            return badRequest(\"No device in session.\");\n        }\n\n        User initiator = ctx.channel().attr(AuthHeadersBaseHttpHandler.USER).get();\n        if (initiator != null) {\n            tokenValue.device.updateOTAInfo(initiator.email);\n        }\n\n        return ok(pathToFirmware);\n    }\n\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/handlers/StaticFile.java",
    "content": "package cc.blynk.core.http.handlers;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 11.07.16.\n */\npublic class StaticFile {\n\n    public final String path;\n\n    public StaticFile(String path) {\n        this.path = path;\n    }\n\n    public boolean isStatic(String url) {\n        return url.startsWith(path);\n    }\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/handlers/StaticFileEdsWith.java",
    "content": "package cc.blynk.core.http.handlers;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 11.07.16.\n */\npublic class StaticFileEdsWith extends StaticFile {\n\n    final String folderPathForStatic;\n\n    public StaticFileEdsWith(String folderPathForStatic, String path) {\n        super(path);\n        this.folderPathForStatic = folderPathForStatic;\n    }\n\n    @Override\n    public boolean isStatic(String url) {\n        return url.endsWith(path);\n    }\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/handlers/StaticFileHandler.java",
    "content": "package cc.blynk.core.http.handlers;\n\nimport cc.blynk.core.http.utils.ContentTypeUtil;\nimport cc.blynk.utils.FileUtils;\nimport cc.blynk.utils.properties.ServerProperties;\nimport io.netty.buffer.Unpooled;\nimport io.netty.channel.ChannelFuture;\nimport io.netty.channel.ChannelFutureListener;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.ChannelInboundHandlerAdapter;\nimport io.netty.channel.DefaultFileRegion;\nimport io.netty.handler.codec.http.DefaultFullHttpResponse;\nimport io.netty.handler.codec.http.DefaultHttpResponse;\nimport io.netty.handler.codec.http.FullHttpRequest;\nimport io.netty.handler.codec.http.FullHttpResponse;\nimport io.netty.handler.codec.http.HttpChunkedInput;\nimport io.netty.handler.codec.http.HttpHeaderValues;\nimport io.netty.handler.codec.http.HttpMethod;\nimport io.netty.handler.codec.http.HttpResponse;\nimport io.netty.handler.codec.http.HttpResponseStatus;\nimport io.netty.handler.codec.http.HttpUtil;\nimport io.netty.handler.codec.http.LastHttpContent;\nimport io.netty.handler.ssl.SslHandler;\nimport io.netty.handler.stream.ChunkedFile;\nimport io.netty.util.ReferenceCountUtil;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.io.File;\nimport java.io.FileNotFoundException;\nimport java.io.RandomAccessFile;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.text.SimpleDateFormat;\nimport java.util.Calendar;\nimport java.util.Date;\nimport java.util.GregorianCalendar;\nimport java.util.Locale;\nimport java.util.TimeZone;\nimport java.util.regex.Pattern;\n\nimport static cc.blynk.server.core.protocol.handlers.DefaultExceptionHandler.handleGeneralException;\nimport static io.netty.handler.codec.http.HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN;\nimport static io.netty.handler.codec.http.HttpHeaderNames.CACHE_CONTROL;\nimport static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION;\nimport static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;\nimport static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE;\nimport static io.netty.handler.codec.http.HttpHeaderNames.DATE;\nimport static io.netty.handler.codec.http.HttpHeaderNames.EXPIRES;\nimport static io.netty.handler.codec.http.HttpHeaderNames.IF_MODIFIED_SINCE;\nimport static io.netty.handler.codec.http.HttpHeaderNames.LAST_MODIFIED;\nimport static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;\nimport static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;\nimport static io.netty.handler.codec.http.HttpResponseStatus.NOT_MODIFIED;\nimport static io.netty.handler.codec.http.HttpResponseStatus.OK;\nimport static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 10.12.15.\n */\npublic class StaticFileHandler extends ChannelInboundHandlerAdapter {\n\n    private static final Logger log = LogManager.getLogger(StaticFileHandler.class);\n\n    private static final String HTTP_DATE_FORMAT = \"EEE, dd MMM yyyy HH:mm:ss zzz\";\n    private static final String HTTP_DATE_GMT_TIMEZONE = \"GMT\";\n    private static final int HTTP_CACHE_SECONDS = 60;\n\n    /**\n     * Used for case when server started from IDE and static files wasn't unpacked from jar.\n     */\n    private final boolean isUnpacked;\n    private final StaticFile[] staticPaths;\n    private final String jarPath;\n\n    public StaticFileHandler(ServerProperties props, StaticFile... staticPaths) {\n        this.staticPaths = staticPaths;\n        this.isUnpacked = props.isUnpacked;\n        this.jarPath = props.jarPath;\n    }\n\n    private static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {\n        FullHttpResponse response = new DefaultFullHttpResponse(\n                HTTP_1_1, status, Unpooled.copiedBuffer(\"Failure: \" + status + \"\\r\\n\", StandardCharsets.UTF_8));\n        response.headers().set(CONTENT_TYPE, \"text/plain; charset=UTF-8\");\n\n        // Close the connection as soon as the error message is sent.\n        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);\n    }\n\n    /**\n     * When file timestamp is the same as what the browser is sending up, send a \"304 Not Modified\"\n     *\n     * @param ctx\n     *            Context\n     */\n    private static void sendNotModified(ChannelHandlerContext ctx) {\n        FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, NOT_MODIFIED);\n        setDateHeader(response);\n\n        // Close the connection as soon as the error message is sent.\n        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);\n    }\n\n    /**\n     * Sets the Date header for the HTTP response\n     *\n     * @param response\n     *            HTTP response\n     */\n    private static void setDateHeader(FullHttpResponse response) {\n        SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US);\n        dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE));\n\n        Calendar time = new GregorianCalendar();\n        response.headers().set(DATE, dateFormatter.format(time.getTime()));\n    }\n\n    /**\n     * Sets the Date and Cache headers for the HTTP Response\n     *\n     * @param response\n     *            HTTP response\n     * @param fileToCache\n     *            file to extract content type\n     */\n    private static void setDateAndCacheHeaders(io.netty.handler.codec.http.HttpResponse response, File fileToCache) {\n        SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US);\n        dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE));\n\n        // Date header\n        Calendar time = new GregorianCalendar();\n        response.headers().set(DATE, dateFormatter.format(time.getTime()));\n\n        // Add cache headers\n        time.add(Calendar.SECOND, HTTP_CACHE_SECONDS);\n        response.headers()\n                .set(EXPIRES, dateFormatter.format(time.getTime()))\n                .set(CACHE_CONTROL, \"private, max-age=\" + HTTP_CACHE_SECONDS)\n                .set(LAST_MODIFIED, dateFormatter.format(new Date(fileToCache.lastModified())));\n    }\n\n    @Override\n    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {\n        if (!(msg instanceof FullHttpRequest)) {\n            return;\n        }\n\n        FullHttpRequest req = (FullHttpRequest) msg;\n\n        StaticFile staticFile = getStaticPath(req.uri());\n        if (staticFile != null) {\n            try {\n                serveStatic(ctx, req, staticFile);\n            } finally {\n                ReferenceCountUtil.release(req);\n            }\n            return;\n        }\n\n        ctx.fireChannelRead(req);\n    }\n\n    private StaticFile getStaticPath(String path) {\n        for (StaticFile staticPath : staticPaths) {\n            if (staticPath.isStatic(path)) {\n                return staticPath;\n            }\n        }\n        return null;\n    }\n\n    private void serveStatic(ChannelHandlerContext ctx, FullHttpRequest request, StaticFile staticFile)\n            throws Exception {\n        if (!request.decoderResult().isSuccess()) {\n            sendError(ctx, BAD_REQUEST);\n            return;\n        }\n\n        if (request.method() != HttpMethod.GET) {\n            return;\n        }\n\n        Path path;\n        String uri = request.uri();\n\n        if (isNotSecure(uri)) {\n            sendError(ctx, NOT_FOUND);\n            return;\n        }\n\n        //running from jar\n        if (isUnpacked) {\n            log.trace(\"Is unpacked.\");\n            if (staticFile instanceof StaticFileEdsWith) {\n                StaticFileEdsWith staticFileEdsWith = (StaticFileEdsWith) staticFile;\n                path = Paths.get(staticFileEdsWith.folderPathForStatic, uri);\n            } else {\n                path = Paths.get(jarPath, uri);\n            }\n        } else {\n            //for local mode / running from ide\n            path = FileUtils.getPathForLocalRun(uri);\n        }\n\n        log.trace(\"Getting file from path {}\", path);\n\n        if (path == null || Files.notExists(path) || Files.isDirectory(path)) {\n            sendError(ctx, NOT_FOUND);\n            return;\n        }\n\n        File file = path.toFile();\n\n        // Cache Validation\n        String ifModifiedSince = request.headers().get(IF_MODIFIED_SINCE);\n        if (ifModifiedSince != null && !ifModifiedSince.isEmpty() && !(staticFile instanceof NoCacheStaticFile)) {\n            SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US);\n            Date ifModifiedSinceDate = dateFormatter.parse(ifModifiedSince);\n\n            // Only compare up to the second because the datetime format we send to the client\n            // does not have milliseconds\n            long ifModifiedSinceDateSeconds = ifModifiedSinceDate.getTime() / 1000;\n            long fileLastModifiedSeconds = file.lastModified() / 1000;\n            if (ifModifiedSinceDateSeconds == fileLastModifiedSeconds) {\n                sendNotModified(ctx);\n                return;\n            }\n        }\n\n        RandomAccessFile raf;\n        try {\n            raf = new RandomAccessFile(file, \"r\");\n        } catch (FileNotFoundException ignore) {\n            sendError(ctx, NOT_FOUND);\n            return;\n        }\n        long fileLength = raf.length();\n\n        HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK);\n        response.headers()\n                .set(CONTENT_LENGTH, fileLength)\n                .set(CONTENT_TYPE, ContentTypeUtil.getContentType(file.getName()))\n                .set(ACCESS_CONTROL_ALLOW_ORIGIN, \"*\");\n\n        //todo setup caching for files.\n        setDateAndCacheHeaders(response, file);\n        if (HttpUtil.isKeepAlive(request)) {\n            response.headers().set(CONNECTION, HttpHeaderValues.KEEP_ALIVE);\n        }\n\n        // Write the initial line and the header.\n        ctx.write(response);\n\n        // Write the content.\n        ChannelFuture sendFileFuture;\n        ChannelFuture lastContentFuture;\n        if (ctx.pipeline().get(SslHandler.class) == null) {\n            ctx.write(new DefaultFileRegion(raf.getChannel(), 0, fileLength), ctx.newProgressivePromise());\n            // Write the end marker.\n            lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);\n        } else {\n            sendFileFuture =\n                    ctx.writeAndFlush(new HttpChunkedInput(new ChunkedFile(raf, 128 * 1024)),\n                            ctx.newProgressivePromise());\n            // HttpChunkedInput will write the end marker (LastHttpContent) for us.\n            lastContentFuture = sendFileFuture;\n        }\n\n        // Decide whether to close the connection or not.\n        if (!HttpUtil.isKeepAlive(request)) {\n            // Close the connection when the whole content is written out.\n            lastContentFuture.addListener(ChannelFutureListener.CLOSE);\n        }\n    }\n\n    private static final Pattern INSECURE_URI = Pattern.compile(\".*[<>&\\\"].*\");\n\n    private static boolean isNotSecure(String uri) {\n        if (uri.isEmpty() || uri.charAt(0) != '/') {\n            return true;\n        }\n\n        return uri.contains(\"/.\")\n                || uri.contains(\"./\")\n                || uri.contains(\".\\\\\")\n                || uri.contains(\"\\\\.\")\n                || uri.charAt(0) == '.' || uri.charAt(uri.length() - 1) == '.'\n                || INSECURE_URI.matcher(uri).matches();\n\n    }\n\n    @Override\n    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {\n        if (cause.getMessage() != null && cause.getMessage().contains(\"unknown_ca\")) {\n            log.warn(\"Self-generated certificate.\");\n        } else {\n            handleGeneralException(ctx, cause);\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/handlers/UploadHandler.java",
    "content": "/*\n * Copyright 2012 The Netty Project\n *\n * The Netty Project licenses this file to you under the Apache License,\n * version 2.0 (the \"License\"); you may not use this file except in compliance\n * with the License. You may obtain a copy of the License at:\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n */\npackage cc.blynk.core.http.handlers;\n\nimport cc.blynk.core.http.Response;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.SimpleChannelInboundHandler;\nimport io.netty.handler.codec.http.HttpContent;\nimport io.netty.handler.codec.http.HttpMethod;\nimport io.netty.handler.codec.http.HttpObject;\nimport io.netty.handler.codec.http.HttpRequest;\nimport io.netty.handler.codec.http.LastHttpContent;\nimport io.netty.handler.codec.http.multipart.DefaultHttpDataFactory;\nimport io.netty.handler.codec.http.multipart.DiskFileUpload;\nimport io.netty.handler.codec.http.multipart.HttpDataFactory;\nimport io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.EndOfDataDecoderException;\nimport io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.ErrorDataDecoderException;\nimport io.netty.handler.codec.http.multipart.InterfaceHttpData;\nimport io.netty.handler.codec.http.multipart.InterfaceHttpPostRequestDecoder;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.nio.file.Files;\nimport java.nio.file.NoSuchFileException;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.nio.file.StandardCopyOption;\n\nimport static cc.blynk.core.http.Response.badRequest;\nimport static cc.blynk.core.http.Response.ok;\nimport static cc.blynk.core.http.Response.serverError;\nimport static cc.blynk.server.core.protocol.handlers.DefaultExceptionHandler.handleGeneralException;\n\n\npublic class UploadHandler extends SimpleChannelInboundHandler<HttpObject> {\n\n    private static final Logger log = LogManager.getLogger(UploadHandler.class);\n\n    private static final HttpDataFactory factory = new DefaultHttpDataFactory(true);\n    final String handlerUri;\n    private InterfaceHttpPostRequestDecoder decoder;\n    private final String staticFolderPath;\n    private final String uploadFolder;\n\n    public UploadHandler(String staticFolderPath, String handlerUri, String uploadFolder) {\n        super(false);\n        this.handlerUri = handlerUri;\n        this.staticFolderPath = staticFolderPath;\n        this.uploadFolder = uploadFolder.endsWith(\"/\") ? uploadFolder : uploadFolder + \"/\";\n    }\n\n    @Override\n    public void channelInactive(ChannelHandlerContext ctx) {\n        if (decoder != null) {\n            decoder.cleanFiles();\n        }\n    }\n\n    public boolean accept(ChannelHandlerContext ctx, HttpRequest req) {\n        return req.method() == HttpMethod.POST && req.uri().startsWith(handlerUri);\n    }\n\n    @Override\n    public void channelRead0(ChannelHandlerContext ctx, HttpObject msg) {\n        if (msg instanceof HttpRequest) {\n            HttpRequest req = (HttpRequest) msg;\n\n            if (!accept(ctx, req)) {\n                ctx.fireChannelRead(msg);\n                return;\n            }\n\n            try {\n                log.debug(\"Incoming {} {}\", req.method(), req.uri());\n                decoder = new BlynkHttpPostRequestDecoder(factory, req);\n            } catch (ErrorDataDecoderException e) {\n                log.error(\"Error creating http post request decoder.\", e);\n                ctx.writeAndFlush(badRequest(e.getMessage()));\n                return;\n            }\n\n        }\n\n        if (decoder != null && msg instanceof HttpContent) {\n                // New chunk is received\n            HttpContent chunk = (HttpContent) msg;\n            try {\n                decoder.offer(chunk);\n            } catch (ErrorDataDecoderException e) {\n                log.error(\"Error creating http post offer.\", e);\n                ctx.writeAndFlush(badRequest(e.getMessage()));\n                return;\n            } finally {\n                chunk.release();\n            }\n\n            // example of reading only if at the end\n            if (chunk instanceof LastHttpContent) {\n                Response response;\n                try {\n                    String path = finishUpload();\n                    if (path != null) {\n                        response = afterUpload(ctx, path);\n                    } else {\n                        response = serverError(\"Can't find binary data in request.\");\n                    }\n                } catch (NoSuchFileException e) {\n                    log.error(\"Unable to copy uploaded file to static folder. Reason : {}\", e.getMessage());\n                    response = (serverError());\n                } catch (Exception e) {\n                    log.error(\"Error during file upload.\", e);\n                    response = (serverError());\n                }\n                ctx.writeAndFlush(response);\n            }\n        }\n    }\n\n    public Response afterUpload(ChannelHandlerContext ctx, String path) {\n        return ok(path);\n    }\n\n    private String finishUpload() throws Exception {\n        String pathTo = null;\n        try {\n            while (decoder.hasNext()) {\n                InterfaceHttpData data = decoder.next();\n                if (data != null) {\n                    if (data instanceof DiskFileUpload) {\n                        DiskFileUpload diskFileUpload = (DiskFileUpload) data;\n                        Path tmpFile = diskFileUpload.getFile().toPath();\n                        String uploadedFilename = diskFileUpload.getFilename();\n                        String extension = \"\";\n                        if (uploadedFilename.contains(\".\")) {\n                            extension = uploadedFilename.substring(uploadedFilename.lastIndexOf(\".\"),\n                                    uploadedFilename.length());\n                        }\n                        String finalName = tmpFile.getFileName().toString() + extension;\n\n                        //this is just to make it work on team city.\n                        Path staticPath = Paths.get(staticFolderPath, uploadFolder);\n                        if (!Files.exists(staticPath)) {\n                            Files.createDirectories(staticPath);\n                        }\n\n                        Files.move(tmpFile, Paths.get(staticFolderPath, uploadFolder, finalName),\n                                StandardCopyOption.REPLACE_EXISTING);\n                        pathTo =  uploadFolder + finalName;\n                    }\n                }\n            }\n        } catch (EndOfDataDecoderException endOfData) {\n            //ignore. that's fine.\n        } finally {\n            // destroy the decoder to release all resources\n            decoder.destroy();\n            decoder = null;\n        }\n\n        return pathTo;\n    }\n\n    @Override\n    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {\n        handleGeneralException(ctx, cause);\n    }\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/handlers/url/UrlMapper.java",
    "content": "package cc.blynk.core.http.handlers.url;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 31.03.17.\n */\npublic class UrlMapper {\n\n    public final String from;\n    public final String to;\n\n    public UrlMapper(String from, String to) {\n        this.from = from;\n        this.to = to;\n    }\n\n    public boolean isMatch(String uri) {\n        return from.equals(uri);\n    }\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/handlers/url/UrlReWriterHandler.java",
    "content": "package cc.blynk.core.http.handlers.url;\n\nimport io.netty.channel.ChannelHandler;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.ChannelInboundHandlerAdapter;\nimport io.netty.handler.codec.http.FullHttpRequest;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 13.05.16.\n */\n@ChannelHandler.Sharable\npublic class UrlReWriterHandler extends ChannelInboundHandlerAdapter {\n\n    private static final Logger log = LogManager.getLogger(UrlReWriterHandler.class);\n\n    private final UrlMapper[] mappers;\n\n    public UrlReWriterHandler(String from, String to) {\n        this(new UrlMapper(from, to));\n    }\n\n    public UrlReWriterHandler(UrlMapper mapper) {\n        this.mappers = new UrlMapper[] {mapper};\n    }\n\n    public UrlReWriterHandler(UrlMapper... mappers) {\n        this.mappers = mappers;\n    }\n\n    @Override\n    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {\n        if (msg instanceof FullHttpRequest) {\n            FullHttpRequest request = (FullHttpRequest) msg;\n\n            String requestUri = request.uri();\n            String mapToURI = mapTo(requestUri);\n            log.trace(\"Mapping from {} to {}\", requestUri, mapToURI);\n            request.setUri(mapToURI);\n        }\n\n        super.channelRead(ctx, msg);\n    }\n\n    private String mapTo(String uri) {\n        for (UrlMapper urlMapper : mappers) {\n            if (urlMapper.isMatch(uri)) {\n                return urlMapper.to;\n            }\n        }\n        return uri;\n    }\n\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/model/Filter.java",
    "content": "package cc.blynk.core.http.model;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 07.12.15.\n */\npublic class Filter {\n\n    public String name;\n\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/model/NameCountResponse.java",
    "content": "package cc.blynk.core.http.model;\n\nimport java.util.Map;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 07.12.15.\n */\npublic class NameCountResponse {\n\n    public final String name;\n\n    public final int count;\n\n    public NameCountResponse(Map.Entry<String, ?> entry) {\n        this(entry.getKey(), ((Number) entry.getValue()).intValue());\n    }\n\n    public NameCountResponse(String name, int val) {\n        this.name = name;\n        this.count = val;\n    }\n\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/rest/HandlerHolder.java",
    "content": "package cc.blynk.core.http.rest;\n\nimport java.util.Map;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 12.03.17.\n */\npublic final class HandlerHolder {\n\n    public final HandlerWrapper handler;\n\n    public final Map<String, String> extractedParams;\n\n    public HandlerHolder(HandlerWrapper handler, Map<String, String> extractedParams) {\n        this.handler = handler;\n        this.extractedParams = extractedParams;\n    }\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/rest/HandlerWrapper.java",
    "content": "package cc.blynk.core.http.rest;\n\nimport cc.blynk.core.http.Response;\nimport cc.blynk.core.http.UriTemplate;\nimport cc.blynk.core.http.annotation.DELETE;\nimport cc.blynk.core.http.annotation.Metric;\nimport cc.blynk.core.http.annotation.POST;\nimport cc.blynk.core.http.annotation.PUT;\nimport cc.blynk.core.http.rest.params.Param;\nimport cc.blynk.server.core.stats.GlobalStats;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.handler.codec.http.FullHttpResponse;\nimport io.netty.handler.codec.http.HttpMethod;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.lang.reflect.Method;\n\nimport static cc.blynk.server.core.protocol.enums.Command.HTTP_TOTAL;\n\n/**\n * Wrapper around Singleton Services.\n * Holds all info about annotations and service purpose.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 09.12.15.\n */\npublic class HandlerWrapper {\n\n    private static final Logger log = LogManager.getLogger(HandlerWrapper.class);\n\n    public final UriTemplate uriTemplate;\n\n    public final HttpMethod httpMethod;\n\n    public final Method classMethod;\n\n    public final Object handler;\n\n    public final Param[] params;\n\n    public final short metricIndex;\n\n    public final GlobalStats globalStats;\n\n    public HandlerWrapper(UriTemplate uriTemplate, Method method, Object handler, GlobalStats globalStats) {\n        this.uriTemplate = uriTemplate;\n        this.classMethod = method;\n        this.handler = handler;\n\n        if (method.isAnnotationPresent(POST.class)) {\n            this.httpMethod = HttpMethod.POST;\n        } else if (method.isAnnotationPresent(PUT.class)) {\n            this.httpMethod = HttpMethod.PUT;\n        } else if (method.isAnnotationPresent(DELETE.class)) {\n            this.httpMethod = HttpMethod.DELETE;\n        } else {\n            this.httpMethod = HttpMethod.GET;\n        }\n\n        Metric metricAnnotation = method.getAnnotation(Metric.class);\n        if (metricAnnotation != null) {\n            metricIndex = metricAnnotation.value();\n        } else {\n            metricIndex = -1;\n        }\n\n        this.params = new Param[method.getParameterCount()];\n        this.globalStats = globalStats;\n    }\n\n    public Object[] fetchParams(ChannelHandlerContext ctx, URIDecoder uriDecoder) {\n        Object[] res = new Object[params.length];\n        for (int i = 0; i < params.length; i++) {\n            res[i] = params[i].get(ctx, uriDecoder);\n        }\n\n        return res;\n    }\n\n    public FullHttpResponse invoke(Object[] params) {\n        try {\n            mark();\n            return (FullHttpResponse) classMethod.invoke(handler, params);\n        } catch (Exception e) {\n            Throwable cause = e.getCause();\n            if (cause == null) {\n                log.error(\"Error invoking handler. Reason : {}.\", e.getMessage());\n                log.debug(e);\n            } else {\n                log.error(\"Error invoking handler. Reason : {}.\", cause.getMessage());\n                log.debug(cause);\n            }\n\n            return Response.serverError(e.getMessage());\n        }\n    }\n\n    private void mark() {\n        globalStats.mark(HTTP_TOTAL);\n        if (metricIndex > -1) {\n            globalStats.markSpecificCounterOnly(metricIndex);\n        }\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (!(o instanceof HandlerWrapper)) {\n            return false;\n        }\n\n        HandlerWrapper that = (HandlerWrapper) o;\n\n        if (uriTemplate != null ? !uriTemplate.equals(that.uriTemplate) : that.uriTemplate != null) {\n            return false;\n        }\n        return !(httpMethod != null ? !httpMethod.equals(that.httpMethod) : that.httpMethod != null);\n\n    }\n\n    @Override\n    public int hashCode() {\n        int result = uriTemplate != null ? uriTemplate.hashCode() : 0;\n        result = 31 * result + (httpMethod != null ? httpMethod.hashCode() : 0);\n        return result;\n    }\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/rest/RequestHeaderParam.java",
    "content": "package cc.blynk.core.http.rest;\n\nimport cc.blynk.core.http.rest.params.Param;\nimport io.netty.channel.ChannelHandlerContext;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 09.12.15.\n */\npublic class RequestHeaderParam extends Param {\n\n    public RequestHeaderParam(String name, Class<?> type) {\n        super(name, type);\n    }\n\n    @Override\n    public Object get(ChannelHandlerContext ctx, URIDecoder uriDecoder) {\n        String header = uriDecoder.headers.get(name);\n        if (header == null) {\n            return null;\n        }\n        return header;\n    }\n\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/rest/URIDecoder.java",
    "content": "package cc.blynk.core.http.rest;\n\nimport cc.blynk.utils.http.MediaType;\nimport io.netty.buffer.ByteBuf;\nimport io.netty.handler.codec.http.HttpContent;\nimport io.netty.handler.codec.http.HttpHeaderNames;\nimport io.netty.handler.codec.http.HttpMethod;\nimport io.netty.handler.codec.http.HttpRequest;\nimport io.netty.handler.codec.http.QueryStringDecoder;\nimport io.netty.handler.codec.http.multipart.DefaultHttpDataFactory;\nimport io.netty.handler.codec.http.multipart.HttpPostRequestDecoder;\nimport io.netty.handler.codec.http.multipart.InterfaceHttpData;\n\nimport java.io.Closeable;\nimport java.nio.charset.StandardCharsets;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 06.12.15.\n */\npublic class URIDecoder extends QueryStringDecoder implements Closeable {\n\n    public final String[] paths;\n    public final Map<String, String> pathData;\n    public String contentType;\n    public Map<String, String> headers;\n\n    private HttpPostRequestDecoder decoder;\n    private ByteBuf bodyData;\n\n    public URIDecoder(HttpRequest httpRequest, Map<String, String> extractedParams) {\n        super(httpRequest.uri());\n        this.paths = path().split(\"/\");\n        if (httpRequest.method() == HttpMethod.PUT || httpRequest.method() == HttpMethod.POST) {\n            if (httpRequest instanceof HttpContent) {\n                this.contentType = httpRequest.headers().get(HttpHeaderNames.CONTENT_TYPE);\n                if (contentType != null && contentType.equals(MediaType.APPLICATION_FORM_URLENCODED)) {\n                    this.decoder = new HttpPostRequestDecoder(new DefaultHttpDataFactory(false), httpRequest);\n                } else {\n                    this.bodyData = ((HttpContent) httpRequest).content();\n                }\n            }\n        }\n        this.pathData = extractedParams;\n    }\n\n    public List<InterfaceHttpData> getBodyHttpDatas() {\n        return decoder.getBodyHttpDatas();\n    }\n\n    public String getContentAsString() {\n        return bodyData.toString(StandardCharsets.UTF_8);\n    }\n\n    @Override\n    public void close() {\n        if (decoder != null) {\n            decoder.destroy();\n        }\n    }\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/rest/params/BodyParam.java",
    "content": "package cc.blynk.core.http.rest.params;\n\nimport cc.blynk.core.http.rest.URIDecoder;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.utils.http.MediaType;\nimport com.fasterxml.jackson.core.JsonParseException;\nimport com.fasterxml.jackson.databind.JsonMappingException;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 09.12.15.\n */\npublic class BodyParam extends Param {\n\n    private static final Logger log = LogManager.getLogger(BodyParam.class);\n\n    private final String expectedContentType;\n\n    public BodyParam(String name, Class<?> type, String expectedContentType) {\n        super(name, type);\n        this.expectedContentType = expectedContentType;\n    }\n\n    @Override\n    public Object get(ChannelHandlerContext ctx, URIDecoder uriDecoder) {\n        if (uriDecoder.contentType == null || !uriDecoder.contentType.contains(expectedContentType)) {\n            throw new RuntimeException(\"Unexpected content type. Expecting \" + expectedContentType + \".\");\n        }\n\n        switch (expectedContentType) {\n            case MediaType.APPLICATION_JSON :\n                String data = \"\";\n                try {\n                    data = uriDecoder.getContentAsString();\n                    return JsonParser.MAPPER.readValue(data, type);\n                } catch (JsonParseException | JsonMappingException jsonParseError) {\n                    log.debug(\"Error parsing body param : '{}'.\", data);\n                    throw new RuntimeException(\"Error parsing body param. \" + data);\n                } catch (Exception e) {\n                    log.error(\"Unexpected error during parsing body param.\", e);\n                    throw new RuntimeException(\"Unexpected error during parsing body param.\", e);\n                }\n            default :\n                return uriDecoder.getContentAsString();\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/rest/params/ContextParam.java",
    "content": "package cc.blynk.core.http.rest.params;\n\nimport cc.blynk.core.http.rest.URIDecoder;\nimport io.netty.channel.ChannelHandlerContext;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 09.12.15.\n */\npublic class ContextParam extends Param {\n\n    public ContextParam(Class<?> type) {\n        super(null, type);\n    }\n\n    @Override\n    public Object get(ChannelHandlerContext ctx, URIDecoder uriDecoder) {\n        return ctx;\n    }\n\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/rest/params/EnumQueryParam.java",
    "content": "package cc.blynk.core.http.rest.params;\n\nimport cc.blynk.core.http.rest.URIDecoder;\nimport io.netty.channel.ChannelHandlerContext;\n\nimport java.util.AbstractMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 09.12.15.\n */\npublic class EnumQueryParam extends Param {\n\n    public EnumQueryParam(Class enumType) {\n        super(null, enumType);\n        if (!type.isEnum()) {\n            throw new RuntimeException(\"Should be enum.\");\n        }\n    }\n\n    @Override\n    public Object get(ChannelHandlerContext ctx, URIDecoder uriDecoder) {\n        Map<String, List<String>> params = uriDecoder.parameters();\n        for (Object enumValue : type.getEnumConstants()) {\n            List<String> paramsValues = params.get(enumValue.toString());\n            if (paramsValues != null) {\n                return new AbstractMap.SimpleImmutableEntry<>(enumValue, paramsValues.get(0));\n            }\n        }\n        return null;\n    }\n\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/rest/params/FormParam.java",
    "content": "package cc.blynk.core.http.rest.params;\n\nimport cc.blynk.core.http.rest.URIDecoder;\nimport cc.blynk.utils.ReflectionUtil;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.handler.codec.http.multipart.Attribute;\nimport io.netty.handler.codec.http.multipart.InterfaceHttpData;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.io.IOException;\nimport java.util.List;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 09.12.15.\n */\npublic class FormParam extends Param {\n\n    private static final Logger log = LogManager.getLogger(FormParam.class);\n\n    public FormParam(String name, Class<?> type) {\n        super(name, type);\n    }\n\n    @Override\n    public Object get(ChannelHandlerContext ctx, URIDecoder uriDecoder) {\n        List<InterfaceHttpData> bodyHttpDatas = uriDecoder.getBodyHttpDatas();\n        if (bodyHttpDatas == null || bodyHttpDatas.size() == 0) {\n            return null;\n        }\n\n        for (InterfaceHttpData data : bodyHttpDatas) {\n            if (name.equals(data.getName())) {\n                if (data.getHttpDataType() == InterfaceHttpData.HttpDataType.Attribute) {\n                    Attribute attribute = (Attribute) data;\n                    try {\n                        return ReflectionUtil.castTo(type, attribute.getValue());\n                    } catch (IOException e) {\n                        log.error(\"Error getting form params. Reason : {}\", e.getMessage(), e);\n                    }\n                }\n            }\n        }\n\n        return null;\n    }\n\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/rest/params/Param.java",
    "content": "package cc.blynk.core.http.rest.params;\n\nimport cc.blynk.core.http.rest.URIDecoder;\nimport io.netty.channel.ChannelHandlerContext;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 09.12.15.\n */\npublic abstract class Param {\n\n    protected final String name;\n\n    protected final Class<?> type;\n\n    public Param(String name, Class<?> type) {\n        this.name = name;\n        this.type = type;\n    }\n\n    public abstract Object get(ChannelHandlerContext ctx, URIDecoder uriDecoder);\n\n    Object convertTo(String value) {\n        if (type == long.class) {\n            return Long.valueOf(value);\n        }\n        if (type == int.class || type == Integer.class) {\n            return Integer.valueOf(value);\n        }\n        if (type == short.class || type == Short.class) {\n            return Short.valueOf(value);\n        }\n        if (type == boolean.class) {\n            return Boolean.valueOf(value);\n        }\n        return value;\n    }\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/rest/params/PathParam.java",
    "content": "package cc.blynk.core.http.rest.params;\n\nimport cc.blynk.core.http.rest.URIDecoder;\nimport io.netty.channel.ChannelHandlerContext;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 09.12.15.\n */\npublic class PathParam extends Param {\n\n    public PathParam(String name, Class<?> type) {\n        super(name, type);\n    }\n\n    @Override\n    public Object get(ChannelHandlerContext ctx, URIDecoder uriDecoder) {\n        return convertTo(uriDecoder.pathData.get(name));\n    }\n\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/rest/params/QueryParam.java",
    "content": "package cc.blynk.core.http.rest.params;\n\nimport cc.blynk.core.http.rest.URIDecoder;\nimport io.netty.channel.ChannelHandlerContext;\n\nimport java.lang.reflect.Array;\nimport java.util.List;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 09.12.15.\n */\npublic class QueryParam extends Param {\n\n    public QueryParam(String name, Class<?> type) {\n        super(name, type);\n    }\n\n    @Override\n    public Object get(ChannelHandlerContext ctx, URIDecoder uriDecoder) {\n        List<String> params = uriDecoder.parameters().get(name);\n        if (params == null) {\n            return null;\n        }\n\n        if (type == List.class) {\n            return params;\n        }\n\n        if (type.isArray()) {\n            //don't know how to make it better\n            if (type.getComponentType() == String.class) {\n                return params.toArray((String[]) Array.newInstance(type.getComponentType(), params.size()));\n            } else {\n                throw new IllegalStateException(\"Not supported.\");\n            }\n        }\n\n        return convertTo(params.get(0));\n    }\n\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/utils/AdminHttpUtil.java",
    "content": "package cc.blynk.core.http.utils;\n\nimport cc.blynk.core.http.model.NameCountResponse;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.stats.model.CommandStat;\n\nimport java.lang.reflect.Field;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 09.12.15.\n */\npublic final class AdminHttpUtil {\n\n    private AdminHttpUtil() {\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static List<?> sortStringAsInt(List<?> list, String field, String order) {\n        if (list.size() == 0) {\n            return list;\n        }\n\n        Comparator c = new GenericStringAsIntComparator(list.get(0).getClass(), field);\n        list.sort(\"asc\".equalsIgnoreCase(order) ? c : Collections.reverseOrder(c));\n\n        return list;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static List<?> sort(List<?> list, String field, String order) {\n        if (list.size() == 0) {\n            return list;\n        }\n\n        Comparator c = new GenericComparator(list.get(0).getClass(), field);\n        list.sort(\"asc\".equalsIgnoreCase(order) ? c : Collections.reverseOrder(c));\n\n        return list;\n    }\n\n    public static List<NameCountResponse> convertMapToPair(Map<String, ?> map) {\n        return map.entrySet().stream().map(NameCountResponse::new).collect(Collectors.toList());\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static List<NameCountResponse> convertObjectToMap(CommandStat commandStat) {\n        return convertMapToPair(JsonParser.MAPPER.convertValue(commandStat, Map.class));\n    }\n\n    /**\n     * The Blynk Project.\n     * Created by Dmitriy Dumanskiy.\n     * Created on 10.12.15.\n     */\n    public static class GenericComparator implements Comparator {\n\n        private final Class<?> fieldType;\n        private final Field field;\n\n        GenericComparator(Class<?> type, String sortField) {\n            try {\n                this.field = type.getField(sortField);\n            } catch (NoSuchFieldException nsfe) {\n                throw new RuntimeException(\"Can't find field \" + sortField + \" for \" + type.getName());\n            }\n            this.fieldType = field.getType();\n        }\n\n        @Override\n        public int compare(Object o1, Object o2) {\n            try {\n                Object v1 = field.get(o1);\n                Object v2 = field.get(o2);\n\n                return compareActual(v1, v2, fieldType);\n            } catch (Exception e) {\n                throw new RuntimeException(\"Error on compare during sorting. Type : \" + e.getMessage());\n            }\n        }\n\n        public int compareActual(Object v1, Object v2, Class<?> returnType) {\n            if (returnType == int.class || returnType == Integer.class) {\n                return Integer.compare((int) v1, (int) v2);\n            }\n            if (returnType == long.class || returnType == Long.class) {\n                return Long.compare((long) v1, (long) v2);\n            }\n            if (returnType == String.class) {\n                return ((String) v1).compareTo((String) v2);\n            }\n\n            throw new RuntimeException(\"Unexpected field type. Type : \" + returnType.getName());\n        }\n\n    }\n\n    public static class GenericStringAsIntComparator extends GenericComparator {\n\n        GenericStringAsIntComparator(Class<?> type, String sortField) {\n            super(type, sortField);\n        }\n\n        @Override\n        public int compareActual(Object v1, Object v2, Class<?> returnType) {\n            if (returnType == int.class || returnType == Integer.class) {\n                return Integer.compare((int) v1, (int) v2);\n            }\n            if (returnType == long.class || returnType == Long.class) {\n                return Long.compare((long) v1, (long) v2);\n            }\n            if (returnType == String.class) {\n                return Integer.valueOf((String) v1).compareTo(Integer.valueOf((String) v2));\n            }\n\n            throw new RuntimeException(\"Unexpected field type. Type : \" + returnType.getName());\n        }\n\n    }\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/utils/ContentTypeUtil.java",
    "content": "package cc.blynk.core.http.utils;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 07.07.16.\n */\npublic final class ContentTypeUtil {\n\n    private ContentTypeUtil() {\n    }\n\n    public static String getContentType(String fileName) {\n        if (fileName.endsWith(\".ico\")) {\n            return \"image/x-icon\";\n        }\n        if (fileName.endsWith(\".js\")) {\n            return \"application/javascript\";\n        }\n        if (fileName.endsWith(\".css\")) {\n            return \"text/css\";\n        }\n        if (fileName.endsWith(\".png\")) {\n            return \"image/png\";\n        }\n        if (fileName.endsWith(\".gz\")) {\n            return \"application/x-gzip\";\n        }\n        if (fileName.endsWith(\".zip\")) {\n            return \"application/zip\";\n        }\n        if (fileName.endsWith(\".bin\")) {\n            return \"application/octet-stream\";\n        }\n\n        return \"text/html\";\n    }\n\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/cc/blynk/core/http/utils/ListUtils.java",
    "content": "package cc.blynk.core.http.utils;\n\nimport java.util.List;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 06.12.15.\n */\npublic final class ListUtils {\n\n    private ListUtils() {\n    }\n\n    public static List<?> subList(List<?> list, int page, int size) {\n        return list.subList(\n                Math.min(list.size(), (page - 1)  * size),\n                Math.min(list.size(), size * page)\n        );\n\n        //below doesn't work with java 1.8_32 due to java bug.\n        /*\n        return list.stream()\n                .skip((page - 1)  * size)\n                .limit(size)\n                .collect(Collectors.toList());\n        */\n    }\n\n\n}\n"
  },
  {
    "path": "server/http-core/src/main/java/module-info.java",
    "content": "/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 17.05.18.\n */\nmodule cc.blynk.http.core {\n    exports cc.blynk.core.http;\n    exports cc.blynk.core.http.annotation;\n    exports cc.blynk.core.http.utils;\n    exports cc.blynk.core.http.model;\n    requires io.netty.transport;\n    requires io.netty.codec.http;\n    requires cc.blynk.core;\n    requires io.netty.common;\n    requires org.apache.logging.log4j;\n    requires cc.blynk.utils;\n    requires io.netty.buffer;\n    requires io.netty.handler;\n    requires com.fasterxml.jackson.core;\n    requires com.fasterxml.jackson.databind;\n}"
  },
  {
    "path": "server/http-core/src/test/java/cc/blynk/core/http/UriTemplateTest.java",
    "content": "package cc.blynk.core.http;\n\nimport org.junit.Test;\n\nimport static org.junit.Assert.assertFalse;\nimport static org.junit.Assert.assertTrue;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 05.01.16.\n */\npublic class UriTemplateTest {\n\n    @Test\n    public void testCorrectMatch() {\n        UriTemplate template1 = new UriTemplate(\"http://example.com/admin/users/{name}\");\n        UriTemplate template2 = new UriTemplate(\"http://example.com/admin/users/changePass/{name}\");\n\n        assertFalse(template1.matcher(\"http://example.com/admin/users/changePass/dmitriy@blynk.cc\").matches());\n        assertTrue(template1.matcher(\"http://example.com/admin/users/dmitriy@blynk.cc\").matches());\n\n        assertTrue(template2.matcher(\"http://example.com/admin/users/changePass/dmitriy@blynk.cc\").matches());\n        assertFalse(template2.matcher(\"http://example.com/admin/users/dmitriy@blynk.cc\").matches());\n    }\n\n}\n"
  },
  {
    "path": "server/http-core/src/test/java/cc/blynk/test/ListUtilsTest.java",
    "content": "package cc.blynk.test;\n\nimport cc.blynk.core.http.utils.ListUtils;\nimport org.junit.Test;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 24.03.16.\n */\npublic class ListUtilsTest {\n\n    private static final int SIZE = 10;\n\n    @Test\n    public void testEmptyList() {\n        List<Integer> l = new ArrayList<>();\n        List<?> res;\n\n        res = ListUtils.subList(l, 1, SIZE);\n        assertNotNull(res);\n        assertEquals(0, res.size());\n\n        res = ListUtils.subList(l, 10, SIZE);\n        assertNotNull(res);\n        assertEquals(0, res.size());\n    }\n\n    @Test\n    public void test1List() {\n        List<Integer> l = new ArrayList<>();\n        l.add(1);\n        List<?> res;\n\n        res = ListUtils.subList(l, 1, SIZE);\n        assertNotNull(res);\n        assertEquals(1, res.size());\n        assertEquals(1, res.get(0));\n    }\n\n    @Test\n    public void testCorrectResponse() {\n        List<Integer> l = new ArrayList<>();\n        for (int i = 1; i <= 100; i++) {\n            l.add(i);\n        }\n        List<?> res;\n\n        res = ListUtils.subList(l, 1, SIZE);\n        assertNotNull(res);\n        assertEquals(10, res.size());\n        int index;\n\n        index = 0;\n        for (int i = 1; i <= 10; i++) {\n            assertEquals(i, res.get(index++));\n        }\n\n        res = ListUtils.subList(l, 9, SIZE);\n        assertNotNull(res);\n        assertEquals(10, res.size());\n\n        index = 0;\n        for (int i = 81; i <= 90; i++) {\n            assertEquals(i, res.get(index++));\n        }\n\n        res = ListUtils.subList(l, 10, SIZE);\n        assertNotNull(res);\n        assertEquals(10, res.size());\n\n        index = 0;\n        for (int i = 91; i <= 100; i++) {\n            assertEquals(i, res.get(index++));\n        }\n    }\n\n\n    @Test\n    public void testCorrectResponse2() {\n        List<Integer> l = new ArrayList<>();\n        for (int i = 1; i <= 99; i++) {\n            l.add(i);\n        }\n        List<?> res;\n        int index;\n\n        res = ListUtils.subList(l, 10, SIZE);\n        assertNotNull(res);\n        assertEquals(9, res.size());\n\n        index = 0;\n        for (int i = 91; i <= 99; i++) {\n            assertEquals(i, res.get(index++));\n        }\n    }\n\n\n}\n"
  },
  {
    "path": "server/launcher/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <artifactId>server</artifactId>\n        <groupId>cc.blynk.server</groupId>\n        <version>0.41.17-SNAPSHOT</version>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n\n    <artifactId>launcher</artifactId>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-shade-plugin</artifactId>\n                <version>${maven-shade-plugin.version}</version>\n                <executions>\n                    <execution>\n                        <phase>package</phase>\n                        <goals>\n                            <goal>shade</goal>\n                        </goals>\n                        <configuration>\n                            <finalName>server-${project.version}</finalName>\n                            <transformers>\n                                <transformer implementation=\"org.apache.maven.plugins.shade.resource.ManifestResourceTransformer\">\n                                    <manifestEntries>\n                                        <Main-Class>cc.blynk.server.launcher.ServerLauncher</Main-Class>\n                                        <Build-Number>${project.version}</Build-Number>\n                                        <Build-By>Blynk Inc.</Build-By>\n                                        <Multi-Release>true</Multi-Release>\n                                    </manifestEntries>\n                                </transformer>\n                            </transformers>\n                            <filters>\n                                <filter>\n                                    <artifact>*:*</artifact>\n                                    <excludes>\n                                        <exclude>META-INF/maven/**</exclude>\n                                        <exclude>META-INF/*.SF</exclude>\n                                        <exclude>META-INF/*.DSA</exclude>\n                                        <exclude>META-INF/*.RSA</exclude>\n                                    </excludes>\n                                </filter>\n                            </filters>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n\n\n    <dependencies>\n\n        <dependency>\n            <groupId>cc.blynk.server</groupId>\n            <artifactId>core</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>cc.blynk.server.application</groupId>\n            <artifactId>tcp-app-server</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>cc.blynk.server.hardware</groupId>\n            <artifactId>tcp-hardware-server</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>cc.blynk.server.admin</groupId>\n            <artifactId>http-admin</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>cc.blynk.server.api</groupId>\n            <artifactId>http-api</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n\n    </dependencies>\n\n</project>"
  },
  {
    "path": "server/launcher/src/main/java/cc/blynk/cli/AlreadySelectedException.java",
    "content": "package cc.blynk.cli;\n\n/**\n * Thrown when more than one option in an option group\n * has been provided.\n *\n * @version $Id: AlreadySelectedException.java 1443102 2013-02-06 18:12:16Z tn $\n */\npublic class AlreadySelectedException extends ParseException {\n    /**\n     * This exception {@code serialVersionUID}.\n     */\n    private static final long serialVersionUID = 3674381532418544760L;\n\n    /**\n     * The option that triggered the exception.\n     */\n    private Option option;\n\n    /**\n     * Construct a new <code>AlreadySelectedException</code>\n     * with the specified detail message.\n     *\n     * @param message the detail message\n     */\n    private AlreadySelectedException(String message) {\n        super(message);\n    }\n\n    /**\n     * Construct a new <code>AlreadySelectedException</code>\n     * for the specified option group.\n     *\n     * @param group  the option group already selected\n     * @param option the option that triggered the exception\n     * @since 1.2\n     */\n    AlreadySelectedException(OptionGroup group, Option option) {\n        this(\"The option '\" + option.getKey() + \"' was specified but an option from this group \"\n                + \"has already been selected: '\" + group.getSelected() + \"'\");\n        this.option = option;\n    }\n\n    /**\n     * Returns the option that was added to the group and triggered the exception.\n     *\n     * @return the related option\n     * @since 1.2\n     */\n    public Option getOption() {\n        return option;\n    }\n}\n"
  },
  {
    "path": "server/launcher/src/main/java/cc/blynk/cli/AmbiguousOptionException.java",
    "content": "package cc.blynk.cli;\n\nimport java.util.Collection;\nimport java.util.Iterator;\n\n/**\n * Exception thrown when an option can't be identified from a partial name.\n *\n * @version $Id: AmbiguousOptionException.java 1669814 2015-03-28 18:09:26Z britter $\n * @since 1.3\n */\nclass AmbiguousOptionException extends UnrecognizedOptionException {\n    /**\n     * This exception {@code serialVersionUID}.\n     */\n    private static final long serialVersionUID = 5829816121277947229L;\n\n    /**\n     * Constructs a new AmbiguousOptionException.\n     *\n     * @param option          the partial option name\n     * @param matchingOptions the options matching the name\n     */\n    AmbiguousOptionException(String option, Collection<String> matchingOptions) {\n        super(createMessage(option, matchingOptions), option);\n    }\n\n    /**\n     * Build the exception message from the specified list of options.\n     */\n    private static String createMessage(String option, Collection<String> matchingOptions) {\n        StringBuilder buf = new StringBuilder(\"Ambiguous option: '\");\n        buf.append(option);\n        buf.append(\"'  (could be: \");\n\n        Iterator<String> it = matchingOptions.iterator();\n        while (it.hasNext()) {\n            buf.append(\"'\");\n            buf.append(it.next());\n            buf.append(\"'\");\n            if (it.hasNext()) {\n                buf.append(\", \");\n            }\n        }\n        buf.append(\")\");\n\n        return buf.toString();\n    }\n}\n"
  },
  {
    "path": "server/launcher/src/main/java/cc/blynk/cli/CommandLine.java",
    "content": "package cc.blynk.cli;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Represents list of arguments parsed against a {@link Options} descriptor.\n * <p>\n * It allows querying of a boolean {@link #hasOption(String opt)},\n * in addition to retrieving the {@link #getOptionValue(String opt)}\n * for options requiring arguments.\n * <p>\n * Additionally, any left-over or unrecognized arguments,\n * are available for further processing.\n *\n * @version $Id: CommandLine.java 1786144 2017-03-09 11:34:57Z britter $\n */\npublic class CommandLine {\n\n    /**\n     * the processed options\n     */\n    private final List<Option> options = new ArrayList<>();\n\n    /**\n     * Creates a command line.\n     */\n    CommandLine() {\n        // nothing to do\n    }\n\n    /**\n     * Query to see if an option has been set.\n     *\n     * @param opt Short name of the option\n     * @return true if set, false if not\n     */\n    public boolean hasOption(String opt) {\n        return options.contains(resolveOption(opt));\n    }\n\n\n    /**\n     * Retrieve the first argument, if any, of this option.\n     *\n     * @param opt the name of the option\n     * @return Value of the argument if option is set, and has an argument,\n     * otherwise null.\n     */\n    public String getOptionValue(String opt) {\n        String[] values = getOptionValues(opt);\n\n        return (values == null) ? null : values[0];\n    }\n\n    /**\n     * Retrieves the array of values, if any, of an option.\n     *\n     * @param opt string name of the option\n     * @return Values of the argument if option is set, and has an argument,\n     * otherwise null.\n     */\n    private String[] getOptionValues(String opt) {\n        List<String> values = new ArrayList<>();\n\n        for (Option option : options) {\n            if (opt.equals(option.getOpt()) || opt.equals(option.getLongOpt())) {\n                values.addAll(option.getValuesList());\n            }\n        }\n\n        return values.isEmpty() ? null : values.toArray(new String[0]);\n    }\n\n    /**\n     * Retrieves the option object given the long or short option as a String\n     *\n     * @param opt short or long name of the option\n     * @return Canonicalized option\n     */\n    private Option resolveOption(String opt) {\n        opt = Util.stripLeadingHyphens(opt);\n        for (Option option : options) {\n            if (opt.equals(option.getOpt())) {\n                return option;\n            }\n\n            if (opt.equals(option.getLongOpt())) {\n                return option;\n            }\n\n        }\n        return null;\n    }\n\n    /**\n     * Add an option to the command line.  The values of the option are stored.\n     *\n     * @param opt the processed option\n     */\n    void addOption(Option opt) {\n        options.add(opt);\n    }\n\n}\n"
  },
  {
    "path": "server/launcher/src/main/java/cc/blynk/cli/DefaultParser.java",
    "content": "package cc.blynk.cli;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Default parser.\n *\n * @version $Id: DefaultParser.java 1783175 2017-02-16 07:52:05Z britter $\n * @since 1.3\n */\npublic class DefaultParser {\n    /**\n     * The command-line instance.\n     */\n    private CommandLine cmd;\n\n    /**\n     * The current options.\n     */\n    private Options options;\n\n    /**\n     * Flag indicating how unrecognized tokens are handled. <tt>true</tt> to stop\n     * the parsing and add the remaining tokens to the args list.\n     * <tt>false</tt> to throw an exception.\n     */\n    private boolean stopAtNonOption;\n\n    /**\n     * The token currently processed.\n     */\n    private String currentToken;\n\n    /**\n     * The last option parsed.\n     */\n    private Option currentOption;\n\n    /**\n     * Flag indicating if tokens should no longer be analyzed and simply added as arguments of the command line.\n     */\n    private boolean skipParsing;\n\n    /**\n     * The required options and groups expected to be found when parsing the command line.\n     */\n    private List expectedOpts;\n\n    /**\n     * Parse the arguments according to the specified options and properties.\n     *\n     * @param options         the specified Options\n     * @param arguments       the command line arguments\n     *                        the parsing and the remaining arguments are added to the\n     *                        {@link CommandLine}s args list. If <tt>false</tt> an unrecognized\n     *                        argument triggers a ParseException.\n     * @return the list of atomic option and value tokens\n     * @throws ParseException if there are any problems encountered\n     *                        while parsing the command line tokens.\n     */\n    @SuppressWarnings(\"unchecked\")\n    public CommandLine parse(Options options, String[] arguments)\n            throws ParseException {\n        this.options = options;\n        this.stopAtNonOption = false;\n        skipParsing = false;\n        currentOption = null;\n        expectedOpts = new ArrayList(options.getRequiredOptions());\n\n        // clear the data from the groups\n        for (OptionGroup group : options.getOptionGroups()) {\n            group.setSelected(null);\n        }\n\n        cmd = new CommandLine();\n\n        if (arguments != null) {\n            for (String argument : arguments) {\n                handleToken(argument);\n            }\n        }\n\n        // check the arguments of the last option\n        checkRequiredArgs();\n\n        checkRequiredOptions();\n\n        return cmd;\n    }\n\n    /**\n     * Throws a {@link MissingOptionException} if all of the required options\n     * are not present.\n     *\n     * @throws MissingOptionException if any of the required Options\n     *                                are not present.\n     */\n    private void checkRequiredOptions() throws MissingOptionException {\n        // if there are required options that have not been processed\n        if (!expectedOpts.isEmpty()) {\n            throw new MissingOptionException(expectedOpts);\n        }\n    }\n\n    /**\n     * Throw a {@link MissingArgumentException} if the current option\n     * didn't receive the number of arguments expected.\n     */\n    private void checkRequiredArgs() throws ParseException {\n        if (currentOption != null && currentOption.requiresArg()) {\n            throw new MissingArgumentException(currentOption);\n        }\n    }\n\n    /**\n     * Handle any command line token.\n     *\n     * @param token the command line token to handle\n     */\n    private void handleToken(String token) throws ParseException {\n        currentToken = token;\n\n        if (!skipParsing) {\n            if (\"--\".equals(token)) {\n                skipParsing = true;\n            } else if (currentOption != null && currentOption.acceptsArg() && isArgument(token)) {\n                currentOption.addValueForProcessing(Util.stripLeadingAndTrailingQuotes(token));\n            } else if (token.startsWith(\"--\")) {\n                handleLongOption(token);\n            } else if (token.startsWith(\"-\") && !\"-\".equals(token)) {\n                handleShortAndLongOption(token);\n            } else {\n                handleUnknownToken(token);\n            }\n        }\n\n        if (currentOption != null && !currentOption.acceptsArg()) {\n            currentOption = null;\n        }\n    }\n\n    /**\n     * Returns true is the token is a valid argument.\n     */\n    private boolean isArgument(String token) {\n        return !isOption(token) || isNegativeNumber(token);\n    }\n\n    /**\n     * Check if the token is a negative number.\n     */\n    private boolean isNegativeNumber(String token) {\n        try {\n            Double.parseDouble(token);\n            return true;\n        } catch (NumberFormatException e) {\n            return false;\n        }\n    }\n\n    /**\n     * Tells if the token looks like an option.\n     */\n    private boolean isOption(String token) {\n        return isLongOption(token) || isShortOption(token);\n    }\n\n    /**\n     * Tells if the token looks like a short option.\n     */\n    private boolean isShortOption(String token) {\n        // short options (-S, -SV, -S=V, -SV1=V2, -S1S2)\n        if (!token.startsWith(\"-\") || token.length() == 1) {\n            return false;\n        }\n\n        // remove leading \"-\" and \"=value\"\n        int pos = token.indexOf(\"=\");\n        String optName = pos == -1 ? token.substring(1) : token.substring(1, pos);\n        if (options.hasShortOption(optName)) {\n            return true;\n        }\n        // check for several concatenated short options\n        return optName.length() > 0 && options.hasShortOption(String.valueOf(optName.charAt(0)));\n    }\n\n    /**\n     * Tells if the token looks like a long option.\n     */\n    private boolean isLongOption(String token) {\n        if (!token.startsWith(\"-\") || token.length() == 1) {\n            return false;\n        }\n\n        int pos = token.indexOf(\"=\");\n        String t = pos == -1 ? token : token.substring(0, pos);\n\n        if (!options.getMatchingOptions(t).isEmpty()) {\n            // long or partial long options (--L, -L, --L=V, -L=V, --l, --l=V)\n            return true;\n        }\n        return getLongPrefix(token) != null && !token.startsWith(\"--\");\n    }\n\n    /**\n     * Handles an unknown token. If the token starts with a dash an\n     * UnrecognizedOptionException is thrown. Otherwise the token is added\n     * to the arguments of the command line. If the stopAtNonOption flag\n     * is set, this stops the parsing and the remaining tokens are added\n     * as-is in the arguments of the command line.\n     *\n     * @param token the command line token to handle\n     */\n    private void handleUnknownToken(String token) throws ParseException {\n        if (token.startsWith(\"-\") && token.length() > 1 && !stopAtNonOption) {\n            throw new UnrecognizedOptionException(\"Unrecognized option: \" + token, token);\n        }\n\n        if (stopAtNonOption) {\n            skipParsing = true;\n        }\n    }\n\n    /**\n     * Handles the following tokens:\n     * <p>\n     * --L\n     * --L=V\n     * --L V\n     * --l\n     *\n     * @param token the command line token to handle\n     */\n    private void handleLongOption(String token) throws ParseException {\n        if (token.indexOf('=') == -1) {\n            handleLongOptionWithoutEqual(token);\n        } else {\n            handleLongOptionWithEqual(token);\n        }\n    }\n\n    /**\n     * Handles the following tokens:\n     * <p>\n     * --L\n     * -L\n     * --l\n     * -l\n     *\n     * @param token the command line token to handle\n     */\n    private void handleLongOptionWithoutEqual(String token) throws ParseException {\n        List<String> matchingOpts = options.getMatchingOptions(token);\n        if (matchingOpts.isEmpty()) {\n            handleUnknownToken(currentToken);\n        } else if (matchingOpts.size() > 1) {\n            throw new AmbiguousOptionException(token, matchingOpts);\n        } else {\n            handleOption(options.getOption(matchingOpts.get(0)));\n        }\n    }\n\n    /**\n     * Handles the following tokens:\n     * <p>\n     * --L=V\n     * -L=V\n     * --l=V\n     * -l=V\n     *\n     * @param token the command line token to handle\n     */\n    private void handleLongOptionWithEqual(String token) throws ParseException {\n        int pos = token.indexOf('=');\n\n        String value = token.substring(pos + 1);\n\n        String opt = token.substring(0, pos);\n\n        List<String> matchingOpts = options.getMatchingOptions(opt);\n        if (matchingOpts.isEmpty()) {\n            handleUnknownToken(currentToken);\n        } else if (matchingOpts.size() > 1) {\n            throw new AmbiguousOptionException(opt, matchingOpts);\n        } else {\n            Option option = options.getOption(matchingOpts.get(0));\n\n            if (option.acceptsArg()) {\n                handleOption(option);\n                currentOption.addValueForProcessing(value);\n                currentOption = null;\n            } else {\n                handleUnknownToken(currentToken);\n            }\n        }\n    }\n\n    /**\n     * Handles the following tokens:\n     * <p>\n     * -S\n     * -SV\n     * -S V\n     * -S=V\n     * -S1S2\n     * -S1S2 V\n     * -SV1=V2\n     * <p>\n     * -L\n     * -LV\n     * -L V\n     * -L=V\n     * -l\n     *\n     * @param token the command line token to handle\n     */\n    private void handleShortAndLongOption(String token) throws ParseException {\n        String t = Util.stripLeadingHyphens(token);\n\n        int pos = t.indexOf('=');\n\n        if (t.length() == 1) {\n            // -S\n            if (options.hasShortOption(t)) {\n                handleOption(options.getOption(t));\n            } else {\n                handleUnknownToken(token);\n            }\n        } else if (pos == -1) {\n            // no equal sign found (-xxx)\n            if (options.hasShortOption(t)) {\n                handleOption(options.getOption(t));\n            } else if (!options.getMatchingOptions(t).isEmpty()) {\n                // -L or -l\n                handleLongOptionWithoutEqual(token);\n            } else {\n                // look for a long prefix (-Xmx512m)\n                String opt = getLongPrefix(t);\n\n                if (opt != null && options.getOption(opt).acceptsArg()) {\n                    handleOption(options.getOption(opt));\n                    currentOption.addValueForProcessing(t.substring(opt.length()));\n                    currentOption = null;\n                } else if (isJavaProperty(t)) {\n                    // -SV1 (-Dflag)\n                    handleOption(options.getOption(t.substring(0, 1)));\n                    currentOption.addValueForProcessing(t.substring(1));\n                    currentOption = null;\n                } else {\n                    // -S1S2S3 or -S1S2V\n                    handleConcatenatedOptions(token);\n                }\n            }\n        } else {\n            // equal sign found (-xxx=yyy)\n            String opt = t.substring(0, pos);\n            String value = t.substring(pos + 1);\n\n            if (opt.length() == 1) {\n                // -S=V\n                Option option = options.getOption(opt);\n                if (option != null && option.acceptsArg()) {\n                    handleOption(option);\n                    currentOption.addValueForProcessing(value);\n                    currentOption = null;\n                } else {\n                    handleUnknownToken(token);\n                }\n            } else if (isJavaProperty(opt)) {\n                // -SV1=V2 (-Dkey=value)\n                handleOption(options.getOption(opt.substring(0, 1)));\n                currentOption.addValueForProcessing(opt.substring(1));\n                currentOption.addValueForProcessing(value);\n                currentOption = null;\n            } else {\n                // -L=V or -l=V\n                handleLongOptionWithEqual(token);\n            }\n        }\n    }\n\n    /**\n     * Search for a prefix that is the long name of an option (-Xmx512m)\n     */\n    private String getLongPrefix(String token) {\n        String t = Util.stripLeadingHyphens(token);\n\n        int i;\n        String opt = null;\n        for (i = t.length() - 2; i > 1; i--) {\n            String prefix = t.substring(0, i);\n            if (options.hasLongOption(prefix)) {\n                opt = prefix;\n                break;\n            }\n        }\n\n        return opt;\n    }\n\n    /**\n     * Check if the specified token is a Java-like property (-Dkey=value).\n     */\n    private boolean isJavaProperty(String token) {\n        String opt = token.substring(0, 1);\n        Option option = options.getOption(opt);\n\n        return option != null && (option.getArgs() >= 2 || option.getArgs() == Option.UNLIMITED_VALUES);\n    }\n\n    private void handleOption(Option option) throws ParseException {\n        // check the previous option before handling the next one\n        checkRequiredArgs();\n\n        option = (Option) option.clone();\n\n        updateRequiredOptions(option);\n\n        cmd.addOption(option);\n\n        if (option.hasArg()) {\n            currentOption = option;\n        } else {\n            currentOption = null;\n        }\n    }\n\n    /**\n     * Removes the option or its group from the list of expected elements.\n     */\n    private void updateRequiredOptions(Option option) throws AlreadySelectedException {\n        if (option.isRequired()) {\n            expectedOpts.remove(option.getKey());\n        }\n\n        // if the option is in an OptionGroup make that option the selected option of the group\n        if (options.getOptionGroup(option) != null) {\n            OptionGroup group = options.getOptionGroup(option);\n\n            if (group.isRequired()) {\n                expectedOpts.remove(group);\n            }\n\n            group.setSelected(option);\n        }\n    }\n\n    /**\n     * Breaks <code>token</code> into its constituent parts\n     * using the following algorithm.\n     * <p>\n     * <ul>\n     * <li>ignore the first character (\"<b>-</b>\")</li>\n     * <li>for each remaining character check if an {@link Option}\n     * exists with that id.</li>\n     * <li>if an {@link Option} does exist then add that character\n     * prepended with \"<b>-</b>\" to the list of processed tokens.</li>\n     * <li>if the {@link Option} can have an argument value and there\n     * are remaining characters in the token then add the remaining\n     * characters as a token to the list of processed tokens.</li>\n     * <li>if an {@link Option} does <b>NOT</b> exist <b>AND</b>\n     * <code>stopAtNonOption</code> <b>IS</b> set then add the special token\n     * \"<b>--</b>\" followed by the remaining characters and also\n     * the remaining tokens directly to the processed tokens list.</li>\n     * <li>if an {@link Option} does <b>NOT</b> exist <b>AND</b>\n     * <code>stopAtNonOption</code> <b>IS NOT</b> set then add that\n     * character prepended with \"<b>-</b>\".</li>\n     * </ul>\n     *\n     * @param token The current token to be <b>burst</b>\n     *              at the first non-Option encountered.\n     * @throws ParseException if there are any problems encountered\n     *                        while parsing the command line token.\n     */\n    private void handleConcatenatedOptions(String token) throws ParseException {\n        for (int i = 1; i < token.length(); i++) {\n            String ch = String.valueOf(token.charAt(i));\n\n            if (options.hasOption(ch)) {\n                handleOption(options.getOption(ch));\n\n                if (currentOption != null && token.length() != i + 1) {\n                    // add the trail as an argument of the option\n                    currentOption.addValueForProcessing(token.substring(i + 1));\n                    break;\n                }\n            } else {\n                handleUnknownToken(stopAtNonOption && i > 1 ? token.substring(i) : token);\n                break;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "server/launcher/src/main/java/cc/blynk/cli/MissingArgumentException.java",
    "content": "package cc.blynk.cli;\n\n/* Thrown when an option requiring an argument\n        * is not provided with an argument.\n        *\n        * @version $Id: MissingArgumentException.java 1443102 2013-02-06 18:12:16Z tn $\n        */\npublic class MissingArgumentException extends ParseException {\n    /**\n     * This exception {@code serialVersionUID}.\n     */\n    private static final long serialVersionUID = -7098538588704965017L;\n\n    /**\n     * The option requiring additional arguments\n     */\n    private Option option;\n\n    /**\n     * Construct a new <code>MissingArgumentException</code>\n     * with the specified detail message.\n     *\n     * @param message the detail message\n     */\n    private MissingArgumentException(String message) {\n        super(message);\n    }\n\n    /**\n     * Construct a new <code>MissingArgumentException</code>\n     * with the specified detail message.\n     *\n     * @param option the option requiring an argument\n     * @since 1.2\n     */\n    MissingArgumentException(Option option) {\n        this(\"Missing argument for option: \" + option.getKey());\n        this.option = option;\n    }\n\n    /**\n     * Return the option requiring an argument that wasn't provided\n     * on the command line.\n     *\n     * @return the related option\n     * @since 1.2\n     */\n    public Option getOption() {\n        return option;\n    }\n}\n"
  },
  {
    "path": "server/launcher/src/main/java/cc/blynk/cli/MissingOptionException.java",
    "content": "package cc.blynk.cli;\n\nimport java.util.Iterator;\nimport java.util.List;\n\n/**\n * Thrown when a required option has not been provided.\n *\n * @version $Id: MissingOptionException.java 1443102 2013-02-06 18:12:16Z tn $\n */\nclass MissingOptionException extends ParseException {\n    /**\n     * This exception {@code serialVersionUID}.\n     */\n    private static final long serialVersionUID = 8161889051578563249L;\n\n    /**\n     * Construct a new <code>MissingSelectedException</code>\n     * with the specified detail message.\n     *\n     * @param message the detail message\n     */\n    private MissingOptionException(String message) {\n        super(message);\n    }\n\n    /**\n     * Constructs a new <code>MissingSelectedException</code> with the\n     * specified list of missing options.\n     *\n     * @param missingOptions the list of missing options and groups\n     * @since 1.2\n     */\n    MissingOptionException(List missingOptions) {\n        this(createMessage(missingOptions));\n\n    }\n\n    /**\n     * Build the exception message from the specified list of options.\n     *\n     * @param missingOptions the list of missing options and groups\n     * @since 1.2\n     */\n    private static String createMessage(List<?> missingOptions) {\n        StringBuilder buf = new StringBuilder(\"Missing required option\");\n        buf.append(missingOptions.size() == 1 ? \"\" : \"s\");\n        buf.append(\": \");\n\n        Iterator<?> it = missingOptions.iterator();\n        while (it.hasNext()) {\n            buf.append(it.next());\n            if (it.hasNext()) {\n                buf.append(\", \");\n            }\n        }\n\n        return buf.toString();\n    }\n}\n"
  },
  {
    "path": "server/launcher/src/main/java/cc/blynk/cli/Option.java",
    "content": "package cc.blynk.cli;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Describes a single command-line option.  It maintains\n * information regarding the short-name of the option, the long-name,\n * if any exists, a flag indicating if an argument is required for\n * this option, and a self-documenting description of the option.\n * <p>\n * An Option is not created independently, but is created through\n * an instance of {@link Options}. An Option is required to have\n * at least a short or a long-name.\n * <p>\n * <b>Note:</b> once an {@link Option} has been added to an instance\n * of {@link Options}, it's required flag may not be changed anymore.\n *\n * @version $Id: Option.java 1756753 2016-08-18 10:18:43Z britter $\n */\npublic class Option implements Cloneable {\n    /**\n     * constant that specifies the number of argument values has not been specified\n     */\n    private static final int UNINITIALIZED = -1;\n\n    /**\n     * constant that specifies the number of argument values is infinite\n     */\n    static final int UNLIMITED_VALUES = -2;\n\n    /**\n     * the name of the option\n     */\n    private final String opt;\n\n    /**\n     * the long representation of the option\n     */\n    private String longOpt;\n\n    /**\n     * description of the option\n     */\n    private final String description;\n\n    /**\n     * specifies whether this option is required to be present\n     */\n    private boolean required;\n\n    /**\n     * specifies whether the argument value of this Option is optional\n     */\n    private boolean optionalArg;\n\n    /**\n     * the number of argument values this option can have\n     */\n    private int numberOfArgs = UNINITIALIZED;\n\n    /**\n     * the type of this Option\n     */\n    private Class<?> type = String.class;\n\n    /**\n     * the list of argument values\n     **/\n    private List<String> values = new ArrayList<>();\n\n    /**\n     * the character that is the value separator\n     */\n    private char valuesep;\n\n    /**\n     * Creates an Option using the specified parameters.\n     *\n     * @param opt         short representation of the option\n     * @param longOpt     the long representation of the option\n     * @param hasArg      specifies whether the Option takes an argument or not\n     * @param description describes the function of the option\n     * @throws IllegalArgumentException if there are any non valid\n     *                                  Option characters in <code>opt</code>.\n     */\n    Option(String opt, String longOpt, boolean hasArg, String description)\n            throws IllegalArgumentException {\n        // ensure that the option is valid\n        OptionValidator.validateOption(opt);\n\n        this.opt = opt;\n        this.longOpt = longOpt;\n\n        // if hasArg is set then the number of arguments is 1\n        if (hasArg) {\n            this.numberOfArgs = 1;\n        }\n\n        this.description = description;\n    }\n\n    /**\n     * Returns the id of this Option.  This is only set when the\n     * Option shortOpt is a single character.  This is used for switch\n     * statements.\n     *\n     * @return the id of this Option\n     */\n    public int getId() {\n        return getKey().charAt(0);\n    }\n\n    /**\n     * Returns the 'unique' Option identifier.\n     *\n     * @return the 'unique' Option identifier\n     */\n    String getKey() {\n        // if 'opt' is null, then it is a 'long' option\n        return (opt == null) ? longOpt : opt;\n    }\n\n    /**\n     * Retrieve the name of this Option.\n     * <p>\n     * It is this String which can be used with\n     * {@link CommandLine#hasOption(String opt)} and\n     * {@link CommandLine#getOptionValue(String opt)} to check\n     * for existence and argument.\n     *\n     * @return The name of this option\n     */\n    String getOpt() {\n        return opt;\n    }\n\n    /**\n     * Retrieve the type of this Option.\n     *\n     * @return The type of this option\n     */\n    public Object getType() {\n        return type;\n    }\n\n    /**\n     * Sets the type of this Option.\n     *\n     * @param type the type of this Option\n     * @since 1.3\n     */\n    public void setType(Class<?> type) {\n        this.type = type;\n    }\n\n    /**\n     * Retrieve the long name of this Option.\n     *\n     * @return Long name of this option, or null, if there is no long name\n     */\n    String getLongOpt() {\n        return longOpt;\n    }\n\n    /**\n     * @return whether this Option can have an optional argument\n     */\n    private boolean hasOptionalArg() {\n        return optionalArg;\n    }\n\n    /**\n     * Query to see if this Option has a long name\n     *\n     * @return boolean flag indicating existence of a long name\n     */\n    boolean hasLongOpt() {\n        return longOpt != null;\n    }\n\n    /**\n     * Query to see if this Option requires an argument\n     *\n     * @return boolean flag indicating if an argument is required\n     */\n    boolean hasArg() {\n        return numberOfArgs > 0 || numberOfArgs == UNLIMITED_VALUES;\n    }\n\n    /**\n     * Retrieve the self-documenting description of this Option\n     *\n     * @return The string description of this option\n     */\n    String getDescription() {\n        return description;\n    }\n\n    /**\n     * Query to see if this Option is mandatory\n     *\n     * @return boolean flag indicating whether this Option is mandatory\n     */\n    public boolean isRequired() {\n        return required;\n    }\n\n    /**\n     * Query to see if this Option can take many values.\n     *\n     * @return boolean flag indicating if multiple values are allowed\n     */\n    private boolean hasArgs() {\n        return numberOfArgs > 1 || numberOfArgs == UNLIMITED_VALUES;\n    }\n\n\n    /**\n     * Returns the value separator character.\n     *\n     * @return the value separator character.\n     */\n    private char getValueSeparator() {\n        return valuesep;\n    }\n\n    /**\n     * Return whether this Option has specified a value separator.\n     *\n     * @return whether this Option has specified a value separator.\n     * @since 1.1\n     */\n    private boolean hasValueSeparator() {\n        return valuesep > 0;\n    }\n\n    /**\n     * Returns the number of argument values this Option can take.\n     * <p>\n     * <p>\n     * A value equal to the constant {@link #UNINITIALIZED} (= -1) indicates\n     * the number of arguments has not been specified.\n     * A value equal to the constant {@link #UNLIMITED_VALUES} (= -2) indicates\n     * that this options takes an unlimited amount of values.\n     * </p>\n     *\n     * @return num the number of argument values\n     * @see #UNINITIALIZED\n     * @see #UNLIMITED_VALUES\n     */\n    int getArgs() {\n        return numberOfArgs;\n    }\n\n    /**\n     * Adds the specified value to this Option.\n     *\n     * @param value is a/the value of this Option\n     */\n    void addValueForProcessing(String value) {\n        if (numberOfArgs == UNINITIALIZED) {\n            throw new RuntimeException(\"NO_ARGS_ALLOWED\");\n        }\n        processValue(value);\n    }\n\n    /**\n     * Processes the value.  If this Option has a value separator\n     * the value will have to be parsed into individual tokens.  When\n     * n-1 tokens have been processed and there are more value separators\n     * in the value, parsing is ceased and the remaining characters are\n     * added as a single token.\n     *\n     * @param value The String to be processed.\n     * @since 1.0.1\n     */\n    private void processValue(String value) {\n        // this Option has a separator character\n        if (hasValueSeparator()) {\n            // get the separator character\n            char sep = getValueSeparator();\n\n            // store the index for the value separator\n            int index = value.indexOf(sep);\n\n            // while there are more value separators\n            while (index != -1) {\n                // next value to be added\n                if (values.size() == numberOfArgs - 1) {\n                    break;\n                }\n\n                // store\n                add(value.substring(0, index));\n\n                // parse\n                value = value.substring(index + 1);\n\n                // get new index\n                index = value.indexOf(sep);\n            }\n        }\n\n        // store the actual value or the last value that has been parsed\n        add(value);\n    }\n\n    /**\n     * Add the value to this Option.  If the number of arguments\n     * is greater than zero and there is enough space in the list then\n     * add the value.  Otherwise, throw a runtime exception.\n     *\n     * @param value The value to be added to this Option\n     * @since 1.0.1\n     */\n    private void add(String value) {\n        if (!acceptsArg()) {\n            throw new RuntimeException(\"Cannot add value, list full.\");\n        }\n\n        // store value\n        values.add(value);\n    }\n\n    /**\n     * Returns the specified value of this Option or\n     * <code>null</code> if there is no value.\n     *\n     * @return the value/first value of this Option or\n     * <code>null</code> if there is no value.\n     */\n    public String getValue() {\n        return hasNoValues() ? null : values.get(0);\n    }\n\n    /**\n     * Return the values of this Option as a String array\n     * or null if there are no values\n     *\n     * @return the values of this Option as a String array\n     * or null if there are no values\n     */\n    public String[] getValues() {\n        return hasNoValues() ? null : values.toArray(new String[0]);\n    }\n\n    /**\n     * @return the values of this Option as a List\n     * or null if there are no values\n     */\n    List<String> getValuesList() {\n        return values;\n    }\n\n    /**\n     * Returns whether this Option has any values.\n     *\n     * @return whether this Option has any values.\n     */\n    private boolean hasNoValues() {\n        return values.isEmpty();\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n\n        Option option = (Option) o;\n\n\n        if (opt != null ? !opt.equals(option.opt) : option.opt != null) {\n            return false;\n        }\n        return longOpt != null ? longOpt.equals(option.longOpt) : option.longOpt == null;\n    }\n\n    @Override\n    public int hashCode() {\n        int result;\n        result = opt != null ? opt.hashCode() : 0;\n        result = 31 * result + (longOpt != null ? longOpt.hashCode() : 0);\n        return result;\n    }\n\n    /**\n     * A rather odd clone method - due to incorrect code in 1.0 it is public\n     * and in 1.1 rather than throwing a CloneNotSupportedException it throws\n     * a RuntimeException so as to maintain backwards compat at the API level.\n     * <p>\n     * After calling this method, it is very likely you will want to call\n     * clearValues().\n     *\n     * @return a clone of this Option instance\n     * @throws RuntimeException if a {@link CloneNotSupportedException} has been thrown\n     *                          by {@code super.clone()}\n     */\n    @Override\n    public Object clone() {\n        try {\n            Option option = (Option) super.clone();\n            option.values = new ArrayList<>(values);\n            return option;\n        } catch (CloneNotSupportedException cnse) {\n            throw new RuntimeException(\"A CloneNotSupportedException was thrown: \" + cnse.getMessage());\n        }\n    }\n\n    /**\n     * Tells if the option can accept more arguments.\n     *\n     * @return false if the maximum number of arguments is reached\n     * @since 1.3\n     */\n    boolean acceptsArg() {\n        return (hasArg() || hasArgs() || hasOptionalArg()) && (numberOfArgs <= 0 || values.size() < numberOfArgs);\n    }\n\n    /**\n     * Tells if the option requires more arguments to be valid.\n     *\n     * @return false if the option doesn't require more arguments\n     * @since 1.3\n     */\n    boolean requiresArg() {\n        if (optionalArg) {\n            return false;\n        }\n        if (numberOfArgs == UNLIMITED_VALUES) {\n            return values.isEmpty();\n        }\n        return acceptsArg();\n    }\n}\n"
  },
  {
    "path": "server/launcher/src/main/java/cc/blynk/cli/OptionGroup.java",
    "content": "package cc.blynk.cli;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\n/**\n * A group of mutually exclusive options.\n *\n * @version $Id: OptionGroup.java 1749596 2016-06-21 20:27:06Z britter $\n */\npublic class OptionGroup {\n\n    /**\n     * hold the options\n     */\n    private final Map<String, Option> optionMap = new LinkedHashMap<>();\n\n    /**\n     * the name of the selected option\n     */\n    private String selected;\n\n    /**\n     * specified whether this group is required\n     */\n    private boolean required;\n\n    /**\n     * Set the selected option of this group to <code>name</code>.\n     *\n     * @param option the option that is selected\n     * @throws AlreadySelectedException if an option from this group has\n     *                                  already been selected.\n     */\n    void setSelected(Option option) throws AlreadySelectedException {\n        if (option == null) {\n            // reset the option previously selected\n            selected = null;\n            return;\n        }\n\n        // if no option has already been selected or the\n        // same option is being reselected then set the\n        // selected member variable\n        if (selected == null || selected.equals(option.getKey())) {\n            selected = option.getKey();\n        } else {\n            throw new AlreadySelectedException(this, option);\n        }\n    }\n\n    /**\n     * @return the selected option name\n     */\n    String getSelected() {\n        return selected;\n    }\n\n    /**\n     * Returns whether this option group is required.\n     *\n     * @return whether this option group is required\n     */\n    public boolean isRequired() {\n        return required;\n    }\n\n}\n"
  },
  {
    "path": "server/launcher/src/main/java/cc/blynk/cli/OptionValidator.java",
    "content": "package cc.blynk.cli;\n\n/**\n * Validates an Option string.\n *\n * @version $Id: OptionValidator.java 1544819 2013-11-23 15:34:31Z tn $\n * @since 1.1\n */\nfinal class OptionValidator {\n\n    private OptionValidator() {\n    }\n\n    /**\n     * Validates whether <code>opt</code> is a permissible Option\n     * shortOpt.  The rules that specify if the <code>opt</code>\n     * is valid are:\n     * <p>\n     * <ul>\n     * <li>a single character <code>opt</code> that is either\n     * ' '(special case), '?', '@' or a letter</li>\n     * <li>a multi character <code>opt</code> that only contains\n     * letters.</li>\n     * </ul>\n     * <p>\n     * In case {@code opt} is {@code null} no further validation is performed.\n     *\n     * @param opt The option string to validate, may be null\n     * @throws IllegalArgumentException if the Option is not valid.\n     */\n    static void validateOption(String opt) throws IllegalArgumentException {\n        // if opt is NULL do not check further\n        if (opt == null) {\n            return;\n        }\n\n        // handle the single character opt\n        if (opt.length() == 1) {\n            char ch = opt.charAt(0);\n\n            if (!isValidOpt(ch)) {\n                throw new IllegalArgumentException(\"Illegal option name '\" + ch + \"'\");\n            }\n        } else {\n            for (char ch : opt.toCharArray()) {\n                if (!isValidChar(ch)) {\n                    throw new IllegalArgumentException(\"The option '\" + opt + \"' contains an illegal \"\n                            + \"character : '\" + ch + \"'\");\n                }\n            }\n        }\n    }\n\n    /**\n     * Returns whether the specified character is a valid Option.\n     *\n     * @param c the option to validate\n     * @return true if <code>c</code> is a letter, '?' or '@', otherwise false.\n     */\n    private static boolean isValidOpt(char c) {\n        return isValidChar(c) || c == '?' || c == '@';\n    }\n\n    /**\n     * Returns whether the specified character is a valid character.\n     *\n     * @param c the character to validate\n     * @return true if <code>c</code> is a letter.\n     */\n    private static boolean isValidChar(char c) {\n        return Character.isJavaIdentifierPart(c);\n    }\n}\n"
  },
  {
    "path": "server/launcher/src/main/java/cc/blynk/cli/Options.java",
    "content": "package cc.blynk.cli;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.HashSet;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Main entry-point into the library.\n * <p>\n * Options represents a collection of {@link Option} objects, which\n * describe the possible options for a command-line.\n * <p>\n * It may flexibly parse long and short options, with or without\n * values.  Additionally, it may parse only a portion of a commandline,\n * allowing for flexible multi-stage parsing.\n *\n *\n * @version $Id: Options.java 1754332 2016-07-27 18:47:57Z britter $\n */\npublic class Options {\n\n    /**\n     * a map of the options with the character key\n     */\n    private final Map<String, Option> shortOpts = new LinkedHashMap<>();\n\n    /**\n     * a map of the options with the long key\n     */\n    private final Map<String, Option> longOpts = new LinkedHashMap<>();\n\n    /**\n     * a map of the required options\n     */\n    // N.B. This can contain either a String (addOption) or an OptionGroup (addOptionGroup)\n    // TODO this seems wrong\n    private final List<Object> requiredOpts = new ArrayList<>();\n\n    /**\n     * a map of the option groups\n     */\n    private final Map<String, OptionGroup> optionGroups = new LinkedHashMap<>();\n\n\n    /**\n     * Lists the OptionGroups that are members of this Options instance.\n     *\n     * @return a Collection of OptionGroup instances.\n     */\n    Collection<OptionGroup> getOptionGroups() {\n        return new HashSet<>(optionGroups.values());\n    }\n\n    /**\n     * Add an option that only contains a short-name.\n     * <p>\n     * <p>\n     * It may be specified as requiring an argument.\n     * </p>\n     *\n     * @param opt         Short single-character name of the option.\n     * @param hasArg      flag signally if an argument is required after this option\n     * @param description Self-documenting description\n     * @return the resulting Options instance\n     */\n    public Options addOption(String opt, boolean hasArg, String description) {\n        addOption(opt, null, hasArg, description);\n        return this;\n    }\n\n    /**\n     * Add an option that contains a short-name and a long-name.\n     * <p>\n     * <p>\n     * It may be specified as requiring an argument.\n     * </p>\n     *\n     * @param opt         Short single-character name of the option.\n     * @param longOpt     Long multi-character name of the option.\n     * @param hasArg      flag signally if an argument is required after this option\n     * @param description Self-documenting description\n     */\n    private void addOption(String opt, String longOpt, boolean hasArg, String description) {\n        addOption(new Option(opt, longOpt, hasArg, description));\n    }\n\n    /**\n     * Adds an option instance\n     *\n     * @param opt the option that is to be added\n     */\n    private void addOption(Option opt) {\n        String key = opt.getKey();\n\n        // add it to the long option list\n        if (opt.hasLongOpt()) {\n            longOpts.put(opt.getLongOpt(), opt);\n        }\n\n        // if the option is required add it to the required list\n        if (opt.isRequired()) {\n            requiredOpts.remove(key);\n            requiredOpts.add(key);\n        }\n\n        shortOpts.put(key, opt);\n    }\n\n    /**\n     * Returns the required options.\n     *\n     * @return read-only List of required options\n     */\n    List getRequiredOptions() {\n        return Collections.unmodifiableList(requiredOpts);\n    }\n\n    /**\n     * Retrieve the {@link Option} matching the long or short name specified.\n     * <p>\n     * <p>\n     * The leading hyphens in the name are ignored (up to 2).\n     * </p>\n     *\n     * @param opt short or long name of the {@link Option}\n     * @return the option represented by opt\n     */\n    Option getOption(String opt) {\n        opt = Util.stripLeadingHyphens(opt);\n\n        if (shortOpts.containsKey(opt)) {\n            return shortOpts.get(opt);\n        }\n\n        return longOpts.get(opt);\n    }\n\n    /**\n     * Returns the options with a long name starting with the name specified.\n     *\n     * @param opt the partial name of the option\n     * @return the options matching the partial name specified, or an empty list if none matches\n     * @since 1.3\n     */\n    List<String> getMatchingOptions(String opt) {\n        opt = Util.stripLeadingHyphens(opt);\n\n        List<String> matchingOpts = new ArrayList<>();\n\n        // for a perfect match return the single option only\n        if (longOpts.keySet().contains(opt)) {\n            return Collections.singletonList(opt);\n        }\n\n        for (String longOpt : longOpts.keySet()) {\n            if (longOpt.startsWith(opt)) {\n                matchingOpts.add(longOpt);\n            }\n        }\n\n        return matchingOpts;\n    }\n\n    /**\n     * Returns whether the named {@link Option} is a member of this {@link Options}.\n     *\n     * @param opt short or long name of the {@link Option}\n     * @return true if the named {@link Option} is a member of this {@link Options}\n     */\n    boolean hasOption(String opt) {\n        opt = Util.stripLeadingHyphens(opt);\n\n        return shortOpts.containsKey(opt) || longOpts.containsKey(opt);\n    }\n\n    /**\n     * Returns whether the named {@link Option} is a member of this {@link Options}.\n     *\n     * @param opt long name of the {@link Option}\n     * @return true if the named {@link Option} is a member of this {@link Options}\n     * @since 1.3\n     */\n    boolean hasLongOption(String opt) {\n        opt = Util.stripLeadingHyphens(opt);\n\n        return longOpts.containsKey(opt);\n    }\n\n    /**\n     * Returns whether the named {@link Option} is a member of this {@link Options}.\n     *\n     * @param opt short name of the {@link Option}\n     * @return true if the named {@link Option} is a member of this {@link Options}\n     * @since 1.3\n     */\n    boolean hasShortOption(String opt) {\n        opt = Util.stripLeadingHyphens(opt);\n\n        return shortOpts.containsKey(opt);\n    }\n\n    /**\n     * Returns the OptionGroup the <code>opt</code> belongs to.\n     *\n     * @param opt the option whose OptionGroup is being queried.\n     * @return the OptionGroup if <code>opt</code> is part of an OptionGroup, otherwise return null\n     */\n    OptionGroup getOptionGroup(Option opt) {\n        return optionGroups.get(opt.getKey());\n    }\n\n}\n"
  },
  {
    "path": "server/launcher/src/main/java/cc/blynk/cli/ParseException.java",
    "content": "package cc.blynk.cli;\n\n/**\n * Base for Exceptions thrown during parsing of a command-line.\n *\n * @version $Id: ParseException.java 1443102 2013-02-06 18:12:16Z tn $\n */\npublic class ParseException extends Exception {\n\n    /**\n     * Construct a new <code>ParseException</code>\n     * with the specified detail message.\n     *\n     * @param message the detail message\n     */\n    ParseException(String message) {\n        super(message);\n    }\n}\n"
  },
  {
    "path": "server/launcher/src/main/java/cc/blynk/cli/UnrecognizedOptionException.java",
    "content": "package cc.blynk.cli;\n\n/**\n * Exception thrown during parsing signalling an unrecognized\n * option was seen.\n *\n * @version $Id: UnrecognizedOptionException.java 1443102 2013-02-06 18:12:16Z tn $\n */\npublic class UnrecognizedOptionException extends ParseException {\n    /**\n     * This exception {@code serialVersionUID}.\n     */\n    private static final long serialVersionUID = -252504690284625623L;\n\n    /**\n     * The  unrecognized option\n     */\n    private String option;\n\n    /**\n     * Construct a new <code>UnrecognizedArgumentException</code>\n     * with the specified detail message.\n     *\n     * @param message the detail message\n     */\n    private UnrecognizedOptionException(String message) {\n        super(message);\n    }\n\n    /**\n     * Construct a new <code>UnrecognizedArgumentException</code>\n     * with the specified option and detail message.\n     *\n     * @param message the detail message\n     * @param option  the unrecognized option\n     * @since 1.2\n     */\n    UnrecognizedOptionException(String message, String option) {\n        this(message);\n        this.option = option;\n    }\n\n    /**\n     * Returns the unrecognized option.\n     *\n     * @return the related option\n     * @since 1.2\n     */\n    public String getOption() {\n        return option;\n    }\n}\n"
  },
  {
    "path": "server/launcher/src/main/java/cc/blynk/cli/Util.java",
    "content": "package cc.blynk.cli;\n\n/**\n * Contains useful helper methods for classes within this package.\n *\n * @version $Id: Util.java 1443102 2013-02-06 18:12:16Z tn $\n */\nfinal class Util {\n\n    private Util() {\n    }\n\n    /**\n     * Remove the hyphens from the beginning of <code>str</code> and\n     * return the new String.\n     *\n     * @param str The string from which the hyphens should be removed.\n     * @return the new String.\n     */\n    static String stripLeadingHyphens(String str) {\n        if (str == null) {\n            return null;\n        }\n        if (str.startsWith(\"--\")) {\n            return str.substring(2, str.length());\n        } else if (str.startsWith(\"-\")) {\n            return str.substring(1, str.length());\n        }\n\n        return str;\n    }\n\n    /**\n     * Remove the leading and trailing quotes from <code>str</code>.\n     * E.g. if str is '\"one two\"', then 'one two' is returned.\n     *\n     * @param str The string from which the leading and trailing quotes\n     *            should be removed.\n     * @return The string without the leading and trailing quotes.\n     */\n    static String stripLeadingAndTrailingQuotes(String str) {\n        int length = str.length();\n        if (length > 1 && str.startsWith(\"\\\"\") && str.endsWith(\"\\\"\")\n                && str.substring(1, length - 1).indexOf('\"') == -1) {\n            str = str.substring(1, length - 1);\n        }\n\n        return str;\n    }\n}\n"
  },
  {
    "path": "server/launcher/src/main/java/cc/blynk/server/launcher/ArgumentsParser.java",
    "content": "package cc.blynk.server.launcher;\n\nimport cc.blynk.cli.CommandLine;\nimport cc.blynk.cli.DefaultParser;\nimport cc.blynk.cli.Options;\nimport cc.blynk.cli.ParseException;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static cc.blynk.utils.properties.MailProperties.MAIL_PROPERTIES_FILENAME;\nimport static cc.blynk.utils.properties.ServerProperties.SERVER_PROPERTIES_FILENAME;\nimport static cc.blynk.utils.properties.SmsProperties.SMS_PROPERTIES_FILENAME;\n\n/**\n * Simple class for command line arguments parsing.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 25.03.15.\n */\nfinal class ArgumentsParser {\n\n    private static final Options options;\n\n    private static final String HARDWARE_PORT_OPTION = \"hardPort\";\n    private static final String APPLICATION_PORT_OPTION = \"appPort\";\n    private static final String WORKER_THREADS_OPTION = \"workerThreads\";\n    private static final String DATA_FOLDER_OPTION = \"dataFolder\";\n    private static final String SERVER_CONFIG_PATH_OPTION = \"serverConfig\";\n    private static final String MAIL_CONFIG_PATH_OPTION = \"mailConfig\";\n    private static final String SMS_CONFIG_PATH_OPTION = \"smsConfig\";\n    static final String RESTORE_OPTION = \"restore\";\n\n    static  {\n        options = new Options()\n               .addOption(HARDWARE_PORT_OPTION, true, \"Hardware server port.\")\n               .addOption(APPLICATION_PORT_OPTION, true, \"Application server port.\")\n               .addOption(WORKER_THREADS_OPTION, true, \"Server worker threads.\")\n               .addOption(DATA_FOLDER_OPTION, true, \"Folder where user profiles will be stored.\")\n               .addOption(SERVER_CONFIG_PATH_OPTION, true, \"Path to server.properties config file.\")\n               .addOption(MAIL_CONFIG_PATH_OPTION, true, \"Path to mail.properties config file.\")\n               .addOption(SMS_CONFIG_PATH_OPTION, true, \"Path to sms.properties config file.\")\n               .addOption(RESTORE_OPTION, false, \"Restore data from DB.\");\n    }\n\n    private ArgumentsParser() {\n    }\n\n    /**\n     * Simply parsers command line arguments and sets it to server properties for future use.\n     *\n     * @param args - command line arguments\n     */\n    @SuppressWarnings(\"ResultOfMethodCallIgnored\")\n    static Map<String, String> parse(String[] args) throws ParseException {\n        CommandLine cmd = new DefaultParser().parse(options, args);\n\n        String hardPort = cmd.getOptionValue(HARDWARE_PORT_OPTION);\n        String appPort = cmd.getOptionValue(APPLICATION_PORT_OPTION);\n        String workerThreadsString = cmd.getOptionValue(WORKER_THREADS_OPTION);\n        String dataFolder = cmd.getOptionValue(DATA_FOLDER_OPTION);\n        String serverConfigPath = cmd.getOptionValue(SERVER_CONFIG_PATH_OPTION);\n        String mailConfigPath = cmd.getOptionValue(MAIL_CONFIG_PATH_OPTION);\n        String smsConfigPath = cmd.getOptionValue(SMS_CONFIG_PATH_OPTION);\n        boolean restore = cmd.hasOption(RESTORE_OPTION);\n\n        Map<String, String> properties = new HashMap<>();\n\n        if (hardPort != null) {\n            Integer.parseInt(hardPort);\n            properties.put(\"http.port\", hardPort);\n        }\n\n        if (appPort != null) {\n            Integer.parseInt(appPort);\n            properties.put(\"https.port\", appPort);\n        }\n\n        if (workerThreadsString != null) {\n            Integer.parseInt(workerThreadsString);\n            properties.put(\"server.worker.threads\", workerThreadsString);\n        }\n        if (dataFolder != null) {\n            properties.put(\"data.folder\", dataFolder);\n        }\n        if (serverConfigPath != null) {\n            properties.put(SERVER_PROPERTIES_FILENAME, serverConfigPath);\n        }\n        if (mailConfigPath != null) {\n            properties.put(MAIL_PROPERTIES_FILENAME, mailConfigPath);\n        }\n        if (smsConfigPath != null) {\n            properties.put(SMS_PROPERTIES_FILENAME, smsConfigPath);\n        }\n\n        properties.put(RESTORE_OPTION, Boolean.toString(restore));\n\n        return properties;\n    }\n\n}\n"
  },
  {
    "path": "server/launcher/src/main/java/cc/blynk/server/launcher/JobLauncher.java",
    "content": "package cc.blynk.server.launcher;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.core.reporting.average.AverageAggregatorProcessor;\nimport cc.blynk.server.servers.BaseServer;\nimport cc.blynk.server.workers.CertificateRenewalWorker;\nimport cc.blynk.server.workers.HistoryGraphUnusedPinDataCleanerWorker;\nimport cc.blynk.server.workers.ProfileSaverWorker;\nimport cc.blynk.server.workers.ReportingTruncateWorker;\nimport cc.blynk.server.workers.ReportingWorker;\nimport cc.blynk.server.workers.ShutdownHookWorker;\nimport cc.blynk.server.workers.StatsWorker;\nimport cc.blynk.utils.BlynkTPFactory;\nimport cc.blynk.utils.structure.LRUCache;\n\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.ScheduledExecutorService;\nimport java.util.concurrent.TimeUnit;\n\nimport static java.util.concurrent.TimeUnit.DAYS;\nimport static java.util.concurrent.TimeUnit.HOURS;\nimport static java.util.concurrent.TimeUnit.MILLISECONDS;\n\n/**\n * Launches a bunch of separate jobs/schedulers responsible for different aspects of business logic\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 28.09.15.\n */\nfinal class JobLauncher {\n\n    private JobLauncher() {\n    }\n\n    public static void start(Holder holder, BaseServer[] servers) {\n        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1, BlynkTPFactory.build(\"DataSaver\"));\n\n        long startDelay;\n\n        ReportingWorker reportingWorker = new ReportingWorker(\n                holder.reportingDiskDao,\n                holder.props.getReportingFolder(),\n                holder.reportingDBManager\n        );\n\n        //to start at the beggining of an minute\n        startDelay = AverageAggregatorProcessor.MINUTE\n                - (System.currentTimeMillis() % AverageAggregatorProcessor.MINUTE);\n        scheduler.scheduleAtFixedRate(reportingWorker, startDelay,\n                AverageAggregatorProcessor.MINUTE, MILLISECONDS);\n\n        var profileSaverWorker = new ProfileSaverWorker(holder.userDao, holder.fileManager, holder.dbManager);\n\n        //running 1 sec later after reporting\n        scheduler.scheduleAtFixedRate(profileSaverWorker, startDelay + 1000,\n                holder.props.getIntProperty(\"profile.save.worker.period\"), MILLISECONDS);\n\n        var statsWorker = new StatsWorker(holder);\n        scheduler.scheduleAtFixedRate(statsWorker, 1000,\n                holder.props.getIntProperty(\"stats.print.worker.period\"), MILLISECONDS);\n\n        if (holder.sslContextHolder.runRenewalWorker()) {\n            if (holder.props.isRenewalDisabled()) {\n                System.out.println(\"Certificate renewal disabled.\");\n            } else {\n                scheduler.scheduleAtFixedRate(\n                        new CertificateRenewalWorker(holder.sslContextHolder), 1, 1, TimeUnit.DAYS\n                );\n            }\n        }\n        scheduler.scheduleAtFixedRate(LRUCache.LOGIN_TOKENS_CACHE::clear, 1, 1, HOURS);\n        scheduler.scheduleAtFixedRate(holder.tokenManager::clearTemporaryTokens, 7, 1, DAYS);\n\n        //running once every 3 day\n        //todo could be removed?\n        var reportingDataDiskCleaner =\n                new HistoryGraphUnusedPinDataCleanerWorker(holder.userDao, holder.reportingDiskDao);\n        //once every 7 days\n        scheduler.scheduleAtFixedRate(reportingDataDiskCleaner, 1, 7, DAYS);\n\n        ReportingTruncateWorker reportingTruncateWorker = new ReportingTruncateWorker(holder.reportingDiskDao,\n                holder.limits.storeMinuteRecordDays, holder.limits.storeReportCSVDays);\n\n        //once every week\n        scheduler.scheduleAtFixedRate(reportingTruncateWorker, 1, 24 * 7, HOURS);\n\n        //millis we need to wait to start scheduler at the beginning of a second.\n        startDelay = 1000 - (System.currentTimeMillis() % 1000);\n\n        //separate thread for timer and reading widgets\n        var ses = Executors.newScheduledThreadPool(1, BlynkTPFactory.build(\"TimerAndReading\"));\n        ses.scheduleAtFixedRate(holder.timerWorker, startDelay, 1000, MILLISECONDS);\n        ses.scheduleAtFixedRate(holder.readingWidgetsWorker, startDelay + 400, 1000, MILLISECONDS);\n\n        //shutdown hook thread catcher\n        Runtime.getRuntime().addShutdownHook(new Thread(\n                new ShutdownHookWorker(servers, holder, scheduler, profileSaverWorker)\n        ));\n    }\n\n\n}\n"
  },
  {
    "path": "server/launcher/src/main/java/cc/blynk/server/launcher/ServerLauncher.java",
    "content": "package cc.blynk.server.launcher;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.servers.BaseServer;\nimport cc.blynk.server.servers.application.MobileAndHttpsServer;\nimport cc.blynk.server.servers.hardware.HardwareAndHttpAPIServer;\nimport cc.blynk.server.servers.hardware.MQTTHardwareServer;\nimport cc.blynk.utils.AppNameUtil;\nimport cc.blynk.utils.JarUtil;\nimport cc.blynk.utils.LoggerUtil;\nimport cc.blynk.utils.SHA256Util;\nimport cc.blynk.utils.StringUtils;\nimport cc.blynk.utils.properties.GCMProperties;\nimport cc.blynk.utils.properties.MailProperties;\nimport cc.blynk.utils.properties.ServerProperties;\nimport cc.blynk.utils.properties.SmsProperties;\nimport cc.blynk.utils.properties.TwitterProperties;\nimport org.bouncycastle.jce.provider.BouncyCastleProvider;\n\nimport java.io.File;\nimport java.net.BindException;\nimport java.security.Security;\nimport java.util.Map;\n\n/**\n * Entry point for server launch.\n *\n * By default starts 4 servers on different ports:\n *\n * 1 server socket for HTTP API, Blynk hardware protocol, web sockets (8080 default)\n * 1 server socket for HTTPS API, Blynk app protocol, hardware secured blynkapp, web sockets (9443 default)\n * 1 server socket for MQTT (8440 default)\n *\n * In addition launcher start all related to business logic threads like saving user profiles thread, timers\n * processing thread, properties reload thread and shutdown hook tread.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/16/2015.\n */\npublic final class ServerLauncher {\n\n    //required for QR generation\n    static {\n        System.setProperty(\"java.awt.headless\", \"true\");\n    }\n\n    private ServerLauncher() {\n    }\n\n    public static void main(String[] args) throws Exception {\n        Map<String, String> cmdProperties = ArgumentsParser.parse(args);\n\n        ServerProperties serverProperties = new ServerProperties(cmdProperties);\n\n        LoggerUtil.configureLogging(serverProperties);\n\n        //required for logging dynamic context\n        System.setProperty(\"data.folder\", serverProperties.getProperty(\"data.folder\"));\n\n        //required to avoid dependencies within model to server.properties\n        setGlobalProperties(serverProperties);\n\n        MailProperties mailProperties = new MailProperties(cmdProperties);\n        SmsProperties smsProperties = new SmsProperties(cmdProperties);\n        GCMProperties gcmProperties = new GCMProperties(cmdProperties);\n        TwitterProperties twitterProperties = new TwitterProperties(cmdProperties);\n\n        Security.addProvider(new BouncyCastleProvider());\n\n        boolean restore = Boolean.parseBoolean(cmdProperties.get(ArgumentsParser.RESTORE_OPTION));\n        start(serverProperties, mailProperties, smsProperties,\n                gcmProperties, twitterProperties, restore);\n    }\n\n    private static void setGlobalProperties(ServerProperties serverProperties) {\n        Map<String, String> globalProps = Map.of(\n                \"terminal.strings.pool.size\", \"25\",\n                \"initial.energy\", \"2000\",\n                \"table.rows.pool.size\", \"100\",\n                \"csv.export.data.points.max\", \"43200\",\n                \"map.strings.pool.size\", \"25\"\n        );\n\n        for (var entry : globalProps.entrySet()) {\n            String name = entry.getKey();\n            String value = serverProperties.getProperty(name, entry.getValue());\n            System.setProperty(name, value);\n        }\n    }\n\n    private static void start(ServerProperties serverProperties, MailProperties mailProperties,\n                              SmsProperties smsProperties, GCMProperties gcmProperties,\n                              TwitterProperties twitterProperties,\n                              boolean restore) {\n        Holder holder = new Holder(serverProperties,\n                mailProperties, smsProperties, gcmProperties, twitterProperties,\n                restore);\n\n        BaseServer[] servers = new BaseServer[] {\n                new HardwareAndHttpAPIServer(holder),\n                new MobileAndHttpsServer(holder),\n                new MQTTHardwareServer(holder)\n        };\n\n        if (startServers(servers)) {\n            //Launching all background jobs.\n            JobLauncher.start(holder, servers);\n\n            System.out.println();\n            System.out.println(\"Blynk Server \" + JarUtil.getServerVersion() + \" successfully started.\");\n            String path = new File(System.getProperty(\"logs.folder\")).getAbsolutePath().replace(\"/./\", \"/\");\n            System.out.println(\"All server output is stored in folder '\" + path + \"' file.\");\n\n            holder.sslContextHolder.generateInitialCertificates(holder.props);\n\n            createSuperUser(holder);\n        }\n    }\n\n    private static void createSuperUser(Holder holder) {\n        ServerProperties props = holder.props;\n        String url = props.getAdminUrl(props.host);\n        String email = props.getProperty(\"admin.email\", \"admin@blynk.cc\");\n        String pass = props.getProperty(\"admin.pass\");\n\n        if (!holder.userDao.isSuperAdminExists()) {\n            if (pass == null || pass.isEmpty()) {\n                System.out.println(\"Admin password not specified. Random password generated.\");\n                pass = StringUtils.randomPassword(24);\n            }\n\n            System.out.println(\"Your Admin url is \" + url);\n            System.out.println(\"Your Admin login email is \" + email);\n            System.out.println(\"Your Admin password is \" + pass);\n\n            String hash = SHA256Util.makeHash(pass, email);\n            holder.userDao.add(email, hash, AppNameUtil.BLYNK, true);\n        }\n    }\n\n    private static boolean startServers(BaseServer[] servers) {\n        //start servers\n        try {\n            for (BaseServer server : servers) {\n                server.start();\n            }\n            return true;\n        } catch (BindException bindException) {\n            System.out.println(\"Server ports are busy. Most probably server already launched. See \"\n                    + new File(System.getProperty(\"logs.folder\")).getAbsolutePath() + \" for more info.\");\n        } catch (Exception e) {\n            System.out.println(\"Error starting Blynk server. Stopping.\");\n        }\n\n        return false;\n    }\n\n}\n"
  },
  {
    "path": "server/launcher/src/main/java/cc/blynk/server/servers/BaseServer.java",
    "content": "package cc.blynk.server.servers;\n\nimport cc.blynk.server.transport.TransportTypeHolder;\nimport io.netty.bootstrap.ServerBootstrap;\nimport io.netty.channel.ChannelFuture;\nimport io.netty.channel.ChannelInitializer;\nimport io.netty.channel.ChannelOption;\nimport io.netty.channel.EventLoopGroup;\nimport io.netty.channel.ServerChannel;\nimport io.netty.channel.socket.SocketChannel;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.net.InetSocketAddress;\n\n/**\n * Base server abstraction. Class responsible for Netty EventLoops starting amd port listening.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 3/10/2015.\n */\npublic abstract class BaseServer {\n\n    protected static final Logger log = LogManager.getLogger(BaseServer.class);\n\n    private final String listenAddress;\n    protected final int port;\n    private final TransportTypeHolder transportTypeHolder;\n\n    private ChannelFuture cf;\n\n    protected BaseServer(String listenAddress, int port, TransportTypeHolder transportTypeHolder) {\n        this.listenAddress = listenAddress;\n        this.port = port;\n        this.transportTypeHolder = transportTypeHolder;\n    }\n\n    public BaseServer start() throws Exception {\n        buildServerAndRun(\n                transportTypeHolder.bossGroup,\n                transportTypeHolder.workerGroup,\n                transportTypeHolder.channelClass\n        );\n\n        return this;\n    }\n\n    private void buildServerAndRun(EventLoopGroup bossGroup, EventLoopGroup workerGroup,\n                                   Class<? extends ServerChannel> channelClass) throws Exception {\n\n        var b = new ServerBootstrap();\n        try {\n            b.group(bossGroup, workerGroup)\n                    .channel(channelClass)\n                    .childOption(ChannelOption.SO_KEEPALIVE, true)\n                    .childHandler(getChannelInitializer());\n\n            var listenTo = (listenAddress == null || listenAddress.isEmpty())\n                    ? new InetSocketAddress(port)\n                    : new InetSocketAddress(listenAddress, port);\n            this.cf = b.bind(listenTo).sync();\n        } catch (Exception e) {\n            log.error(\"Error initializing {}, port {}\", getServerName(), port, e);\n            throw e;\n        }\n\n        log.info(\"{} server listening at {} port.\", getServerName(), port);\n    }\n\n    protected abstract ChannelInitializer<SocketChannel> getChannelInitializer();\n\n    protected abstract String getServerName();\n\n    public ChannelFuture close() {\n        return cf.channel().close();\n    }\n}\n"
  },
  {
    "path": "server/launcher/src/main/java/cc/blynk/server/servers/application/MobileAndHttpsServer.java",
    "content": "package cc.blynk.server.servers.application;\n\nimport cc.blynk.core.http.handlers.CookieBasedUrlReWriterHandler;\nimport cc.blynk.core.http.handlers.NoCacheStaticFile;\nimport cc.blynk.core.http.handlers.NoMatchHandler;\nimport cc.blynk.core.http.handlers.OTAHandler;\nimport cc.blynk.core.http.handlers.StaticFile;\nimport cc.blynk.core.http.handlers.StaticFileEdsWith;\nimport cc.blynk.core.http.handlers.StaticFileHandler;\nimport cc.blynk.core.http.handlers.UploadHandler;\nimport cc.blynk.core.http.handlers.url.UrlReWriterHandler;\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.admin.http.handlers.IpFilterHandler;\nimport cc.blynk.server.admin.http.logic.ConfigsLogic;\nimport cc.blynk.server.admin.http.logic.HardwareStatsLogic;\nimport cc.blynk.server.admin.http.logic.OTALogic;\nimport cc.blynk.server.admin.http.logic.StatsLogic;\nimport cc.blynk.server.admin.http.logic.UsersLogic;\nimport cc.blynk.server.api.http.handlers.BaseHttpAndBlynkUnificationHandler;\nimport cc.blynk.server.api.http.handlers.BaseWebSocketUnificator;\nimport cc.blynk.server.api.http.logic.HttpAPILogic;\nimport cc.blynk.server.api.http.logic.ResetPasswordHttpLogic;\nimport cc.blynk.server.api.http.logic.business.AdminAuthHandler;\nimport cc.blynk.server.api.http.logic.business.AuthCookieHandler;\nimport cc.blynk.server.api.websockets.handlers.WSHandler;\nimport cc.blynk.server.api.websockets.handlers.WSWrapperEncoder;\nimport cc.blynk.server.application.handlers.main.MobileChannelStateHandler;\nimport cc.blynk.server.application.handlers.main.MobileResetPasswordHandler;\nimport cc.blynk.server.application.handlers.main.auth.MobileGetServerHandler;\nimport cc.blynk.server.application.handlers.main.auth.MobileLoginHandler;\nimport cc.blynk.server.application.handlers.main.auth.MobileRegisterHandler;\nimport cc.blynk.server.application.handlers.sharing.auth.MobileShareLoginHandler;\nimport cc.blynk.server.common.handlers.AlreadyLoggedHandler;\nimport cc.blynk.server.common.handlers.UserNotLoggedHandler;\nimport cc.blynk.server.core.protocol.handlers.decoders.MessageDecoder;\nimport cc.blynk.server.core.protocol.handlers.decoders.MobileMessageDecoder;\nimport cc.blynk.server.core.protocol.handlers.decoders.WSMessageDecoder;\nimport cc.blynk.server.core.protocol.handlers.encoders.MessageEncoder;\nimport cc.blynk.server.core.protocol.handlers.encoders.MobileMessageEncoder;\nimport cc.blynk.server.core.protocol.handlers.encoders.WSMessageEncoder;\nimport cc.blynk.server.hardware.handlers.hardware.HardwareChannelStateHandler;\nimport cc.blynk.server.hardware.handlers.hardware.auth.HardwareLoginHandler;\nimport cc.blynk.server.servers.BaseServer;\nimport cc.blynk.utils.FileUtils;\nimport cc.blynk.utils.NumberUtil;\nimport io.netty.channel.ChannelFuture;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.ChannelInitializer;\nimport io.netty.channel.ChannelPipeline;\nimport io.netty.channel.socket.SocketChannel;\nimport io.netty.handler.codec.http.FullHttpRequest;\nimport io.netty.handler.codec.http.HttpObjectAggregator;\nimport io.netty.handler.codec.http.HttpServerCodec;\nimport io.netty.handler.codec.http.HttpServerKeepAliveHandler;\nimport io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;\nimport io.netty.handler.stream.ChunkedWriteHandler;\nimport io.netty.handler.timeout.IdleStateHandler;\n\nimport static cc.blynk.core.http.Response.redirect;\nimport static cc.blynk.utils.StringUtils.BLYNK_LANDING;\nimport static cc.blynk.utils.StringUtils.WEBSOCKETS_PATH;\nimport static cc.blynk.utils.StringUtils.WEBSOCKET_PATH;\nimport static cc.blynk.utils.StringUtils.WEBSOCKET_WEB_PATH;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 1/12/2015.\n */\npublic class MobileAndHttpsServer extends BaseServer {\n\n    private final ChannelInitializer<SocketChannel> channelInitializer;\n\n    public MobileAndHttpsServer(Holder holder) {\n        super(holder.props.getProperty(\"listen.address\"),\n                holder.props.getIntProperty(\"https.port\"), holder.transportTypeHolder);\n\n        var appChannelStateHandler = new MobileChannelStateHandler(holder.sessionDao);\n        var registerHandler = new MobileRegisterHandler(holder);\n        MobileLoginHandler appLoginHandler = new MobileLoginHandler(holder);\n        var appShareLoginHandler = new MobileShareLoginHandler(holder);\n        var userNotLoggedHandler = new UserNotLoggedHandler();\n        var getServerHandler = new MobileGetServerHandler(holder);\n        var resetPasswordHandler = new MobileResetPasswordHandler(holder);\n\n        var hardwareIdleTimeout = holder.limits.hardwareIdleTimeout;\n        var appIdleTimeout = holder.limits.appIdleTimeout;\n\n        var hardwareChannelStateHandler = new HardwareChannelStateHandler(holder);\n        var hardwareLoginHandler = new HardwareLoginHandler(holder, port);\n\n        var rootPath = holder.props.getAdminRootPath();\n\n        var ipFilterHandler = new IpFilterHandler(\n                holder.props.getCommaSeparatedValueAsArray(\"allowed.administrator.ips\"));\n\n        var stats = holder.stats;\n\n        //http API handlers\n        var resetPasswordLogic = new ResetPasswordHttpLogic(holder);\n        var httpAPILogic = new HttpAPILogic(holder);\n        var noMatchHandler = new NoMatchHandler();\n        var webSocketHandler = new WSHandler(stats);\n        var webSocketWrapperEncoder = new WSWrapperEncoder();\n\n        var webAppMessageEncoder = new WSMessageEncoder();\n\n        //admin API handlers\n        var otaLogic = new OTALogic(holder, rootPath);\n        var usersLogic = new UsersLogic(holder, rootPath);\n        var statsLogic = new StatsLogic(holder, rootPath);\n        var configsLogic = new ConfigsLogic(holder, rootPath);\n        var hardwareStatsLogic = new HardwareStatsLogic(holder, rootPath);\n        var adminAuthHandler = new AdminAuthHandler(holder, rootPath);\n        var authCookieHandler = new AuthCookieHandler(holder.sessionDao);\n        var cookieBasedUrlReWriterHandler =\n                new CookieBasedUrlReWriterHandler(rootPath, \"/static/admin.html\", \"/static/login.html\");\n\n        var alreadyLoggedHandler = new AlreadyLoggedHandler();\n        int hardTimeoutSecs = NumberUtil.calcHeartbeatTimeout(holder.limits.hardwareIdleTimeout);\n\n        var baseWebSocketUnificator = new BaseWebSocketUnificator() {\n            @Override\n            public void channelRead(ChannelHandlerContext ctx, Object msg) {\n                var req = (FullHttpRequest) msg;\n                var uri = req.uri();\n\n                log.trace(\"In http and websocket unificator handler.\");\n                if (uri.equals(\"/\")) {\n                    //for local server do redirect to admin page\n                    try {\n                        ctx.writeAndFlush(redirect(holder.props.isLocalRegion() ? rootPath : BLYNK_LANDING));\n                    } finally {\n                        req.release();\n                    }\n                    return;\n                } else if (uri.startsWith(rootPath)) {\n                    initAdminPipeline(ctx);\n                } else if (uri.startsWith(WEBSOCKET_PATH)) {\n                    initWebSocketPipeline(ctx, WEBSOCKETS_PATH);\n                } else if (uri.equals(WEBSOCKET_WEB_PATH)) {\n                    initWebDashboardSocket(ctx);\n                } else {\n                    initHttpPipeline(ctx);\n                }\n\n                ctx.fireChannelRead(msg);\n            }\n\n            private void initAdminPipeline(ChannelHandlerContext ctx) {\n                if (!ipFilterHandler.accept(ctx)) {\n                    ctx.close();\n                    return;\n                }\n\n                var pipeline = ctx.pipeline();\n\n                pipeline.addLast(new UploadHandler(holder.props.jarPath, \"/upload\", \"/static/ota\"))\n                        .addLast(new OTAHandler(holder, rootPath + \"/ota/start\", \"/static/ota\"))\n                        .addLast(adminAuthHandler)\n                        .addLast(authCookieHandler)\n                        .addLast(cookieBasedUrlReWriterHandler);\n\n                pipeline.remove(StaticFileHandler.class);\n                pipeline.addLast(new StaticFileHandler(holder.props, new NoCacheStaticFile(\"/static\")))\n                        .addLast(otaLogic)\n                        .addLast(usersLogic)\n                        .addLast(statsLogic)\n                        .addLast(configsLogic)\n                        .addLast(hardwareStatsLogic)\n                        .addLast(httpAPILogic)\n                        .addLast(noMatchHandler)\n                        .remove(this);\n                if (log.isTraceEnabled()) {\n                    log.trace(\"Initialized admin pipeline. {}\", ctx.pipeline().names());\n                }\n            }\n\n            private void initHttpPipeline(ChannelHandlerContext ctx) {\n                ctx.pipeline()\n                        .addLast(resetPasswordLogic)\n                        .addLast(httpAPILogic)\n                        .addLast(noMatchHandler)\n                        .remove(this);\n                if (log.isTraceEnabled()) {\n                    log.trace(\"Initialized https pipeline. {}\", ctx.pipeline().names());\n                }\n            }\n\n            private void initWebDashboardSocket(ChannelHandlerContext ctx) {\n                var pipeline = ctx.pipeline();\n\n                //websockets specific handlers\n                pipeline.addFirst(\"AChannelState\", appChannelStateHandler)\n                        .addFirst(\"AReadTimeout\", new IdleStateHandler(appIdleTimeout, 0, 0))\n                        .addLast(\"WSWebSocketServerProtocolHandler\",\n                        new WebSocketServerProtocolHandler(WEBSOCKET_WEB_PATH))\n                        .addLast(\"WSMessageDecoder\", new WSMessageDecoder(stats, holder.limits))\n                        .addLast(\"WSMessageEncoder\", webAppMessageEncoder)\n                        .addLast(\"AGetServer\", getServerHandler)\n                        .addLast(\"ALogin\", appLoginHandler)\n                        .addLast(\"ANotLogged\", userNotLoggedHandler);\n                pipeline.remove(ChunkedWriteHandler.class);\n                pipeline.remove(UrlReWriterHandler.class);\n                pipeline.remove(StaticFileHandler.class);\n                pipeline.remove(this);\n                if (log.isTraceEnabled()) {\n                    log.trace(\"Initialized web dashboard pipeline. {}\", ctx.pipeline().names());\n                }\n            }\n\n            private void initWebSocketPipeline(ChannelHandlerContext ctx, String websocketPath) {\n                var pipeline = ctx.pipeline();\n\n                //websockets specific handlers\n                pipeline.addFirst(\"WSIdleStateHandler\", new IdleStateHandler(hardwareIdleTimeout, 0, 0))\n                        .addLast(\"WSChannelState\", hardwareChannelStateHandler)\n                        .addLast(\"WSWebSocketServerProtocolHandler\",\n                        new WebSocketServerProtocolHandler(websocketPath, true))\n                        .addLast(\"WSWebSocket\", webSocketHandler)\n                        .addLast(\"WSMessageDecoder\", new MessageDecoder(stats, holder.limits))\n                        .addLast(\"WSSocketWrapper\", webSocketWrapperEncoder)\n                        .addLast(\"WSMessageEncoder\", new MessageEncoder(stats))\n                        .addLast(\"WSLogin\", hardwareLoginHandler)\n                        .addLast(\"WSNotLogged\", alreadyLoggedHandler);\n                pipeline.remove(ChunkedWriteHandler.class);\n                pipeline.remove(UrlReWriterHandler.class);\n                pipeline.remove(StaticFileHandler.class);\n                pipeline.remove(this);\n                if (log.isTraceEnabled()) {\n                    log.trace(\"Initialized secured hardware websocket pipeline. {}\", ctx.pipeline().names());\n                }\n            }\n        };\n\n        channelInitializer = new ChannelInitializer<>() {\n            @Override\n            protected void initChannel(SocketChannel ch) {\n                ch.pipeline()\n                .addLast(holder.sslContextHolder.sslCtx.newHandler(ch.alloc()))\n                .addLast(new BaseHttpAndBlynkUnificationHandler() {\n                    @Override\n                    public ChannelPipeline buildHttpPipeline(ChannelPipeline pipeline) {\n                        log.trace(\"HTTPS connection detected.\", pipeline.channel());\n                        return pipeline\n                                .addLast(\"HttpsServerCodec\", new HttpServerCodec())\n                                .addLast(\"HttpsServerKeepAlive\", new HttpServerKeepAliveHandler())\n                                .addLast(\"HttpsObjectAggregator\",\n                                        new HttpObjectAggregator(holder.limits.webRequestMaxSize, true))\n                                .addLast(\"HttpChunkedWrite\", new ChunkedWriteHandler())\n                                .addLast(\"HttpUrlMapper\",\n                                        new UrlReWriterHandler(\"/favicon.ico\", \"/static/favicon.ico\"))\n                                .addLast(\"HttpStaticFile\",\n                                        new StaticFileHandler(holder.props, new StaticFile(\"/static\"),\n                                                new StaticFileEdsWith(FileUtils.CSV_DIR, \".gz\"),\n                                                new StaticFileEdsWith(FileUtils.CSV_DIR, \".zip\")))\n                                .addLast(\"HttpsWebSocketUnificator\", baseWebSocketUnificator);\n                    }\n\n                    @Override\n                    public ChannelPipeline buildAppPipeline(ChannelPipeline pipeline) {\n                        log.trace(\"Blynk app protocol connection detected.\", pipeline.channel());\n                        return pipeline\n                                .addFirst(\"AChannelState\", appChannelStateHandler)\n                                .addFirst(\"AReadTimeout\", new IdleStateHandler(appIdleTimeout, 0, 0))\n                                .addLast(\"AMessageDecoder\", new MobileMessageDecoder(holder.stats, holder.limits))\n                                .addLast(\"AMessageEncoder\", new MobileMessageEncoder(holder.stats))\n                                .addLast(\"AGetServer\", getServerHandler)\n                                .addLast(\"ARegister\", registerHandler)\n                                .addLast(\"ALogin\", appLoginHandler)\n                                .addLast(\"AResetPass\", resetPasswordHandler)\n                                .addLast(\"AShareLogin\", appShareLoginHandler)\n                                .addLast(\"ANotLogged\", userNotLoggedHandler);\n                    }\n\n                    @Override\n                    public ChannelPipeline buildHardwarePipeline(ChannelPipeline pipeline) {\n                        log.trace(\"Blynk ssl hardware protocol connection detected.\", pipeline.channel());\n                        return pipeline\n                                .addFirst(\"H_IdleStateHandler\",\n                                        new IdleStateHandler(hardTimeoutSecs, 0, 0))\n                                .addLast(\"H_ChannelState\", hardwareChannelStateHandler)\n                                .addLast(\"H_MessageDecoder\", new MessageDecoder(holder.stats, holder.limits))\n                                .addLast(\"H_MessageEncoder\", new MessageEncoder(holder.stats))\n                                .addLast(\"H_Login\", hardwareLoginHandler)\n                                .addLast(\"H_AlreadyLogged\", alreadyLoggedHandler);\n                    }\n                });\n            }\n        };\n    }\n\n    @Override\n    public ChannelInitializer<SocketChannel> getChannelInitializer() {\n        return channelInitializer;\n    }\n\n    @Override\n    protected String getServerName() {\n        return \"HTTPS API, WebSockets and Admin page\";\n    }\n\n    @Override\n    public ChannelFuture close() {\n        System.out.println(\"Shutting down HTTPS API, WebSockets and Admin server...\");\n        return super.close();\n    }\n\n}\n"
  },
  {
    "path": "server/launcher/src/main/java/cc/blynk/server/servers/hardware/HardwareAndHttpAPIServer.java",
    "content": "package cc.blynk.server.servers.hardware;\n\nimport cc.blynk.core.http.handlers.NoMatchHandler;\nimport cc.blynk.core.http.handlers.StaticFile;\nimport cc.blynk.core.http.handlers.StaticFileEdsWith;\nimport cc.blynk.core.http.handlers.StaticFileHandler;\nimport cc.blynk.core.http.handlers.url.UrlReWriterHandler;\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.api.http.handlers.BaseHttpAndBlynkUnificationHandler;\nimport cc.blynk.server.api.http.handlers.BaseWebSocketUnificator;\nimport cc.blynk.server.api.http.handlers.LetsEncryptHandler;\nimport cc.blynk.server.api.http.logic.HttpAPILogic;\nimport cc.blynk.server.api.http.logic.ResetPasswordHttpLogic;\nimport cc.blynk.server.api.websockets.handlers.WSHandler;\nimport cc.blynk.server.api.websockets.handlers.WSWrapperEncoder;\nimport cc.blynk.server.common.handlers.AlreadyLoggedHandler;\nimport cc.blynk.server.core.protocol.handlers.decoders.MessageDecoder;\nimport cc.blynk.server.core.protocol.handlers.encoders.MessageEncoder;\nimport cc.blynk.server.core.stats.GlobalStats;\nimport cc.blynk.server.hardware.handlers.hardware.HardwareChannelStateHandler;\nimport cc.blynk.server.hardware.handlers.hardware.auth.HardwareLoginHandler;\nimport cc.blynk.server.servers.BaseServer;\nimport cc.blynk.utils.FileUtils;\nimport cc.blynk.utils.NumberUtil;\nimport io.netty.channel.ChannelFuture;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.ChannelInitializer;\nimport io.netty.channel.ChannelPipeline;\nimport io.netty.channel.socket.SocketChannel;\nimport io.netty.handler.codec.http.FullHttpRequest;\nimport io.netty.handler.codec.http.HttpObjectAggregator;\nimport io.netty.handler.codec.http.HttpServerCodec;\nimport io.netty.handler.codec.http.HttpServerKeepAliveHandler;\nimport io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;\nimport io.netty.handler.stream.ChunkedWriteHandler;\nimport io.netty.handler.timeout.IdleStateHandler;\n\nimport static cc.blynk.core.http.Response.redirect;\nimport static cc.blynk.utils.StringUtils.BLYNK_LANDING;\nimport static cc.blynk.utils.StringUtils.WEBSOCKETS_PATH;\nimport static cc.blynk.utils.StringUtils.WEBSOCKET_PATH;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 1/12/2015.\n */\npublic class HardwareAndHttpAPIServer extends BaseServer {\n\n    private final ChannelInitializer<SocketChannel> channelInitializer;\n\n    public HardwareAndHttpAPIServer(Holder holder) {\n        super(holder.props.getProperty(\"listen.address\"),\n                holder.props.getIntProperty(\"http.port\"), holder.transportTypeHolder);\n\n        LetsEncryptHandler letsEncryptHandler = new LetsEncryptHandler(holder.sslContextHolder.contentHolder);\n        HardwareLoginHandler hardwareLoginHandler = new HardwareLoginHandler(holder, port);\n        HardwareChannelStateHandler hardwareChannelStateHandler = new HardwareChannelStateHandler(holder);\n        AlreadyLoggedHandler alreadyLoggedHandler = new AlreadyLoggedHandler();\n        int maxWebLength = holder.limits.webRequestMaxSize;\n        int hardTimeoutSecs = NumberUtil.calcHeartbeatTimeout(holder.limits.hardwareIdleTimeout);\n\n        GlobalStats stats = holder.stats;\n\n        //http API handlers\n        ResetPasswordHttpLogic resetPasswordLogic = new ResetPasswordHttpLogic(holder);\n        HttpAPILogic httpAPILogic = new HttpAPILogic(holder);\n        NoMatchHandler noMatchHandler = new NoMatchHandler();\n        BaseWebSocketUnificator baseWebSocketUnificator = new BaseWebSocketUnificator() {\n            @Override\n            public void channelRead(ChannelHandlerContext ctx, Object msg) {\n                var req = (FullHttpRequest) msg;\n                var uri = req.uri();\n\n                log.trace(\"In http and websocket unificator handler.\");\n                if (uri.equals(\"/\")) {\n                    //for local server do redirect to admin page\n                    try {\n                        ctx.writeAndFlush(redirect(BLYNK_LANDING));\n                    } finally {\n                        req.release();\n                    }\n                    return;\n                } else if (uri.startsWith(WEBSOCKET_PATH)) {\n                    initWebSocketPipeline(ctx, WEBSOCKETS_PATH);\n                } else {\n                    initHttpPipeline(ctx);\n                }\n\n                ctx.fireChannelRead(msg);\n            }\n\n            private void initHttpPipeline(ChannelHandlerContext ctx) {\n                ctx.pipeline()\n                        .addLast(letsEncryptHandler)\n                        .addLast(\"HttpChunkedWrite\", new ChunkedWriteHandler())\n                        .addLast(\"HttpUrlMapper\", new UrlReWriterHandler(\"/favicon.ico\", \"/static/favicon.ico\"))\n                        .addLast(\"HttpStaticFile\", new StaticFileHandler(holder.props, new StaticFile(\"/static\"),\n                                        new StaticFileEdsWith(FileUtils.CSV_DIR, \".gz\"),\n                                        new StaticFileEdsWith(FileUtils.CSV_DIR, \".zip\")))\n                        .addLast(resetPasswordLogic)\n                        .addLast(httpAPILogic)\n                        .addLast(noMatchHandler)\n                        .remove(this);\n                if (log.isTraceEnabled()) {\n                    log.trace(\"Initialized http pipeline. {}\", ctx.pipeline().names());\n                }\n            }\n\n            private void initWebSocketPipeline(ChannelHandlerContext ctx, String websocketPath) {\n                var pipeline = ctx.pipeline();\n\n                //websockets specific handlers\n                pipeline.addFirst(\"WSIdleStateHandler\", new IdleStateHandler(hardTimeoutSecs, 0, 0))\n                        .addLast(\"WSChannelState\", hardwareChannelStateHandler)\n                        .addLast(\"WSWebSocketServerProtocolHandler\",\n                        new WebSocketServerProtocolHandler(websocketPath, true))\n                        .addLast(\"WSWebSocket\", new WSHandler(stats))\n                        .addLast(\"WSMessageDecoder\", new MessageDecoder(stats, holder.limits))\n                        .addLast(\"WSSocketWrapper\", new WSWrapperEncoder())\n                        .addLast(\"WSMessageEncoder\", new MessageEncoder(stats))\n                        .addLast(\"WSLogin\", hardwareLoginHandler)\n                        .addLast(\"WSNotLogged\", alreadyLoggedHandler)\n                        .remove(this);\n                if (log.isTraceEnabled()) {\n                    log.trace(\"Initialized hardware websocket pipeline. {}\", ctx.pipeline().names());\n                }\n            }\n        };\n\n        channelInitializer = new ChannelInitializer<>() {\n            @Override\n            protected void initChannel(SocketChannel ch) {\n                ch.pipeline().addLast(\n                        new BaseHttpAndBlynkUnificationHandler() {\n                            @Override\n                            public ChannelPipeline buildHttpPipeline(ChannelPipeline pipeline) {\n                                log.trace(\"HTTP connection detected.\", pipeline.channel());\n                                return pipeline\n                                        .addLast(\"HttpServerCodec\", new HttpServerCodec())\n                                        .addLast(\"HttpServerKeepAlive\", new HttpServerKeepAliveHandler())\n                                        .addLast(\"HttpObjectAggregator\", new HttpObjectAggregator(maxWebLength, true))\n                                        .addLast(\"HttpWebSocketUnificator\", baseWebSocketUnificator);\n                            }\n\n                            @Override\n                            //for hardware port we always expecting hardware and never app\n                            public ChannelPipeline buildAppPipeline(ChannelPipeline pipeline) {\n                                return buildHardwarePipeline(pipeline);\n                            }\n\n                            @Override\n                            public ChannelPipeline buildHardwarePipeline(ChannelPipeline pipeline) {\n                                log.trace(\"Blynk hardware plain protocol connection detected.\", pipeline.channel());\n                                return pipeline\n                                        .addFirst(\"H_IdleStateHandler\",\n                                                new IdleStateHandler(hardTimeoutSecs, 0, 0))\n                                        .addLast(\"H_ChannelState\", hardwareChannelStateHandler)\n                                        .addLast(\"H_MessageDecoder\", new MessageDecoder(holder.stats, holder.limits))\n                                        .addLast(\"H_MessageEncoder\", new MessageEncoder(holder.stats))\n                                        .addLast(\"H_Login\", hardwareLoginHandler)\n                                        .addLast(\"H_AlreadyLogged\", alreadyLoggedHandler);\n                            }\n                        }\n                );\n            }\n        };\n    }\n\n    @Override\n    public ChannelInitializer<SocketChannel> getChannelInitializer() {\n        return channelInitializer;\n    }\n\n    @Override\n    protected String getServerName() {\n        return \"HTTP API and WebSockets\";\n    }\n\n    @Override\n    public ChannelFuture close() {\n        System.out.println(\"Shutting down HTTP API and WebSockets server...\");\n        return super.close();\n    }\n\n}\n"
  },
  {
    "path": "server/launcher/src/main/java/cc/blynk/server/servers/hardware/MQTTHardwareServer.java",
    "content": "package cc.blynk.server.servers.hardware;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.common.handlers.AlreadyLoggedHandler;\nimport cc.blynk.server.hardware.handlers.hardware.HardwareChannelStateHandler;\nimport cc.blynk.server.hardware.handlers.hardware.mqtt.auth.MqttHardwareLoginHandler;\nimport cc.blynk.server.servers.BaseServer;\nimport io.netty.channel.ChannelFuture;\nimport io.netty.channel.ChannelInitializer;\nimport io.netty.channel.socket.SocketChannel;\nimport io.netty.handler.codec.mqtt.MqttDecoder;\nimport io.netty.handler.codec.mqtt.MqttEncoder;\nimport io.netty.handler.timeout.IdleStateHandler;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n */\npublic class MQTTHardwareServer extends BaseServer {\n\n    private final ChannelInitializer<SocketChannel> channelInitializer;\n\n    public MQTTHardwareServer(Holder holder) {\n        super(holder.props.getProperty(\"listen.address\"),\n                holder.props.getIntProperty(\"hardware.mqtt.port\"), holder.transportTypeHolder);\n\n        var hardTimeoutSecs = holder.limits.hardwareIdleTimeout;\n        var mqttHardwareLoginHandler = new MqttHardwareLoginHandler(holder);\n        var alreadyLoggedHandler = new AlreadyLoggedHandler();\n        var hardwareChannelStateHandler = new HardwareChannelStateHandler(holder);\n\n        channelInitializer = new ChannelInitializer<>() {\n            @Override\n            protected void initChannel(SocketChannel ch) {\n                ch.pipeline()\n                    .addLast(\"MqttIdleStateHandler\", new IdleStateHandler(hardTimeoutSecs, hardTimeoutSecs, 0))\n                    .addLast(hardwareChannelStateHandler)\n                    .addLast(new MqttDecoder())\n                    .addLast(MqttEncoder.INSTANCE)\n                    .addLast(mqttHardwareLoginHandler)\n                    .addLast(alreadyLoggedHandler);\n            }\n        };\n\n        log.debug(\"hard.socket.idle.timeout = {}\", hardTimeoutSecs);\n    }\n\n    @Override\n    public ChannelInitializer<SocketChannel> getChannelInitializer() {\n        return channelInitializer;\n    }\n\n    @Override\n    protected String getServerName() {\n        return \"Mqtt hardware\";\n    }\n\n    @Override\n    public ChannelFuture close() {\n        System.out.println(\"Shutting down Mqtt hardware server...\");\n        return super.close();\n    }\n\n}\n"
  },
  {
    "path": "server/launcher/src/main/java/cc/blynk/server/workers/CertificateRenewalWorker.java",
    "content": "package cc.blynk.server.workers;\n\nimport cc.blynk.server.SslContextHolder;\nimport cc.blynk.server.acme.AcmeClient;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.security.cert.CertificateException;\nimport java.security.cert.CertificateFactory;\nimport java.security.cert.X509Certificate;\nimport java.util.Date;\nimport java.util.concurrent.TimeUnit;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.05.17.\n */\npublic class CertificateRenewalWorker implements Runnable {\n\n    private static final Logger log = LogManager.getLogger(CertificateRenewalWorker.class);\n\n    private final SslContextHolder sslContextHolder;\n    private final static int renewBeforeDays = 21;\n\n    public CertificateRenewalWorker(SslContextHolder sslContextHolder) {\n        this.sslContextHolder = sslContextHolder;\n    }\n\n    private static long getDateDiff(Date expirationDate) {\n        long now = System.currentTimeMillis();\n        return TimeUnit.MILLISECONDS.toDays(expirationDate.getTime() - now);\n    }\n\n    private static X509Certificate readX509Certificate() throws IOException {\n        try (InputStream fis = new FileInputStream(AcmeClient.DOMAIN_CHAIN_FILE)) {\n            CertificateFactory certificateFactory = CertificateFactory.getInstance(\"X.509\");\n            return (X509Certificate) certificateFactory.generateCertificate(fis);\n        } catch (CertificateException ex) {\n            throw new IOException(ex);\n        }\n    }\n\n    @Override\n    public void run() {\n        try {\n            if (AcmeClient.DOMAIN_CHAIN_FILE.exists()) {\n                //stream closed inside utilities method\n                X509Certificate cert = readX509Certificate();\n\n                Date expirationDate = cert.getNotAfter();\n                long daysToExpire = getDateDiff(expirationDate);\n                log.info(\"Certificate expiration date is {}. Days left : {}\", expirationDate, daysToExpire);\n\n                if (daysToExpire <= renewBeforeDays) {\n                    renew();\n                }\n            } else {\n                renew();\n            }\n        } catch (Exception e) {\n            log.error(\"Error during certificate renewal.\", e);\n        }\n    }\n\n    private void renew() throws Exception {\n        log.warn(\"Trying to renew...\");\n        sslContextHolder.regenerate();\n        log.info(\"Success! The certificate for your domain has been renewed!\");\n    }\n\n}\n"
  },
  {
    "path": "server/launcher/src/main/java/cc/blynk/server/workers/HistoryGraphUnusedPinDataCleanerWorker.java",
    "content": "package cc.blynk.server.workers;\n\nimport cc.blynk.server.core.dao.ReportingDiskDao;\nimport cc.blynk.server.core.dao.UserDao;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.DataStream;\nimport cc.blynk.server.core.model.Profile;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Tag;\nimport cc.blynk.server.core.model.widgets.Target;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.outputs.graph.GraphDataStream;\nimport cc.blynk.server.core.model.widgets.outputs.graph.GraphGranularityType;\nimport cc.blynk.server.core.model.widgets.outputs.graph.Superchart;\nimport cc.blynk.server.core.model.widgets.ui.DeviceSelector;\nimport cc.blynk.server.core.model.widgets.ui.reporting.Report;\nimport cc.blynk.server.core.model.widgets.ui.reporting.ReportingWidget;\nimport cc.blynk.server.core.model.widgets.ui.reporting.source.ReportDataStream;\nimport cc.blynk.server.core.model.widgets.ui.reporting.source.ReportSource;\nimport cc.blynk.server.core.model.widgets.ui.tiles.DeviceTiles;\nimport cc.blynk.server.core.model.widgets.ui.tiles.TileTemplate;\nimport cc.blynk.server.internal.EmptyArraysUtil;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.util.HashSet;\nimport java.util.Set;\n\n/**\n * Daily job used to clean reporting data that is not used by the history graphs\n * but stored anyway on the disk.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 04.01.18.\n */\npublic class HistoryGraphUnusedPinDataCleanerWorker implements Runnable {\n\n    private static final Logger log = LogManager.getLogger(HistoryGraphUnusedPinDataCleanerWorker.class);\n\n    private final UserDao userDao;\n    private final ReportingDiskDao reportingDao;\n\n    private long lastStart;\n\n    public HistoryGraphUnusedPinDataCleanerWorker(UserDao userDao, ReportingDiskDao reportingDao) {\n        this.userDao = userDao;\n        this.reportingDao = reportingDao;\n        this.lastStart = System.currentTimeMillis();\n\n    }\n\n    @Override\n    public void run() {\n        try {\n            log.info(\"Start removing unused reporting data...\");\n\n            long now = System.currentTimeMillis();\n            int result = removeUnsedInHistoryGraphData();\n\n            lastStart = now;\n\n            log.info(\"Removed {} files. Time : {} ms.\", result, System.currentTimeMillis() - now);\n        } catch (Throwable t) {\n            log.error(\"Error removing unused reporting data.\", t);\n        }\n    }\n\n    private int removeUnsedInHistoryGraphData() {\n        int removedFilesCounter = 0;\n        Set<String> doNotRemovePaths = new HashSet<>();\n\n        for (User user : userDao.getUsers().values()) {\n            //we don't want to do a lot of work here,\n            //so we check only active profiles that actually write data\n            if (user.isUpdated(lastStart)) {\n                doNotRemovePaths.clear();\n                try {\n                    Profile profile = user.profile;\n                    for (DashBoard dashBoard : profile.dashBoards) {\n                        for (Widget widget : dashBoard.widgets) {\n                            if (widget instanceof DeviceTiles) {\n                                DeviceTiles deviceTiles = (DeviceTiles) widget;\n                                for (TileTemplate tileTemplate : deviceTiles.templates) {\n                                    for (Widget tilesWidget : tileTemplate.widgets) {\n                                        add(doNotRemovePaths, profile,\n                                                dashBoard, tilesWidget, tileTemplate.deviceIds);\n                                    }\n                                }\n                            } else {\n                                add(doNotRemovePaths, profile, dashBoard, widget, null);\n                            }\n                        }\n                    }\n\n                    removedFilesCounter += reportingDao.delete(user,\n                            reportingFile -> !doNotRemovePaths.contains(reportingFile.getFileName().toString()));\n                } catch (Exception e) {\n                    log.error(\"Error cleaning reporting record for user {}. {}\", user.email, e.getMessage());\n                }\n            }\n        }\n        return removedFilesCounter;\n    }\n\n    private static void add(Set<String> doNotRemovePaths, Profile profile,\n                            DashBoard dash, Widget widget, int[] deviceIds) {\n        if (widget instanceof Superchart) {\n            Superchart enhancedHistoryGraph = (Superchart) widget;\n            add(doNotRemovePaths, profile, dash, enhancedHistoryGraph, deviceIds);\n        } else if (widget instanceof ReportingWidget) {\n            //reports can't be assigned to device tiles so we ignore deviceIds parameter\n            ReportingWidget reportingWidget = (ReportingWidget) widget;\n            add(doNotRemovePaths, dash, reportingWidget);\n        }\n    }\n\n    private static void add(Set<String> doNotRemovePaths, DashBoard dash,\n                            ReportingWidget reportingWidget) {\n        for (Report report : reportingWidget.reports) {\n            for (ReportSource reportSource : report.reportSources) {\n                int[] deviceIds = reportSource.getDeviceIds();\n                for (ReportDataStream reportDataStream : reportSource.reportDataStreams) {\n                    for (int deviceId : deviceIds) {\n                        for (GraphGranularityType type : GraphGranularityType.getValues()) {\n                            String filename = ReportingDiskDao.generateFilename(dash.id,\n                                    deviceId,\n                                    reportDataStream.pinType, reportDataStream.pin, type);\n                            doNotRemovePaths.add(filename);\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    private static void add(Set<String> doNotRemovePaths, Profile profile,\n                            DashBoard dash, Superchart graph, int[] deviceIds) {\n        for (GraphDataStream graphDataStream : graph.dataStreams) {\n            if (graphDataStream != null && graphDataStream.dataStream != null && graphDataStream.dataStream.isValid()) {\n                DataStream dataStream = graphDataStream.dataStream;\n\n                int[] resultIds;\n                if (deviceIds == null) {\n                    Target target;\n                    int targetId = graphDataStream.targetId;\n                    if (targetId < Tag.START_TAG_ID) {\n                        target = profile.getDeviceById(dash, targetId);\n                    } else if (targetId < DeviceSelector.DEVICE_SELECTOR_STARTING_ID) {\n                        target = profile.getTagById(dash, targetId);\n                    } else {\n                        //means widget assigned to device selector widget.\n                        target = dash.getDeviceSelector(targetId);\n                    }\n                    if (target != null) {\n                        resultIds = target.getAssignedDeviceIds();\n                    } else {\n                        resultIds = EmptyArraysUtil.EMPTY_INTS;\n                    }\n                } else {\n                    resultIds = deviceIds;\n                }\n\n                for (int deviceId : resultIds) {\n                    for (GraphGranularityType type : GraphGranularityType.getValues()) {\n                        String filename = ReportingDiskDao.generateFilename(dash.id,\n                                deviceId,\n                                dataStream.pinType, dataStream.pin, type);\n                        doNotRemovePaths.add(filename);\n                    }\n                }\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/launcher/src/main/java/cc/blynk/server/workers/ProfileSaverWorker.java",
    "content": "package cc.blynk.server.workers;\n\nimport cc.blynk.server.core.dao.FileManager;\nimport cc.blynk.server.core.dao.UserDao;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.db.DBManager;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.io.Closeable;\nimport java.nio.file.Path;\nimport java.util.ArrayList;\n\n/**\n * Background thread that once a minute stores all user DB to disk in case profile was changed since last saving.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/12/2015.\n */\npublic class ProfileSaverWorker implements Runnable, Closeable {\n\n    private static final Logger log = LogManager.getLogger(ProfileSaverWorker.class);\n\n    //1 min\n    private final UserDao userDao;\n    private final FileManager fileManager;\n    private final DBManager dbManager;\n    private long lastStart;\n    private long backupTs;\n\n    public ProfileSaverWorker(UserDao userDao, FileManager fileManager, DBManager dbManager) {\n        this.userDao = userDao;\n        this.fileManager = fileManager;\n        this.dbManager = dbManager;\n        this.lastStart = System.currentTimeMillis();\n        this.backupTs = 0;\n    }\n\n    @Override\n    public void run() {\n        try {\n            log.debug(\"Starting saving user db.\");\n\n            final long now = System.currentTimeMillis();\n\n            ArrayList<User> users = saveModified();\n\n            dbManager.saveUsers(users);\n\n            //backup only for local mode\n            if (dbManager.dbIsNotEnabled() && users.size() > 0) {\n                archiveUser(now);\n            }\n\n            lastStart = now;\n\n            log.debug(\"Saving user db finished. Modified {} users.\", users.size());\n        } catch (Throwable t) {\n            log.error(\"Error saving users.\", t);\n        }\n    }\n\n    private void archiveUser(long now) {\n        if (now - backupTs > 86_400_000) {\n            //it is time for backup, once per day.\n            log.info(\"Backup for user DB started...\");\n            backupTs = now;\n            for (User user : userDao.users.values()) {\n                try {\n                    Path path = fileManager.generateBackupFileName(user.email, user.appName);\n                    JsonParser.writeUser(path.toFile(), user);\n                } catch (Exception e) {\n                    //ignore\n                }\n            }\n            log.info(\"Backup for user DB finished.\");\n        }\n    }\n\n    private ArrayList<User> saveModified() {\n        var users = new ArrayList<User>();\n\n        for (User user : userDao.getUsers().values()) {\n            if (user.isUpdated(lastStart)) {\n                try {\n                    fileManager.overrideUserFile(user);\n                    users.add(user);\n                } catch (Exception e) {\n                    log.error(\"Error saving : {}.\", user);\n                }\n            }\n        }\n\n        return users;\n    }\n\n    @Override\n    public void close() {\n        run();\n    }\n}\n"
  },
  {
    "path": "server/launcher/src/main/java/cc/blynk/server/workers/ReportingTruncateWorker.java",
    "content": "package cc.blynk.server.workers;\n\nimport cc.blynk.server.core.dao.CSVGenerator;\nimport cc.blynk.server.core.dao.ReportingDiskDao;\nimport cc.blynk.utils.FileUtils;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.nio.ByteBuffer;\nimport java.nio.file.DirectoryStream;\nimport java.nio.file.FileSystems;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.PathMatcher;\nimport java.nio.file.Paths;\nimport java.util.concurrent.TimeUnit;\n\nimport static cc.blynk.utils.ReportingUtil.REPORTING_RECORD_SIZE;\nimport static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 12.01.18.\n */\npublic class ReportingTruncateWorker implements Runnable {\n\n    private static final Logger log = LogManager.getLogger(ReportingTruncateWorker.class);\n\n    private final ReportingDiskDao reportingDao;\n    private final long exportExpirePeriod;\n    private final int maxRecordsCount;\n\n    public ReportingTruncateWorker(ReportingDiskDao reportingDao, int storeMinuteRecordDays, long storeReportCSVDays) {\n        //storing minute points only for 10 days\n        this.reportingDao = reportingDao;\n        this.maxRecordsCount = (int) TimeUnit.DAYS.toMinutes(storeMinuteRecordDays);\n        this.exportExpirePeriod = TimeUnit.DAYS.toMillis(storeReportCSVDays);\n    }\n\n    @Override\n    public void run() {\n        long now;\n\n        try {\n            now = System.currentTimeMillis();\n            int result = truncateOutdatedData();\n            log.info(\"Truncated {} files. Time : {} ms.\", result, System.currentTimeMillis() - now);\n        } catch (Throwable t) {\n            log.error(\"Error truncating unused reporting data.\", t);\n        }\n\n        try {\n            now = System.currentTimeMillis();\n            int result = deleteOldExportCsvFiles();\n            log.info(\"Removed {} old export files. Time : {} ms.\", result, System.currentTimeMillis() - now);\n        } catch (Throwable t) {\n            log.error(\"Error deleting outdated export files.\", t);\n        }\n    }\n\n    private int deleteOldExportCsvFiles() throws IOException {\n        long now = System.currentTimeMillis();\n        int counter = 0;\n        try (DirectoryStream<Path> csvFolder = Files.newDirectoryStream(Paths.get(FileUtils.CSV_DIR), \"*\")) {\n            for (Path csvFile : csvFolder) {\n                if (csvFile.getFileName().toString().endsWith(CSVGenerator.EXPORT_CSV_EXTENSION)\n                        && isOutdated(csvFile, now)) {\n                    counter++;\n                    Files.delete(csvFile);\n                }\n            }\n        }\n        return counter;\n    }\n\n    private boolean isOutdated(Path filePath, long now)  throws IOException {\n        long lastModified = FileUtils.getLastModified(filePath);\n        return lastModified + exportExpirePeriod < now;\n    }\n\n    private int truncateOutdatedData() throws Exception {\n        int truncatedFilesCounter = 0;\n\n        Path reportingFolderPath = Paths.get(reportingDao.dataFolder);\n        if (Files.notExists(reportingFolderPath)) {\n            return 0;\n        }\n\n        try (DirectoryStream<Path> reportingFolder = Files.newDirectoryStream(reportingFolderPath, \"*\")) {\n            for (Path userReportingDirectory : reportingFolder) {\n                if (Files.isDirectory(userReportingDirectory)) {\n                    int filesCounter = 0;\n                    try {\n                        try (DirectoryStream<Path> userReportingFolder = directoryStream(userReportingDirectory)) {\n                            for (Path userReportingFile : userReportingFolder) {\n                                filesCounter++;\n                                long fileSize = Files.size(userReportingFile);\n                                if (fileSize > maxRecordsCount * REPORTING_RECORD_SIZE) {\n                                    ByteBuffer userReportingData = FileUtils.read(userReportingFile, maxRecordsCount);\n                                    try (OutputStream os =\n                                                 Files.newOutputStream(userReportingFile, TRUNCATE_EXISTING)) {\n                                        os.write(userReportingData.array());\n                                    }\n                                    truncatedFilesCounter++;\n                                }\n                            }\n                        }\n                        if (filesCounter == 0) {\n                            Files.delete(userReportingDirectory);\n                        }\n                    } catch (Exception e) {\n                        log.error(\"Truncation failed for {}. Reason : {}.\", userReportingDirectory, e.getMessage());\n                    }\n                }\n            }\n        }\n        return truncatedFilesCounter;\n    }\n\n    private static final PathMatcher matcher = FileSystems.getDefault().getPathMatcher(\"glob:*_minute.bin\");\n    private static final DirectoryStream.Filter<Path> filter = entry -> matcher.matches(entry.getFileName());\n\n    //utility method to avoid allocation of PathMatcher\n    private DirectoryStream<Path> directoryStream(Path dir) throws IOException {\n        // create a matcher and return a filter that uses it.\n        return dir.getFileSystem().provider().newDirectoryStream(dir, filter);\n    }\n}\n"
  },
  {
    "path": "server/launcher/src/main/java/cc/blynk/server/workers/ReportingWorker.java",
    "content": "package cc.blynk.server.workers;\n\nimport cc.blynk.server.core.dao.ReportingDiskDao;\nimport cc.blynk.server.core.model.widgets.outputs.graph.GraphGranularityType;\nimport cc.blynk.server.core.reporting.average.AggregationKey;\nimport cc.blynk.server.core.reporting.average.AggregationValue;\nimport cc.blynk.server.db.ReportingDBManager;\nimport cc.blynk.utils.FileUtils;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.time.Instant;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Set;\n\nimport static cc.blynk.server.core.dao.ReportingDiskDao.generateFilename;\n\n/**\n * Worker that runs once a minute. During run - stores all aggregated reporting data\n * to disk. Also sends all data in batches to RDBMS in case DBManager was initialized.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 10.08.15.\n */\npublic class ReportingWorker implements Runnable {\n\n    private static final Logger log = LogManager.getLogger(ReportingWorker.class);\n\n    private final ReportingDiskDao reportingDao;\n    private final String reportingPath;\n    private final ReportingDBManager reportingDBManager;\n\n    public ReportingWorker(ReportingDiskDao reportingDao,\n                           String reportingPath, ReportingDBManager reportingDBManager) {\n        this.reportingDao = reportingDao;\n        this.reportingPath = reportingPath;\n        this.reportingDBManager = reportingDBManager;\n    }\n\n    @Override\n    public void run() {\n        try {\n            Map<AggregationKey, AggregationValue> removedKeysMinute =\n                    process(reportingDao.averageAggregator.getMinute(), GraphGranularityType.MINUTE);\n            Map<AggregationKey, AggregationValue> removedKeysHour =\n                    process(reportingDao.averageAggregator.getHourly(), GraphGranularityType.HOURLY);\n            Map<AggregationKey, AggregationValue> removedKeysDay =\n                    process(reportingDao.averageAggregator.getDaily(), GraphGranularityType.DAILY);\n\n            reportingDBManager.insertReporting(removedKeysMinute, GraphGranularityType.MINUTE);\n            reportingDBManager.insertReporting(removedKeysHour, GraphGranularityType.HOURLY);\n            reportingDBManager.insertReporting(removedKeysDay, GraphGranularityType.DAILY);\n\n            reportingDBManager.insertReportingRaw(reportingDao.rawDataProcessor.rawStorage);\n\n            reportingDBManager.cleanOldReportingRecords(Instant.now());\n        } catch (Exception e) {\n            log.error(\"Error during reporting job.\", e);\n        }\n    }\n\n    /**\n     * Iterates over all reporting entries that were created during last minute.\n     * And stores all entries one by one to disk.\n     *\n     * @param map - reporting entires that were created during last minute.\n     * @param type - type of reporting. Could be minute, hourly, daily.\n     * @return - returns list of reporting entries that were successfully flushed to disk.\n     */\n    private Map<AggregationKey, AggregationValue>  process(Map<AggregationKey, AggregationValue> map,\n                                                           GraphGranularityType type) {\n        if (map.size() == 0) {\n            return Collections.emptyMap();\n        }\n\n        Set<AggregationKey> aggregationKeySet = map.keySet();\n        AggregationKey[] keys = aggregationKeySet.toArray(new AggregationKey[0]);\n        Arrays.sort(keys, AggregationKey.AGGREGATION_KEY_COMPARATOR);\n\n        var removedKeys = new HashMap<AggregationKey, AggregationValue>();\n\n        long nowTruncatedToPeriod = System.currentTimeMillis() / type.period;\n        for (AggregationKey keyToRemove : keys) {\n            //if prev hour\n            if (keyToRemove.isOutdated(nowTruncatedToPeriod)) {\n                AggregationValue value = map.get(keyToRemove);\n\n                try {\n                    Path userReportFolder = Paths.get(reportingPath,\n                            FileUtils.getUserStorageDir(keyToRemove.getEmail(), keyToRemove.getAppName()));\n                    if (Files.notExists(userReportFolder)) {\n                        Files.createDirectories(userReportFolder);\n                    }\n\n                    String fileName = generateFilename(keyToRemove.getDashId(),\n                            keyToRemove.getDeviceId(), keyToRemove.getPinType(), keyToRemove.getPin(), type);\n                    Path filePath = Paths.get(userReportFolder.toString(), fileName);\n\n                    FileUtils.write(filePath, value.calcAverage(), keyToRemove.getTs(type));\n\n                    removedKeys.put(keyToRemove, value);\n                } catch (Exception ioe) {\n                    log.error(\"Error writing reporting file. Reason : {}\", ioe.getMessage());\n                } finally {\n                    map.remove(keyToRemove);\n                }\n            }\n        }\n\n        return removedKeys;\n    }\n\n}\n"
  },
  {
    "path": "server/launcher/src/main/java/cc/blynk/server/workers/ShutdownHookWorker.java",
    "content": "package cc.blynk.server.workers;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.servers.BaseServer;\n\nimport java.util.concurrent.ScheduledExecutorService;\n\n/**\n * Used to close and store all important info to disk.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 25.03.15.\n */\npublic class ShutdownHookWorker implements Runnable {\n\n    private final BaseServer[] servers;\n    private final Holder holder;\n    private final ProfileSaverWorker profileSaverWorker;\n    private final ScheduledExecutorService scheduler;\n\n    public ShutdownHookWorker(BaseServer[] servers, Holder holder,\n                              ScheduledExecutorService scheduler,\n                              ProfileSaverWorker profileSaverWorker) {\n        this.servers = servers;\n        this.holder = holder;\n        this.profileSaverWorker = profileSaverWorker;\n        this.scheduler = scheduler;\n    }\n\n    @Override\n    public void run() {\n        System.out.println(\"Catch shutdown hook.\");\n        System.out.println(\"Stopping servers...\");\n\n        for (var server : servers) {\n            try {\n                server.close().sync();\n            } catch (Throwable t) {\n                System.out.println(\"Error on server shutdown : \" + t.getCause());\n            }\n        }\n\n        System.out.println(\"Stopping scheduler...\");\n        scheduler.shutdown();\n\n        try {\n            holder.close();\n        } catch (Exception e) {\n            System.out.println(\"Error stopping holder...\");\n        }\n\n        System.out.println(\"Saving user profiles...\");\n        profileSaverWorker.close();\n\n        System.out.println(\"Done.\");\n    }\n\n}\n"
  },
  {
    "path": "server/launcher/src/main/java/cc/blynk/server/workers/StatsWorker.java",
    "content": "package cc.blynk.server.workers;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.core.BlockingIOProcessor;\nimport cc.blynk.server.core.dao.SessionDao;\nimport cc.blynk.server.core.dao.UserDao;\nimport cc.blynk.server.core.model.widgets.ui.reporting.ReportScheduler;\nimport cc.blynk.server.core.stats.GlobalStats;\nimport cc.blynk.server.core.stats.model.Stat;\nimport cc.blynk.server.db.ReportingDBManager;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\n/**\n * Worker responsible for logging current request rate,\n * methods invocation statistics, active channels count and\n * currently pending blocking tasks.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 18.04.15.\n */\npublic class StatsWorker implements Runnable {\n\n    private static final Logger log = LogManager.getLogger(StatsWorker.class);\n\n    private final GlobalStats stats;\n    private final SessionDao sessionDao;\n    private final UserDao userDao;\n    private final ReportingDBManager reportingDBManager;\n    private final String region;\n    private final BlockingIOProcessor blockingIOProcessor;\n    private final ReportScheduler reportScheduler;\n\n    public StatsWorker(Holder holder) {\n        this.stats = holder.stats;\n        this.sessionDao = holder.sessionDao;\n        this.userDao = holder.userDao;\n        this.reportingDBManager = holder.reportingDBManager;\n        this.region = holder.props.region;\n        this.blockingIOProcessor = holder.blockingIOProcessor;\n        this.reportScheduler = holder.reportScheduler;\n    }\n\n    @Override\n    public void run() {\n        try {\n            var stat = new Stat(sessionDao, userDao, blockingIOProcessor, stats, reportScheduler, true);\n            log.info(stat);\n            reportingDBManager.insertStat(this.region, stat);\n        } catch (Exception e) {\n            log.error(\"Error making stats.\", e);\n        }\n    }\n\n}\n\n"
  },
  {
    "path": "server/launcher/src/main/java/cc/blynk/utils/LoggerUtil.java",
    "content": "package cc.blynk.utils;\n\nimport cc.blynk.utils.properties.BaseProperties;\nimport org.apache.logging.log4j.Level;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.core.config.Configurator;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 28.09.15.\n */\npublic final class LoggerUtil {\n\n    private LoggerUtil() {\n    }\n\n    /**\n     * - Sets async logger for all logs\n     * - Defines logging folder\n     * - Sets logging level based on properties\n     */\n    public static void configureLogging(BaseProperties serverProperties) {\n        //required to make all loggers async with LMAX disruptor\n        System.setProperty(\"log4j2.contextSelector\", \"org.apache.logging.log4j.core.async.AsyncLoggerContextSelector\");\n        System.setProperty(\"AsyncLogger.RingBufferSize\",\n                serverProperties.getProperty(\"async.logger.ring.buffer.size\", \"2048\"));\n\n        //configurable folder for logs via property.\n        if (serverProperties.getProperty(\"logs.folder\") == null) {\n            System.out.println(\"logs.folder property is empty.\");\n            System.exit(1);\n        }\n        System.setProperty(\"logs.folder\", serverProperties.getProperty(\"logs.folder\"));\n\n        //changing log level based on properties file\n        changeLogLevel(serverProperties.getProperty(\"log.level\"));\n    }\n\n    /**\n     * Sets desired log level from properties.\n     *\n     * @param level - desired log level. error|info|debug|trace, etc.\n     */\n    private static void changeLogLevel(String level) {\n        Level newLevel = Level.valueOf(level);\n        Configurator.setAllLevels(LogManager.getRootLogger().getName(), newLevel);\n    }\n\n}\n"
  },
  {
    "path": "server/launcher/src/main/resources/log4j2.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Configuration>\n\n    <Appenders>\n\n        <RollingFile name=\"postgresDBLog\" fileName=\"${sys:logs.folder}/postgres.log\"\n              filePattern=\"${sys:logs.folder}/archive/postgres.log.%d{yyyy-MM-dd}\">\n            <PatternLayout>\n                <pattern>%d{HH:mm:ss.SSS} - %msg%n</pattern>\n            </PatternLayout>\n            <Policies>\n                <TimeBasedTriggeringPolicy/>\n            </Policies>\n        </RollingFile>\n\n        <RollingFile name=\"workersLog\" fileName=\"${sys:logs.folder}/worker.log\"\n                     filePattern=\"${sys:logs.folder}/archive/worker.log.%d{yyyy-MM-dd}\">\n            <PatternLayout>\n                <pattern>%d{HH:mm:ss.SSS} - %msg%n</pattern>\n            </PatternLayout>\n            <Policies>\n                <TimeBasedTriggeringPolicy/>\n            </Policies>\n        </RollingFile>\n\n        <RollingFile name=\"statsLog\" fileName=\"${sys:logs.folder}/stats.log\"\n                     filePattern=\"${sys:logs.folder}/archive/stats.log.%d{yyyy-MM-dd}\">\n            <PatternLayout>\n                <pattern>%msg%n</pattern>\n            </PatternLayout>\n            <Policies>\n                <TimeBasedTriggeringPolicy/>\n            </Policies>\n        </RollingFile>\n\n        <RollingFile name=\"userLog\" fileName=\"${sys:logs.folder}/blynk.log\"\n                     filePattern=\"${sys:logs.folder}/archive/blynk.log.%d{yyyy-MM-dd}\">\n            <PatternLayout>\n                <pattern>%d{HH:mm:ss.SSS} %-5level- %msg%n</pattern>\n            </PatternLayout>\n            <Policies>\n                <TimeBasedTriggeringPolicy/>\n            </Policies>\n        </RollingFile>\n\n    </Appenders>\n\n    <Loggers>\n\n        <Logger name=\"cc.blynk.server.workers\" level=\"debug\" additivity=\"false\">\n            <appender-ref ref=\"workersLog\"/>\n        </Logger>\n        <Logger name=\"cc.blynk.server.workers.StatsWorker\" level=\"debug\" additivity=\"false\">\n            <appender-ref ref=\"statsLog\"/>\n        </Logger>\n        <Logger name=\"cc.blynk.server.db\" level=\"debug\" additivity=\"false\">\n            <appender-ref ref=\"postgresDBLog\"/>\n        </Logger>\n        <Logger name=\"com.zaxxer.hikari\" level=\"OFF\" additivity=\"false\">\n        </Logger>\n\n        <Logger name=\"org.asynchttpclient.netty.channel\" level=\"OFF\" additivity=\"false\" />\n\n        <!-- turn off netty errors in debug mode for native library loading\n         https://github.com/blynkkk/blynk-server/issues/751 -->\n        <Logger name=\"io.netty\" level=\"INFO\" additivity=\"false\" />\n\n        <Root level=\"info\">\n            <AppenderRef ref=\"userLog\"/>\n        </Root>\n\n    </Loggers>\n</Configuration>"
  },
  {
    "path": "server/launcher/src/test/java/cc/blynk/server/workers/ProfileSaverWorkerTest.java",
    "content": "package cc.blynk.server.workers;\n\nimport cc.blynk.server.core.BlockingIOProcessor;\nimport cc.blynk.server.core.dao.FileManager;\nimport cc.blynk.server.core.dao.UserDao;\nimport cc.blynk.server.core.dao.UserKey;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.stats.GlobalStats;\nimport cc.blynk.server.db.DBManager;\nimport cc.blynk.utils.AppNameUtil;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport java.io.IOException;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ConcurrentMap;\n\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.verifyNoMoreInteractions;\nimport static org.mockito.Mockito.when;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 3/3/2015.\n */\n@RunWith(MockitoJUnitRunner.Silent.class)\npublic class ProfileSaverWorkerTest {\n\n    @Mock\n    private UserDao userDao;\n\n    @Mock\n    private FileManager fileManager;\n\n    @Mock\n    private GlobalStats stats;\n\n    private BlockingIOProcessor blockingIOProcessor = new BlockingIOProcessor(4, 1);\n\n    @Test\n    public void testCorrectProfilesAreSaved() throws IOException {\n        ProfileSaverWorker profileSaverWorker = new ProfileSaverWorker(userDao, fileManager, new DBManager(blockingIOProcessor, true));\n\n        User user1 = new User(\"1\", \"\", AppNameUtil.BLYNK, \"local\", \"127.0.0.1\", false, false);\n        User user2 = new User(\"2\", \"\", AppNameUtil.BLYNK, \"local\", \"127.0.0.1\", false, false);\n        User user3 = new User(\"3\", \"\", AppNameUtil.BLYNK, \"local\", \"127.0.0.1\", false, false);\n        User user4 = new User(\"4\", \"\", AppNameUtil.BLYNK, \"local\", \"127.0.0.1\", false, false);\n\n        ConcurrentMap<UserKey, User> userMap = new ConcurrentHashMap<>();\n        userMap.put(new UserKey(user1), user1);\n        userMap.put(new UserKey(user2), user2);\n        userMap.put(new UserKey(user3), user3);\n        userMap.put(new UserKey(user4), user4);\n\n        when(userDao.getUsers()).thenReturn(userMap);\n        profileSaverWorker.run();\n\n        verify(fileManager, times(4)).overrideUserFile(any());\n        verify(fileManager).overrideUserFile(user1);\n        verify(fileManager).overrideUserFile(user2);\n        verify(fileManager).overrideUserFile(user3);\n        verify(fileManager).overrideUserFile(user4);\n    }\n\n    @Test\n    public void testNoProfileChanges() throws Exception {\n        User user1 = new User(\"1\", \"\", AppNameUtil.BLYNK, \"local\", \"127.0.0.1\", false, false);\n        User user2 = new User(\"2\", \"\", AppNameUtil.BLYNK, \"local\", \"127.0.0.1\", false, false);\n        User user3 = new User(\"3\", \"\", AppNameUtil.BLYNK, \"local\", \"127.0.0.1\", false, false);\n        User user4 = new User(\"4\", \"\", AppNameUtil.BLYNK, \"local\", \"127.0.0.1\", false, false);\n\n        Map<UserKey, User> userMap = new HashMap<>();\n        userMap.put(new UserKey(\"1\", AppNameUtil.BLYNK), user1);\n        userMap.put(new UserKey(\"2\", AppNameUtil.BLYNK), user2);\n        userMap.put(new UserKey(\"3\", AppNameUtil.BLYNK), user3);\n        userMap.put(new UserKey(\"4\", AppNameUtil.BLYNK), user4);\n\n        Thread.sleep(1);\n\n        ProfileSaverWorker profileSaverWorker = new ProfileSaverWorker(userDao, fileManager, new DBManager(blockingIOProcessor, true));\n\n        when(userDao.getUsers()).thenReturn(userMap);\n        profileSaverWorker.run();\n\n        verifyNoMoreInteractions(fileManager);\n    }\n\n}\n"
  },
  {
    "path": "server/launcher/src/test/java/cc/blynk/server/workers/ReportingWorkerTest.java",
    "content": "package cc.blynk.server.workers;\n\nimport cc.blynk.server.core.BlockingIOProcessor;\nimport cc.blynk.server.core.dao.ReportingDiskDao;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.widgets.outputs.graph.GraphGranularityType;\nimport cc.blynk.server.core.reporting.average.AggregationKey;\nimport cc.blynk.server.core.reporting.average.AggregationValue;\nimport cc.blynk.server.core.reporting.average.AverageAggregatorProcessor;\nimport cc.blynk.server.db.ReportingDBManager;\nimport cc.blynk.utils.AppNameUtil;\nimport cc.blynk.utils.properties.ServerProperties;\nimport org.apache.commons.io.FileUtils;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport java.io.IOException;\nimport java.nio.ByteBuffer;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.text.ParseException;\nimport java.text.SimpleDateFormat;\nimport java.util.Date;\nimport java.util.concurrent.ConcurrentHashMap;\n\nimport static cc.blynk.server.core.dao.ReportingDiskDao.generateFilename;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertFalse;\nimport static org.junit.Assert.assertNotNull;\nimport static org.junit.Assert.assertTrue;\nimport static org.mockito.Mockito.when;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 11.08.15.\n */\n@RunWith(MockitoJUnitRunner.Silent.class)\npublic class ReportingWorkerTest {\n\n    private final static Logger log = LogManager.getLogger(ReportingWorkerTest.class);\n\n    private final String reportingFolder = Paths.get(System.getProperty(\"java.io.tmpdir\"), \"data\").toString();\n\n    @Mock\n    public AverageAggregatorProcessor averageAggregator;\n\n    public ReportingDiskDao reportingDaoMock;\n\n    @Mock\n    public ServerProperties properties;\n\n    private BlockingIOProcessor blockingIOProcessor;\n\n    @Before\n    public void cleanup() throws IOException {\n        blockingIOProcessor = new BlockingIOProcessor(4, 1);\n        Path dataFolder1 = Paths.get(reportingFolder, \"test\");\n        FileUtils.deleteDirectory(dataFolder1.toFile());\n        createReportingFolder(reportingFolder, \"test\");\n\n        Path dataFolder2 = Paths.get(reportingFolder, \"test2\");\n        FileUtils.deleteDirectory(dataFolder2.toFile());\n        createReportingFolder(reportingFolder, \"test2\");\n\n        reportingDaoMock = new ReportingDiskDao(reportingFolder, averageAggregator, true);\n    }\n\n    private static void createReportingFolder(String reportingFolder, String email) {\n        Path reportingPath = Paths.get(reportingFolder, email);\n        if (Files.notExists(reportingPath)) {\n            try {\n                Files.createDirectories(reportingPath);\n            } catch (IOException ioe) {\n                log.error(\"Error creating report folder. {}\", reportingPath);\n            }\n        }\n    }\n\n    @Test\n    public void testFailure() {\n        User user = new User();\n        user.email = \"test\";\n        user.appName = AppNameUtil.BLYNK;\n        ReportingWorker reportingWorker = new ReportingWorker(reportingDaoMock,\n                reportingFolder, new ReportingDBManager(blockingIOProcessor, true));\n\n        ConcurrentHashMap<AggregationKey, AggregationValue> map = new ConcurrentHashMap<>();\n\n        long ts = getTS() / AverageAggregatorProcessor.HOUR;\n\n        AggregationKey aggregationKey = new AggregationKey(\"ddd\\0+123@gmail.com\",\n                AppNameUtil.BLYNK, 1, 0, PinType.ANALOG, (short) 1, ts);\n        AggregationValue aggregationValue = new AggregationValue();\n        aggregationValue.update(100);\n\n        map.put(aggregationKey, aggregationValue);\n\n        when(averageAggregator.getMinute()).thenReturn(map);\n\n        reportingWorker.run();\n        assertTrue(map.isEmpty());\n    }\n\n    @Test\n    public void testStore() {\n        User user = new User();\n        user.email = \"test\";\n        user.appName = AppNameUtil.BLYNK;\n        ReportingWorker reportingWorker = new ReportingWorker(reportingDaoMock,\n                reportingFolder, new ReportingDBManager(blockingIOProcessor, true));\n\n        ConcurrentHashMap<AggregationKey, AggregationValue> map = new ConcurrentHashMap<>();\n\n        long ts = getTS() / AverageAggregatorProcessor.HOUR;\n\n        AggregationKey aggregationKey = new AggregationKey(\"test\", AppNameUtil.BLYNK, 1, 0, PinType.ANALOG, (short) 1, ts);\n        AggregationValue aggregationValue = new AggregationValue();\n        aggregationValue.update(100);\n        AggregationKey aggregationKey2 = new AggregationKey(\"test\", AppNameUtil.BLYNK, 1, 0, PinType.ANALOG, (short) 1, ts - 1);\n        AggregationValue aggregationValue2 = new AggregationValue();\n        aggregationValue2.update(150.54);\n        AggregationKey aggregationKey3 = new AggregationKey(\"test2\", AppNameUtil.BLYNK, 2, 0, PinType.ANALOG, (short) 2, ts);\n        AggregationValue aggregationValue3 = new AggregationValue();\n        aggregationValue3.update(200);\n\n        map.put(aggregationKey, aggregationValue);\n        map.put(aggregationKey2, aggregationValue2);\n        map.put(aggregationKey3, aggregationValue3);\n\n        when(averageAggregator.getMinute()).thenReturn(new ConcurrentHashMap<>());\n        when(averageAggregator.getHourly()).thenReturn(map);\n        when(averageAggregator.getDaily()).thenReturn(new ConcurrentHashMap<>());\n\n        reportingWorker.run();\n\n        assertTrue(Files.exists(Paths.get(reportingFolder, \"test\",\n                generateFilename(1, 0, PinType.ANALOG, (short) 1, GraphGranularityType.HOURLY))));\n        assertTrue(Files.exists(Paths.get(reportingFolder, \"test2\",\n                generateFilename(2, 0, PinType.ANALOG, (short) 2, GraphGranularityType.HOURLY))));\n\n        assertTrue(map.isEmpty());\n\n        ByteBuffer data = reportingDaoMock.getByteBufferFromDisk(user, 1, 0, PinType.ANALOG, (short) 1, 2, GraphGranularityType.HOURLY, 0);\n        assertNotNull(data);\n        assertEquals(32, data.capacity());\n\n        assertEquals(150.54, data.getDouble(), 0.001);\n        assertEquals((ts - 1) * AverageAggregatorProcessor.HOUR, data.getLong());\n\n        assertEquals(100.0, data.getDouble(), 0.001);\n        assertEquals(ts * AverageAggregatorProcessor.HOUR, data.getLong());\n\n        User user2 = new User();\n        user2.email = \"test2\";\n        user2.appName = AppNameUtil.BLYNK;\n        data = reportingDaoMock.getByteBufferFromDisk(user2, 2, 0, PinType.ANALOG, (short) 2, 1, GraphGranularityType.HOURLY, 0);\n        assertNotNull(data);\n        assertEquals(16, data.capacity());\n        assertEquals(200.0, data.getDouble(), 0.001);\n        assertEquals(ts * AverageAggregatorProcessor.HOUR, data.getLong());\n    }\n\n    @Test\n    public void testStore2() {\n        ReportingWorker reportingWorker = new ReportingWorker(reportingDaoMock,\n                reportingFolder, new ReportingDBManager(blockingIOProcessor, true));\n\n        ConcurrentHashMap<AggregationKey, AggregationValue> map = new ConcurrentHashMap<>();\n\n        long ts = getTS() / AverageAggregatorProcessor.HOUR;\n\n        AggregationKey aggregationKey = new AggregationKey(\"test\", AppNameUtil.BLYNK, 1, 0, PinType.ANALOG, (short) 1, ts);\n        AggregationValue aggregationValue = new AggregationValue();\n        aggregationValue.update(100);\n        AggregationKey aggregationKey2 = new AggregationKey(\"test\", AppNameUtil.BLYNK, 1, 0, PinType.ANALOG, (short) 1, ts - 1);\n        AggregationValue aggregationValue2 = new AggregationValue();\n        aggregationValue2.update(150.54);\n        AggregationKey aggregationKey3 = new AggregationKey(\"test\", AppNameUtil.BLYNK, 1, 0, PinType.ANALOG, (short) 1, ts - 2);\n        AggregationValue aggregationValue3 = new AggregationValue();\n        aggregationValue3.update(200);\n\n        map.put(aggregationKey, aggregationValue);\n        map.put(aggregationKey2, aggregationValue2);\n        map.put(aggregationKey3, aggregationValue3);\n\n        when(averageAggregator.getMinute()).thenReturn(new ConcurrentHashMap<>());\n        when(averageAggregator.getHourly()).thenReturn(map);\n        when(averageAggregator.getDaily()).thenReturn(new ConcurrentHashMap<>());\n\n        reportingWorker.run();\n\n        assertTrue(Files.exists(Paths.get(reportingFolder, \"test\",\n                generateFilename(1, 0, PinType.ANALOG, (short) 1, GraphGranularityType.HOURLY))));\n\n        assertTrue(map.isEmpty());\n\n        User user = new User();\n        user.email = \"test\";\n        user.appName = AppNameUtil.BLYNK;\n\n        //take less\n        ByteBuffer data = reportingDaoMock.getByteBufferFromDisk(user, 1, 0, PinType.ANALOG, (short) 1, 1, GraphGranularityType.HOURLY, 0);\n        assertNotNull(data);\n        assertEquals(16, data.capacity());\n\n        assertEquals(100.0, data.getDouble(), 0.001);\n        assertEquals(ts * AverageAggregatorProcessor.HOUR, data.getLong());\n\n\n        //take more\n        data = reportingDaoMock.getByteBufferFromDisk(user, 1, 0, PinType.ANALOG, (short) 1, 24, GraphGranularityType.HOURLY, 0);\n        assertNotNull(data);\n        assertEquals(48, data.capacity());\n\n        assertEquals(200.0, data.getDouble(), 0.001);\n        assertEquals((ts - 2) * AverageAggregatorProcessor.HOUR, data.getLong());\n\n        assertEquals(150.54, data.getDouble(), 0.001);\n        assertEquals((ts - 1) * AverageAggregatorProcessor.HOUR, data.getLong());\n\n        assertEquals(100.0, data.getDouble(), 0.001);\n        assertEquals(ts * AverageAggregatorProcessor.HOUR, data.getLong());\n    }\n\n\n    @Test\n    public void testDeleteCommand() {\n        ReportingWorker reportingWorker = new ReportingWorker(reportingDaoMock,\n                reportingFolder, new ReportingDBManager(blockingIOProcessor, true));\n\n        ConcurrentHashMap<AggregationKey, AggregationValue> map = new ConcurrentHashMap<>();\n\n        long ts = getTS() / AverageAggregatorProcessor.HOUR;\n\n        AggregationKey aggregationKey = new AggregationKey(\"test\", AppNameUtil.BLYNK, 1, 0, PinType.ANALOG, (short) 1, ts);\n        AggregationValue aggregationValue = new AggregationValue();\n        aggregationValue.update(100);\n        AggregationKey aggregationKey2 = new AggregationKey(\"test\", AppNameUtil.BLYNK, 1, 0, PinType.ANALOG, (short) 1, ts - 1);\n        AggregationValue aggregationValue2 = new AggregationValue();\n        aggregationValue2.update(150.54);\n        AggregationKey aggregationKey3 = new AggregationKey(\"test2\", AppNameUtil.BLYNK, 2, 0, PinType.ANALOG, (short) 2, ts);\n        AggregationValue aggregationValue3 = new AggregationValue();\n        aggregationValue3.update(200);\n\n        map.put(aggregationKey, aggregationValue);\n        map.put(aggregationKey2, aggregationValue2);\n        map.put(aggregationKey3, aggregationValue3);\n\n        when(averageAggregator.getMinute()).thenReturn(new ConcurrentHashMap<>());\n        when(averageAggregator.getHourly()).thenReturn(map);\n        when(averageAggregator.getDaily()).thenReturn(new ConcurrentHashMap<>());\n        when(properties.getProperty(\"data.folder\")).thenReturn(System.getProperty(\"java.io.tmpdir\"));\n\n        reportingWorker.run();\n\n        assertTrue(Files.exists(Paths.get(reportingFolder, \"test\", generateFilename(1, 0, PinType.ANALOG, (short) 1, GraphGranularityType.HOURLY))));\n        assertTrue(Files.exists(Paths.get(reportingFolder, \"test2\", generateFilename(2, 0, PinType.ANALOG, (short) 2, GraphGranularityType.HOURLY))));\n\n        User user = new User();\n        user.email = \"test\";\n        user.appName = AppNameUtil.BLYNK;\n\n        new ReportingDiskDao(reportingFolder, true).delete(user, 1, 0, PinType.ANALOG, (short) 1);\n        assertFalse(Files.exists(Paths.get(reportingFolder, \"test\", generateFilename(1, 0, PinType.ANALOG, (short) 1, GraphGranularityType.HOURLY))));\n    }\n\n    private long getTS() {\n        SimpleDateFormat formatter = new SimpleDateFormat(\"MMM dd, yyyy HH:mm:ss\");\n        String dateInString = \"Aug 10, 2015 12:10:56\";\n\n        try {\n\n            Date date = formatter.parse(dateInString);\n            return date.getTime();\n        } catch (ParseException e) {\n            e.printStackTrace();\n        }\n\n        return 0;\n    }\n\n}\n"
  },
  {
    "path": "server/notifications/email/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <artifactId>notifications</artifactId>\n        <groupId>cc.blynk.server.notifications</groupId>\n        <version>0.41.17-SNAPSHOT</version>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n\n    <artifactId>email</artifactId>\n\n    <dependencies>\n\n        <dependency>\n            <groupId>com.sun.mail</groupId>\n            <artifactId>javax.mail</artifactId>\n            <version>${javax.mail.version}</version>\n            <exclusions>\n                <exclusion>\n                    <groupId>javax.activation</groupId>\n                    <artifactId>activation</artifactId>\n                </exclusion>\n            </exclusions>\n        </dependency>\n\n        <dependency>\n            <groupId>com.sun.activation</groupId>\n            <artifactId>javax.activation</artifactId>\n            <version>${javax.activation.version}</version>\n        </dependency>\n\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n            <version>${jackson-databind.version}</version>\n        </dependency>\n\n        <dependency>\n            <groupId>com.github.kenglxn.qrgen</groupId>\n            <artifactId>javase</artifactId>\n            <version>${qrgen.version}</version>\n        </dependency>\n\n        <dependency>\n            <groupId>cc.blynk.utils</groupId>\n            <artifactId>utils</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n\n    </dependencies>\n\n</project>"
  },
  {
    "path": "server/notifications/email/src/main/java/cc/blynk/server/notifications/mail/GMailClient.java",
    "content": "package cc.blynk.server.notifications.mail;\n\nimport cc.blynk.utils.properties.MailProperties;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport javax.activation.DataHandler;\nimport javax.mail.Message;\nimport javax.mail.Multipart;\nimport javax.mail.PasswordAuthentication;\nimport javax.mail.Session;\nimport javax.mail.Transport;\nimport javax.mail.internet.AddressException;\nimport javax.mail.internet.InternetAddress;\nimport javax.mail.internet.MimeBodyPart;\nimport javax.mail.internet.MimeMessage;\nimport javax.mail.internet.MimeMultipart;\nimport javax.mail.util.ByteArrayDataSource;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 14.09.16.\n */\npublic class GMailClient implements MailClient {\n\n    private static final Logger log = LogManager.getLogger(MailWrapper.class);\n\n    private final Session session;\n    private final InternetAddress from;\n\n    GMailClient(MailProperties mailProperties) {\n        String username = mailProperties.getSMTPUsername();\n        String password = mailProperties.getSMTPPassword();\n\n        log.info(\"Initializing gmail smtp mail transport. Username : {}. SMTP host : {}:{}\",\n                username, mailProperties.getSMTPHost(), mailProperties.getSMTPort());\n\n        this.session = Session.getInstance(mailProperties, new javax.mail.Authenticator() {\n            protected PasswordAuthentication getPasswordAuthentication() {\n                return new PasswordAuthentication(username, password);\n            }\n        });\n        try {\n            this.from = new InternetAddress(username);\n        } catch (AddressException e) {\n            throw new RuntimeException(\"Error initializing MailWrapper.\" + e.getMessage());\n        }\n    }\n\n    @Override\n    public void sendText(String to, String subj, String body) throws Exception {\n        send(to, subj, body, TEXT_PLAIN_CHARSET_UTF_8);\n    }\n\n    @Override\n    public void sendHtml(String to, String subj, String body) throws Exception {\n        send(to, subj, body, TEXT_HTML_CHARSET_UTF_8);\n    }\n\n    @Override\n    public void sendHtmlWithAttachment(String to, String subj, String body,\n                                       QrHolder[] attachmentData) throws Exception {\n        MimeMessage message = new MimeMessage(session);\n        message.setFrom(from);\n        message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to));\n        message.setSubject(subj, \"UTF-8\");\n\n        Multipart multipart = new MimeMultipart();\n\n        MimeBodyPart bodyMessagePart = new MimeBodyPart();\n        bodyMessagePart.setContent(body, TEXT_HTML_CHARSET_UTF_8);\n\n        multipart.addBodyPart(bodyMessagePart);\n\n        attachQRs(multipart, attachmentData);\n        attachCSV(multipart, attachmentData);\n\n        message.setContent(multipart);\n\n        Transport.send(message);\n\n        log.trace(\"Mail to {} was sent. Subj : {}, body : {}\", to, subj, body);\n    }\n\n    private void attachCSV(Multipart multipart, QrHolder[] attachmentData) throws Exception {\n        StringBuilder sb = new StringBuilder();\n        for (QrHolder qrHolder : attachmentData) {\n            sb.append(qrHolder.token)\n            .append(\",\")\n            .append(qrHolder.deviceId)\n            .append(\",\")\n            .append(qrHolder.dashId)\n            .append(\"\\n\");\n        }\n        MimeBodyPart attachmentsPart = new MimeBodyPart();\n        ByteArrayDataSource source = new ByteArrayDataSource(sb.toString(), \"text/csv\");\n        attachmentsPart.setDataHandler(new DataHandler(source));\n        attachmentsPart.setFileName(\"tokens.csv\");\n\n        multipart.addBodyPart(attachmentsPart);\n    }\n\n    private void attachQRs(Multipart multipart, QrHolder[] attachmentData) throws Exception {\n        for (QrHolder qrHolder : attachmentData) {\n            MimeBodyPart attachmentsPart = new MimeBodyPart();\n            ByteArrayDataSource source = new ByteArrayDataSource(qrHolder.data, \"image/jpeg\");\n            attachmentsPart.setDataHandler(new DataHandler(source));\n            attachmentsPart.setFileName(qrHolder.makeQRFilename());\n            multipart.addBodyPart(attachmentsPart);\n        }\n    }\n\n    private void send(String to, String subj, String body, String contentType) throws Exception {\n        MimeMessage message = new MimeMessage(session);\n        message.setFrom(from);\n        message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to));\n        message.setSubject(subj, \"UTF-8\");\n        message.setContent(body, contentType);\n\n        Transport.send(message);\n        log.trace(\"Mail to {} was sent. Subj : {}, body : {}\", to, subj, body);\n    }\n\n}\n"
  },
  {
    "path": "server/notifications/email/src/main/java/cc/blynk/server/notifications/mail/MailClient.java",
    "content": "package cc.blynk.server.notifications.mail;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 14.09.16.\n */\npublic interface MailClient {\n\n    String TEXT_PLAIN_CHARSET_UTF_8 = \"text/plain; charset=UTF-8\";\n    String TEXT_HTML_CHARSET_UTF_8 = \"text/html; charset=UTF-8\";\n\n    void sendText(String to, String subj, String body) throws Exception;\n\n    void sendHtml(String to, String subj, String body) throws Exception;\n\n    void sendHtmlWithAttachment(String to, String subj, String body, QrHolder[] attachments) throws Exception;\n\n}\n"
  },
  {
    "path": "server/notifications/email/src/main/java/cc/blynk/server/notifications/mail/MailWrapper.java",
    "content": "package cc.blynk.server.notifications.mail;\n\nimport cc.blynk.utils.FileLoaderUtil;\nimport cc.blynk.utils.properties.MailProperties;\nimport cc.blynk.utils.properties.Placeholders;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 06.04.15.\n */\npublic class MailWrapper {\n\n    private final MailClient client;\n    private final String reportBody;\n    private final String productName;\n\n    public MailWrapper(MailProperties mailProperties, String productName) {\n        String host = mailProperties.getProperty(\"mail.smtp.host\");\n        if (host != null) {\n            mailProperties.put(\"mail.smtp.ssl.trust\", host);\n        }\n        if (host != null && (host.endsWith(\"sparkpostmail.com\") || host.endsWith(\"amazonaws.com\"))) {\n            // Amazon AWS Simple Email Service uses an account (mail.from) distinct from the username,\n            // which is just like SparkPost.\n            client = new ThirdPartyMailClient(mailProperties, productName);\n        } else {\n            client = new GMailClient(mailProperties);\n        }\n        this.reportBody = FileLoaderUtil.readReportEmailTemplate();\n        this.productName = productName;\n    }\n\n    public void sendReportEmail(String to,\n                                String subj,\n                                String downloadUrl,\n                                String dynamicSection) throws Exception  {\n        String body = reportBody\n                .replace(Placeholders.DOWNLOAD_URL, downloadUrl)\n                .replace(Placeholders.DYNAMIC_SECTION, dynamicSection)\n                .replace(Placeholders.PRODUCT_NAME, productName);\n        sendHtml(to, subj, body);\n    }\n\n    public void sendText(String to, String subj, String body) throws Exception {\n        client.sendText(to, subj, body);\n    }\n\n    public void sendHtml(String to, String subj, String body) throws Exception {\n        client.sendHtml(to, subj, body);\n    }\n\n    public void sendWithAttachment(String to, String subj, String body, QrHolder attachment) throws Exception {\n        client.sendHtmlWithAttachment(to, subj, body, new QrHolder[] {attachment});\n    }\n\n    public void sendWithAttachment(String to, String subj, String body, QrHolder[] attachments) throws Exception {\n        client.sendHtmlWithAttachment(to, subj, body, attachments);\n    }\n\n}\n"
  },
  {
    "path": "server/notifications/email/src/main/java/cc/blynk/server/notifications/mail/QrHolder.java",
    "content": "package cc.blynk.server.notifications.mail;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.03.17.\n */\npublic class QrHolder {\n\n    public final int dashId;\n\n    public final int deviceId;\n\n    public final String deviceName;\n\n    public final String token;\n\n    public final byte[] data;\n\n    public QrHolder(int dashId, int deviceId, String deviceName, String token, byte[] data) {\n        this.dashId = dashId;\n        this.deviceId = deviceId;\n        this.deviceName = deviceName;\n        this.token = token;\n        this.data = data;\n    }\n\n    String makeQRFilename() {\n        return token + \"_\" + dashId + \"_\" + deviceId + \".jpg\";\n    }\n\n    public void attach(StringBuilder sb) {\n        sb.append(\"<br>\")\n                .append(deviceName)\n                .append(\": \")\n                .append(token);\n    }\n\n    //for tests only\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (!(o instanceof QrHolder)) {\n            return false;\n        }\n\n        QrHolder qrHolder = (QrHolder) o;\n\n        if (dashId != qrHolder.dashId) {\n            return false;\n        }\n        if (deviceId != qrHolder.deviceId) {\n            return false;\n        }\n        return !(token != null ? !token.equals(qrHolder.token) : qrHolder.token != null);\n\n    }\n\n    @Override\n    public int hashCode() {\n        int result = dashId;\n        result = 31 * result + deviceId;\n        result = 31 * result + (token != null ? token.hashCode() : 0);\n        return result;\n    }\n}\n"
  },
  {
    "path": "server/notifications/email/src/main/java/cc/blynk/server/notifications/mail/ResetQrHolder.java",
    "content": "package cc.blynk.server.notifications.mail;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 21.03.17.\n */\npublic class ResetQrHolder extends QrHolder {\n\n    public ResetQrHolder(String token, byte[] data) {\n        super(-1, -1, null, token, data);\n    }\n\n    @Override\n    String makeQRFilename() {\n        return token;\n    }\n\n}\n"
  },
  {
    "path": "server/notifications/email/src/main/java/cc/blynk/server/notifications/mail/ThirdPartyMailClient.java",
    "content": "package cc.blynk.server.notifications.mail;\n\nimport cc.blynk.utils.properties.MailProperties;\nimport cc.blynk.utils.properties.Placeholders;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport javax.activation.DataHandler;\nimport javax.mail.Message;\nimport javax.mail.Multipart;\nimport javax.mail.Session;\nimport javax.mail.Transport;\nimport javax.mail.internet.AddressException;\nimport javax.mail.internet.InternetAddress;\nimport javax.mail.internet.MimeBodyPart;\nimport javax.mail.internet.MimeMessage;\nimport javax.mail.internet.MimeMultipart;\nimport javax.mail.util.ByteArrayDataSource;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 14.09.16.\n */\npublic class ThirdPartyMailClient implements MailClient {\n\n    private static final Logger log = LogManager.getLogger(ThirdPartyMailClient.class);\n\n    private final Session session;\n    private final InternetAddress from;\n    private final String host;\n    private final String username;\n    private final String password;\n\n    ThirdPartyMailClient(MailProperties mailProperties, String productName) {\n        this.username = mailProperties.getSMTPUsername();\n        this.password = mailProperties.getSMTPPassword();\n        this.host = mailProperties.getSMTPHost();\n\n        log.info(\"Initializing SparkPost smtp mail transport. Username : {}. SMTP host : {}:{}\",\n                username, host, mailProperties.getSMTPort());\n\n        this.session = Session.getInstance(mailProperties);\n        try {\n            String mailFrom = mailProperties.getProperty(\"mail.from\");\n            if (mailFrom != null) {\n                mailFrom = mailFrom.replace(Placeholders.PRODUCT_NAME, productName);\n            }\n            this.from = new InternetAddress(mailFrom);\n        } catch (AddressException e) {\n            throw new RuntimeException(\"Error initializing MailWrapper.\");\n        }\n    }\n\n    @Override\n    public void sendText(String to, String subj, String body) throws Exception {\n        send(to, subj, body, TEXT_PLAIN_CHARSET_UTF_8);\n    }\n\n    @Override\n    public void sendHtml(String to, String subj, String body) throws Exception {\n        send(to, subj, body, TEXT_HTML_CHARSET_UTF_8);\n    }\n\n    private void send(String to, String subj, String body, String contentType) throws Exception {\n        MimeMessage message = new MimeMessage(session);\n        message.setFrom(from);\n        message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to));\n        message.setSubject(subj, \"UTF-8\");\n        message.setContent(body, contentType);\n\n        try (Transport transport = session.getTransport()) {\n            transport.connect(host, username, password);\n            transport.sendMessage(message, message.getAllRecipients());\n        }\n\n        log.debug(\"Mail sent to {}. Subj: {}\", to, subj);\n        log.trace(\"Mail body: {}\", body);\n    }\n\n    @Override\n    public void sendHtmlWithAttachment(String to, String subj, String body, QrHolder[] attachments) throws Exception {\n        MimeMessage message = new MimeMessage(session);\n        message.setFrom(from);\n        message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to));\n        message.setSubject(subj, \"UTF-8\");\n\n        Multipart multipart = new MimeMultipart();\n\n        MimeBodyPart bodyMessagePart = new MimeBodyPart();\n        bodyMessagePart.setContent(body, TEXT_HTML_CHARSET_UTF_8);\n        multipart.addBodyPart(bodyMessagePart);\n\n        for (QrHolder qrHolder : attachments) {\n            MimeBodyPart attachmentsPart = new MimeBodyPart();\n            attachmentsPart.setDataHandler(new DataHandler(new ByteArrayDataSource(qrHolder.data, \"image/jpeg\")));\n            attachmentsPart.setFileName(qrHolder.makeQRFilename());\n            multipart.addBodyPart(attachmentsPart);\n        }\n\n        message.setContent(multipart);\n\n        try (Transport transport = session.getTransport()) {\n            transport.connect(host, username, password);\n            transport.sendMessage(message, message.getAllRecipients());\n        }\n\n        log.debug(\"Mail sent to {}. Subj: {}\", to, subj);\n        log.trace(\"Mail body: {}\", body);\n    }\n\n}\n"
  },
  {
    "path": "server/notifications/email/src/main/java/module-info.java",
    "content": "/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 17.05.18.\n */\nmodule cc.blynk.server.notifications.mail {\n    requires org.apache.logging.log4j;\n    requires cc.blynk.utils;\n    requires java.mail;\n    requires java.activation;\n\n    exports cc.blynk.server.notifications.mail;\n}"
  },
  {
    "path": "server/notifications/email/src/main/resources/mail.properties",
    "content": "mail.smtp.auth=true\nmail.smtp.starttls.enable=true\nmail.smtp.host=smtp.gmail.com\nmail.smtp.port=587\nmail.smtp.username=example@gmail.com\nmail.smtp.password=\nmail.smtp.connectiontimeout=30000\nmail.smtp.timeout=120000"
  },
  {
    "path": "server/notifications/email/src/test/java/cc/blynk/server/notifications/mail/MailWrapperTest.java",
    "content": "package cc.blynk.server.notifications.mail;\n\nimport cc.blynk.utils.AppNameUtil;\nimport cc.blynk.utils.properties.MailProperties;\nimport net.glxn.qrgen.core.image.ImageType;\nimport net.glxn.qrgen.javase.QRCode;\nimport org.junit.Ignore;\nimport org.junit.Test;\n\nimport java.io.InputStream;\nimport java.util.Collections;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 06.04.15.\n */\npublic class MailWrapperTest {\n\n    @Test\n    @Ignore\n    public void sendMailForStaticProvisioning() throws Exception {\n        String body =\n                \"Hi there,<br>\\n\" +\n                        \"<br>\\n\" +\n                        \"Nice app you made with Blynk!<br>\\n\" +\n                        \"<br>\\n\" +\n                        \"Here is what's next:\\n\" +\n                        \"\\n\" +\n                        \"<ul>\\n\" +\n                        \"    <li>For Static Provisioning you need to upload Auth Tokens provided in this email to your devices. Tokens are in the attachment.</li>\\n\" +\n                        \"\\n\" +\n                        \"    <li>During the provisioning process, device will be connected to your network. You need to scan provided QRs in order to connect your app to devices. Learn <a href=\\\"http://help.blynk.cc/publishing-apps-made-with-blynk/1240196-provisioning-products-with-auth-tokens/static-auth-token-provisioning\\\">how Static Device Provisioning works</a>.</li>\\n\" +\n                        \"</ul>\\n\" +\n                        \"\\n\" +\n                        \"<b>If you would like to publish your app to App Store and Google Play, check out our <a href=\\\"https://www.blynk.io/plans/\\\">plans</a> and send a request.</b><br>\\n\" +\n                        \"<br>\\n\" +\n                        \"Let’s build a connected world together!<br>\\n\" +\n                        \"<br>\\n\" +\n                        \"--<br>\\n\" +\n                        \"<br>\\n\" +\n                        \"Blynk Team<br>\\n\" +\n                        \"<br>\\n\" +\n                        \"<a href=\\\"https://www.blynk.io\\\">blynk.io</a>\\n\" +\n                        \"<br>\\n\" +\n                        \"<a href=\\\"https://www.blynk.cc\\\">blynk.cc</a>\";\n        QrHolder[] qrHolders = new QrHolder[] {\n                new QrHolder(1, 0, \"My device\", \"12345678901\", QRCode.from(\"21321321\").to(ImageType.JPG).stream().toByteArray()),\n                new QrHolder(1, 1, \"My device2\", \"12345678902\", QRCode.from(\"21321321\").to(ImageType.JPG).stream().toByteArray())\n        };\n\n        MailProperties properties = new MailProperties(Collections.emptyMap());\n        try (InputStream classPath = MailWrapperTest.class.getResourceAsStream(\"/mail.properties\")) {\n            if (classPath != null) {\n                properties.load(classPath);\n            }\n        }\n\n        MailWrapper mailWrapper = new MailWrapper(properties, AppNameUtil.BLYNK);\n        mailWrapper.sendWithAttachment(\"dmitriy@blynk.cc\", \"yo\", body, qrHolders);\n    }\n\n    @Test\n    @Ignore\n    public void sendMailWithAttachments() throws Exception {\n        MailProperties properties = new MailProperties(Collections.emptyMap());\n        try (InputStream classPath = MailWrapperTest.class.getResourceAsStream(\"/mail.properties\")) {\n            if (classPath != null) {\n                properties.load(classPath);\n            }\n        }\n\n        QrHolder qrHolder = new QrHolder(1, 0, \"device name\", \"123\", QRCode.from(\"123\").to(ImageType.JPG).stream().toByteArray());\n        QrHolder qrHolder2 = new QrHolder(1, 1, \"device name\", \"123\",  QRCode.from(\"124\").to(ImageType.JPG).stream().toByteArray());\n\n        String to = \"doom369@gmail.com\";\n        MailWrapper mailWrapper = new MailWrapper(properties, AppNameUtil.BLYNK);\n        mailWrapper.sendWithAttachment(to, \"Hello\", \"Body!\", new QrHolder[]{qrHolder, qrHolder2});\n    }\n\n    @Test\n    @Ignore\n    public void sendMail() throws Exception {\n        MailProperties properties = new MailProperties(Collections.emptyMap());\n        try (InputStream classPath = MailWrapperTest.class.getResourceAsStream(\"/mail.properties\")) {\n            if (classPath != null) {\n                properties.load(classPath);\n            }\n        }\n\n        String to = \"\";\n        MailWrapper mailWrapper = new MailWrapper(properties, AppNameUtil.BLYNK);\n        mailWrapper.sendText(to, \"Hello\", \"Body!\");\n    }\n\n    @Test\n    @Ignore\n    public void sendMail2() throws Exception {\n        MailProperties properties = new MailProperties(Collections.emptyMap());\n        try (InputStream classPath = MailWrapperTest.class.getResourceAsStream(\"/mail.properties\")) {\n            if (classPath != null) {\n                properties.load(classPath);\n            }\n        }\n\n        String to = \"doom369@gmail.com\";\n        MailWrapper mailWrapper = new MailWrapper(properties, AppNameUtil.BLYNK);\n\n        mailWrapper.sendText(to, \"Hello\", \"Body!\");\n    }\n\n    @Test\n    @Ignore\n    public void sendMailWithHttpProvider() throws Exception {\n        MailProperties properties = new MailProperties(Collections.emptyMap());\n        try (InputStream classPath = MailWrapperTest.class.getResourceAsStream(\"/mail.properties\")) {\n            if (classPath != null) {\n                properties.load(classPath);\n            }\n        }\n\n        String to = \"\";\n\n        MailWrapper mailWrapper = new MailWrapper(properties, AppNameUtil.BLYNK);\n\n        mailWrapper.sendText(to, \"Hello\", \"Happy Blynking!\\n\" +\n                \"-\\n\" +\n                \"Getting Started Guide -> https://www.blynk.cc/getting-started\\n\" +\n                \"Documentation -> http://docs.blynk.cc/\\n\" +\n                \"Sketch generator -> https://examples.blynk.cc/\\n\" +\n                \"\\n\" +\n                \"Latest Blynk library -> https://github.com/blynkkk/blynk-library/releases/download/v0.6.1/Blynk_v0.6.1.zip\\n\" +\n                \"Latest Blynk server -> https://github.com/blynkkk/blynk-server/releases/download/v0.18.1/server-0.18.1.jar\\n\" +\n                \"-\\n\" +\n                \"https://www.blynk.cc\\n\" +\n                \"twitter.com/blynk_app\\n\" +\n                \"www.facebook.com/blynkapp\");\n    }\n\n}\n"
  },
  {
    "path": "server/notifications/email/src/test/resources/amazon.properties",
    "content": "mail.smtp.auth=true\nmail.smtp.starttls.enable=true\nmail.smtp.starttls.required=true\nmail.smtp.host=email-smtp.us-east-1.amazonaws.com\nmail.smtp.port=25\nmail.smtp.username=\nmail.smtp.password=\nmail.transport.protocol=\nmail.from=d\n"
  },
  {
    "path": "server/notifications/email/src/test/resources/mail.properties",
    "content": "mail.smtp.auth=true\nmail.smtp.starttls.enable=true\nmail.smtp.starttls.required=true\nmail.smtp.host=smtp.gmail.com\nmail.smtp.port=587\nmail.smtp.username=\nmail.smtp.password="
  },
  {
    "path": "server/notifications/email/src/test/resources/sparkpost-http.properties",
    "content": "mail.host=https://api.sparkpost.com/api/v1/transmissions\nmail.api.key=\nmail.from="
  },
  {
    "path": "server/notifications/email/src/test/resources/sparkpost-smtp.properties",
    "content": "mail.smtp.auth=true\nmail.smtp.starttls.enable=true\nmail.smtp.starttls.required=true\nmail.smtp.host=smtp.sparkpostmail.com\nmail.smtp.port=587\nmail.smtp.username=\nmail.smtp.password=\nmail.transport.protocol=\nmail.from=\n"
  },
  {
    "path": "server/notifications/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <artifactId>server</artifactId>\n        <groupId>cc.blynk.server</groupId>\n        <version>0.41.17-SNAPSHOT</version>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n\n    <artifactId>notifications</artifactId>\n    <groupId>cc.blynk.server.notifications</groupId>\n\n    <packaging>pom</packaging>\n\n    <modules>\n        <module>email</module>\n        <module>push</module>\n        <module>twitter</module>\n        <module>sms</module>\n    </modules>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.asynchttpclient</groupId>\n            <artifactId>async-http-client</artifactId>\n            <version>${async-http-client.version}</version>\n            <exclusions>\n                <exclusion>\n                    <groupId>io.netty</groupId>\n                    <artifactId>*</artifactId>\n                </exclusion>\n            </exclusions>\n        </dependency>\n        <dependency>\n            <groupId>io.netty</groupId>\n            <artifactId>netty-codec-http</artifactId>\n            <version>${netty.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>io.netty</groupId>\n            <artifactId>netty-handler</artifactId>\n            <version>${netty.version}</version>\n        </dependency>\n    </dependencies>\n\n</project>"
  },
  {
    "path": "server/notifications/push/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <artifactId>notifications</artifactId>\n        <groupId>cc.blynk.server.notifications</groupId>\n        <version>0.41.17-SNAPSHOT</version>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n\n    <artifactId>push</artifactId>\n\n    <dependencies>\n        <dependency>\n            <groupId>cc.blynk.utils</groupId>\n            <artifactId>utils</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n            <version>${jackson-databind.version}</version>\n        </dependency>\n\n        <dependency>\n            <groupId>io.netty</groupId>\n            <artifactId>netty-transport-native-epoll</artifactId>\n            <version>${netty.version}</version>\n            <scope>test</scope>\n        </dependency>\n\n    </dependencies>\n\n</project>"
  },
  {
    "path": "server/notifications/push/src/main/java/cc/blynk/server/notifications/push/GCMMessage.java",
    "content": "package cc.blynk.server.notifications.push;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 16.07.15.\n */\npublic interface GCMMessage {\n\n    String getToken();\n\n    String toJson() throws JsonProcessingException;\n\n    default void setTitle(String title) {\n    }\n\n}\n"
  },
  {
    "path": "server/notifications/push/src/main/java/cc/blynk/server/notifications/push/GCMResponseMessage.java",
    "content": "package cc.blynk.server.notifications.push;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 26.06.15.\n */\npublic class GCMResponseMessage {\n\n    private final int success;\n\n    final int failure;\n\n    @JsonProperty(\"multicast_id\")\n    private final long multicastId;\n\n    public final GCMResult[] results;\n\n    @JsonCreator\n    public GCMResponseMessage(@JsonProperty(\"success\") int success,\n                              @JsonProperty(\"failure\") int failure,\n                              @JsonProperty(\"multicast_id\") long multicastId,\n                              @JsonProperty(\"results\") GCMResult[] results) {\n        this.success = success;\n        this.failure = failure;\n        this.multicastId = multicastId;\n        this.results = results;\n    }\n}\n"
  },
  {
    "path": "server/notifications/push/src/main/java/cc/blynk/server/notifications/push/GCMResult.java",
    "content": "package cc.blynk.server.notifications.push;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 26.06.15.\n */\npublic class GCMResult {\n\n    public final String error;\n\n    @JsonCreator\n    public GCMResult(@JsonProperty(\"error\") String error) {\n        this.error = error;\n    }\n}\n"
  },
  {
    "path": "server/notifications/push/src/main/java/cc/blynk/server/notifications/push/GCMWrapper.java",
    "content": "package cc.blynk.server.notifications.push;\n\nimport cc.blynk.utils.properties.GCMProperties;\nimport cc.blynk.utils.properties.Placeholders;\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.DeserializationFeature;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.ObjectReader;\nimport io.netty.handler.codec.http.HttpHeaderNames;\nimport io.netty.handler.codec.http.HttpResponseStatus;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\nimport org.asynchttpclient.AsyncCompletionHandler;\nimport org.asynchttpclient.AsyncHttpClient;\nimport org.asynchttpclient.Response;\n\nimport java.util.Map;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 26.06.15.\n */\npublic class GCMWrapper {\n\n    private static final Logger log = LogManager.getLogger(GCMWrapper.class);\n\n    private final String apiKey;\n    private final AsyncHttpClient httpclient;\n    private final String gcmURI;\n    private final String title;\n    private final ObjectReader gcmResponseReader = new ObjectMapper()\n            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)\n            .readerFor(GCMResponseMessage.class);\n\n    public GCMWrapper(GCMProperties props, AsyncHttpClient httpclient, String productName) {\n        this.apiKey = \"key=\" + props.getGCMApiKey();\n        this.httpclient = httpclient;\n        this.gcmURI = props.getGCMServer();\n\n        String title = props.getNotificationTitle();\n        this.title = title.replace(Placeholders.PRODUCT_NAME, productName);\n    }\n\n    private static void processError(String errorMessage, Map<String, String> tokens, String uid) {\n        log.error(\"Error sending push. Reason {}\", errorMessage);\n        clean(errorMessage, tokens, uid);\n    }\n\n    private static void clean(String errorMessage, Map<String, String> tokens, String uid) {\n        if (errorMessage != null && errorMessage.contains(\"NotRegistered\")) {\n            log.error(\"Removing invalid token. UID {}\", uid);\n            tokens.remove(uid);\n        }\n    }\n\n    public void send(GCMMessage messageBase, final Map<String, String> tokens, final String uid) {\n        if (gcmURI == null) {\n            log.error(\"Error sending push. Google cloud messaging properties not provided.\");\n            return;\n        }\n\n        String message;\n        try {\n            messageBase.setTitle(title);\n            message = messageBase.toJson();\n        } catch (JsonProcessingException e) {\n            log.error(\"Error sending push. Wrong message format.\");\n            return;\n        }\n\n        httpclient.preparePost(gcmURI).setHeader(\"Authorization\", apiKey)\n                .setHeader(HttpHeaderNames.CONTENT_TYPE, \"application/json; charset=utf-8\")\n                .setBody(message)\n                .execute(new AsyncCompletionHandler<Response>() {\n                    @Override\n                    public Response onCompleted(Response response) throws Exception {\n                        if (response.getStatusCode() == HttpResponseStatus.OK.code()) {\n                            GCMResponseMessage gcmResponseMessage =\n                                    gcmResponseReader.readValue(response.getResponseBody());\n                            if (gcmResponseMessage.failure == 1) {\n                                String errorMessage =\n                                        gcmResponseMessage.results != null && gcmResponseMessage.results.length > 0\n                                                ? gcmResponseMessage.results[0].error\n                                                : messageBase.getToken();\n                                processError(errorMessage, tokens, uid);\n                            }\n                            return response;\n                        }\n\n                        return response;\n                    }\n\n                    @Override\n                    public void onThrowable(Throwable t) {\n                        processError(t.getMessage(), tokens, uid);\n                    }\n                });\n\n    }\n\n}\n"
  },
  {
    "path": "server/notifications/push/src/main/java/cc/blynk/server/notifications/push/android/AndroidBody.java",
    "content": "package cc.blynk.server.notifications.push.android;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 06.05.17.\n */\nclass AndroidBody {\n\n    private final String message;\n    private final int dashId;\n\n    AndroidBody(String message, int dashId) {\n        this.message = message;\n        this.dashId = dashId;\n    }\n\n}\n"
  },
  {
    "path": "server/notifications/push/src/main/java/cc/blynk/server/notifications/push/android/AndroidGCMMessage.java",
    "content": "package cc.blynk.server.notifications.push.android;\n\nimport cc.blynk.server.notifications.push.GCMMessage;\nimport cc.blynk.server.notifications.push.enums.Priority;\nimport com.fasterxml.jackson.annotation.JsonAutoDetect;\nimport com.fasterxml.jackson.annotation.PropertyAccessor;\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.ObjectWriter;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 26.06.15.\n */\npublic class AndroidGCMMessage implements GCMMessage {\n\n    private static final ObjectWriter WRITER = new ObjectMapper()\n            .setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE)\n            .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)\n            .writerFor(AndroidGCMMessage.class);\n    private final String to;\n    private final Priority priority;\n    private final AndroidBody data;\n\n    public AndroidGCMMessage(String to, Priority priority, String message, int dashId) {\n        this.to = to;\n        this.priority = priority;\n        this.data = new AndroidBody(message, dashId);\n    }\n\n    @Override\n    public String getToken() {\n        return to;\n    }\n\n    @Override\n    public String toJson() throws JsonProcessingException {\n        return WRITER.writeValueAsString(this);\n    }\n\n}\n"
  },
  {
    "path": "server/notifications/push/src/main/java/cc/blynk/server/notifications/push/enums/Priority.java",
    "content": "package cc.blynk.server.notifications.push.enums;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 03.11.15.\n */\npublic enum Priority {\n\n    normal,\n    high\n\n}\n"
  },
  {
    "path": "server/notifications/push/src/main/java/cc/blynk/server/notifications/push/ios/IOSBody.java",
    "content": "package cc.blynk.server.notifications.push.ios;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 14.11.17.\n */\nclass IOSBody {\n\n    private final String body;\n    private final int dashId;\n    private final String sound;\n    private String title;\n\n    IOSBody(String body, int dashId) {\n        this.body = body;\n        this.dashId = dashId;\n        this.sound = \"default\";\n    }\n\n    void setTitle(String title) {\n        this.title = title;\n    }\n}\n"
  },
  {
    "path": "server/notifications/push/src/main/java/cc/blynk/server/notifications/push/ios/IOSGCMMessage.java",
    "content": "package cc.blynk.server.notifications.push.ios;\n\nimport cc.blynk.server.notifications.push.GCMMessage;\nimport cc.blynk.server.notifications.push.enums.Priority;\nimport com.fasterxml.jackson.annotation.JsonAutoDetect;\nimport com.fasterxml.jackson.annotation.PropertyAccessor;\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.ObjectWriter;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 16.07.15.\n */\npublic class IOSGCMMessage implements GCMMessage {\n\n    private static final ObjectWriter WRITER = new ObjectMapper()\n            .setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE)\n            .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)\n            .writerFor(IOSGCMMessage.class);\n    private final String to;\n    private final Priority priority;\n    private final IOSBody notification;\n\n    public IOSGCMMessage(String to, Priority priority, String message, int dashId) {\n        this.to = to;\n        this.priority = priority;\n        this.notification = new IOSBody(message, dashId);\n    }\n\n    @Override\n    public String getToken() {\n        return to;\n    }\n\n    @Override\n    public void setTitle(String title) {\n        this.notification.setTitle(title);\n    }\n\n    @Override\n    public String toJson() throws JsonProcessingException {\n        return WRITER.writeValueAsString(this);\n    }\n}\n"
  },
  {
    "path": "server/notifications/push/src/main/java/module-info.java",
    "content": "/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 29.09.17.\n */\nopen module cc.blynk.server.notifications.push {\n    requires org.apache.logging.log4j;\n    requires com.fasterxml.jackson.annotation;\n    requires com.fasterxml.jackson.core;\n    requires com.fasterxml.jackson.databind;\n    requires io.netty.codec.http;\n    requires async.http.client;\n    requires cc.blynk.utils;\n\n    exports cc.blynk.server.notifications.push;\n    exports cc.blynk.server.notifications.push.android;\n    exports cc.blynk.server.notifications.push.enums;\n    exports cc.blynk.server.notifications.push.ios;\n}\n"
  },
  {
    "path": "server/notifications/push/src/main/resources/gcm.properties",
    "content": "#yes, this is not secure, but who cares. here is a public key for GCM notifications on local servers\ngcm.server=https://fcm.googleapis.com/fcm/send\ngcm.api.key=AAAAucxWLNg:APA91bHqxdmVmvu6rpENVXfSM0HAK6pfYr0iCpcgkzmKrLWpH-8ljrTps534tx0Ok0ZrpmB_vUIRRVW2yYnVqGgT1btT_d6WpU0RV8qnzzMSeHPwm2yXd37Lyi05H3C7Fz-7ZimilslN\nnotification.title={PRODUCT_NAME} Notification\nnotification.body=Your {DEVICE_NAME} went offline."
  },
  {
    "path": "server/notifications/push/src/test/java/cc/blynk/server/notifications/push/GCMWrapperTest.java",
    "content": "package cc.blynk.server.notifications.push;\n\nimport cc.blynk.server.notifications.push.android.AndroidGCMMessage;\nimport cc.blynk.server.notifications.push.enums.Priority;\nimport cc.blynk.server.notifications.push.ios.IOSGCMMessage;\nimport cc.blynk.utils.AppNameUtil;\nimport cc.blynk.utils.properties.GCMProperties;\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport io.netty.channel.epoll.Epoll;\nimport org.asynchttpclient.AsyncHttpClient;\nimport org.asynchttpclient.DefaultAsyncHttpClient;\nimport org.asynchttpclient.DefaultAsyncHttpClientConfig;\nimport org.junit.AfterClass;\nimport org.junit.Ignore;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.mockito.Mockito.when;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 26.06.15.\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class GCMWrapperTest {\n\n    private static final AsyncHttpClient client = new DefaultAsyncHttpClient(new DefaultAsyncHttpClientConfig.Builder()\n                .setUserAgent(null)\n                .setKeepAlive(true)\n                .setUseNativeTransport(Epoll.isAvailable())\n            .build()\n        );\n\n    @AfterClass\n    public static void closeHttpClient() throws Exception {\n        client.close();\n    }\n\n    @Mock\n    private GCMProperties props;\n\n    @Test\n    @Ignore\n    public void testIOS() {\n        GCMWrapper gcmWrapper = new GCMWrapper(props, client, AppNameUtil.BLYNK);\n        gcmWrapper.send(new IOSGCMMessage(\"to\", Priority.normal, \"yo!!!\", 1), null, null);\n    }\n\n    @Test\n    @Ignore\n    public void testAndroid() throws Exception {\n        when(props.getProperty(\"gcm.api.key\")).thenReturn(\"\");\n        when(props.getProperty(\"gcm.server\")).thenReturn(\"\");\n        GCMWrapper gcmWrapper = new GCMWrapper(props, client, AppNameUtil.BLYNK);\n        gcmWrapper.send(new AndroidGCMMessage(\"\", Priority.normal, \"yo!!!\", 1), null, null);\n        Thread.sleep(5000);\n    }\n\n    @Test\n    public void testValidAndroidJson() throws JsonProcessingException {\n        assertEquals(\"{\\\"to\\\":\\\"to\\\",\\\"priority\\\":\\\"normal\\\",\\\"data\\\":{\\\"message\\\":\\\"yo!!!\\\",\\\"dashId\\\":1}}\", new AndroidGCMMessage(\"to\", Priority.normal, \"yo!!!\", 1).toJson());\n    }\n\n    @Test\n    public void testValidIOSJson() throws JsonProcessingException {\n        IOSGCMMessage iosgcmMessage = new IOSGCMMessage(\"to\", Priority.normal, \"yo!!!\", 1);\n        iosgcmMessage.setTitle(\"Blynk Notification\");\n        assertEquals(\"{\\\"to\\\":\\\"to\\\",\\\"priority\\\":\\\"normal\\\",\\\"notification\\\":{\\\"body\\\":\\\"yo!!!\\\",\\\"dashId\\\":1,\\\"sound\\\":\\\"default\\\",\\\"title\\\":\\\"Blynk Notification\\\"}}\", iosgcmMessage.toJson());\n    }\n\n}\n"
  },
  {
    "path": "server/notifications/sms/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <artifactId>notifications</artifactId>\n        <groupId>cc.blynk.server.notifications</groupId>\n        <version>0.41.17-SNAPSHOT</version>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n\n    <artifactId>sms</artifactId>\n\n    <dependencies>\n\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n            <version>${jackson-databind.version}</version>\n        </dependency>\n\n    </dependencies>\n\n\n</project>"
  },
  {
    "path": "server/notifications/sms/src/main/java/cc/blynk/server/notifications/sms/SMSWrapper.java",
    "content": "package cc.blynk.server.notifications.sms;\n\nimport com.fasterxml.jackson.databind.DeserializationFeature;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.ObjectReader;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\nimport org.asynchttpclient.AsyncCompletionHandler;\nimport org.asynchttpclient.AsyncHttpClient;\nimport org.asynchttpclient.Param;\nimport org.asynchttpclient.Response;\n\nimport java.util.ArrayList;\nimport java.util.Properties;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 19.03.16.\n */\npublic class SMSWrapper {\n\n    private static final Logger log = LogManager.getLogger(SMSWrapper.class);\n\n    private final String key;\n    private final String secret;\n    private final AsyncHttpClient httpclient;\n\n    private final ObjectReader smsResponseReader = new ObjectMapper()\n            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)\n            .readerFor(SmsResponse.class);\n\n    public SMSWrapper(Properties props, AsyncHttpClient httpclient) {\n        this(props.getProperty(\"nexmo.api.key\"), props.getProperty(\"nexmo.api.secret\"), httpclient);\n    }\n\n    private SMSWrapper(String key, String secret, AsyncHttpClient httpclient) {\n        this.key = key;\n        this.secret = secret;\n        this.httpclient = httpclient;\n    }\n\n    public void send(String to, String text) {\n        ArrayList<Param> params = new ArrayList<>();\n        params.add(new Param(\"api_key\", key));\n        params.add(new Param(\"api_secret\", secret));\n        params.add(new Param(\"from\", \"Blynk\"));\n        params.add(new Param(\"to\", to));\n        params.add(new Param(\"text\", text));\n\n        httpclient.preparePost(\"https://rest.nexmo.com/sms/json\")\n                .setQueryParams(params)\n                .execute(new AsyncCompletionHandler<Response>() {\n                    @Override\n                    public Response onCompleted(org.asynchttpclient.Response response) throws Exception {\n                        if (response.getStatusCode() == 200) {\n                            SmsResponse smsResponse = smsResponseReader.readValue(response.getResponseBody());\n                            if (!smsResponse.messages[0].status.equals(\"0\")) {\n                                log.error(smsResponse.messages[0].error);\n                            }\n                        }\n                        return response;\n                    }\n                });\n\n\n    }\n\n}\n"
  },
  {
    "path": "server/notifications/sms/src/main/java/cc/blynk/server/notifications/sms/SmsResponse.java",
    "content": "package cc.blynk.server.notifications.sms;\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 20.03.16.\n */\npublic class SmsResponse {\n\n    @JsonProperty(\"message-count\")\n    public String messageCount;\n\n    public Message[] messages;\n\n    public static class Message {\n\n        public String status;\n\n        @JsonProperty(\"message-id\")\n        public String messageId;\n\n        @JsonProperty(\"error-text\")\n        public String error;\n\n        @JsonProperty(\"message-price\")\n        public String price;\n\n        @JsonProperty(\"remaining-balance\")\n        public String remainingBalance;\n\n    }\n}\n"
  },
  {
    "path": "server/notifications/sms/src/main/java/module-info.java",
    "content": "/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 29.09.17.\n */\nmodule cc.blynk.server.notifications.sms {\n    requires com.fasterxml.jackson.databind;\n    requires org.apache.logging.log4j;\n    requires async.http.client;\n    requires com.fasterxml.jackson.annotation;\n\n    exports cc.blynk.server.notifications.sms;\n}"
  },
  {
    "path": "server/notifications/sms/src/main/resources/sms.properties",
    "content": "nexmo.api.key=\nnexmo.api.secret="
  },
  {
    "path": "server/notifications/sms/src/test/java/cc/blynk/server/notifications/sms/TestSendSms.java",
    "content": "package cc.blynk.server.notifications.sms;\n\nimport org.junit.Ignore;\nimport org.junit.Test;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 19.03.16.\n */\n@Ignore\npublic class TestSendSms {\n\n    @Test\n    public void testSend() {\n        SMSWrapper smsWrapper = new SMSWrapper(null, null);\n        smsWrapper.send(\"\", \"Hello!!!!!!!!\");\n    }\n\n}\n"
  },
  {
    "path": "server/notifications/twitter/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <artifactId>notifications</artifactId>\n        <groupId>cc.blynk.server.notifications</groupId>\n        <version>0.41.17-SNAPSHOT</version>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n\n    <artifactId>twitter</artifactId>\n\n    <dependencies>\n        <dependency>\n            <groupId>cc.blynk.utils</groupId>\n            <artifactId>utils</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>io.netty</groupId>\n            <artifactId>netty-transport-native-epoll</artifactId>\n            <version>${netty.version}</version>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n</project>"
  },
  {
    "path": "server/notifications/twitter/src/main/java/cc/blynk/server/notifications/twitter/TwitterWrapper.java",
    "content": "package cc.blynk.server.notifications.twitter;\n\nimport cc.blynk.utils.properties.TwitterProperties;\nimport org.asynchttpclient.AsyncCompletionHandler;\nimport org.asynchttpclient.AsyncHttpClient;\nimport org.asynchttpclient.Response;\nimport org.asynchttpclient.oauth.ConsumerKey;\nimport org.asynchttpclient.oauth.OAuthSignatureCalculator;\nimport org.asynchttpclient.oauth.RequestToken;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/6/2015.\n */\npublic class TwitterWrapper {\n\n    private static final String TWITTER_UPDATE_STATUS_URL = \"https://api.twitter.com/1.1/statuses/update.json\";\n    private final ConsumerKey consumerKey;\n    private final AsyncHttpClient asyncHttpClient;\n\n    public TwitterWrapper(TwitterProperties twitterProperties, AsyncHttpClient asyncHttpClient) {\n        this.consumerKey = new ConsumerKey(\n                twitterProperties.getConsumerKey(),\n                twitterProperties.getConsumerSecret()\n        );\n        this.asyncHttpClient = asyncHttpClient;\n    }\n\n    public void send(String token, String secret, String message,\n                     AsyncCompletionHandler<Response> handler) {\n        asyncHttpClient\n                .preparePost(TWITTER_UPDATE_STATUS_URL)\n                .addQueryParam(\"status\", message)\n                .setSignatureCalculator(new OAuthSignatureCalculator(consumerKey, new RequestToken(token, secret)))\n                .execute(handler);\n    }\n}\n"
  },
  {
    "path": "server/notifications/twitter/src/main/java/module-info.java",
    "content": "/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 29.09.17.\n */\nmodule cc.blynk.server.notifications.twitter {\n    requires async.http.client;\n    requires cc.blynk.utils;\n\n    exports cc.blynk.server.notifications.twitter;\n}"
  },
  {
    "path": "server/notifications/twitter/src/test/java/cc/blynk/server/notifications/twitter/TwitterWrapperTest.java",
    "content": "package cc.blynk.server.notifications.twitter;\n\nimport cc.blynk.utils.properties.TwitterProperties;\nimport io.netty.channel.epoll.Epoll;\nimport io.netty.handler.codec.http.HttpResponseStatus;\nimport org.asynchttpclient.AsyncCompletionHandler;\nimport org.asynchttpclient.AsyncHttpClient;\nimport org.asynchttpclient.DefaultAsyncHttpClient;\nimport org.asynchttpclient.DefaultAsyncHttpClientConfig;\nimport org.asynchttpclient.Response;\nimport org.junit.AfterClass;\nimport org.junit.Ignore;\nimport org.junit.Test;\n\nimport java.util.Collections;\n\nimport static org.junit.Assert.assertEquals;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 25.01.18.\n */\npublic class TwitterWrapperTest {\n\n    private static final AsyncHttpClient client = new DefaultAsyncHttpClient(new DefaultAsyncHttpClientConfig.Builder()\n            .setUserAgent(null)\n            .setKeepAlive(true)\n            .setUseNativeTransport(Epoll.isAvailable())\n            .build()\n    );\n\n    @AfterClass\n    public static void closeHttpClient() throws Exception {\n        client.close();\n    }\n\n    @Test\n    @Ignore(\"requires real credentials\")\n    public void testSend() throws Exception {\n        TwitterProperties twitterProperties = new TwitterProperties(Collections.emptyMap());\n        TwitterWrapper twitterWrapper = new TwitterWrapper(twitterProperties, client);\n\n        String userToken = \"\";\n        String userSecret = \"\";\n        String message = \"Hello!!!\";\n\n        twitterWrapper.send(userToken, userSecret, message,\n                new AsyncCompletionHandler<>() {\n                    @Override\n                    public Response onCompleted(Response response) {\n                        if (response.getStatusCode() != HttpResponseStatus.OK.code()) {\n                            System.out.println(\"Error sending twit. Reason : \" + response.getResponseBody());\n                        }\n                        assertEquals(200, response.getStatusCode());\n                        return response;\n                    }\n\n                    @Override\n                    public void onThrowable(Throwable t) {\n                        t.printStackTrace();\n                        System.out.println(\"Error sending twit.\");\n                    }\n                });\n        Thread.sleep(5000);\n    }\n\n}\n"
  },
  {
    "path": "server/notifications/twitter/src/test/resources/twitter4j.properties",
    "content": "oauth.consumerKey=\noauth.consumerSecret="
  },
  {
    "path": "server/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <artifactId>blynk</artifactId>\n        <groupId>cc.blynk</groupId>\n        <version>0.41.17-SNAPSHOT</version>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n\n    <artifactId>server</artifactId>\n    <groupId>cc.blynk.server</groupId>\n\n    <packaging>pom</packaging>\n\n    <modules>\n        <module>notifications</module>\n\n        <module>utils</module>\n        <module>acme</module>\n        <module>core</module>\n\n        <module>http-core</module>\n        <module>http-admin</module>\n        <module>http-api</module>\n\n        <module>tcp-app-server</module>\n        <module>tcp-hardware-server</module>\n        <module>tcp-web-server</module>\n\n        <module>launcher</module>\n        <module>tools</module>\n    </modules>\n\n</project>"
  },
  {
    "path": "server/tcp-app-server/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <artifactId>server</artifactId>\n        <groupId>cc.blynk.server</groupId>\n        <version>0.41.17-SNAPSHOT</version>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n\n    <groupId>cc.blynk.server.application</groupId>\n    <artifactId>tcp-app-server</artifactId>\n\n    <dependencies>\n\n        <dependency>\n            <groupId>cc.blynk.server</groupId>\n            <artifactId>core</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n\n    </dependencies>\n\n</project>"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/MobileChannelStateHandler.java",
    "content": "package cc.blynk.server.application.handlers.main;\n\nimport cc.blynk.server.core.dao.SessionDao;\nimport cc.blynk.server.core.protocol.enums.Command;\nimport io.netty.channel.ChannelHandler;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.ChannelInboundHandlerAdapter;\nimport io.netty.handler.timeout.IdleStateEvent;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.utils.MobileStateHolderUtil.getAppState;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/20/2015.\n *\n * Removes channel from session in case it became inactive (closed from client side).\n */\n@ChannelHandler.Sharable\npublic class MobileChannelStateHandler extends ChannelInboundHandlerAdapter {\n\n    private static final Logger log = LogManager.getLogger(MobileChannelStateHandler.class);\n\n    private final SessionDao sessionDao;\n\n    public MobileChannelStateHandler(SessionDao sessionDao) {\n        this.sessionDao = sessionDao;\n    }\n\n    @Override\n    public void channelInactive(ChannelHandlerContext ctx) {\n        var state = getAppState(ctx.channel());\n        if (state != null) {\n            var session = sessionDao.get(state.userKey);\n            if (session != null) {\n                log.trace(\"Application channel disconnect. {}\", ctx.channel());\n\n                for (var dashBoard : state.user.profile.dashBoards) {\n                    if (dashBoard.isAppConnectedOn && dashBoard.isActive) {\n                        log.trace(\"{}-{}. Sending App Disconnected event to hardware.\",\n                                state.user.email, state.user.appName);\n                        session.sendMessageToHardware(dashBoard.id, Command.BLYNK_INTERNAL, 7777, \"adis\");\n                    }\n                }\n            }\n        }\n    }\n\n    @Override\n    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {\n        if (evt instanceof IdleStateEvent) {\n            log.trace(\"State handler. App timeout disconnect. Event : {}. Closing.\", ((IdleStateEvent) evt).state());\n            ctx.close();\n        } else {\n            ctx.fireUserEventTriggered(evt);\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/MobileHandler.java",
    "content": "package cc.blynk.server.application.handlers.main;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.application.handlers.main.auth.MobileStateHolder;\nimport cc.blynk.server.application.handlers.main.logic.MobileActivateDashboardLogic;\nimport cc.blynk.server.application.handlers.main.logic.MobileAddPushLogic;\nimport cc.blynk.server.application.handlers.main.logic.MobileAssignTokenLogic;\nimport cc.blynk.server.application.handlers.main.logic.MobileDeActivateDashboardLogic;\nimport cc.blynk.server.application.handlers.main.logic.MobileGetCloneCodeLogic;\nimport cc.blynk.server.application.handlers.main.logic.MobileGetEnergyLogic;\nimport cc.blynk.server.application.handlers.main.logic.MobileGetProjectByClonedTokenLogic;\nimport cc.blynk.server.application.handlers.main.logic.MobileGetProjectByTokenLogic;\nimport cc.blynk.server.application.handlers.main.logic.MobileGetProvisionTokenLogic;\nimport cc.blynk.server.application.handlers.main.logic.MobileHardwareLogic;\nimport cc.blynk.server.application.handlers.main.logic.MobileHardwareResendFromBTLogic;\nimport cc.blynk.server.application.handlers.main.logic.MobileLoadProfileGzippedLogic;\nimport cc.blynk.server.application.handlers.main.logic.MobileLogoutLogic;\nimport cc.blynk.server.application.handlers.main.logic.MobileMailLogic;\nimport cc.blynk.server.application.handlers.main.logic.MobilePurchaseLogic;\nimport cc.blynk.server.application.handlers.main.logic.MobileRedeemLogic;\nimport cc.blynk.server.application.handlers.main.logic.MobileRefreshTokenLogic;\nimport cc.blynk.server.application.handlers.main.logic.MobileSetWidgetPropertyLogic;\nimport cc.blynk.server.application.handlers.main.logic.MobileSyncLogic;\nimport cc.blynk.server.application.handlers.main.logic.dashboard.MobileCreateDashLogic;\nimport cc.blynk.server.application.handlers.main.logic.dashboard.MobileDeleteDashLogic;\nimport cc.blynk.server.application.handlers.main.logic.dashboard.MobileUpdateDashLogic;\nimport cc.blynk.server.application.handlers.main.logic.dashboard.MobileUpdateDashSettingLogic;\nimport cc.blynk.server.application.handlers.main.logic.dashboard.device.MobileCreateDeviceLogic;\nimport cc.blynk.server.application.handlers.main.logic.dashboard.device.MobileDeleteDeviceLogic;\nimport cc.blynk.server.application.handlers.main.logic.dashboard.device.MobileGetDeviceLogic;\nimport cc.blynk.server.application.handlers.main.logic.dashboard.device.MobileGetDevicesLogic;\nimport cc.blynk.server.application.handlers.main.logic.dashboard.device.MobileUpdateDeviceLogic;\nimport cc.blynk.server.application.handlers.main.logic.dashboard.tags.MobileCreateTagLogic;\nimport cc.blynk.server.application.handlers.main.logic.dashboard.tags.MobileDeleteTagLogic;\nimport cc.blynk.server.application.handlers.main.logic.dashboard.tags.MobileGetTagsLogic;\nimport cc.blynk.server.application.handlers.main.logic.dashboard.tags.MobileUpdateTagLogic;\nimport cc.blynk.server.application.handlers.main.logic.dashboard.widget.MobileCreateWidgetLogic;\nimport cc.blynk.server.application.handlers.main.logic.dashboard.widget.MobileDeleteWidgetLogic;\nimport cc.blynk.server.application.handlers.main.logic.dashboard.widget.MobileGetWidgetLogic;\nimport cc.blynk.server.application.handlers.main.logic.dashboard.widget.MobileUpdateWidgetLogic;\nimport cc.blynk.server.application.handlers.main.logic.dashboard.widget.tile.MobileCreateTileTemplateLogic;\nimport cc.blynk.server.application.handlers.main.logic.dashboard.widget.tile.MobileDeleteTileTemplateLogic;\nimport cc.blynk.server.application.handlers.main.logic.dashboard.widget.tile.MobileUpdateTileTemplateLogic;\nimport cc.blynk.server.application.handlers.main.logic.face.MobileCreateAppLogic;\nimport cc.blynk.server.application.handlers.main.logic.face.MobileDeleteAppLogic;\nimport cc.blynk.server.application.handlers.main.logic.face.MobileMailQRsLogic;\nimport cc.blynk.server.application.handlers.main.logic.face.MobileUpdateAppLogic;\nimport cc.blynk.server.application.handlers.main.logic.face.MobileUpdateFaceLogic;\nimport cc.blynk.server.application.handlers.main.logic.graph.MobileDeleteDeviceDataLogic;\nimport cc.blynk.server.application.handlers.main.logic.graph.MobileDeleteEnhancedGraphDataLogic;\nimport cc.blynk.server.application.handlers.main.logic.graph.MobileExportGraphDataLogic;\nimport cc.blynk.server.application.handlers.main.logic.graph.MobileGetEnhancedGraphDataLogic;\nimport cc.blynk.server.application.handlers.main.logic.reporting.MobileCreateReportLogic;\nimport cc.blynk.server.application.handlers.main.logic.reporting.MobileDeleteReportLogic;\nimport cc.blynk.server.application.handlers.main.logic.reporting.MobileExportReportLogic;\nimport cc.blynk.server.application.handlers.main.logic.reporting.MobileUpdateReportLogic;\nimport cc.blynk.server.application.handlers.main.logic.sharing.MobileGetShareTokenLogic;\nimport cc.blynk.server.application.handlers.main.logic.sharing.MobileRefreshShareTokenLogic;\nimport cc.blynk.server.application.handlers.main.logic.sharing.MobileShareLogic;\nimport cc.blynk.server.common.BaseSimpleChannelInboundHandler;\nimport cc.blynk.server.common.handlers.logic.PingLogic;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.server.core.session.StateHolderBase;\nimport io.netty.channel.ChannelHandlerContext;\n\nimport static cc.blynk.server.core.protocol.enums.Command.ACTIVATE_DASHBOARD;\nimport static cc.blynk.server.core.protocol.enums.Command.ADD_ENERGY;\nimport static cc.blynk.server.core.protocol.enums.Command.ADD_PUSH_TOKEN;\nimport static cc.blynk.server.core.protocol.enums.Command.APP_SYNC;\nimport static cc.blynk.server.core.protocol.enums.Command.ASSIGN_TOKEN;\nimport static cc.blynk.server.core.protocol.enums.Command.CREATE_APP;\nimport static cc.blynk.server.core.protocol.enums.Command.CREATE_DASH;\nimport static cc.blynk.server.core.protocol.enums.Command.CREATE_DEVICE;\nimport static cc.blynk.server.core.protocol.enums.Command.CREATE_REPORT;\nimport static cc.blynk.server.core.protocol.enums.Command.CREATE_TAG;\nimport static cc.blynk.server.core.protocol.enums.Command.CREATE_TILE_TEMPLATE;\nimport static cc.blynk.server.core.protocol.enums.Command.CREATE_WIDGET;\nimport static cc.blynk.server.core.protocol.enums.Command.DEACTIVATE_DASHBOARD;\nimport static cc.blynk.server.core.protocol.enums.Command.DELETE_APP;\nimport static cc.blynk.server.core.protocol.enums.Command.DELETE_DASH;\nimport static cc.blynk.server.core.protocol.enums.Command.DELETE_DEVICE;\nimport static cc.blynk.server.core.protocol.enums.Command.DELETE_DEVICE_DATA;\nimport static cc.blynk.server.core.protocol.enums.Command.DELETE_ENHANCED_GRAPH_DATA;\nimport static cc.blynk.server.core.protocol.enums.Command.DELETE_REPORT;\nimport static cc.blynk.server.core.protocol.enums.Command.DELETE_TAG;\nimport static cc.blynk.server.core.protocol.enums.Command.DELETE_TILE_TEMPLATE;\nimport static cc.blynk.server.core.protocol.enums.Command.DELETE_WIDGET;\nimport static cc.blynk.server.core.protocol.enums.Command.EMAIL;\nimport static cc.blynk.server.core.protocol.enums.Command.EMAIL_QR;\nimport static cc.blynk.server.core.protocol.enums.Command.EXPORT_GRAPH_DATA;\nimport static cc.blynk.server.core.protocol.enums.Command.EXPORT_REPORT;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_CLONE_CODE;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_DEVICES;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_ENERGY;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_ENHANCED_GRAPH_DATA;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_PROJECT_BY_CLONE_CODE;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_PROJECT_BY_TOKEN;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_PROVISION_TOKEN;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_SHARE_TOKEN;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_TAGS;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_WIDGET;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE_RESEND_FROM_BLUETOOTH;\nimport static cc.blynk.server.core.protocol.enums.Command.LOAD_PROFILE_GZIPPED;\nimport static cc.blynk.server.core.protocol.enums.Command.LOGOUT;\nimport static cc.blynk.server.core.protocol.enums.Command.MOBILE_GET_DEVICE;\nimport static cc.blynk.server.core.protocol.enums.Command.PING;\nimport static cc.blynk.server.core.protocol.enums.Command.REDEEM;\nimport static cc.blynk.server.core.protocol.enums.Command.REFRESH_SHARE_TOKEN;\nimport static cc.blynk.server.core.protocol.enums.Command.REFRESH_TOKEN;\nimport static cc.blynk.server.core.protocol.enums.Command.SET_WIDGET_PROPERTY;\nimport static cc.blynk.server.core.protocol.enums.Command.SHARING;\nimport static cc.blynk.server.core.protocol.enums.Command.UPDATE_APP;\nimport static cc.blynk.server.core.protocol.enums.Command.UPDATE_DASH;\nimport static cc.blynk.server.core.protocol.enums.Command.UPDATE_DEVICE;\nimport static cc.blynk.server.core.protocol.enums.Command.UPDATE_FACE;\nimport static cc.blynk.server.core.protocol.enums.Command.UPDATE_PROJECT_SETTINGS;\nimport static cc.blynk.server.core.protocol.enums.Command.UPDATE_REPORT;\nimport static cc.blynk.server.core.protocol.enums.Command.UPDATE_TAG;\nimport static cc.blynk.server.core.protocol.enums.Command.UPDATE_TILE_TEMPLATE;\nimport static cc.blynk.server.core.protocol.enums.Command.UPDATE_WIDGET;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic class MobileHandler extends BaseSimpleChannelInboundHandler<StringMessage> {\n\n    public final MobileStateHolder state;\n    private final Holder holder;\n    private final MobileHardwareLogic hardwareLogic;\n    private final MobileAddPushLogic mobileAddPushLogic;\n\n    private MobileHardwareResendFromBTLogic hardwareResendFromBTLogic;\n    private MobileExportGraphDataLogic exportGraphData;\n    private MobileMailLogic mailLogic;\n    private MobilePurchaseLogic purchaseLogic;\n    private MobileDeleteAppLogic deleteAppLogic;\n    private MobileMailQRsLogic mailQRsLogic;\n    private MobileGetProjectByClonedTokenLogic getProjectByCloneCodeLogic;\n\n    public MobileHandler(Holder holder, MobileStateHolder state) {\n        super(StringMessage.class);\n        this.state = state;\n        this.holder = holder;\n\n        this.hardwareLogic = new MobileHardwareLogic(holder, state.user.email);\n        this.mobileAddPushLogic = new MobileAddPushLogic(holder);\n    }\n\n    @Override\n    public void messageReceived(ChannelHandlerContext ctx, StringMessage msg) {\n        holder.stats.incrementAppStat();\n        switch (msg.command) {\n            case HARDWARE :\n                hardwareLogic.messageReceived(ctx, state, msg);\n                break;\n            case HARDWARE_RESEND_FROM_BLUETOOTH :\n                if (hardwareResendFromBTLogic == null) {\n                    this.hardwareResendFromBTLogic = new MobileHardwareResendFromBTLogic(holder, state.user.email);\n                }\n                hardwareResendFromBTLogic.messageReceived(ctx, state, msg);\n                break;\n            case ACTIVATE_DASHBOARD :\n                MobileActivateDashboardLogic.messageReceived(holder, ctx, state, msg);\n                break;\n            case DEACTIVATE_DASHBOARD :\n                MobileDeActivateDashboardLogic.messageReceived(holder, ctx, state, msg);\n                break;\n            case LOAD_PROFILE_GZIPPED :\n                MobileLoadProfileGzippedLogic.messageReceived(holder, ctx, state, msg);\n                break;\n            case SHARING :\n                MobileShareLogic.messageReceived(holder, ctx, state, msg);\n                break;\n\n            case ASSIGN_TOKEN :\n                MobileAssignTokenLogic.messageReceived(holder, ctx, state.user, msg);\n                break;\n            case ADD_PUSH_TOKEN :\n                mobileAddPushLogic.messageReceived(ctx, state, msg);\n                break;\n            case REFRESH_TOKEN :\n                MobileRefreshTokenLogic.messageReceived(holder, ctx, state, msg);\n                break;\n\n            case GET_ENHANCED_GRAPH_DATA :\n                MobileGetEnhancedGraphDataLogic.messageReceived(holder, ctx, state, msg);\n                break;\n            case DELETE_ENHANCED_GRAPH_DATA :\n                MobileDeleteEnhancedGraphDataLogic.messageReceived(holder, ctx, state.user, msg);\n                break;\n            case EXPORT_GRAPH_DATA :\n                if (exportGraphData == null) {\n                    this.exportGraphData = new MobileExportGraphDataLogic(holder);\n                }\n                exportGraphData.messageReceived(ctx, state.user, msg);\n                break;\n            case PING :\n                PingLogic.messageReceived(ctx, msg.id);\n                break;\n\n            case GET_SHARE_TOKEN :\n                MobileGetShareTokenLogic.messageReceived(holder, ctx, state.user, msg);\n                break;\n            case REFRESH_SHARE_TOKEN :\n                MobileRefreshShareTokenLogic.messageReceived(holder, ctx, state, msg);\n                break;\n\n            case EMAIL :\n                if (mailLogic == null) {\n                    this.mailLogic = new MobileMailLogic(holder);\n                }\n                mailLogic.messageReceived(ctx, state.user, msg);\n                break;\n\n            case CREATE_DASH :\n                MobileCreateDashLogic.messageReceived(holder, ctx, state, msg);\n                break;\n            case UPDATE_DASH:\n                MobileUpdateDashLogic.messageReceived(holder, ctx, state, msg);\n                break;\n            case DELETE_DASH :\n                MobileDeleteDashLogic.messageReceived(holder, ctx, state, msg);\n                break;\n\n            case CREATE_WIDGET :\n                MobileCreateWidgetLogic.messageReceived(holder, ctx, state, msg);\n                break;\n            case UPDATE_WIDGET :\n                MobileUpdateWidgetLogic.messageReceived(holder, ctx, state, msg);\n                break;\n            case DELETE_WIDGET :\n                MobileDeleteWidgetLogic.messageReceived(holder, ctx, state, msg);\n                break;\n            case GET_WIDGET :\n                MobileGetWidgetLogic.messageReceived(ctx, state, msg);\n                break;\n\n            case CREATE_TILE_TEMPLATE :\n                MobileCreateTileTemplateLogic.messageReceived(ctx, state, msg);\n                break;\n            case UPDATE_TILE_TEMPLATE :\n                MobileUpdateTileTemplateLogic.messageReceived(ctx, state, msg);\n                break;\n            case DELETE_TILE_TEMPLATE :\n                MobileDeleteTileTemplateLogic.messageReceived(ctx, state, msg);\n                break;\n\n            case REDEEM :\n                MobileRedeemLogic.messageReceived(holder, ctx, state.user, msg);\n                break;\n\n            case GET_ENERGY :\n                MobileGetEnergyLogic.messageReceived(ctx, state.user, msg);\n                break;\n            case ADD_ENERGY :\n                if (purchaseLogic == null) {\n                    this.purchaseLogic = new MobilePurchaseLogic(holder);\n                }\n                purchaseLogic.messageReceived(ctx, state, msg);\n                break;\n\n            case UPDATE_PROJECT_SETTINGS :\n                MobileUpdateDashSettingLogic.messageReceived(ctx, state, msg, holder.limits.widgetSizeLimitBytes);\n                break;\n\n            case CREATE_DEVICE :\n                MobileCreateDeviceLogic.messageReceived(holder, ctx, state.user, msg);\n                break;\n            case UPDATE_DEVICE :\n                MobileUpdateDeviceLogic.messageReceived(ctx, state.user, msg);\n                break;\n            case DELETE_DEVICE :\n                MobileDeleteDeviceLogic.messageReceived(holder, ctx, state, msg);\n                break;\n            case GET_DEVICES :\n                MobileGetDevicesLogic.messageReceived(ctx, state.user, msg);\n                break;\n            case MOBILE_GET_DEVICE:\n                MobileGetDeviceLogic.messageReceived(ctx, state.user, msg);\n                break;\n\n            case CREATE_TAG :\n                MobileCreateTagLogic.messageReceived(ctx, state.user, msg);\n                break;\n            case UPDATE_TAG :\n                MobileUpdateTagLogic.messageReceived(ctx, state.user, msg);\n                break;\n            case DELETE_TAG :\n                MobileDeleteTagLogic.messageReceived(ctx, state.user, msg);\n                break;\n            case GET_TAGS :\n                MobileGetTagsLogic.messageReceived(ctx, state.user, msg);\n                break;\n\n            case APP_SYNC :\n                MobileSyncLogic.messageReceived(ctx, state, msg);\n                break;\n\n            case CREATE_APP :\n                MobileCreateAppLogic.messageReceived(ctx, state, msg, holder.limits.widgetSizeLimitBytes);\n                break;\n            case UPDATE_APP :\n                MobileUpdateAppLogic.messageReceived(ctx, state, msg, holder.limits.widgetSizeLimitBytes);\n                break;\n            case DELETE_APP :\n                if (deleteAppLogic == null) {\n                    this.deleteAppLogic = new MobileDeleteAppLogic(holder);\n                }\n                deleteAppLogic.messageReceived(ctx, state, msg);\n                break;\n\n            case GET_PROJECT_BY_TOKEN :\n                MobileGetProjectByTokenLogic.messageReceived(holder, ctx, state.user, msg);\n                break;\n            case EMAIL_QR :\n                if (mailQRsLogic == null) {\n                    this.mailQRsLogic = new MobileMailQRsLogic(holder);\n                }\n                mailQRsLogic.messageReceived(ctx, state.user, msg);\n                break;\n            case UPDATE_FACE :\n                MobileUpdateFaceLogic.messageReceived(holder, ctx, state.user, msg);\n                break;\n            case GET_CLONE_CODE :\n                MobileGetCloneCodeLogic.messageReceived(holder, ctx, state.user, msg);\n                break;\n            case GET_PROJECT_BY_CLONE_CODE :\n                if (getProjectByCloneCodeLogic == null) {\n                    this.getProjectByCloneCodeLogic = new MobileGetProjectByClonedTokenLogic(holder);\n                }\n                getProjectByCloneCodeLogic.messageReceived(ctx, state.user, msg);\n                break;\n            case LOGOUT :\n                MobileLogoutLogic.messageReceived(ctx, state.user, msg);\n                break;\n            case SET_WIDGET_PROPERTY :\n                MobileSetWidgetPropertyLogic.messageReceived(ctx, state.user, msg);\n                break;\n            case GET_PROVISION_TOKEN :\n                MobileGetProvisionTokenLogic.messageReceived(holder, ctx, state.user, msg);\n                break;\n            case DELETE_DEVICE_DATA :\n                MobileDeleteDeviceDataLogic.messageReceived(holder, ctx, state, msg);\n                break;\n            case CREATE_REPORT :\n                MobileCreateReportLogic.messageReceived(holder, ctx, state.user, msg);\n                break;\n            case UPDATE_REPORT :\n                MobileUpdateReportLogic.messageReceived(holder, ctx, state.user, msg);\n                break;\n            case DELETE_REPORT :\n                MobileDeleteReportLogic.messageReceived(holder, ctx, state.user, msg);\n                break;\n            case EXPORT_REPORT :\n                MobileExportReportLogic.messageReceived(holder, ctx, state.user, msg);\n                break;\n        }\n    }\n\n    @Override\n    public StateHolderBase getState() {\n        return state;\n    }\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/MobileResetPasswordHandler.java",
    "content": "package cc.blynk.server.application.handlers.main;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.core.BlockingIOProcessor;\nimport cc.blynk.server.core.dao.UserDao;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.protocol.model.messages.appllication.ResetPasswordMessage;\nimport cc.blynk.server.internal.token.BaseToken;\nimport cc.blynk.server.internal.token.ResetPassToken;\nimport cc.blynk.server.internal.token.TokensPool;\nimport cc.blynk.server.notifications.mail.MailWrapper;\nimport cc.blynk.server.notifications.mail.QrHolder;\nimport cc.blynk.server.notifications.mail.ResetQrHolder;\nimport cc.blynk.utils.StringUtils;\nimport cc.blynk.utils.TokenGeneratorUtil;\nimport cc.blynk.utils.properties.Placeholders;\nimport cc.blynk.utils.validators.BlynkEmailValidator;\nimport io.netty.channel.ChannelHandler;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.SimpleChannelInboundHandler;\nimport net.glxn.qrgen.core.image.ImageType;\nimport net.glxn.qrgen.javase.QRCode;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.illegalCommand;\nimport static cc.blynk.server.internal.CommonByteBufUtil.notAllowed;\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\nimport static cc.blynk.server.internal.CommonByteBufUtil.serverError;\nimport static cc.blynk.utils.StringUtils.encode;\n\n@ChannelHandler.Sharable\npublic class MobileResetPasswordHandler extends SimpleChannelInboundHandler<ResetPasswordMessage> {\n\n    private static final Logger log = LogManager.getLogger(MobileResetPasswordHandler.class);\n\n    private final TokensPool tokensPool;\n    private final String resetEmailSubj;\n    private final String resetEmailBody;\n    private final String resetConfirmationSubj;\n    private final String resetConfirmationBody;\n    private final MailWrapper mailWrapper;\n    private final UserDao userDao;\n    private final BlockingIOProcessor blockingIOProcessor;\n    private final String host;\n\n    public MobileResetPasswordHandler(Holder holder) {\n        this.tokensPool = holder.tokensPool;\n        String productName = holder.props.productName;\n        this.resetEmailSubj = \"Password restoration for your \" + productName + \" account.\";\n        this.resetEmailBody = holder.textHolder.appResetEmailTemplate\n                .replace(Placeholders.PRODUCT_NAME, productName);\n        this.resetConfirmationSubj = \"Your new password on \" + productName;\n        this.resetConfirmationBody = holder.textHolder.appResetEmailConfirmationTemplate\n                .replace(Placeholders.PRODUCT_NAME, productName);\n        this.mailWrapper = holder.mailWrapper;\n        this.userDao = holder.userDao;\n        this.blockingIOProcessor = holder.blockingIOProcessor;\n        this.host = holder.props.getRestoreHost();\n    }\n\n    @Override\n    protected void channelRead0(ChannelHandlerContext ctx, ResetPasswordMessage message) {\n        String[] messageParts = message.body.split(StringUtils.BODY_SEPARATOR_STRING);\n\n        switch (messageParts[0]) {\n            case \"start\" :\n                if (messageParts.length < 3) {\n                    log.debug(\"Wrong income message format.\");\n                    ctx.writeAndFlush(illegalCommand(message.id), ctx.voidPromise());\n                    return;\n                }\n                sendResetEMail(ctx, messageParts[1], messageParts[2], message.id);\n                break;\n            case \"verify\" :\n                if (messageParts.length < 2) {\n                    log.debug(\"Wrong income message format.\");\n                    ctx.writeAndFlush(illegalCommand(message.id), ctx.voidPromise());\n                    return;\n                }\n                verifyToken(ctx, messageParts[1], message.id);\n                break;\n            case \"reset\" :\n                if (messageParts.length < 3) {\n                    log.debug(\"Wrong income message format.\");\n                    ctx.writeAndFlush(illegalCommand(message.id), ctx.voidPromise());\n                    return;\n                }\n                reset(ctx, messageParts[1], messageParts[2], message.id);\n                break;\n        }\n    }\n\n    private void reset(ChannelHandlerContext ctx, String token, String passHash, int msgId) {\n        ResetPassToken resetPassToken = tokensPool.getResetPassToken(token);\n        if (resetPassToken == null) {\n            log.warn(\"Invalid token for reset pass {}\", token);\n            ctx.writeAndFlush(notAllowed(msgId), ctx.voidPromise());\n        } else {\n            String email = resetPassToken.email;\n            User user = userDao.getByName(email, resetPassToken.appName);\n            if (user == null) {\n                log.warn(\"User is not exists anymore. {}\", resetPassToken);\n                ctx.writeAndFlush(serverError(msgId), ctx.voidPromise());\n                return;\n            }\n            user.resetPass(passHash);\n            tokensPool.removeToken(token);\n            blockingIOProcessor.execute(() -> {\n                try {\n                    mailWrapper.sendHtml(email, resetConfirmationSubj,\n                            resetConfirmationBody.replace(Placeholders.EMAIL, email));\n                    log.debug(\"Confirmation {} mail sent.\", email);\n                } catch (Exception e) {\n                    log.error(\"Error sending confirmation mail for {}. Reason : {}\", email, e.getMessage());\n                }\n            });\n            ctx.writeAndFlush(ok(msgId), ctx.voidPromise());\n        }\n    }\n\n    private void verifyToken(ChannelHandlerContext ctx, String token, int msgId) {\n        BaseToken tokenBase = tokensPool.getBaseToken(token);\n        if (tokenBase == null) {\n            log.warn(\"Invalid token for reset pass {}\", token);\n            ctx.writeAndFlush(notAllowed(msgId), ctx.voidPromise());\n        } else {\n            ctx.writeAndFlush(ok(msgId), ctx.voidPromise());\n        }\n    }\n\n    private void sendResetEMail(ChannelHandlerContext ctx, String inEMail, String appName, int msgId) {\n        String trimmedEmail = inEMail.trim().toLowerCase();\n\n        if (BlynkEmailValidator.isNotValidEmail(trimmedEmail)) {\n            log.debug(\"Wrong income email for reset pass.\");\n            ctx.writeAndFlush(illegalCommand(msgId), ctx.voidPromise());\n            return;\n        }\n\n        User user = userDao.getByName(trimmedEmail, appName);\n\n        if (user == null) {\n            log.debug(\"User does not exists.\");\n            ctx.writeAndFlush(illegalCommand(msgId), ctx.voidPromise());\n            return;\n        }\n\n        if (tokensPool.hasResetToken(trimmedEmail, appName)) {\n            tokensPool.cleanupOldTokens();\n            log.warn(\"Reset code was already generated.\");\n            ctx.writeAndFlush(notAllowed(msgId), ctx.voidPromise());\n            return;\n        }\n\n        String token = TokenGeneratorUtil.generateNewToken();\n        log.info(\"{} trying to reset pass.\", trimmedEmail);\n\n        ResetPassToken userToken = new ResetPassToken(trimmedEmail, appName);\n        tokensPool.addToken(token, userToken);\n\n        String resetUrl = \"http://\" + host + \"/restore?token=\" + token + \"&email=\" + trimmedEmail;\n        String body = resetEmailBody.replace(Placeholders.RESET_URL, resetUrl);\n        String qrString = appName.toLowerCase() + \"://restore?token=\" + token + \"&email=\" + encode(trimmedEmail);\n        byte[] qrBytes = QRCode.from(qrString).to(ImageType.JPG).withSize(250, 250).stream().toByteArray();\n        QrHolder qrHolder = new ResetQrHolder(\"resetPassQr.jpg\", qrBytes);\n\n        blockingIOProcessor.execute(() -> {\n            try {\n                mailWrapper.sendWithAttachment(trimmedEmail, resetEmailSubj, body, qrHolder);\n                log.debug(\"{} mail sent.\", trimmedEmail);\n                ctx.writeAndFlush(ok(msgId), ctx.voidPromise());\n            } catch (Exception e) {\n                log.error(\"Error sending mail for {}. Reason : {}\", trimmedEmail, e.getMessage());\n                ctx.writeAndFlush(serverError(msgId), ctx.voidPromise());\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/auth/LimitChecker.java",
    "content": "package cc.blynk.server.application.handlers.main.auth;\n\nimport java.util.concurrent.atomic.AtomicInteger;\n\n/**\n * Threadsafe limit checker that resets counter once per specified period.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 08.04.18.\n */\nclass LimitChecker {\n\n    private final int limit;\n    private final long resetPeriodMillis;\n    private final AtomicInteger counter;\n    private volatile long lastResetTime;\n\n    LimitChecker(int limit, long resetPeriodMillis) {\n        this.limit = limit;\n        this.resetPeriodMillis = resetPeriodMillis;\n        this.counter = new AtomicInteger();\n        this.lastResetTime = System.currentTimeMillis();\n    }\n\n    boolean isLimitReached() {\n        var now = System.currentTimeMillis();\n        if (now - lastResetTime >= resetPeriodMillis) {\n            this.counter.set(0);\n            lastResetTime = System.currentTimeMillis();\n        }\n\n        var val = counter.get();\n        if (val > limit) {\n            return true;\n        }\n\n        counter.incrementAndGet();\n        return false;\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/auth/MobileGetServerHandler.java",
    "content": "package cc.blynk.server.application.handlers.main.auth;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.core.BlockingIOProcessor;\nimport cc.blynk.server.core.dao.UserDao;\nimport cc.blynk.server.core.protocol.model.messages.appllication.GetServerMessage;\nimport cc.blynk.server.db.DBManager;\nimport cc.blynk.utils.StringUtils;\nimport cc.blynk.utils.validators.BlynkEmailValidator;\nimport io.netty.channel.ChannelHandler;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.SimpleChannelInboundHandler;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.illegalCommand;\nimport static cc.blynk.server.internal.CommonByteBufUtil.illegalCommandBody;\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeASCIIStringMessage;\n\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 13.10.16.\n */\n@ChannelHandler.Sharable\npublic class MobileGetServerHandler extends SimpleChannelInboundHandler<GetServerMessage> {\n\n    private static final Logger log = LogManager.getLogger(MobileGetServerHandler.class);\n\n    private final BlockingIOProcessor blockingIOProcessor;\n    private final DBManager dbManager;\n    private final UserDao userDao;\n    private final String currentIp;\n\n    public MobileGetServerHandler(Holder holder) {\n        super();\n        this.blockingIOProcessor = holder.blockingIOProcessor;\n        this.dbManager = holder.dbManager;\n        this.userDao = holder.userDao;\n        this.currentIp = holder.props.host;\n    }\n\n    @Override\n    protected void channelRead0(ChannelHandlerContext ctx, GetServerMessage msg) {\n        String[] parts = StringUtils.split2(msg.body);\n\n        if (parts.length < 2) {\n            ctx.writeAndFlush(illegalCommand(msg.id), ctx.voidPromise());\n            return;\n        }\n\n        //.trim() is not used for back compatibility\n        String email = parts[0] == null ? null : parts[0].toLowerCase();\n        String appName = parts[1];\n\n        if (appName == null || appName.isEmpty() || appName.length() > 100) {\n            ctx.writeAndFlush(illegalCommand(msg.id), ctx.voidPromise());\n            return;\n        }\n\n        if (BlynkEmailValidator.isNotValidEmail(email)) {\n            ctx.writeAndFlush(illegalCommandBody(msg.id), ctx.voidPromise());\n            return;\n        }\n\n        if (userDao.contains(email, appName)) {\n            //user exists on current server. so returning ip of current server\n            ctx.writeAndFlush(makeASCIIStringMessage(msg.command, msg.id, currentIp), ctx.voidPromise());\n        } else {\n            log.debug(\"Searching user {}-{} on another server.\", email, appName);\n            //user is on other server\n            blockingIOProcessor.executeDBGetServer(() -> {\n                String userServer = dbManager.getUserServerIp(email, appName);\n                if (userServer == null || userServer.isEmpty()) {\n                    log.info(\"Could not find user ip for {}-{}. Returning current ip.\", email, appName);\n                    userServer = currentIp;\n                } else {\n                    log.info(\"Redirecting user {}-{} to server {}.\", email, appName, userServer);\n                }\n                ctx.writeAndFlush(makeASCIIStringMessage(msg.command, msg.id, userServer), ctx.voidPromise());\n            });\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/auth/MobileLoginHandler.java",
    "content": "package cc.blynk.server.application.handlers.main.auth;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.application.handlers.main.MobileHandler;\nimport cc.blynk.server.application.handlers.main.MobileResetPasswordHandler;\nimport cc.blynk.server.application.handlers.sharing.auth.MobileShareLoginHandler;\nimport cc.blynk.server.common.handlers.UserNotLoggedHandler;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.FacebookTokenResponse;\nimport cc.blynk.server.core.model.auth.Session;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.protocol.model.messages.appllication.LoginMessage;\nimport cc.blynk.server.internal.ReregisterChannelUtil;\nimport cc.blynk.utils.AppNameUtil;\nimport cc.blynk.utils.IPUtils;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelHandler;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.DefaultChannelPipeline;\nimport io.netty.channel.SimpleChannelInboundHandler;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\nimport org.asynchttpclient.AsyncCompletionHandler;\nimport org.asynchttpclient.DefaultAsyncHttpClient;\nimport org.asynchttpclient.Response;\n\nimport static cc.blynk.server.core.protocol.enums.Command.BLYNK_INTERNAL;\nimport static cc.blynk.server.core.protocol.enums.Command.OUTDATED_APP_NOTIFICATION;\nimport static cc.blynk.server.core.protocol.handlers.DefaultExceptionHandler.handleGeneralException;\nimport static cc.blynk.server.internal.CommonByteBufUtil.facebookUserLoginWithPass;\nimport static cc.blynk.server.internal.CommonByteBufUtil.illegalCommand;\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeASCIIStringMessage;\nimport static cc.blynk.server.internal.CommonByteBufUtil.notAllowed;\nimport static cc.blynk.server.internal.CommonByteBufUtil.notAuthenticated;\nimport static cc.blynk.server.internal.CommonByteBufUtil.notRegistered;\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\nimport static cc.blynk.utils.StringUtils.BODY_SEPARATOR_STRING;\n\n\n/**\n * Handler responsible for managing apps login messages.\n * Initializes netty channel with a state tied with user.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\n@ChannelHandler.Sharable\npublic class MobileLoginHandler extends SimpleChannelInboundHandler<LoginMessage> {\n\n    private static final String URL = \"https://graph.facebook.com/me?fields=email&access_token=\";\n    private static final Logger log = LogManager.getLogger(MobileLoginHandler.class);\n\n    private final Holder holder;\n    private final DefaultAsyncHttpClient asyncHttpClient;\n    private final boolean allowStoreIp;\n\n    public MobileLoginHandler(Holder holder) {\n        this.holder = holder;\n        this.asyncHttpClient = holder.asyncHttpClient;\n        this.allowStoreIp = holder.props.getAllowStoreIp();\n    }\n\n    private static void cleanPipeline(DefaultChannelPipeline pipeline) {\n        pipeline.removeIfExists(MobileLoginHandler.class);\n        pipeline.removeIfExists(UserNotLoggedHandler.class);\n        pipeline.removeIfExists(MobileGetServerHandler.class);\n        pipeline.removeIfExists(MobileRegisterHandler.class);\n        pipeline.removeIfExists(MobileShareLoginHandler.class);\n        pipeline.removeIfExists(MobileResetPasswordHandler.class);\n    }\n\n    @Override\n    protected void channelRead0(ChannelHandlerContext ctx, LoginMessage message) {\n        String[] messageParts = message.body.split(BODY_SEPARATOR_STRING);\n\n        if (messageParts.length < 2) {\n            log.error(\"Wrong income message format.\");\n            ctx.writeAndFlush(illegalCommand(message.id), ctx.voidPromise());\n            return;\n        }\n\n        ///.trim() is not used for back compatibility\n        String email = messageParts[0].toLowerCase();\n\n        Version version = messageParts.length > 3\n                ? new Version(messageParts[2], messageParts[3])\n                : Version.UNKNOWN_VERSION;\n\n        if (messageParts.length == 5) {\n            if (AppNameUtil.FACEBOOK.equals(messageParts[4])) {\n                facebookLogin(ctx, message.id, email, messageParts[1], version);\n            } else {\n                String appName = messageParts[4];\n                blynkLogin(ctx, message.id, email, messageParts[1], version, appName);\n            }\n        } else {\n            //todo this is for back compatibility\n            blynkLogin(ctx, message.id, email, messageParts[1], version, AppNameUtil.BLYNK);\n        }\n    }\n\n    private void facebookLogin(ChannelHandlerContext ctx, int messageId, String email,\n                               String token, Version version) {\n        asyncHttpClient.prepareGet(URL + token)\n                .execute(new AsyncCompletionHandler<Response>() {\n                    @Override\n                    public Response onCompleted(Response response) {\n                        if (response.getStatusCode() != 200) {\n                            String errMessage = response.getResponseBody();\n                            if (errMessage != null && errMessage.contains(\"expired\")) {\n                                log.warn(\"Facebook token expired for user {}.\", email);\n                            } else {\n                                log.warn(\"Error getting facebook token for user {}. Reason : {}\", email, errMessage);\n                            }\n                            ctx.writeAndFlush(notAllowed(messageId), ctx.voidPromise());\n                            return response;\n                        }\n\n                        try {\n                            String responseBody = response.getResponseBody();\n                            FacebookTokenResponse facebookTokenResponse =\n                                    JsonParser.parseFacebookTokenResponse(responseBody);\n                            if (email.equalsIgnoreCase(facebookTokenResponse.email)) {\n                                User user = holder.userDao.getByName(email, AppNameUtil.BLYNK);\n                                if (user == null) {\n                                    user = holder.userDao.addFacebookUser(email, AppNameUtil.BLYNK);\n                                }\n\n                                login(ctx, messageId, user, version);\n                            }\n                        } catch (Exception e) {\n                            log.error(\"Error during facebook response parsing for user {}. Reason : {}\",\n                                    email, response.getResponseBody());\n                            ctx.writeAndFlush(notAllowed(messageId), ctx.voidPromise());\n                        }\n\n                        return response;\n                    }\n\n                    @Override\n                    public void onThrowable(Throwable t) {\n                        log.error(\"Error performing facebook request. Token {} for user {}. Reason : {}\",\n                                token, email, t.getMessage());\n                        ctx.writeAndFlush(notAllowed(messageId), ctx.voidPromise());\n                    }\n                });\n    }\n\n    private void blynkLogin(ChannelHandlerContext ctx, int msgId, String email, String pass,\n                            Version version, String appName) {\n        var user = holder.userDao.getByName(email, appName);\n\n        if (user == null) {\n            log.warn(\"User '{}' not registered. {}\", email, ctx.channel().remoteAddress());\n            ctx.writeAndFlush(notRegistered(msgId), ctx.voidPromise());\n            return;\n        }\n\n        if (user.pass == null) {\n            log.warn(\"Facebook user '{}' tries to login with pass. {}\", email, ctx.channel().remoteAddress());\n            ctx.writeAndFlush(facebookUserLoginWithPass(msgId), ctx.voidPromise());\n            return;\n        }\n\n        if (!user.pass.equals(pass)) {\n            log.warn(\"User '{}' credentials are wrong. {}\", email, ctx.channel().remoteAddress());\n            ctx.writeAndFlush(notAuthenticated(msgId), ctx.voidPromise());\n            return;\n        }\n\n        login(ctx, msgId, user, version);\n    }\n\n    private void login(ChannelHandlerContext ctx, int messageId, User user, Version version) {\n        var pipeline = (DefaultChannelPipeline) ctx.pipeline();\n        cleanPipeline(pipeline);\n\n        var appStateHolder = new MobileStateHolder(user, version);\n        pipeline.addLast(\"AAppHandler\", new MobileHandler(holder, appStateHolder));\n\n        var channel = ctx.channel();\n\n        //todo back compatibility code. remove in future.\n        if (user.region == null || user.region.isEmpty()) {\n            user.region = holder.props.region;\n        }\n\n        var session = holder.sessionDao.getOrCreateSessionByUser(appStateHolder.userKey, channel.eventLoop());\n        if (session.isSameEventLoop(channel)) {\n            completeLogin(channel, session, user, messageId, version);\n        } else {\n            log.debug(\"Re registering app channel. {}\", ctx.channel());\n            ReregisterChannelUtil.reRegisterChannel(ctx, session, channelFuture ->\n                    completeLogin(channelFuture.channel(), session, user, messageId, version));\n        }\n    }\n\n    private void completeLogin(Channel channel, Session session, User user, int msgId, Version version) {\n        if (allowStoreIp) {\n            user.lastLoggedIP = IPUtils.getIp(channel.remoteAddress());\n        }\n        user.lastLoggedAt = System.currentTimeMillis();\n\n        session.addAppChannel(channel);\n        channel.writeAndFlush(ok(msgId), channel.voidPromise());\n        for (DashBoard dashBoard : user.profile.dashBoards) {\n            if (dashBoard.isAppConnectedOn && dashBoard.isActive) {\n                log.trace(\"{}-{}. Sending App Connected event to hardware for project {}.\",\n                        user.email, user.appName, dashBoard.id);\n                session.sendMessageToHardware(dashBoard.id, BLYNK_INTERNAL, 7777, \"acon\");\n            }\n        }\n\n        if (version.isOutdated()) {\n            channel.writeAndFlush(\n                    makeASCIIStringMessage(OUTDATED_APP_NOTIFICATION, msgId,\n                            \"Your app is outdated. Please update to the latest app version. \"\n                                    + \"Ignoring this notice may affect your projects.\"),\n                    channel.voidPromise());\n        }\n\n        log.info(\"{} {}-app ({}) joined.\", user.email, user.appName, version);\n    }\n\n    @Override\n    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {\n        handleGeneralException(ctx, cause);\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/auth/MobileRegisterHandler.java",
    "content": "package cc.blynk.server.application.handlers.main.auth;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.core.BlockingIOProcessor;\nimport cc.blynk.server.core.dao.TokenManager;\nimport cc.blynk.server.core.dao.UserDao;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.protocol.model.messages.appllication.RegisterMessage;\nimport cc.blynk.server.notifications.mail.MailWrapper;\nimport cc.blynk.server.workers.timer.TimerWorker;\nimport cc.blynk.utils.AppNameUtil;\nimport cc.blynk.utils.StringUtils;\nimport cc.blynk.utils.validators.BlynkEmailValidator;\nimport io.netty.channel.ChannelHandler;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.SimpleChannelInboundHandler;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.alreadyRegistered;\nimport static cc.blynk.server.internal.CommonByteBufUtil.illegalCommand;\nimport static cc.blynk.server.internal.CommonByteBufUtil.notAllowed;\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\n\n/**\n * Process register message.\n * Divides input string by nil char on 3 parts:\n * \"username\" \"password\" \"appName\".\n * Checks if user not registered yet. If not - registering.\n *\n * For instance, incoming register message may be : \"user@mail.ua my_password\"\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n */\n@ChannelHandler.Sharable\npublic class MobileRegisterHandler extends SimpleChannelInboundHandler<RegisterMessage> {\n\n    private static final Logger log = LogManager.getLogger(MobileRegisterHandler.class);\n\n    private final UserDao userDao;\n    private final TokenManager tokenManager;\n    private final TimerWorker timerWorker;\n    private final MailWrapper mailWrapper;\n    private final BlockingIOProcessor blockingIOProcessor;\n    private final LimitChecker registrationLimitChecker;\n    private final String emailBody;\n\n    public MobileRegisterHandler(Holder holder) {\n        this.userDao = holder.userDao;\n        this.tokenManager = holder.tokenManager;\n        this.timerWorker = holder.timerWorker;\n        this.mailWrapper = holder.mailWrapper;\n        this.blockingIOProcessor = holder.blockingIOProcessor;\n        this.registrationLimitChecker = new LimitChecker(holder.limits.hourlyRegistrationsLimit, 3_600_000L);\n        this.emailBody = holder.textHolder.registerEmailTemplate;\n    }\n\n    @Override\n    protected void channelRead0(ChannelHandlerContext ctx, RegisterMessage message) {\n        if (registrationLimitChecker.isLimitReached()) {\n            log.error(\"Register Handler. Registration limit reached. {}\", message);\n            ctx.writeAndFlush(notAllowed(message.id), ctx.voidPromise());\n            return;\n        }\n\n        String[] messageParts = StringUtils.split3(message.body);\n\n        //expecting message with 2 parts at least.\n        if (messageParts.length < 3) {\n            log.error(\"Register Handler. Wrong income message format. {}\", message);\n            ctx.writeAndFlush(illegalCommand(message.id), ctx.voidPromise());\n            return;\n        }\n\n        String email = messageParts[0].trim().toLowerCase();\n        String passHash = messageParts[1];\n        String appName = messageParts[2];\n        log.info(\"Trying register user : {}, app : {}\", email, appName);\n\n        if (BlynkEmailValidator.isNotValidEmail(email)) {\n            log.error(\"Register Handler. Wrong email: {}\", email);\n            ctx.writeAndFlush(illegalCommand(message.id), ctx.voidPromise());\n            return;\n        }\n\n        if (userDao.isUserExists(email, appName)) {\n            log.warn(\"User with email {} already exists.\", email);\n            ctx.writeAndFlush(alreadyRegistered(message.id), ctx.voidPromise());\n            return;\n        }\n\n        User newUser = userDao.add(email, passHash, appName);\n\n        log.info(\"Registered {}.\", email);\n\n        //sending greeting email only for Blynk apps\n        if (AppNameUtil.BLYNK.equals(appName)) {\n            blockingIOProcessor.execute(() -> {\n                try {\n                    mailWrapper.sendHtml(email, \"Get started with Blynk\", emailBody);\n                } catch (Exception e) {\n                    log.warn(\"Error sending greeting email for {}.\", email);\n                }\n            });\n        }\n\n        userDao.createProjectForExportedApp(timerWorker, tokenManager, newUser, appName, message.id);\n\n        ctx.pipeline().remove(this);\n        ctx.writeAndFlush(ok(message.id), ctx.voidPromise());\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/auth/MobileStateHolder.java",
    "content": "package cc.blynk.server.application.handlers.main.auth;\n\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.session.StateHolderBase;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 13.09.15.\n */\npublic class MobileStateHolder extends StateHolderBase {\n\n    public final Version version;\n\n    public MobileStateHolder(User user, Version version) {\n        super(user);\n        this.version = version;\n    }\n\n    @Override\n    public boolean contains(String sharedToken) {\n        return true;\n    }\n\n    @Override\n    public boolean isSameDash(int inDashId) {\n        return true;\n    }\n\n    @Override\n    public boolean isSameDevice(int deviceId) {\n        return true;\n    }\n\n    @Override\n    public boolean isSameDashAndDeviceId(int inDashId, int deviceId) {\n        return true;\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/auth/OsType.java",
    "content": "package cc.blynk.server.application.handlers.main.auth;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 16.03.16.\n */\npublic enum OsType {\n\n    ANDROID(\"android\"),\n    IOS(\"iOS\"),\n    //3d party clients or unknown clients\n    OTHER(\"unknown\");\n\n    public final String label;\n\n    OsType(String label) {\n        this.label = label;\n    }\n\n    public static OsType parse(String type) {\n        switch (type.toLowerCase()) {\n            case \"ios\" :\n                return IOS;\n            case \"android\" :\n                return ANDROID;\n            default:\n                return OTHER;\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/auth/Version.java",
    "content": "package cc.blynk.server.application.handlers.main.auth;\n\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.utils.StringUtils.split3;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 24.12.17.\n */\npublic final class Version {\n\n    private static final Logger log = LogManager.getLogger(Version.class);\n\n    public static final Version UNKNOWN_VERSION = new Version(OsType.OTHER, 0);\n\n    public final OsType osType;\n    final int versionSingleNumber;\n\n    public Version(OsType osType, int version) {\n        this.osType = osType;\n        this.versionSingleNumber = version;\n    }\n\n    public Version(String osString, String versionString) {\n        this(OsType.parse(osString), parseToSingleInt(versionString));\n    }\n\n    /**\n     * Expecting only strings like \"1.2.2\"\n     */\n    private static int parseToSingleInt(String version) {\n        try {\n            var parts = split3('.', version);\n            return parse(parts);\n        } catch (Exception e) {\n            log.debug(\"Error parsing app versionSingleNumber {}. Reason : {}.\", version, e.getMessage());\n        }\n        return 0;\n    }\n\n    private static int parse(String[] parts) {\n        return Integer.parseInt(parts[0]) * 10_000\n                + Integer.parseInt(parts[1]) * 100\n                + Integer.parseInt(parts[2]);\n    }\n\n    boolean largerOrEqualThan(int version) {\n        return this.versionSingleNumber >= version;\n    }\n\n    //this method should be changed to notify users that they use outdated app.\n    //done mostly for some changes that cannot be used on old apps.\n    //not used right now.\n    boolean isOutdated() {\n        //hardcoded value for tests\n        return versionSingleNumber == 10101;\n    }\n\n    @Override\n    public String toString() {\n        return osType.label + \"-\" + versionSingleNumber;\n    }\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/LoadSharedProfileGzippedLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic;\n\nimport cc.blynk.server.application.handlers.sharing.auth.MobileShareStateHolder;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.Profile;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport io.netty.channel.ChannelHandlerContext;\n\nimport static cc.blynk.server.core.model.serialization.JsonParser.gzipDashRestrictive;\nimport static cc.blynk.server.core.model.serialization.JsonParser.gzipProfileRestrictive;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic final class LoadSharedProfileGzippedLogic {\n\n    private LoadSharedProfileGzippedLogic() {\n    }\n\n    public static void messageReceived(ChannelHandlerContext ctx, MobileShareStateHolder state, StringMessage message) {\n        byte[] data;\n        var user = state.user;\n        if (message.body.length() == 0) {\n            var dash = user.profile.getDashByIdOrThrow(state.dashId);\n            var profile = new Profile();\n            profile.dashBoards = new DashBoard[] {dash};\n            data = gzipProfileRestrictive(profile);\n        } else {\n            //load specific by id\n            var dashId = Integer.parseInt(message.body);\n            var dash = user.profile.getDashByIdOrThrow(dashId);\n            data = gzipDashRestrictive(dash);\n        }\n        MobileLoadProfileGzippedLogic.write(ctx, data, message.id);\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/MobileActivateDashboardLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.application.handlers.main.auth.MobileStateHolder;\nimport cc.blynk.server.core.dao.SessionDao;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.Session;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.core.model.widgets.MobileSyncWidget.ANY_TARGET;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE;\nimport static cc.blynk.server.internal.CommonByteBufUtil.deviceNotInNetwork;\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeUTF8StringMessage;\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\nimport static cc.blynk.utils.MobileStateHolderUtil.getAppState;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic final class MobileActivateDashboardLogic {\n\n    private static final int PIN_MODE_MSG_ID = 1;\n\n    private static final Logger log = LogManager.getLogger(MobileActivateDashboardLogic.class);\n\n    private MobileActivateDashboardLogic() {\n    }\n\n    public static void messageReceived(Holder holder, ChannelHandlerContext ctx,\n                                       MobileStateHolder state, StringMessage message) {\n        User user = state.user;\n        String dashBoardIdString = message.body;\n\n        int dashId = Integer.parseInt(dashBoardIdString);\n\n        log.debug(\"Activating dash {} for user {}\", dashBoardIdString, user.email);\n        DashBoard dash = user.profile.getDashByIdOrThrow(dashId);\n        dash.activate();\n        user.lastModifiedTs = dash.updatedAt;\n\n        SessionDao sessionDao = holder.sessionDao;\n        Session session = sessionDao.get(state.userKey);\n\n        if (session.isHardwareConnected(dashId)) {\n            for (Device device : dash.devices) {\n                String pmBody = dash.buildPMMessage(device.id);\n                if (pmBody == null) {\n                    if (!session.isHardwareConnected(dashId, device.id)) {\n                        log.debug(\"No device in session.\");\n                        if (ctx.channel().isWritable() && !dash.isNotificationsOff) {\n                            ctx.write(deviceNotInNetwork(PIN_MODE_MSG_ID), ctx.voidPromise());\n                        }\n                    }\n                } else {\n                    if (device.fitsBufferSize(pmBody.length())) {\n                        if (session.sendMessageToHardware(dashId, HARDWARE, PIN_MODE_MSG_ID, pmBody, device.id)) {\n                            log.debug(\"No device in session.\");\n                            if (ctx.channel().isWritable() && !dash.isNotificationsOff) {\n                                ctx.write(deviceNotInNetwork(PIN_MODE_MSG_ID), ctx.voidPromise());\n                            }\n                        }\n                    } else {\n                        ctx.write(deviceNotInNetwork(message.id), ctx.voidPromise());\n                        log.warn(\"PM message is to large for {}, size : {}\", user.email, pmBody.length());\n                    }\n                }\n            }\n\n            ctx.write(ok(message.id), ctx.voidPromise());\n        } else {\n            log.debug(\"No device in session.\");\n            if (dash.isNotificationsOff) {\n                ctx.write(ok(message.id), ctx.voidPromise());\n            } else {\n                ctx.write(deviceNotInNetwork(message.id), ctx.voidPromise());\n            }\n        }\n        ctx.flush();\n\n        for (Channel appChannel : session.appChannels) {\n            //send activate for shared apps\n            MobileStateHolder mobileStateHolder = getAppState(appChannel);\n            if (appChannel != ctx.channel() && mobileStateHolder != null && appChannel.isWritable()) {\n                appChannel.write(makeUTF8StringMessage(message.command, message.id, message.body));\n            }\n\n            user.profile.sendAppSyncs(dash, appChannel, ANY_TARGET);\n            appChannel.flush();\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/MobileAddPushLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.application.handlers.main.auth.MobileStateHolder;\nimport cc.blynk.server.core.BlockingIOProcessor;\nimport cc.blynk.server.core.dao.UserDao;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.widgets.notifications.Notification;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.utils.StringUtils;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.notAllowed;\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic final class MobileAddPushLogic {\n\n    private static final Logger log = LogManager.getLogger(MobileAddPushLogic.class);\n\n    private final BlockingIOProcessor blockingIOProcessor;\n    private final UserDao userDao;\n\n    public MobileAddPushLogic(Holder holder) {\n        this.blockingIOProcessor = holder.blockingIOProcessor;\n        this.userDao = holder.userDao;\n    }\n\n    public void messageReceived(ChannelHandlerContext ctx, MobileStateHolder state, StringMessage message) {\n        String[] splitBody = StringUtils.split3(message.body);\n\n        int dashId = Integer.parseInt(splitBody[0]);\n        String uid = splitBody[1];\n        String token = splitBody[2];\n\n        DashBoard dash = state.user.profile.getDashByIdOrThrow(dashId);\n\n        Notification notification = dash.getNotificationWidget();\n\n        if (notification == null) {\n            log.error(\"No notification widget.\");\n            ctx.writeAndFlush(notAllowed(message.id), ctx.voidPromise());\n            return;\n        }\n\n        switch (state.version.osType) {\n            case ANDROID :\n                notification.androidTokens.put(uid, token);\n                break;\n            case IOS :\n                //this fix is only for iOS, because it doesn't have enough control\n                //over the notifications, so we have to manage this on the server\n                /*\n                blockingIOProcessor.execute(() -> {\n                    try {\n                        for (User user : userDao.users.values()) {\n                            for (DashBoard dashBoard : user.profile.dashBoards) {\n                                Notification tempNotifWidget = dashBoard.getNotificationWidget();\n                                if (tempNotifWidget != null && tempNotifWidget != notification) {\n                                    tempNotifWidget.iOSTokens.remove(uid);\n                                }\n                            }\n                        }\n                    } catch (Exception e) {\n                        log.debug(\"Fail on ios token cleanup.\", e);\n                    }\n                });\n                 */\n                notification.iOSTokens.put(uid, token);\n                break;\n        }\n\n        ctx.writeAndFlush(ok(message.id), ctx.voidPromise());\n    }\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/MobileAssignTokenLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.core.dao.TokenManager;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.server.db.DBManager;\nimport cc.blynk.server.db.model.FlashedToken;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.notAllowed;\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\nimport static cc.blynk.utils.StringUtils.split2;\n\n/**\n * Assigns static generated token to assigned device.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic final class MobileAssignTokenLogic {\n\n    private static final Logger log = LogManager.getLogger(MobileAssignTokenLogic.class);\n\n    private MobileAssignTokenLogic() {\n    }\n\n    public static void messageReceived(Holder holder, ChannelHandlerContext ctx,\n                                       User user, StringMessage message) {\n        String[] split = split2(message.body);\n\n        int dashId = Integer.parseInt(split[0]);\n        String token = split[1];\n        DashBoard dash = user.profile.getDashByIdOrThrow(dashId);\n\n        DBManager dbManager = holder.dbManager;\n        TokenManager tokenManager = holder.tokenManager;\n        holder.blockingIOProcessor.executeDB(() -> {\n            FlashedToken dbFlashedToken = dbManager.selectFlashedToken(token);\n\n            if (dbFlashedToken == null) {\n                log.error(\"{} token not exists for app {}.\", token, user.appName);\n                ctx.writeAndFlush(notAllowed(message.id), ctx.voidPromise());\n                return;\n            }\n\n            if (dbFlashedToken.isActivated) {\n                log.error(\"{} token is already activated for app {}.\", token, user.appName);\n                ctx.writeAndFlush(notAllowed(message.id), ctx.voidPromise());\n                return;\n            }\n\n            Device device = user.profile.getDeviceById(dash, dbFlashedToken.deviceId);\n\n            if (device == null) {\n                log.error(\"Device with {} id not exists in dashboards.\", dbFlashedToken.deviceId);\n                ctx.writeAndFlush(notAllowed(message.id), ctx.voidPromise());\n                return;\n            }\n\n            if (!dbManager.activateFlashedToken(token)) {\n                log.error(\"Error activated flashed token {}\", token);\n                ctx.writeAndFlush(notAllowed(message.id), ctx.voidPromise());\n                return;\n            }\n\n            tokenManager.assignToken(user, dash, device, token);\n\n            ctx.writeAndFlush(ok(message.id), ctx.voidPromise());\n        });\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/MobileDeActivateDashboardLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.application.handlers.main.auth.MobileStateHolder;\nimport cc.blynk.server.core.dao.SessionDao;\nimport cc.blynk.server.core.dao.SharedTokenManager;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic final class MobileDeActivateDashboardLogic {\n\n    private static final Logger log = LogManager.getLogger(MobileActivateDashboardLogic.class);\n\n    private MobileDeActivateDashboardLogic() {\n    }\n\n    public static void messageReceived(Holder holder, ChannelHandlerContext ctx,\n                                       MobileStateHolder state, StringMessage message) {\n        var user = state.user;\n\n        String sharedToken;\n        if (message.body.length() > 0) {\n            log.debug(\"DeActivating dash {} for user {}\", message.body, user.email);\n            int dashId = Integer.parseInt(message.body);\n            DashBoard dashBoard = user.profile.getDashByIdOrThrow(dashId);\n            dashBoard.deactivate();\n            sharedToken = dashBoard.sharedToken;\n        } else {\n            for (DashBoard dashBoard : user.profile.dashBoards) {\n                dashBoard.deactivate();\n            }\n            sharedToken = SharedTokenManager.ALL;\n        }\n        user.lastModifiedTs = System.currentTimeMillis();\n\n        SessionDao sessionDao = holder.sessionDao;\n        var session = sessionDao.get(state.userKey);\n        session.sendToSharedApps(ctx.channel(), sharedToken, message.command, message.id, message.body);\n        ctx.writeAndFlush(ok(message.id), ctx.voidPromise());\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/MobileGetCloneCodeLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.serialization.CopyUtil;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.protocol.model.messages.MessageBase;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.utils.TokenGeneratorUtil;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.core.protocol.enums.Command.GET_CLONE_CODE;\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeASCIIStringMessage;\nimport static cc.blynk.server.internal.CommonByteBufUtil.serverError;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic final class MobileGetCloneCodeLogic {\n\n    private static final Logger log = LogManager.getLogger(MobileGetCloneCodeLogic.class);\n\n    private MobileGetCloneCodeLogic() {\n    }\n\n    public static void messageReceived(Holder holder, ChannelHandlerContext ctx,\n                                       User user, StringMessage message) {\n        int dashId = Integer.parseInt(message.body);\n\n        //todo all this is very ugly, however takes 5 min for implementation, also this is rare feature\n        DashBoard dash = user.profile.getDashByIdOrThrow(dashId);\n        DashBoard copiedDash = CopyUtil.deepCopy(dash);\n        copiedDash.eraseWidgetValues();\n\n        String json = JsonParser.toJsonRestrictiveDashboard(copiedDash);\n        String qrToken = TokenGeneratorUtil.generateNewToken();\n\n        holder.blockingIOProcessor.executeDB(() -> {\n            MessageBase result;\n            try {\n                boolean insertStatus = holder.dbManager.insertClonedProject(qrToken, json);\n                if (insertStatus || holder.fileManager.writeCloneProjectToDisk(qrToken, json)) {\n                    result = makeASCIIStringMessage(GET_CLONE_CODE, message.id, qrToken);\n                } else {\n                    log.error(\"Creating clone project failed for {}\", user.email);\n                    result = serverError(message.id);\n                }\n            } catch (Exception e) {\n                log.error(\"Error cloning project.\", e);\n                result = serverError(message.id);\n            }\n            ctx.writeAndFlush(result, ctx.voidPromise());\n        });\n    }\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/MobileGetEnergyLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic;\n\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport io.netty.channel.ChannelHandlerContext;\n\nimport static cc.blynk.server.core.protocol.enums.Command.GET_ENERGY;\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeASCIIStringMessage;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 14.03.16.\n */\npublic final class MobileGetEnergyLogic {\n\n    private MobileGetEnergyLogic() {\n    }\n\n    public static void messageReceived(ChannelHandlerContext ctx, User user, StringMessage message) {\n        if (ctx.channel().isWritable()) {\n            ctx.writeAndFlush(makeASCIIStringMessage(GET_ENERGY, message.id, \"\" + user.energy), ctx.voidPromise());\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/MobileGetProjectByClonedTokenLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.core.BlockingIOProcessor;\nimport cc.blynk.server.core.dao.FileManager;\nimport cc.blynk.server.core.dao.TokenManager;\nimport cc.blynk.server.core.dao.UserKey;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.protocol.model.messages.MessageBase;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.server.db.DBManager;\nimport cc.blynk.server.workers.timer.TimerWorker;\nimport cc.blynk.utils.ArrayUtil;\nimport cc.blynk.utils.ByteUtils;\nimport cc.blynk.utils.StringUtils;\nimport cc.blynk.utils.TokenGeneratorUtil;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.io.IOException;\n\nimport static cc.blynk.server.core.protocol.enums.Command.GET_PROJECT_BY_CLONE_CODE;\nimport static cc.blynk.server.internal.CommonByteBufUtil.energyLimit;\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeBinaryMessage;\nimport static cc.blynk.server.internal.CommonByteBufUtil.notAllowed;\nimport static cc.blynk.server.internal.CommonByteBufUtil.quotaLimit;\nimport static cc.blynk.server.internal.CommonByteBufUtil.serverError;\nimport static cc.blynk.utils.StringUtils.split2;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic class MobileGetProjectByClonedTokenLogic {\n\n    private static final Logger log = LogManager.getLogger(MobileGetProjectByClonedTokenLogic.class);\n\n    private final BlockingIOProcessor blockingIOProcessor;\n    private final DBManager dbManager;\n    private final FileManager fileManager;\n    private final TimerWorker timerWorker;\n    private final TokenManager tokenManager;\n    private final int dashMaxLimit;\n\n    public MobileGetProjectByClonedTokenLogic(Holder holder) {\n        this.blockingIOProcessor = holder.blockingIOProcessor;\n        this.dbManager = holder.dbManager;\n        this.fileManager = holder.fileManager;\n        this.dashMaxLimit = holder.limits.dashboardsLimit;\n        this.timerWorker = holder.timerWorker;\n        this.tokenManager = holder.tokenManager;\n    }\n\n    public void messageReceived(ChannelHandlerContext ctx, User user, StringMessage message) {\n        String token;\n        boolean newFlow;\n        if (message.body.contains(StringUtils.BODY_SEPARATOR_STRING)) {\n            newFlow = true;\n            token = split2(message.body)[0];\n        } else {\n            newFlow = false;\n            token = message.body;\n        }\n\n        blockingIOProcessor.executeDB(() -> {\n            MessageBase result;\n            try {\n                String json = dbManager.selectClonedProject(token);\n                //no cloned project in DB, checking local storage on disk\n                if (json == null) {\n                    json = fileManager.readClonedProjectFromDisk(token);\n                }\n                if (json == null) {\n                    log.debug(\"Cannot find request clone QR. {}\", token);\n                    result = serverError(message.id);\n                } else {\n                    if (newFlow) {\n                        result = createDashboard(user, json, message.id);\n                    } else {\n                        byte[] data = ByteUtils.compress(json);\n                        result = makeBinaryMessage(GET_PROJECT_BY_CLONE_CODE, message.id, data);\n                    }\n                }\n            } catch (Exception e) {\n                log.error(\"Error getting cloned project.\", e);\n                result = serverError(message.id);\n            }\n            ctx.writeAndFlush(result, ctx.voidPromise());\n        });\n    }\n\n    private MessageBase createDashboard(User user, String dashString, int msgId) throws IOException {\n        DashBoard newDash = JsonParser.parseDashboard(dashString, msgId);\n        newDash.id = max(user.profile.dashBoards) + 1;\n        newDash.isPreview = false;\n        newDash.parentId = -1;\n        newDash.isShared = false;\n\n        if (user.profile.dashBoards.length >= dashMaxLimit) {\n            log.debug(\"Dashboards limit reached.\");\n            return quotaLimit(msgId);\n        }\n\n        for (DashBoard dashBoard : user.profile.dashBoards) {\n            if (dashBoard.id == newDash.id) {\n                log.debug(\"Dashboard already exists.\");\n                return notAllowed(msgId);\n            }\n        }\n\n        log.info(\"Creating new cloned dashboard.\");\n\n        if (newDash.createdAt == 0) {\n            newDash.createdAt = System.currentTimeMillis();\n        }\n\n        int price = newDash.energySum();\n        if (user.notEnoughEnergy(price)) {\n            log.debug(\"Not enough energy.\");\n            return energyLimit(msgId);\n        }\n        user.subtractEnergy(price);\n        user.profile.dashBoards = ArrayUtil.add(user.profile.dashBoards, newDash, DashBoard.class);\n\n        if (newDash.devices != null) {\n            for (Device device : newDash.devices) {\n                String token = TokenGeneratorUtil.generateNewToken();\n                tokenManager.assignToken(user, newDash, device, token);\n            }\n        }\n\n        user.lastModifiedTs = System.currentTimeMillis();\n\n        newDash.addTimers(timerWorker, new UserKey(user));\n\n        byte[] data = ByteUtils.compress(newDash.toString());\n        return makeBinaryMessage(GET_PROJECT_BY_CLONE_CODE, msgId, data);\n    }\n\n    private int max(DashBoard[] data) {\n        int result = 0;\n        for (DashBoard dash : data) {\n            result = Math.max(result, dash.id);\n        }\n        return result;\n    }\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/MobileGetProjectByTokenLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.serialization.CopyUtil;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.server.db.model.FlashedToken;\nimport cc.blynk.utils.AppNameUtil;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.core.protocol.enums.Command.GET_PROJECT_BY_TOKEN;\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeBinaryMessage;\nimport static cc.blynk.server.internal.CommonByteBufUtil.notAllowed;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic final class MobileGetProjectByTokenLogic {\n\n    private static final Logger log = LogManager.getLogger(MobileGetProjectByTokenLogic.class);\n\n    private MobileGetProjectByTokenLogic() {\n    }\n\n    public static void messageReceived(Holder holder, ChannelHandlerContext ctx,\n                                       User user, StringMessage message) {\n        String token = message.body;\n\n        holder.blockingIOProcessor.executeDB(() -> {\n            FlashedToken dbFlashedToken = holder.dbManager.selectFlashedToken(token);\n\n            if (dbFlashedToken == null) {\n                log.error(\"{} token not exists for app {} for {} (GetProject).\", token, user.appName, user.email);\n                ctx.writeAndFlush(notAllowed(message.id), ctx.voidPromise());\n                return;\n            }\n\n            User publishUser = holder.userDao.getByName(dbFlashedToken.email, AppNameUtil.BLYNK);\n\n            DashBoard dash = publishUser.profile.getDashById(dbFlashedToken.dashId);\n            DashBoard copy = CopyUtil.deepCopy(dash);\n\n            if (copy == null) {\n                log.error(\"Dash with {} id not exists in dashboards.\", dbFlashedToken.dashId);\n                ctx.writeAndFlush(notAllowed(message.id), ctx.voidPromise());\n                return;\n            }\n\n            copy.eraseWidgetValues();\n\n            write(ctx, JsonParser.gzipDashRestrictive(copy), message.id);\n        });\n    }\n\n    public static void write(ChannelHandlerContext ctx, byte[] data, int msgId) {\n        if (ctx.channel().isWritable()) {\n            var outputMsg = makeBinaryMessage(GET_PROJECT_BY_TOKEN, msgId, data);\n            ctx.writeAndFlush(outputMsg, ctx.voidPromise());\n        }\n    }\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/MobileGetProvisionTokenLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandException;\nimport cc.blynk.server.core.protocol.exceptions.NotAllowedException;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.utils.TokenGeneratorUtil;\nimport io.netty.channel.ChannelHandlerContext;\n\nimport static cc.blynk.server.core.protocol.enums.Command.GET_PROVISION_TOKEN;\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeASCIIStringMessage;\nimport static cc.blynk.utils.StringUtils.split2;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 06.04.18.\n */\npublic final class MobileGetProvisionTokenLogic {\n\n    private MobileGetProvisionTokenLogic() {\n    }\n\n    public static void messageReceived(Holder holder, ChannelHandlerContext ctx, User user, StringMessage message) {\n        String[] split = split2(message.body);\n\n        int dashId = Integer.parseInt(split[0]);\n        DashBoard dash = user.profile.getDashByIdOrThrow(dashId);\n\n        String deviceString = split[1];\n        if (deviceString == null || deviceString.isEmpty()) {\n            throw new IllegalCommandException(\"Income device message is empty.\");\n        }\n\n        Device temporaryDevice = JsonParser.parseDevice(deviceString, message.id);\n\n        if (temporaryDevice.isNotValid()) {\n            throw new IllegalCommandException(\"Income device message is not valid.\");\n        }\n\n        for (Device device : dash.devices) {\n            if (device.id == temporaryDevice.id) {\n                throw new NotAllowedException(\"Device with same id already exists.\", message.id);\n            }\n        }\n\n        String tempToken = TokenGeneratorUtil.generateNewToken();\n        holder.tokenManager.assignToken(user, dash, temporaryDevice, tempToken, true);\n\n        if (ctx.channel().isWritable()) {\n            ctx.writeAndFlush(makeASCIIStringMessage(GET_PROVISION_TOKEN,\n                    message.id, temporaryDevice.toString()), ctx.voidPromise());\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/MobileHardwareLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.application.handlers.main.auth.MobileStateHolder;\nimport cc.blynk.server.core.dao.SessionDao;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.Profile;\nimport cc.blynk.server.core.model.auth.Session;\nimport cc.blynk.server.core.model.device.Tag;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.widgets.Target;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.ui.DeviceSelector;\nimport cc.blynk.server.core.processors.BaseProcessorHandler;\nimport cc.blynk.server.core.processors.WebhookProcessor;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.utils.NumberUtil;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.core.protocol.enums.Command.APP_SYNC;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE;\nimport static cc.blynk.server.internal.CommonByteBufUtil.deviceNotInNetwork;\nimport static cc.blynk.server.internal.CommonByteBufUtil.illegalCommandBody;\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\nimport static cc.blynk.utils.MobileStateHolderUtil.getAppState;\nimport static cc.blynk.utils.StringUtils.split2;\nimport static cc.blynk.utils.StringUtils.split2Device;\nimport static cc.blynk.utils.StringUtils.split3;\n\n/**\n * Responsible for handling incoming hardware commands from applications and forwarding it to\n * appropriate hardware.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic class MobileHardwareLogic extends BaseProcessorHandler {\n\n    private static final Logger log = LogManager.getLogger(MobileHardwareLogic.class);\n\n    private final SessionDao sessionDao;\n\n    public MobileHardwareLogic(Holder holder, String email) {\n        super(holder.eventorProcessor, new WebhookProcessor(holder.asyncHttpClient,\n                holder.limits.webhookPeriodLimitation,\n                holder.limits.webhookResponseSizeLimitBytes,\n                holder.limits.webhookFailureLimit,\n                holder.stats,\n                email));\n        this.sessionDao = holder.sessionDao;\n    }\n\n    public void messageReceived(ChannelHandlerContext ctx, MobileStateHolder state, StringMessage message) {\n        Session session = sessionDao.get(state.userKey);\n\n        //here expecting command in format \"1-200000 vw 88 1\"\n        String[] split = split2(message.body);\n\n        //here we have \"1-200000\"\n        String[] dashIdAndTargetIdString = split2Device(split[0]);\n        int dashId = Integer.parseInt(dashIdAndTargetIdString[0]);\n\n        Profile profile = state.user.profile;\n        DashBoard dash = profile.getDashByIdOrThrow(dashId);\n\n        //if no active dashboard - do nothing. this could happen only in case of app. bug\n        if (!dash.isActive) {\n            return;\n        }\n\n        //deviceId or tagId or device selector widget id\n        int targetId = 0;\n\n        //new logic for multi devices\n        if (dashIdAndTargetIdString.length == 2) {\n            targetId = Integer.parseInt(dashIdAndTargetIdString[1]);\n        }\n\n        //sending message only if widget assigned to device or tag has assigned devices\n        Target target;\n        if (targetId < Tag.START_TAG_ID) {\n            target = profile.getDeviceById(dash, targetId);\n        } else if (targetId < DeviceSelector.DEVICE_SELECTOR_STARTING_ID) {\n            target = profile.getTagById(dash, targetId);\n        } else {\n            //means widget assigned to device selector widget.\n            target = dash.getDeviceSelector(targetId);\n        }\n\n        if (target == null) {\n            log.debug(\"No assigned target id for received command.\");\n            return;\n        }\n\n        int[] deviceIds = target.getDeviceIds();\n\n        if (deviceIds.length == 0) {\n            log.debug(\"No devices assigned to target.\");\n            return;\n        }\n\n        char operation = split[1].charAt(1);\n        switch (operation) {\n            case 'u' :\n                //splitting \"vu 200000 1\"\n                String[] splitBody = split3(split[1]);\n                processDeviceSelectorCommand(ctx, session, state.user.profile, dash, message, splitBody);\n                break;\n            case 'w' :\n                splitBody = split3(split[1]);\n\n                if (splitBody.length < 3) {\n                    log.debug(\"Not valid write command.\");\n                    ctx.writeAndFlush(illegalCommandBody(message.id), ctx.voidPromise());\n                    return;\n                }\n\n                PinType pinType = PinType.getPinType(splitBody[0].charAt(0));\n                short pin = NumberUtil.parsePin(splitBody[1]);\n                String value = splitBody[2];\n                long now = System.currentTimeMillis();\n\n                for (int deviceId : deviceIds) {\n                    profile.update(dash, deviceId, pin, pinType, value, now);\n                }\n\n                //additional state for tag widget itself\n                if (target.isTag()) {\n                    profile.update(dash, targetId, pin, pinType, value, now);\n                }\n\n                //sending to shared dashes and master-master apps\n                session.sendToSharedApps(ctx.channel(), dash.sharedToken, APP_SYNC, message.id, message.body);\n\n                if (session.sendMessageToHardware(dashId, HARDWARE, message.id, split[1], deviceIds)\n                        && !dash.isNotificationsOff) {\n                    log.debug(\"No device in session.\");\n                    ctx.writeAndFlush(deviceNotInNetwork(message.id), ctx.voidPromise());\n                }\n\n                processEventorAndWebhook(state.user, dash, targetId, session, pin, pinType, value, now);\n                break;\n        }\n    }\n\n    public static void processDeviceSelectorCommand(ChannelHandlerContext ctx,\n                                                    Session session, Profile profile, DashBoard dash,\n                                                    StringMessage message, String[] splitBody) {\n        //in format \"vu 200000 1\"\n        long widgetId = Long.parseLong(splitBody[1]);\n        Widget deviceSelector = dash.getWidgetByIdOrThrow(widgetId);\n        if (deviceSelector instanceof DeviceSelector) {\n            int selectedDeviceId = Integer.parseInt(splitBody[2]);\n            ((DeviceSelector) deviceSelector).value = selectedDeviceId;\n            ctx.write(ok(message.id), ctx.voidPromise());\n\n            //sending to shared dashes and master-master apps\n            session.sendToSharedApps(ctx.channel(), dash.sharedToken, APP_SYNC, message.id, message.body);\n\n            //we need to send syncs not only to main app, but all to all shared apps\n            for (Channel channel : session.appChannels) {\n                MobileStateHolder mobileStateHolder = getAppState(channel);\n                if (mobileStateHolder != null && mobileStateHolder.contains(dash.sharedToken)) {\n                    profile.sendAppSyncs(dash, channel, selectedDeviceId);\n                }\n                channel.flush();\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/MobileHardwareResendFromBTLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.application.handlers.main.auth.MobileStateHolder;\nimport cc.blynk.server.core.dao.ReportingDiskDao;\nimport cc.blynk.server.core.dao.SessionDao;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.Session;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.processors.BaseProcessorHandler;\nimport cc.blynk.server.core.processors.WebhookProcessor;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.utils.NumberUtil;\nimport io.netty.channel.ChannelHandlerContext;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.illegalCommand;\nimport static cc.blynk.utils.StringUtils.split2;\nimport static cc.blynk.utils.StringUtils.split2Device;\nimport static cc.blynk.utils.StringUtils.split3;\n\n/**\n * Handler responsible for processing messages that are forwarded\n * by application to server from Bluetooth module.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic class MobileHardwareResendFromBTLogic extends BaseProcessorHandler {\n\n    private final ReportingDiskDao reportingDao;\n    private final SessionDao sessionDao;\n\n    public MobileHardwareResendFromBTLogic(Holder holder, String email) {\n        super(holder.eventorProcessor, new WebhookProcessor(holder.asyncHttpClient,\n                holder.limits.webhookPeriodLimitation,\n                holder.limits.webhookResponseSizeLimitBytes,\n                holder.limits.webhookFailureLimit,\n                holder.stats,\n                email));\n        this.sessionDao = holder.sessionDao;\n        this.reportingDao = holder.reportingDiskDao;\n    }\n\n    private static boolean isWriteOperation(String body) {\n        return body.charAt(1) == 'w';\n    }\n\n    public void messageReceived(ChannelHandlerContext ctx, MobileStateHolder state, StringMessage message) {\n        //minimum command - \"1-1 vw 1\"\n        if (message.body.length() < 8) {\n            log.debug(\"MobileHardwareResendFromBTLogic command body too short.\");\n            ctx.writeAndFlush(illegalCommand(message.id), ctx.voidPromise());\n            return;\n        }\n\n        String[] split = split2(message.body);\n\n        //here we have \"1-200000\"\n        String[] dashIdAndTargetIdString = split2Device(split[0]);\n        int dashId = Integer.parseInt(dashIdAndTargetIdString[0]);\n        int deviceId = Integer.parseInt(dashIdAndTargetIdString[1]);\n\n        User user = state.user;\n        DashBoard dash = user.profile.getDashByIdOrThrow(dashId);\n\n        if (isWriteOperation(split[1])) {\n            String[] splitBody = split3(split[1]);\n\n            if (splitBody.length < 3 || splitBody[0].length() == 0 || splitBody[2].length() == 0) {\n                log.debug(\"Write command is wrong.\");\n                ctx.writeAndFlush(illegalCommand(message.id), ctx.voidPromise());\n                return;\n            }\n\n            PinType pinType = PinType.getPinType(splitBody[0].charAt(0));\n            short pin = NumberUtil.parsePin(splitBody[1]);\n            String value = splitBody[2];\n            long now = System.currentTimeMillis();\n\n            reportingDao.process(user, dash, deviceId, pin, pinType, value, now);\n            user.profile.update(dash, deviceId, pin, pinType, value, now);\n\n            Session session = sessionDao.get(state.userKey);\n            processEventorAndWebhook(user, dash, deviceId, session, pin, pinType, value, now);\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/MobileLoadProfileGzippedLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.application.handlers.main.auth.MobileStateHolder;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.Profile;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.protocol.model.messages.MessageBase;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.server.db.model.FlashedToken;\nimport cc.blynk.utils.StringUtils;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.core.model.serialization.JsonParser.gzipDash;\nimport static cc.blynk.server.core.model.serialization.JsonParser.gzipDashRestrictive;\nimport static cc.blynk.server.core.model.serialization.JsonParser.gzipProfile;\nimport static cc.blynk.server.core.protocol.enums.Command.LOAD_PROFILE_GZIPPED;\nimport static cc.blynk.server.internal.CommonByteBufUtil.illegalCommand;\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeBinaryMessage;\nimport static cc.blynk.server.internal.CommonByteBufUtil.noData;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic final class MobileLoadProfileGzippedLogic {\n\n    private static final Logger log = LogManager.getLogger(MobileLoadProfileGzippedLogic.class);\n\n    private MobileLoadProfileGzippedLogic() {\n    }\n\n    public static void messageReceived(Holder holder, ChannelHandlerContext ctx,\n                                       MobileStateHolder state, StringMessage message) {\n        //load all\n        int msgId = message.id;\n\n        if (message.body.length() == 0) {\n            Profile profile = state.user.profile;\n            write(ctx, gzipProfile(profile), msgId);\n            return;\n        }\n\n        String[] parts = message.body.split(StringUtils.BODY_SEPARATOR_STRING);\n        if (parts.length == 1) {\n            //load specific by id\n            int dashId = Integer.parseInt(message.body);\n            DashBoard dash = state.user.profile.getDashByIdOrThrow(dashId);\n            write(ctx, gzipDash(dash), msgId);\n        } else {\n            String token = parts[0];\n            int dashId = Integer.parseInt(parts[1]);\n            String publishingEmail = parts[2];\n            //this is for simplification of testing.\n            String appName = parts.length == 4 ? parts[3] : state.userKey.appName;\n\n            holder.blockingIOProcessor.executeDB(() -> {\n                try {\n                    FlashedToken flashedToken = holder.dbManager.selectFlashedToken(token);\n                    if (flashedToken != null) {\n                        User publishingUser = holder.userDao.getByName(publishingEmail, appName);\n                        DashBoard dash = publishingUser.profile.getDashByIdOrThrow(dashId);\n                        //todo ugly. but ok for now\n                        String copyString = JsonParser.toJsonRestrictiveDashboard(dash);\n                        DashBoard copyDash = JsonParser.parseDashboard(copyString, msgId);\n                        copyDash.eraseWidgetValues();\n                        write(ctx, gzipDashRestrictive(copyDash), msgId);\n                    }\n                } catch (Exception e) {\n                    ctx.writeAndFlush(illegalCommand(msgId), ctx.voidPromise());\n                    log.error(\"Error getting publishing profile.\", e.getMessage());\n                }\n            });\n        }\n    }\n\n    public static void write(ChannelHandlerContext ctx, byte[] data, int msgId) {\n        if (ctx.channel().isWritable()) {\n            var outputMsg = makeResponse(data, msgId);\n            ctx.writeAndFlush(outputMsg, ctx.voidPromise());\n        }\n    }\n\n    private static MessageBase makeResponse(byte[] data, int msgId) {\n        if (data == null) {\n            return noData(msgId);\n        }\n        return makeBinaryMessage(LOAD_PROFILE_GZIPPED, msgId, data);\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/MobileLogoutLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic;\n\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.widgets.notifications.Notification;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic final class MobileLogoutLogic {\n\n    private static final Logger log = LogManager.getLogger(MobileLogoutLogic.class);\n\n    private MobileLogoutLogic() {\n    }\n\n    public static void messageReceived(ChannelHandlerContext ctx, User user, StringMessage msg) {\n        log.debug(\"User {}-{} did logout.\", user.email, user.appName);\n        ctx.writeAndFlush(ok(msg.id), ctx.voidPromise());\n\n        String uid = msg.body;\n        for (DashBoard dash : user.profile.dashBoards) {\n            Notification notification = dash.getNotificationWidget();\n            if (notification != null) {\n                if (uid == null || uid.isEmpty()) {\n                    notification.androidTokens.clear();\n                    notification.iOSTokens.clear();\n                } else {\n                    notification.androidTokens.remove(uid);\n                    notification.iOSTokens.remove(uid);\n                }\n            }\n        }\n\n        ctx.close();\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/MobileMailLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.core.BlockingIOProcessor;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.widgets.ui.tiles.DeviceTiles;\nimport cc.blynk.server.core.model.widgets.ui.tiles.TileTemplate;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.server.notifications.mail.MailWrapper;\nimport cc.blynk.utils.StringUtils;\nimport cc.blynk.utils.properties.Placeholders;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.illegalCommandBody;\nimport static cc.blynk.server.internal.CommonByteBufUtil.notificationError;\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\n\n/**\n * Sends email from application.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic class MobileMailLogic {\n\n    private static final Logger log = LogManager.getLogger(MobileMailLogic.class);\n    private final String tokenMailBody;\n\n    private final BlockingIOProcessor blockingIOProcessor;\n    private final MailWrapper mailWrapper;\n    private final String templateIdMailBody;\n\n    public MobileMailLogic(Holder holder) {\n        this.blockingIOProcessor = holder.blockingIOProcessor;\n        this.tokenMailBody = holder.textHolder.tokenBody;\n        this.mailWrapper = holder.mailWrapper;\n        this.templateIdMailBody = holder.textHolder.templateIdMailBody;\n    }\n\n    public void messageReceived(ChannelHandlerContext ctx, User user, StringMessage message) {\n        var splitBody = message.body.split(StringUtils.BODY_SEPARATOR_STRING);\n\n        //mail type\n        switch (splitBody[0]) {\n            case \"template\":\n                sendTemplateIdEmail(ctx, user, splitBody, message.id);\n                break;\n            default:\n                var dashId = Integer.parseInt(splitBody[0]);\n                var dash = user.profile.getDashByIdOrThrow(dashId);\n\n                //dashId deviceId\n                if (splitBody.length == 2) {\n                    int deviceId = Integer.parseInt(splitBody[1]);\n                    Device device = user.profile.getDeviceById(dash, deviceId);\n\n                    if (device == null || device.token == null) {\n                        log.debug(\"Wrong device id.\");\n                        ctx.writeAndFlush(illegalCommandBody(message.id), ctx.voidPromise());\n                        return;\n                    }\n\n                    makeSingleTokenEmail(ctx, dash, device, user.email, message.id);\n\n                    //dashId theme provisionType color appname\n                } else {\n                    if (dash.devices.length == 1) {\n                        makeSingleTokenEmail(ctx, dash, dash.devices[0], user.email, message.id);\n                    } else {\n                        sendMultiTokenEmail(ctx, user, dash, message.id);\n                    }\n                }\n        }\n    }\n\n    private void sendTemplateIdEmail(ChannelHandlerContext ctx, User user, String[] split, int msgId) {\n        int dashId = Integer.parseInt(split[1]);\n        DashBoard dash = user.profile.getDashByIdOrThrow(dashId);\n\n        long widgetId = Long.parseLong(split[2]);\n        DeviceTiles deviceTiles = (DeviceTiles) dash.getWidgetById(widgetId);\n\n        long templateId = Long.parseLong(split[3]);\n        TileTemplate template = deviceTiles.getTileTemplateByIdOrThrow(templateId);\n\n        String subj = \"Template ID for \" + template.name;\n        String body = templateIdMailBody\n                .replace(Placeholders.TEMPLATE_NAME, template.name)\n                .replace(Placeholders.TEMPLATE_ID, template.templateId);\n\n        log.trace(\"Sending template id mail for user {}, with id : '{}'.\", user.email, templateId);\n        mail(ctx.channel(), user.email, subj, body, msgId, true);\n    }\n\n    private void makeSingleTokenEmail(ChannelHandlerContext ctx, DashBoard dash, Device device, String to, int msgId) {\n        String dashName = dash.getNameOrDefault();\n        String deviceName = device.getNameOrDefault();\n        String subj = \"Auth Token for \" + dashName + \" project and device \" + deviceName;\n        String body = \"Auth Token : \" + device.token + \"\\n\";\n\n        log.trace(\"Sending single token mail for user {}, with token : '{}'.\", to, device.token);\n        mail(ctx.channel(), to, subj, body + tokenMailBody, msgId, false);\n    }\n\n    private void sendMultiTokenEmail(ChannelHandlerContext ctx, User user, DashBoard dash, int msgId) {\n        String dashName = dash.getNameOrDefault();\n        String subj = \"Auth Tokens for \" + dashName + \" project and \" + dash.devices.length + \" devices\";\n\n        StringBuilder body = new StringBuilder();\n        for (Device device : dash.devices) {\n            String deviceName = device.getNameOrDefault();\n            body.append(\"Auth Token for device '\")\n                .append(deviceName)\n                .append(\"' : \")\n                .append(device.token)\n                .append(\"\\n\");\n        }\n\n        body.append(tokenMailBody);\n\n        String to = user.email;\n        log.trace(\"Sending multi tokens mail for user {}, with {} tokens.\", to, dash.devices.length);\n        mail(ctx.channel(), to, subj, body.toString(), msgId, false);\n    }\n\n    private void mail(Channel channel, String to, String subj, String body, int msgId, boolean isHtml) {\n        blockingIOProcessor.execute(() -> {\n            try {\n                if (isHtml) {\n                    mailWrapper.sendHtml(to, subj, body);\n                } else {\n                    mailWrapper.sendText(to, subj, body);\n                }\n                channel.writeAndFlush(ok(msgId), channel.voidPromise());\n            } catch (Exception e) {\n                log.error(\"Error sending email auth token to user : {}. Error: {}\", to, e.getMessage());\n                if (channel.isActive() && channel.isWritable()) {\n                    channel.writeAndFlush(notificationError(msgId), channel.voidPromise());\n                }\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/MobilePurchaseLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.application.handlers.main.auth.MobileStateHolder;\nimport cc.blynk.server.core.BlockingIOProcessor;\nimport cc.blynk.server.core.protocol.model.messages.ResponseMessage;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.server.db.DBManager;\nimport cc.blynk.server.db.model.Purchase;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.notAllowed;\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\nimport static cc.blynk.utils.StringUtils.split2;\n\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 14.03.16.\n */\npublic class MobilePurchaseLogic {\n\n    private static final Logger log = LogManager.getLogger(MobilePurchaseLogic.class);\n\n    private final BlockingIOProcessor blockingIOProcessor;\n    private final DBManager dbManager;\n    private boolean wasErrorPrinted;\n\n    public MobilePurchaseLogic(Holder holder) {\n        this.blockingIOProcessor = holder.blockingIOProcessor;\n        this.dbManager = holder.dbManager;\n        this.wasErrorPrinted = false;\n    }\n\n    private static boolean isValidTransactionId(String id) {\n        if (id == null || id.isEmpty() || id.startsWith(\"com.blynk.energy\")) {\n            return false;\n        }\n\n        if (id.length() == 36) {\n            // fake example - \"8077004819764738793.5939465896020147\"\n            String[] transactionParts = id.split(\"\\\\.\");\n            if (transactionParts.length == 2\n                    && transactionParts[0].length() == 19\n                    && transactionParts[1].length() == 16) {\n                return false;\n            }\n        }\n\n        return true;\n    }\n\n    private static double calcPrice(int reward) {\n        switch (reward) {\n            case 200 :\n                return 0D;\n            case 1000 :\n                return 0.99D;\n            case 2400 :\n                return 1.99D;\n            case 5000 :\n                return 3.99D;\n            case 13000 :\n                return 9.99D;\n            case 28000 :\n                return 19.99D;\n            default:\n                return -1D;\n        }\n    }\n\n    public void messageReceived(ChannelHandlerContext ctx, MobileStateHolder state, StringMessage message) {\n        var splitBody = split2(message.body);\n        var user = state.user;\n\n        var energyAmountToAdd = Integer.parseInt(splitBody[0]);\n        ResponseMessage response;\n        if (splitBody.length == 2 && isValidTransactionId(splitBody[1])) {\n            double price = calcPrice(energyAmountToAdd);\n            insertPurchase(user.email, energyAmountToAdd, price, splitBody[1]);\n            user.addEnergy(energyAmountToAdd);\n            response = ok(message.id);\n        } else {\n            if (!wasErrorPrinted) {\n                log.warn(\"Purchase {} with invalid transaction id '{}'. {} ({}).\",\n                        splitBody[0], splitBody[1], user.email, state.version);\n                wasErrorPrinted = true;\n            }\n            response = notAllowed(message.id);\n        }\n        ctx.writeAndFlush(response, ctx.voidPromise());\n    }\n\n    private void insertPurchase(String email, int reward, double price, String transactionId) {\n        if (transactionId.equals(\"AdColonyAward\") || transactionId.equals(\"homeScreen\")) {\n            return;\n        }\n        blockingIOProcessor.executeDB(\n            () -> dbManager.insertPurchase(new Purchase(email, reward, price, transactionId))\n        );\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/MobileRedeemLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.protocol.model.messages.MessageBase;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.server.db.DBManager;\nimport cc.blynk.server.db.model.Redeem;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.notAllowed;\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\n\n/**\n * Handler responsible for handling redeem logic. Unlocks premium content for predefined tokens.\n * Used for kickstarter backers and other companies that paid for redeeming.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 02.03.16.\n */\npublic final class MobileRedeemLogic {\n\n    private static final Logger log = LogManager.getLogger(MobileRedeemLogic.class);\n\n    private MobileRedeemLogic() {\n    }\n\n    public static void messageReceived(Holder holder, ChannelHandlerContext ctx,\n                                       User user, StringMessage message) {\n        String redeemToken = message.body;\n\n        holder.blockingIOProcessor.executeDB(() ->\n                ctx.writeAndFlush(verifyToken(holder, message, redeemToken, user), ctx.voidPromise()));\n    }\n\n    private static MessageBase verifyToken(Holder holder, StringMessage message, String redeemToken, User user) {\n        try {\n            DBManager dbManager = holder.dbManager;\n            Redeem redeem = dbManager.selectRedeemByToken(redeemToken);\n            if (redeem != null) {\n                if (redeem.isRedeemed && redeem.email.equals(user.email)) {\n                    return ok(message.id);\n                } else if (!redeem.isRedeemed && dbManager.updateRedeem(user.email, redeemToken)) {\n                    unlockContent(user, redeem.reward);\n                    return ok(message.id);\n                }\n            }\n        } catch (Exception e) {\n            log.debug(\"Error redeeming token.\", e);\n        }\n\n        return notAllowed(message.id);\n    }\n\n    private static void unlockContent(User user, int reward) {\n        user.addEnergy(reward);\n        log.info(\"Unlocking content for {}. Reward {}.\", user.email, reward);\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/MobileRefreshTokenLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.application.handlers.main.auth.MobileStateHolder;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.Session;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.utils.StringUtils;\nimport io.netty.channel.ChannelHandlerContext;\n\nimport static cc.blynk.server.core.protocol.enums.Command.REFRESH_TOKEN;\nimport static cc.blynk.server.internal.CommonByteBufUtil.illegalCommandBody;\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeUTF8StringMessage;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic final class MobileRefreshTokenLogic {\n\n    private MobileRefreshTokenLogic() {\n    }\n\n    public static void messageReceived(Holder holder, ChannelHandlerContext ctx,\n                                       MobileStateHolder state, StringMessage message) {\n        String[] split = StringUtils.split2(message.body);\n\n        int dashId = Integer.parseInt(split[0]);\n        int deviceId = 0;\n\n        //new value for multi devices\n        if (split.length == 2) {\n            deviceId = Integer.parseInt(split[1]);\n        }\n\n        User user = state.user;\n\n        DashBoard dash = user.profile.getDashByIdOrThrow(dashId);\n        Device device = user.profile.getDeviceById(dash, deviceId);\n\n        if (device == null) {\n            ctx.writeAndFlush(illegalCommandBody(message.id), ctx.voidPromise());\n            return;\n        }\n\n        String token = holder.tokenManager.refreshToken(user, dash, device);\n\n        Session session = holder.sessionDao.get(state.userKey);\n        session.closeHardwareChannelByDeviceId(dashId, deviceId);\n\n        if (ctx.channel().isWritable()) {\n            ctx.writeAndFlush(makeUTF8StringMessage(REFRESH_TOKEN, message.id, token), ctx.voidPromise());\n        }\n    }\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/MobileSetWidgetPropertyLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic;\n\nimport cc.blynk.server.core.dao.SessionDao;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.enums.WidgetProperty;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.utils.StringUtils;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.illegalCommandBody;\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\n\n/**\n * Handler that allows to change widget properties from hardware side.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic final class MobileSetWidgetPropertyLogic {\n\n    private static final Logger log = LogManager.getLogger(MobileSetWidgetPropertyLogic.class);\n\n    private MobileSetWidgetPropertyLogic(SessionDao sessionDao) {\n    }\n\n    public static void messageReceived(ChannelHandlerContext ctx, User user, StringMessage message) {\n        var splitBody = message.body.split(StringUtils.BODY_SEPARATOR_STRING);\n\n        if (splitBody.length != 4) {\n            log.debug(\"AppSetWidgetProperty command body has wrong format. {}\", message.body);\n            ctx.writeAndFlush(illegalCommandBody(message.id), ctx.voidPromise());\n            return;\n        }\n\n        var dashId = Integer.parseInt(splitBody[0]);\n        var widgetId = Long.parseLong(splitBody[1]);\n        var property = splitBody[2];\n        var propertyValue = splitBody[3];\n\n        if (property.length() == 0 || propertyValue.length() == 0) {\n            log.debug(\"AppSetWidgetProperty command body has wrong format. {}\", message.body);\n            ctx.writeAndFlush(illegalCommandBody(message.id), ctx.voidPromise());\n            return;\n        }\n\n        var widgetProperty = WidgetProperty.getProperty(property);\n\n        if (widgetProperty == null) {\n            log.debug(\"Unsupported app set property {}.\", property);\n            ctx.writeAndFlush(illegalCommandBody(message.id), ctx.voidPromise());\n            return;\n        }\n\n        var dash = user.profile.getDashByIdOrThrow(dashId);\n        //for now supporting only virtual pins\n        var widget = dash.getWidgetById(widgetId);\n        if (widget == null) {\n            widget = dash.getWidgetByIdInDeviceTilesOrThrow(widgetId);\n        }\n\n        if (!widget.setProperty(widgetProperty, propertyValue)) {\n            log.debug(\"Property {} with value {} not supported.\", property, propertyValue);\n            ctx.writeAndFlush(illegalCommandBody(message.id), ctx.voidPromise());\n            return;\n        }\n\n        dash.updatedAt = System.currentTimeMillis();\n        ctx.writeAndFlush(ok(message.id), ctx.voidPromise());\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/MobileSyncLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic;\n\nimport cc.blynk.server.application.handlers.main.auth.MobileStateHolder;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.widgets.MobileSyncWidget;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelHandlerContext;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\nimport static cc.blynk.utils.StringUtils.split2Device;\n\n/**\n * Request state sync info for widgets.\n * Supports sync for all widgets and sync for specific target\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic final class MobileSyncLogic {\n\n    private MobileSyncLogic() {\n    }\n\n    public static void messageReceived(ChannelHandlerContext ctx, MobileStateHolder state, StringMessage message) {\n        String[] dashIdAndTargetIdString = split2Device(message.body);\n        int dashId = Integer.parseInt(dashIdAndTargetIdString[0]);\n        int targetId = MobileSyncWidget.ANY_TARGET;\n\n        User user = state.user;\n        DashBoard dash = user.profile.getDashByIdOrThrow(dashId);\n\n        if (dashIdAndTargetIdString.length == 2) {\n            targetId = Integer.parseInt(dashIdAndTargetIdString[1]);\n        }\n\n        ctx.write(ok(message.id), ctx.voidPromise());\n        Channel appChannel = ctx.channel();\n        user.profile.sendAppSyncs(dash, appChannel, targetId);\n        ctx.flush();\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/dashboard/MobileCreateDashLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic.dashboard;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.application.handlers.main.auth.MobileStateHolder;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandException;\nimport cc.blynk.server.core.protocol.exceptions.NotAllowedException;\nimport cc.blynk.server.core.protocol.exceptions.QuotaLimitException;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.server.internal.EmptyArraysUtil;\nimport cc.blynk.utils.ArrayUtil;\nimport cc.blynk.utils.StringUtils;\nimport cc.blynk.utils.TokenGeneratorUtil;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.energyLimit;\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic final class MobileCreateDashLogic {\n\n    private static final Logger log = LogManager.getLogger(MobileCreateDashLogic.class);\n\n    private MobileCreateDashLogic() {\n    }\n\n    public static void messageReceived(Holder holder, ChannelHandlerContext ctx,\n                                       MobileStateHolder state, StringMessage message) {\n        boolean generateTokensForDevices = true;\n        final String dashString;\n        if (message.body.startsWith(\"no_token\")) {\n            generateTokensForDevices = false;\n            dashString = StringUtils.split2(message.body)[1];\n        } else {\n            dashString = message.body;\n        }\n\n        if (dashString == null || dashString.isEmpty()) {\n            throw new IllegalCommandException(\"Income create dash message is empty.\");\n        }\n\n        if (dashString.length() > holder.limits.profileSizeLimitBytes) {\n            throw new NotAllowedException(\"User dashboard is larger then limit.\", message.id);\n        }\n\n        log.debug(\"Trying to parse user newDash : {}\", dashString);\n        DashBoard newDash = JsonParser.parseDashboard(dashString, message.id);\n\n        User user = state.user;\n        if (user.profile.dashBoards.length >= holder.limits.dashboardsLimit) {\n            throw new QuotaLimitException(\"Dashboards limit reached.\", message.id);\n        }\n\n        for (DashBoard dashBoard : user.profile.dashBoards) {\n            if (dashBoard.id == newDash.id) {\n                throw new NotAllowedException(\"Dashboard already exists.\", message.id);\n            }\n        }\n\n        log.info(\"Creating new dashboard.\");\n\n        if (newDash.createdAt == 0) {\n            newDash.createdAt = System.currentTimeMillis();\n        }\n\n        int price = newDash.energySum();\n        if (user.notEnoughEnergy(price)) {\n            log.debug(\"Not enough energy.\");\n            ctx.writeAndFlush(energyLimit(message.id), ctx.voidPromise());\n            return;\n        }\n        user.subtractEnergy(price);\n        user.profile.dashBoards = ArrayUtil.add(user.profile.dashBoards, newDash, DashBoard.class);\n\n        if (newDash.devices == null) {\n            newDash.devices = EmptyArraysUtil.EMPTY_DEVICES;\n        } else {\n            for (Device device : newDash.devices) {\n                //this case only possible for clone,\n                device.erase();\n                if (generateTokensForDevices) {\n                    String token = TokenGeneratorUtil.generateNewToken();\n                    holder.tokenManager.assignToken(user, newDash, device, token);\n                }\n            }\n        }\n\n        user.lastModifiedTs = System.currentTimeMillis();\n\n        newDash.addTimers(holder.timerWorker, state.userKey);\n\n        if (!generateTokensForDevices) {\n            newDash.eraseWidgetValues();\n        }\n\n        ctx.writeAndFlush(ok(message.id), ctx.voidPromise());\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/dashboard/MobileDeleteDashLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic.dashboard;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.application.handlers.main.auth.MobileStateHolder;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.utils.ArrayUtil;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic final class MobileDeleteDashLogic {\n\n    private static final Logger log = LogManager.getLogger(MobileDeleteDashLogic.class);\n\n    private MobileDeleteDashLogic() {\n    }\n\n    public static void messageReceived(Holder holder, ChannelHandlerContext ctx,\n                                       MobileStateHolder state, StringMessage message) {\n        var dashId = Integer.parseInt(message.body);\n\n        deleteDash(holder, state, dashId);\n        state.user.lastModifiedTs = System.currentTimeMillis();\n\n        ctx.writeAndFlush(ok(message.id), ctx.voidPromise());\n    }\n\n    private static void deleteDash(Holder holder, MobileStateHolder state, int dashId) {\n        User user = state.user;\n        int index = user.profile.getDashIndexOrThrow(dashId);\n\n        log.debug(\"Deleting dashboard {}.\", dashId);\n\n        DashBoard dash = user.profile.dashBoards[index];\n\n        user.addEnergy(dash.energySum());\n\n        holder.timerWorker.deleteTimers(state.userKey, dash);\n        holder.tokenManager.deleteDash(dash);\n        holder.sessionDao.closeHardwareChannelByDashId(state.userKey, dashId);\n        holder.reportScheduler.cancelStoredFuture(user, dashId);\n\n        holder.blockingIOProcessor.executeHistory(() -> {\n            for (Device device : dash.devices) {\n                try {\n                    holder.reportingDiskDao.delete(state.user, dashId, device.id);\n                } catch (Exception e) {\n                    log.warn(\"Error removing device data. Reason : {}.\", e.getMessage());\n                }\n            }\n        });\n\n        user.profile.dashBoards = ArrayUtil.remove(user.profile.dashBoards, index, DashBoard.class);\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/dashboard/MobileUpdateDashLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic.dashboard;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.application.handlers.main.auth.MobileStateHolder;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandException;\nimport cc.blynk.server.core.protocol.exceptions.NotAllowedException;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.server.workers.timer.TimerWorker;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic final class MobileUpdateDashLogic {\n\n    private static final Logger log = LogManager.getLogger(MobileUpdateDashLogic.class);\n\n    private MobileUpdateDashLogic() {\n    }\n\n    //todo should accept only dash info and ignore widgets. should be fixed after migration\n    public static void messageReceived(Holder holder, ChannelHandlerContext ctx,\n                                       MobileStateHolder state, StringMessage message) {\n        String dashString = message.body;\n\n        if (dashString == null || dashString.isEmpty()) {\n            throw new IllegalCommandException(\"Income create dash message is empty.\");\n        }\n\n        if (dashString.length() > holder.limits.profileSizeLimitBytes) {\n            throw new NotAllowedException(\"User dashboard is larger then limit.\", message.id);\n        }\n\n        log.debug(\"Trying to parse user dash : {}\", dashString);\n        DashBoard updatedDash = JsonParser.parseDashboard(dashString, message.id);\n\n        if (updatedDash == null) {\n            throw new IllegalCommandException(\"Project parsing error.\");\n        }\n\n        log.debug(\"Saving dashboard.\");\n\n        User user = state.user;\n\n        DashBoard existingDash = user.profile.getDashByIdOrThrow(updatedDash.id);\n\n        TimerWorker timerWorker = holder.timerWorker;\n        timerWorker.deleteTimers(state.userKey, existingDash);\n        updatedDash.addTimers(timerWorker, state.userKey);\n\n        existingDash.updateFields(updatedDash);\n        user.profile.cleanPinStorage(existingDash, false, true);\n\n        user.lastModifiedTs = existingDash.updatedAt;\n\n        ctx.writeAndFlush(ok(message.id), ctx.voidPromise());\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/dashboard/MobileUpdateDashSettingLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic.dashboard;\n\nimport cc.blynk.server.application.handlers.main.auth.MobileStateHolder;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.DashboardSettings;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandException;\nimport cc.blynk.server.core.protocol.exceptions.NotAllowedException;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.utils.StringUtils;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic final class MobileUpdateDashSettingLogic {\n\n    private static final Logger log = LogManager.getLogger(MobileUpdateDashSettingLogic.class);\n\n    private MobileUpdateDashSettingLogic() {\n    }\n\n    public static void messageReceived(ChannelHandlerContext ctx, MobileStateHolder state,\n                                       StringMessage message, int settingsSizeLimit) {\n        String[] split = StringUtils.split2(message.body);\n\n        if (split.length < 2) {\n            throw new IllegalCommandException(\"Wrong income message format.\");\n        }\n\n        int dashId = Integer.parseInt(split[0]);\n        String dashSettingsString = split[1];\n\n        if (dashSettingsString == null || dashSettingsString.isEmpty()) {\n            throw new IllegalCommandException(\"Income dash settings message is empty.\");\n        }\n\n        if (dashSettingsString.length() > settingsSizeLimit) {\n            throw new NotAllowedException(\"User dashboard setting message is larger then limit.\", message.id);\n        }\n\n        log.debug(\"Trying to parse project settings : {}\", dashSettingsString);\n        DashboardSettings settings = JsonParser.parseDashboardSettings(dashSettingsString, message.id);\n\n        User user = state.user;\n        DashBoard existingDash = user.profile.getDashByIdOrThrow(dashId);\n\n        existingDash.updateSettings(settings);\n        user.lastModifiedTs = existingDash.updatedAt;\n\n        ctx.writeAndFlush(ok(message.id), ctx.voidPromise());\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/dashboard/device/MobileCreateDeviceLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic.dashboard.device;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandException;\nimport cc.blynk.server.core.protocol.exceptions.NotAllowedException;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.utils.TokenGeneratorUtil;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.core.protocol.enums.Command.CREATE_DEVICE;\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeUTF8StringMessage;\nimport static cc.blynk.utils.StringUtils.split2;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.02.16.\n */\npublic final class MobileCreateDeviceLogic {\n\n    private static final Logger log = LogManager.getLogger(MobileCreateDeviceLogic.class);\n\n    private MobileCreateDeviceLogic() {\n    }\n\n    public static void messageReceived(Holder holder, ChannelHandlerContext ctx,\n                                       User user, StringMessage message) {\n        String[] split = split2(message.body);\n\n        if (split.length < 2) {\n            throw new IllegalCommandException(\"Wrong income message format.\");\n        }\n\n        int dashId = Integer.parseInt(split[0]);\n        String deviceString = split[1];\n\n        if (deviceString == null || deviceString.isEmpty()) {\n            throw new IllegalCommandException(\"Income device message is empty.\");\n        }\n\n        DashBoard dash = user.profile.getDashByIdOrThrow(dashId);\n\n        if (dash.devices.length > holder.limits.deviceLimit) {\n            throw new NotAllowedException(\"Device limit is reached.\", message.id);\n        }\n\n        Device newDevice = JsonParser.parseDevice(deviceString, message.id);\n\n        log.debug(\"Creating new device {}.\", deviceString);\n\n        if (newDevice.isNotValid()) {\n            throw new IllegalCommandException(\"Income device message is not valid.\");\n        }\n\n        for (Device device : dash.devices) {\n            if (device.id == newDevice.id) {\n                throw new NotAllowedException(\"Device with same id already exists.\", message.id);\n            }\n        }\n\n        user.profile.addDevice(dash, newDevice);\n\n        String newToken = TokenGeneratorUtil.generateNewToken();\n        holder.tokenManager.assignToken(user, dash, newDevice, newToken);\n\n        user.lastModifiedTs = System.currentTimeMillis();\n\n        if (ctx.channel().isWritable()) {\n            ctx.writeAndFlush(\n                    makeUTF8StringMessage(CREATE_DEVICE, message.id, newDevice.toString()), ctx.voidPromise());\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/dashboard/device/MobileDeleteDeviceLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic.dashboard.device;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.application.handlers.main.auth.MobileStateHolder;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.Session;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandException;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\nimport static cc.blynk.utils.StringUtils.split2;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.02.16.\n */\npublic final class MobileDeleteDeviceLogic {\n\n    private static final Logger log = LogManager.getLogger(MobileDeleteDeviceLogic.class);\n\n    private MobileDeleteDeviceLogic() {\n    }\n\n    public static void messageReceived(Holder holder, ChannelHandlerContext ctx,\n                                       MobileStateHolder state, StringMessage message) {\n        String[] split = split2(message.body);\n\n        if (split.length < 2) {\n            throw new IllegalCommandException(\"Wrong income message format.\");\n        }\n\n        int dashId = Integer.parseInt(split[0]);\n        int deviceId = Integer.parseInt(split[1]);\n\n        User user = state.user;\n        DashBoard dash = state.user.profile.getDashByIdOrThrow(dashId);\n\n        log.debug(\"Deleting device with id {}.\", deviceId);\n\n        Device device = user.profile.deleteDevice(dash, deviceId);\n        user.profile.cleanPinStorageForDevice(deviceId);\n        user.profile.deleteDeviceFromTags(dash, deviceId);\n        holder.tokenManager.deleteDevice(device);\n        Session session = holder.sessionDao.get(state.userKey);\n        session.closeHardwareChannelByDeviceId(dashId, deviceId);\n\n        user.lastModifiedTs = System.currentTimeMillis();\n\n        holder.blockingIOProcessor.executeHistory(() -> {\n            try {\n                holder.reportingDiskDao.delete(user, dashId, deviceId);\n            } catch (Exception e) {\n                log.warn(\"Error removing device data. Reason : {}.\", e.getMessage());\n            }\n        });\n\n        ctx.writeAndFlush(ok(message.id), ctx.voidPromise());\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/dashboard/device/MobileGetDeviceLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic.dashboard.device;\n\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.utils.StringUtils;\nimport io.netty.channel.ChannelHandlerContext;\n\nimport static cc.blynk.server.core.protocol.enums.Command.MOBILE_GET_DEVICE;\nimport static cc.blynk.server.internal.CommonByteBufUtil.illegalCommandBody;\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeUTF8StringMessage;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.02.16.\n */\npublic final class MobileGetDeviceLogic {\n\n    private MobileGetDeviceLogic() {\n    }\n\n    public static void messageReceived(ChannelHandlerContext ctx, User user, StringMessage message) {\n        String[] split = StringUtils.split2(message.body);\n        int dashId = Integer.parseInt(split[0]);\n        int deviceId = Integer.parseInt(split[1]);\n\n        DashBoard dash = user.profile.getDashByIdOrThrow(dashId);\n        Device device = user.profile.getDeviceById(dash, deviceId);\n        if (device == null) {\n            ctx.writeAndFlush(illegalCommandBody(message.id), ctx.voidPromise());\n        } else {\n            if (ctx.channel().isWritable()) {\n                ctx.writeAndFlush(makeUTF8StringMessage(MOBILE_GET_DEVICE,\n                        message.id, device.toString()), ctx.voidPromise());\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/dashboard/device/MobileGetDevicesLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic.dashboard.device;\n\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.DeviceStatusDTO;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport io.netty.channel.ChannelHandlerContext;\n\nimport static cc.blynk.server.core.protocol.enums.Command.GET_DEVICES;\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeUTF8StringMessage;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.02.16.\n */\npublic final class MobileGetDevicesLogic {\n\n    private MobileGetDevicesLogic() {\n    }\n\n    public static void messageReceived(ChannelHandlerContext ctx, User user, StringMessage message) {\n        int dashId = Integer.parseInt(message.body);\n\n        DashBoard dash = user.profile.getDashByIdOrThrow(dashId);\n\n        String devicesJson;\n        if (dash.devices == null || dash.devices.length == 0) {\n            devicesJson = \"[]\";\n        } else {\n            DeviceStatusDTO[] deviceStatusDTOS = DeviceStatusDTO.transform(dash.devices);\n            devicesJson = JsonParser.toJson(deviceStatusDTOS);\n        }\n\n        if (ctx.channel().isWritable()) {\n            ctx.writeAndFlush(makeUTF8StringMessage(GET_DEVICES, message.id, devicesJson), ctx.voidPromise());\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/dashboard/device/MobileUpdateDeviceLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic.dashboard.device;\n\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandException;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.illegalCommandBody;\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\nimport static cc.blynk.utils.StringUtils.split2;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.02.16.\n */\npublic final class MobileUpdateDeviceLogic {\n\n    private static final Logger log = LogManager.getLogger(MobileUpdateDeviceLogic.class);\n\n    private MobileUpdateDeviceLogic() {\n    }\n\n    public static void messageReceived(ChannelHandlerContext ctx, User user, StringMessage message) {\n        String[] split = split2(message.body);\n\n        if (split.length < 2) {\n            throw new IllegalCommandException(\"Wrong income message format.\");\n        }\n\n        int dashId = Integer.parseInt(split[0]);\n        String deviceString = split[1];\n\n        if (deviceString == null || deviceString.isEmpty()) {\n            throw new IllegalCommandException(\"Income device message is empty.\");\n        }\n\n        DashBoard dash = user.profile.getDashByIdOrThrow(dashId);\n\n        Device newDevice = JsonParser.parseDevice(deviceString, message.id);\n\n        log.debug(\"Updating new device {}.\", deviceString);\n\n        if (newDevice.isNotValid()) {\n            throw new IllegalCommandException(\"Income device message is not valid.\");\n        }\n\n        Device existingDevice = user.profile.getDeviceById(dash, newDevice.id);\n\n        if (existingDevice == null) {\n            log.debug(\"Attempt to update device with non existing id.\");\n            ctx.writeAndFlush(illegalCommandBody(message.id), ctx.voidPromise());\n            return;\n        }\n\n        existingDevice.update(newDevice);\n        dash.updatedAt = System.currentTimeMillis();\n        user.lastModifiedTs = dash.updatedAt;\n\n        ctx.writeAndFlush(ok(message.id), ctx.voidPromise());\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/dashboard/tags/MobileCreateTagLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic.dashboard.tags;\n\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.Profile;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Tag;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandException;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.core.protocol.enums.Command.CREATE_TAG;\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeUTF8StringMessage;\nimport static cc.blynk.utils.StringUtils.split2;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.02.16.\n */\npublic final class MobileCreateTagLogic {\n\n    private static final Logger log = LogManager.getLogger(MobileCreateTagLogic.class);\n\n    private MobileCreateTagLogic() {\n    }\n\n    public static void messageReceived(ChannelHandlerContext ctx, User user, StringMessage message) {\n        String[] split = split2(message.body);\n\n        if (split.length < 2) {\n            throw new IllegalCommandException(\"Wrong income message format.\");\n        }\n\n        int dashId = Integer.parseInt(split[0]);\n        String deviceString = split[1];\n\n        if (deviceString == null || deviceString.isEmpty()) {\n            throw new IllegalCommandException(\"Income tag message is empty.\");\n        }\n\n        Profile profile = user.profile;\n        DashBoard dash = profile.getDashByIdOrThrow(dashId);\n\n        Tag newTag = JsonParser.parseTag(deviceString, message.id);\n\n        log.debug(\"Creating new tag {}.\", newTag);\n\n        if (newTag.isNotValid()) {\n            throw new IllegalCommandException(\"Income tag name is not valid.\");\n        }\n\n        for (Tag tag : dash.tags) {\n            if (tag.id == newTag.id || tag.name.equals(newTag.name)) {\n                throw new IllegalCommandException(\"Tag with same id/name already exists.\");\n            }\n        }\n\n        profile.addTag(dash, newTag);\n        user.lastModifiedTs = System.currentTimeMillis();\n\n        if (ctx.channel().isWritable()) {\n            ctx.writeAndFlush(makeUTF8StringMessage(CREATE_TAG, message.id, newTag.toString()), ctx.voidPromise());\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/dashboard/tags/MobileDeleteTagLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic.dashboard.tags;\n\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.Profile;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandException;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\nimport static cc.blynk.utils.StringUtils.split2;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.02.16.\n */\npublic final class MobileDeleteTagLogic {\n\n    private static final Logger log = LogManager.getLogger(MobileDeleteTagLogic.class);\n\n    private MobileDeleteTagLogic() {\n    }\n\n    public static void messageReceived(ChannelHandlerContext ctx, User user, StringMessage message) {\n        String[] split = split2(message.body);\n\n        if (split.length < 2) {\n            throw new IllegalCommandException(\"Wrong income message format.\");\n        }\n\n        int dashId = Integer.parseInt(split[0]);\n        int tagId = Integer.parseInt(split[1]);\n\n        Profile profile = user.profile;\n        DashBoard dash = profile.getDashByIdOrThrow(dashId);\n\n        log.debug(\"Deleting tag with id {}.\", tagId);\n\n        profile.deleteTag(dash, tagId);\n        user.lastModifiedTs = System.currentTimeMillis();\n\n        ctx.writeAndFlush(ok(message.id), ctx.voidPromise());\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/dashboard/tags/MobileGetTagsLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic.dashboard.tags;\n\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport io.netty.channel.ChannelHandlerContext;\n\nimport static cc.blynk.server.core.protocol.enums.Command.GET_TAGS;\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeUTF8StringMessage;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.02.16.\n */\npublic final class MobileGetTagsLogic {\n\n    private MobileGetTagsLogic() {\n    }\n\n    public static void messageReceived(ChannelHandlerContext ctx, User user, StringMessage message) {\n        int dashId = Integer.parseInt(message.body);\n\n        DashBoard dash = user.profile.getDashByIdOrThrow(dashId);\n\n        String response = JsonParser.toJson(dash.tags);\n        if (response == null) {\n            response = \"[]\";\n        }\n\n        if (ctx.channel().isWritable()) {\n            ctx.writeAndFlush(makeUTF8StringMessage(GET_TAGS, message.id, response), ctx.voidPromise());\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/dashboard/tags/MobileUpdateTagLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic.dashboard.tags;\n\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Tag;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandException;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\nimport static cc.blynk.utils.StringUtils.split2;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.02.16.\n */\npublic final class MobileUpdateTagLogic {\n\n    private static final Logger log = LogManager.getLogger(MobileUpdateTagLogic.class);\n\n    private MobileUpdateTagLogic() {\n    }\n\n    public static void messageReceived(ChannelHandlerContext ctx, User user, StringMessage message) {\n        String[] split = split2(message.body);\n\n        if (split.length < 2) {\n            throw new IllegalCommandException(\"Wrong income message format.\");\n        }\n\n        int dashId = Integer.parseInt(split[0]);\n        String tagString = split[1];\n\n        if (tagString == null || tagString.isEmpty()) {\n            throw new IllegalCommandException(\"Income tag message is empty.\");\n        }\n\n        DashBoard dash = user.profile.getDashByIdOrThrow(dashId);\n\n        Tag newTag = JsonParser.parseTag(tagString, message.id);\n\n        log.debug(\"Updating new tag {}.\", tagString);\n\n        if (newTag.isNotValid()) {\n            throw new IllegalCommandException(\"Income tag name is not valid.\");\n        }\n\n        Tag existingTag = user.profile.getTagById(dash, newTag.id);\n\n        if (existingTag == null) {\n            throw new IllegalCommandException(\"Attempt to update tag with non existing id.\");\n        }\n\n        existingTag.update(newTag);\n        user.lastModifiedTs = System.currentTimeMillis();\n\n        ctx.writeAndFlush(ok(message.id), ctx.voidPromise());\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/dashboard/widget/MobileCreateWidgetLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic.dashboard.widget;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.application.handlers.main.auth.MobileStateHolder;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.controls.Timer;\nimport cc.blynk.server.core.model.widgets.others.eventor.Eventor;\nimport cc.blynk.server.core.model.widgets.ui.tiles.DeviceTiles;\nimport cc.blynk.server.core.model.widgets.ui.tiles.TileTemplate;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandException;\nimport cc.blynk.server.core.protocol.exceptions.NotAllowedException;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.server.workers.timer.TimerWorker;\nimport cc.blynk.utils.ArrayUtil;\nimport cc.blynk.utils.StringUtils;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.energyLimit;\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.02.16.\n */\npublic final class MobileCreateWidgetLogic {\n\n    private static final Logger log = LogManager.getLogger(MobileCreateWidgetLogic.class);\n\n    private MobileCreateWidgetLogic() {\n    }\n\n    public static void messageReceived(Holder holder, ChannelHandlerContext ctx,\n                                       MobileStateHolder state, StringMessage message) {\n        //format is \"dashId widget_json\" or \"dashId widgetId templateId widget_json\"\n        String[] split = message.body.split(StringUtils.BODY_SEPARATOR_STRING);\n\n        if (split.length < 2) {\n            throw new IllegalCommandException(\"Wrong income message format.\");\n        }\n\n        int dashId = Integer.parseInt(split[0]);\n\n        long widgetAddToId;\n        long templateIdAddToId;\n        String widgetString;\n        if (split.length == 4) {\n            widgetAddToId = Long.parseLong(split[1]);\n            templateIdAddToId = Long.parseLong(split[2]);\n            widgetString = split[3];\n        } else {\n            widgetAddToId = -1;\n            templateIdAddToId = -1;\n            widgetString = split[1];\n        }\n\n        if (widgetString == null || widgetString.isEmpty()) {\n            throw new IllegalCommandException(\"Income widget message is empty.\");\n        }\n\n        if (widgetString.length() > holder.limits.widgetSizeLimitBytes) {\n            throw new NotAllowedException(\"Widget is larger then limit.\", message.id);\n        }\n\n        User user = state.user;\n        DashBoard dash = user.profile.getDashByIdOrThrow(dashId);\n\n        Widget newWidget = JsonParser.parseWidget(widgetString, message.id);\n\n        if (newWidget.width < 1 || newWidget.height < 1) {\n            throw new NotAllowedException(\"Widget has wrong dimensions.\", message.id);\n        }\n\n        log.debug(\"Creating new widget {} for dashId {}.\", widgetString, dashId);\n\n        for (Widget widget : dash.widgets) {\n            if (widget.id == newWidget.id) {\n                throw new NotAllowedException(\"Widget with same id already exists.\", message.id);\n            }\n            if (widget instanceof DeviceTiles) {\n                Widget widgetInTiles = ((DeviceTiles) widget).getWidgetById(newWidget.id);\n                if (widgetInTiles != null) {\n                    throw new NotAllowedException(\"Widget with same id already exists.\", message.id);\n                }\n            }\n        }\n\n        int price = newWidget.getPrice();\n        if (user.notEnoughEnergy(price)) {\n            log.debug(\"Not enough energy.\");\n            ctx.writeAndFlush(energyLimit(message.id), ctx.voidPromise());\n            return;\n        }\n        user.subtractEnergy(price);\n\n        //widget could be added to project or to other widget like DeviceTiles\n        if (widgetAddToId == -1) {\n            dash.widgets = ArrayUtil.add(dash.widgets, newWidget, Widget.class);\n        } else {\n            //right now we can only add to DeviceTiles widget\n            DeviceTiles deviceTiles = (DeviceTiles) dash.getWidgetByIdOrThrow(widgetAddToId);\n            TileTemplate tileTemplate = deviceTiles.getTileTemplateByIdOrThrow(templateIdAddToId);\n            tileTemplate.widgets = ArrayUtil.add(tileTemplate.widgets, newWidget, Widget.class);\n        }\n\n        user.profile.cleanPinStorage(dash, newWidget, true);\n        user.lastModifiedTs = dash.updatedAt;\n\n        TimerWorker timerWorker = holder.timerWorker;\n        if (newWidget instanceof Timer) {\n            timerWorker.add(state.userKey, (Timer) newWidget, dashId, widgetAddToId, templateIdAddToId);\n        } else if (newWidget instanceof Eventor) {\n            timerWorker.add(state.userKey, (Eventor) newWidget, dashId);\n        }\n\n        ctx.writeAndFlush(ok(message.id), ctx.voidPromise());\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/dashboard/widget/MobileDeleteWidgetLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic.dashboard.widget;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.application.handlers.main.auth.MobileStateHolder;\nimport cc.blynk.server.core.dao.UserKey;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.controls.Timer;\nimport cc.blynk.server.core.model.widgets.others.eventor.Eventor;\nimport cc.blynk.server.core.model.widgets.ui.Tabs;\nimport cc.blynk.server.core.model.widgets.ui.tiles.DeviceTiles;\nimport cc.blynk.server.core.model.widgets.ui.tiles.TileTemplate;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandException;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.server.workers.timer.TimerWorker;\nimport cc.blynk.utils.ArrayUtil;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.util.ArrayList;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\nimport static cc.blynk.utils.StringUtils.split2;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.02.16.\n */\npublic final class MobileDeleteWidgetLogic {\n\n    private static final Logger log = LogManager.getLogger(MobileDeleteWidgetLogic.class);\n\n    private MobileDeleteWidgetLogic() {\n    }\n\n    public static void messageReceived(Holder holder, ChannelHandlerContext ctx,\n                                       MobileStateHolder state, StringMessage message) {\n        String[] split = split2(message.body);\n\n        if (split.length < 2) {\n            throw new IllegalCommandException(\"Wrong income message format.\");\n        }\n\n        int dashId = Integer.parseInt(split[0]);\n        long widgetId = Long.parseLong(split[1]);\n\n        User user = state.user;\n        DashBoard dash = user.profile.getDashByIdOrThrow(dashId);\n\n        log.debug(\"Removing widget with id {} for dashId {}.\", widgetId, dashId);\n\n        Widget widgetToDelete = null;\n        DeviceTiles deviceTiles = null;\n\n        long deviceTilesId = -1;\n        long templateId = -1;\n\n        for (Widget widget : dash.widgets) {\n            if (widget.id == widgetId) {\n                widgetToDelete = widget;\n                break;\n            }\n            if (widget instanceof DeviceTiles) {\n                deviceTiles = (DeviceTiles) widget;\n                for (TileTemplate tileTemplate : deviceTiles.templates) {\n                    for (Widget tileTemplateWidget : tileTemplate.widgets) {\n                        if (tileTemplateWidget.id == widgetId) {\n                            widgetToDelete = tileTemplateWidget;\n                            deviceTilesId = deviceTiles.id;\n                            templateId = tileTemplate.id;\n                            break;\n                        }\n                    }\n                }\n            }\n        }\n\n        if (widgetToDelete == null) {\n            throw new IllegalCommandException(\"Widget with passed id not found.\");\n        }\n\n        user.addEnergy(widgetToDelete.getPrice());\n        TimerWorker timerWorker = holder.timerWorker;\n        if (deviceTilesId != -1) {\n            TileTemplate tileTemplate = deviceTiles.getTileTemplateByIdOrThrow(templateId);\n            if (widgetToDelete instanceof Tabs) {\n                tileTemplate.widgets = deleteTabs(timerWorker,\n                        user, state.userKey, dash.id, deviceTilesId,\n                        templateId, tileTemplate.widgets, 0);\n            }\n            int index = tileTemplate.getWidgetIndexByIdOrThrow(widgetId);\n            tileTemplate.widgets = ArrayUtil.remove(tileTemplate.widgets, index, Widget.class);\n        } else {\n            if (widgetToDelete instanceof Tabs) {\n                dash.widgets = deleteTabs(timerWorker, user, state.userKey, dash.id,\n                        deviceTilesId, templateId, dash.widgets, 0);\n            }\n            int index = dash.getWidgetIndexByIdOrThrow(widgetId);\n            dash.widgets = ArrayUtil.remove(dash.widgets, index, Widget.class);\n        }\n\n        user.profile.cleanPinStorage(dash, widgetToDelete, true);\n\n        if (widgetToDelete instanceof Timer) {\n            timerWorker.delete(state.userKey, (Timer) widgetToDelete, dashId, deviceTilesId, templateId);\n        } else if (widgetToDelete instanceof Eventor) {\n            timerWorker.delete(state.userKey, (Eventor) widgetToDelete, dashId);\n        }\n\n        ctx.writeAndFlush(ok(message.id), ctx.voidPromise());\n    }\n\n    /**\n     * Removes all widgets with tabId greater than lastTabIndex\n     */\n    static Widget[] deleteTabs(TimerWorker timerWorker, User user, UserKey userKey,\n                               int dashId, long deviceTilesId, long templateId,\n                               Widget[] widgets, int lastTabIndex) {\n        ArrayList<Widget> zeroTabWidgets = new ArrayList<>();\n        int removedWidgetPrice = 0;\n        for (Widget widgetToDelete : widgets) {\n            if (widgetToDelete.tabId > lastTabIndex) {\n                removedWidgetPrice += widgetToDelete.getPrice();\n                if (widgetToDelete instanceof Timer) {\n                    timerWorker.delete(userKey, (Timer) widgetToDelete, dashId, deviceTilesId, templateId);\n                } else if (widgetToDelete instanceof Eventor) {\n                    timerWorker.delete(userKey, (Eventor) widgetToDelete, dashId);\n                }\n            } else {\n                zeroTabWidgets.add(widgetToDelete);\n            }\n        }\n\n        user.addEnergy(removedWidgetPrice);\n        return zeroTabWidgets.toArray(new Widget[0]);\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/dashboard/widget/MobileGetWidgetLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic.dashboard.widget;\n\nimport cc.blynk.server.application.handlers.main.auth.MobileStateHolder;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandException;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.core.protocol.enums.Command.GET_WIDGET;\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeUTF8StringMessage;\nimport static cc.blynk.utils.StringUtils.split2;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.02.16.\n */\npublic final class MobileGetWidgetLogic {\n\n    private static final Logger log = LogManager.getLogger(MobileGetWidgetLogic.class);\n\n    private MobileGetWidgetLogic() {\n    }\n\n    public static void messageReceived(ChannelHandlerContext ctx, MobileStateHolder state, StringMessage message) {\n        var split = split2(message.body);\n\n        if (split.length < 2) {\n            throw new IllegalCommandException(\"Wrong income message format.\");\n        }\n\n        var dashId = Integer.parseInt(split[0]);\n        var widgetId = Long.parseLong(split[1]);\n\n        var user = state.user;\n        var dash = user.profile.getDashByIdOrThrow(dashId);\n\n        var widget = dash.getWidgetByIdOrThrow(widgetId);\n\n        if (ctx.channel().isWritable()) {\n            var widgetString = JsonParser.toJson(widget);\n            ctx.writeAndFlush(\n                    makeUTF8StringMessage(GET_WIDGET, message.id, widgetString),\n                    ctx.voidPromise()\n            );\n            log.debug(\"Get widget {}.\", widgetString);\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/dashboard/widget/MobileUpdateWidgetLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic.dashboard.widget;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.application.handlers.main.auth.MobileStateHolder;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.controls.Timer;\nimport cc.blynk.server.core.model.widgets.notifications.Mail;\nimport cc.blynk.server.core.model.widgets.notifications.Notification;\nimport cc.blynk.server.core.model.widgets.others.eventor.Eventor;\nimport cc.blynk.server.core.model.widgets.others.rtc.RTC;\nimport cc.blynk.server.core.model.widgets.ui.Tabs;\nimport cc.blynk.server.core.model.widgets.ui.TimeInput;\nimport cc.blynk.server.core.model.widgets.ui.reporting.ReportingWidget;\nimport cc.blynk.server.core.model.widgets.ui.tiles.DeviceTiles;\nimport cc.blynk.server.core.model.widgets.ui.tiles.TileTemplate;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandException;\nimport cc.blynk.server.core.protocol.exceptions.NotAllowedException;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.server.workers.timer.TimerWorker;\nimport cc.blynk.utils.ArrayUtil;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\nimport static cc.blynk.utils.StringUtils.split2;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.02.16.\n */\npublic final class MobileUpdateWidgetLogic {\n\n    private static final Logger log = LogManager.getLogger(MobileUpdateWidgetLogic.class);\n\n    private MobileUpdateWidgetLogic() {\n    }\n\n    public static void messageReceived(Holder holder, ChannelHandlerContext ctx,\n                                       MobileStateHolder state, StringMessage message) {\n        String[] split = split2(message.body);\n\n        if (split.length < 2) {\n            throw new IllegalCommandException(\"Wrong income message format.\");\n        }\n\n        int dashId = Integer.parseInt(split[0]);\n        String widgetString = split[1];\n\n        if (widgetString == null || widgetString.isEmpty()) {\n            throw new IllegalCommandException(\"Income widget message is empty.\");\n        }\n\n        if (widgetString.length() > holder.limits.widgetSizeLimitBytes) {\n            throw new NotAllowedException(\"Widget is larger then limit.\", message.id);\n        }\n\n        User user = state.user;\n        DashBoard dash = user.profile.getDashByIdOrThrow(dashId);\n\n        Widget newWidget = JsonParser.parseWidget(widgetString, message.id);\n\n        if (newWidget.width < 1 || newWidget.height < 1) {\n            throw new NotAllowedException(\"Widget has wrong dimensions.\", message.id);\n        }\n\n        log.debug(\"Updating widget {}.\", widgetString);\n\n        Widget prevWidget = null;\n        DeviceTiles deviceTiles = null;\n\n        long deviceTilesId = -1;\n        long deviceTilesTemplateId = -1;\n\n        long widgetId = newWidget.id;\n        for (Widget widget : dash.widgets) {\n            if (widget.id == widgetId) {\n                prevWidget = widget;\n                break;\n            }\n            if (widget instanceof DeviceTiles) {\n                deviceTiles = (DeviceTiles) widget;\n                for (TileTemplate tileTemplate : deviceTiles.templates) {\n                    for (Widget tileTemplateWidget : tileTemplate.widgets) {\n                        if (tileTemplateWidget.id == widgetId) {\n                            prevWidget = tileTemplateWidget;\n                            deviceTilesId = deviceTiles.id;\n                            deviceTilesTemplateId = tileTemplate.id;\n                            break;\n                        }\n                    }\n                }\n            }\n        }\n\n        if (prevWidget == null) {\n            throw new IllegalCommandException(\"Widget with passed id not found.\");\n        }\n\n        if (!prevWidget.getClass().equals(newWidget.getClass())) {\n            throw new IllegalCommandException(\"Widget class was changed.\");\n        }\n\n        if (prevWidget instanceof Notification) {\n            Notification prevNotif = (Notification) prevWidget;\n            Notification newNotif = (Notification) newWidget;\n            newNotif.iOSTokens.putAll(prevNotif.iOSTokens);\n            newNotif.androidTokens.putAll(prevNotif.androidTokens);\n        }\n\n        //do not update template, tile fields for DeviceTiles.\n        if (newWidget instanceof DeviceTiles) {\n            DeviceTiles prevDeviceTiles = (DeviceTiles) prevWidget;\n            DeviceTiles newDeviceTiles = (DeviceTiles) newWidget;\n            newDeviceTiles.tiles = prevDeviceTiles.tiles;\n            newDeviceTiles.templates = prevDeviceTiles.templates;\n        } else {\n            //this is special widgets that should not preserve values on update\n            if (!(newWidget instanceof Timer)\n                    && !(newWidget instanceof ReportingWidget)\n                    && !(newWidget instanceof TimeInput)\n                    && !(newWidget instanceof Mail)\n                    && !(newWidget instanceof RTC)) {\n                newWidget.updateValue(prevWidget);\n            }\n        }\n\n        if (newWidget instanceof ReportingWidget) {\n            ReportingWidget prevReporting = (ReportingWidget) prevWidget;\n            ReportingWidget newReporting = (ReportingWidget) newWidget;\n            newReporting.reports = prevReporting.reports;\n        }\n\n        TimerWorker timerWorker = holder.timerWorker;\n        if (deviceTilesId != -1) {\n            TileTemplate tileTemplate = deviceTiles.getTileTemplateByWidgetIdOrThrow(newWidget.id);\n            if (newWidget instanceof Tabs) {\n                Tabs newTabs = (Tabs) newWidget;\n                tileTemplate.widgets = MobileDeleteWidgetLogic.deleteTabs(timerWorker,\n                        user, state.userKey, dash.id, deviceTilesId, deviceTilesTemplateId,\n                        tileTemplate.widgets, newTabs.tabs.length - 1);\n            }\n            tileTemplate.widgets = ArrayUtil.copyAndReplace(\n                    tileTemplate.widgets, newWidget, tileTemplate.getWidgetIndexByIdOrThrow(newWidget.id));\n        } else {\n            if (newWidget instanceof Tabs) {\n                Tabs newTabs = (Tabs) newWidget;\n                dash.widgets = MobileDeleteWidgetLogic.deleteTabs(timerWorker,\n                        user, state.userKey, dash.id, deviceTilesId, deviceTilesTemplateId,\n                        dash.widgets, newTabs.tabs.length - 1);\n            }\n            dash.widgets = ArrayUtil.copyAndReplace(\n                    dash.widgets, newWidget, dash.getWidgetIndexByIdOrThrow(newWidget.id));\n        }\n\n        user.profile.cleanPinStorage(dash, newWidget, true);\n        user.lastModifiedTs = dash.updatedAt;\n\n        if (prevWidget instanceof Timer) {\n            timerWorker.delete(state.userKey, (Timer) prevWidget, dashId, deviceTilesId, deviceTilesTemplateId);\n        } else if (prevWidget instanceof Eventor) {\n            timerWorker.delete(state.userKey, (Eventor) prevWidget, dashId);\n        }\n\n        if (newWidget instanceof Timer) {\n            timerWorker.add(state.userKey, (Timer) newWidget, dashId, deviceTilesId, deviceTilesTemplateId);\n        } else if (newWidget instanceof Eventor) {\n            timerWorker.add(state.userKey, (Eventor) newWidget, dashId);\n        }\n\n        ctx.writeAndFlush(ok(message.id), ctx.voidPromise());\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/dashboard/widget/tile/MobileCreateTileTemplateLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic.dashboard.widget.tile;\n\nimport cc.blynk.server.application.handlers.main.auth.MobileStateHolder;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.ui.tiles.DeviceTiles;\nimport cc.blynk.server.core.model.widgets.ui.tiles.TileTemplate;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandException;\nimport cc.blynk.server.core.protocol.exceptions.NotAllowedException;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.utils.ArrayUtil;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\nimport static cc.blynk.utils.StringUtils.split3;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.02.16.\n */\npublic final class MobileCreateTileTemplateLogic {\n\n    private static final Logger log = LogManager.getLogger(MobileCreateTileTemplateLogic.class);\n\n    private MobileCreateTileTemplateLogic() {\n    }\n\n    public static void messageReceived(ChannelHandlerContext ctx, MobileStateHolder state, StringMessage message) {\n        String[] split = split3(message.body);\n\n        if (split.length < 3) {\n            throw new IllegalCommandException(\"Wrong income message format.\");\n        }\n\n        int dashId = Integer.parseInt(split[0]);\n        long widgetId = Long.parseLong(split[1]);\n        String tileTemplateString = split[2];\n\n        if (tileTemplateString == null || tileTemplateString.isEmpty()) {\n            throw new IllegalCommandException(\"Income tile template message is empty.\");\n        }\n\n        User user = state.user;\n        DashBoard dash = user.profile.getDashByIdOrThrow(dashId);\n        Widget widget = dash.getWidgetByIdOrThrow(widgetId);\n\n        if (!(widget instanceof DeviceTiles)) {\n            throw new IllegalCommandException(\"Income widget id is not DeviceTiles.\");\n        }\n\n        DeviceTiles deviceTiles = (DeviceTiles) widget;\n\n        TileTemplate newTileTemplate = JsonParser.parseTileTemplate(tileTemplateString, message.id);\n\n        if (newTileTemplate.isEmptyTemplateId()) {\n            newTileTemplate.templateId = \"TMPL\" + newTileTemplate.id;\n        }\n\n        for (TileTemplate tileTemplate : deviceTiles.templates) {\n            if (tileTemplate.id == newTileTemplate.id) {\n                throw new NotAllowedException(\"tile template with same id already exists.\", message.id);\n            }\n        }\n\n        log.debug(\"Creating tile template {}.\", tileTemplateString);\n\n        deviceTiles.templates = ArrayUtil.add(deviceTiles.templates, newTileTemplate, TileTemplate.class);\n        deviceTiles.recreateTilesIfNecessary(newTileTemplate, null);\n\n        user.profile.cleanPinStorage(dash, deviceTiles, true);\n\n        ctx.writeAndFlush(ok(message.id), ctx.voidPromise());\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/dashboard/widget/tile/MobileDeleteTileTemplateLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic.dashboard.widget.tile;\n\nimport cc.blynk.server.application.handlers.main.auth.MobileStateHolder;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.ui.tiles.DeviceTiles;\nimport cc.blynk.server.core.model.widgets.ui.tiles.TileTemplate;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandException;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.utils.ArrayUtil;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\nimport static cc.blynk.utils.StringUtils.split3;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.02.16.\n */\npublic final class MobileDeleteTileTemplateLogic {\n\n    private static final Logger log = LogManager.getLogger(MobileDeleteTileTemplateLogic.class);\n\n    private MobileDeleteTileTemplateLogic() {\n    }\n\n    public static void messageReceived(ChannelHandlerContext ctx, MobileStateHolder state, StringMessage message) {\n        String[] split = split3(message.body);\n\n        if (split.length < 2) {\n            throw new IllegalCommandException(\"Wrong income message format.\");\n        }\n\n        int dashId = Integer.parseInt(split[0]);\n        long widgetId = Long.parseLong(split[1]);\n        long tileId = Long.parseLong(split[2]);\n\n        User user = state.user;\n        DashBoard dash = user.profile.getDashByIdOrThrow(dashId);\n        Widget widget = dash.getWidgetByIdOrThrow(widgetId);\n\n        if (!(widget instanceof DeviceTiles)) {\n            throw new IllegalCommandException(\"Income widget id is not DeviceTiles.\");\n        }\n\n        DeviceTiles deviceTiles = (DeviceTiles) widget;\n        int existingTileIndex = deviceTiles.getTileTemplateIndexByIdOrThrow(tileId);\n\n        log.debug(\"Deleting tile template dashId : {}, widgetId : {}, tileId : {}.\", dash, widgetId, tileId);\n\n        TileTemplate tileTemplate = deviceTiles.templates[existingTileIndex];\n        user.addEnergy(tileTemplate.getPrice());\n\n        deviceTiles.templates = ArrayUtil.remove(deviceTiles.templates, existingTileIndex, TileTemplate.class);\n        deviceTiles.deleteDeviceTilesByTemplateId(tileId);\n        user.profile.cleanPinStorageForTileTemplate(dash, tileTemplate, true);\n\n        dash.updatedAt = System.currentTimeMillis();\n\n        ctx.writeAndFlush(ok(message.id), ctx.voidPromise());\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/dashboard/widget/tile/MobileUpdateTileTemplateLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic.dashboard.widget.tile;\n\nimport cc.blynk.server.application.handlers.main.auth.MobileStateHolder;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.ui.tiles.DeviceTiles;\nimport cc.blynk.server.core.model.widgets.ui.tiles.TileTemplate;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandException;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\nimport static cc.blynk.utils.StringUtils.split3;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.02.16.\n */\npublic final class MobileUpdateTileTemplateLogic {\n\n    private static final Logger log = LogManager.getLogger(MobileUpdateTileTemplateLogic.class);\n\n    private MobileUpdateTileTemplateLogic() {\n    }\n\n    public static void messageReceived(ChannelHandlerContext ctx, MobileStateHolder state, StringMessage message) {\n        String[] split = split3(message.body);\n\n        if (split.length < 3) {\n            throw new IllegalCommandException(\"Wrong income message format.\");\n        }\n\n        int dashId = Integer.parseInt(split[0]);\n        long widgetId = Long.parseLong(split[1]);\n        String tileTemplateString = split[2];\n\n        if (tileTemplateString == null || tileTemplateString.isEmpty()) {\n            throw new IllegalCommandException(\"Income tile template message is empty.\");\n        }\n\n        User user = state.user;\n        DashBoard dash = user.profile.getDashByIdOrThrow(dashId);\n        Widget widget = dash.getWidgetByIdOrThrow(widgetId);\n\n        if (!(widget instanceof DeviceTiles)) {\n            throw new IllegalCommandException(\"Income widget id is not DeviceTiles.\");\n        }\n\n        DeviceTiles deviceTiles = (DeviceTiles) widget;\n\n        TileTemplate newTileTemplate = JsonParser.parseTileTemplate(tileTemplateString, message.id);\n        int existingTileTemplateIndex = deviceTiles.getTileTemplateIndexByIdOrThrow(newTileTemplate.id);\n        TileTemplate existingTileTemplate = deviceTiles.templates[existingTileTemplateIndex];\n\n        deviceTiles.recreateTilesIfNecessary(newTileTemplate, existingTileTemplate);\n\n        log.debug(\"Updating tile template {}.\", tileTemplateString);\n        deviceTiles.replaceTileTemplate(newTileTemplate, existingTileTemplateIndex);\n\n        user.profile.cleanPinStorage(dash, deviceTiles, false);\n\n        ctx.writeAndFlush(ok(message.id), ctx.voidPromise());\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/face/MobileCreateAppLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic.face;\n\nimport cc.blynk.server.application.handlers.main.auth.MobileStateHolder;\nimport cc.blynk.server.core.model.auth.App;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandException;\nimport cc.blynk.server.core.protocol.exceptions.NotAllowedException;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.utils.AppNameUtil;\nimport cc.blynk.utils.ArrayUtil;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.core.protocol.enums.Command.CREATE_APP;\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeUTF8StringMessage;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.02.16.\n */\npublic final class MobileCreateAppLogic {\n\n    private static final Logger log = LogManager.getLogger(MobileCreateAppLogic.class);\n\n    private MobileCreateAppLogic() {\n    }\n\n    public static void messageReceived(ChannelHandlerContext ctx, MobileStateHolder state,\n                                       StringMessage message, int maxWidgetSize) {\n        var appString = message.body;\n\n        if (appString == null || appString.isEmpty()) {\n            throw new IllegalCommandException(\"Income app message is empty.\");\n        }\n\n        if (appString.length() > maxWidgetSize) {\n            throw new NotAllowedException(\"App is larger then limit.\", message.id);\n        }\n\n        var newApp = JsonParser.parseApp(appString, message.id);\n\n        newApp.id = AppNameUtil.generateAppId();\n\n        if (newApp.isNotValid()) {\n            throw new NotAllowedException(\"App is not valid.\", message.id);\n        }\n\n        log.debug(\"Creating new app {}.\", newApp);\n\n        var user = state.user;\n\n        if (user.profile.apps.length > 25) {\n            throw new NotAllowedException(\"App limit is reached.\", message.id);\n        }\n\n        for (App app : user.profile.apps) {\n            if (app.id.equals(newApp.id)) {\n                throw new NotAllowedException(\"App with same id already exists.\", message.id);\n            }\n        }\n\n        user.profile.apps = ArrayUtil.add(user.profile.apps, newApp, App.class);\n        user.lastModifiedTs = System.currentTimeMillis();\n\n        ctx.writeAndFlush(makeUTF8StringMessage(CREATE_APP, message.id, JsonParser.toJson(newApp)), ctx.voidPromise());\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/face/MobileDeleteAppLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic.face;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.application.handlers.main.auth.MobileStateHolder;\nimport cc.blynk.server.core.dao.SessionDao;\nimport cc.blynk.server.core.dao.TokenManager;\nimport cc.blynk.server.core.dao.UserDao;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.App;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.protocol.exceptions.NotAllowedException;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.server.workers.timer.TimerWorker;\nimport cc.blynk.utils.ArrayUtil;\nimport io.netty.channel.ChannelHandlerContext;\n\nimport java.util.ArrayList;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.02.16.\n */\npublic final class MobileDeleteAppLogic {\n\n    private final TokenManager tokenManager;\n    private final TimerWorker timerWorker;\n    private final SessionDao sessionDao;\n    private final UserDao userDao;\n\n    public MobileDeleteAppLogic(Holder holder) {\n        this.tokenManager = holder.tokenManager;\n        this.timerWorker = holder.timerWorker;\n        this.sessionDao = holder.sessionDao;\n        this.userDao = holder.userDao;\n    }\n\n    public void messageReceived(ChannelHandlerContext ctx, MobileStateHolder state, StringMessage message) {\n        String id = message.body;\n\n        User user = state.user;\n\n        int existingAppIndex = user.profile.getAppIndexById(id);\n\n        if (existingAppIndex == -1) {\n            throw new NotAllowedException(\"App with passed is not exists.\", message.id);\n        }\n\n        App app = user.profile.apps[existingAppIndex];\n        int[] projectIds = app.projectIds;\n\n        for (User tmpUser : userDao.users.values()) {\n            if (app.id.equals(tmpUser.appName) && !user.email.equals(tmpUser.email)) {\n                throw new NotAllowedException(\"App has users assigned. You can't remove it.\", message.id);\n            }\n        }\n\n        var result = new ArrayList<DashBoard>();\n        for (DashBoard dash : user.profile.dashBoards) {\n            if (ArrayUtil.contains(projectIds, dash.id)) {\n                timerWorker.deleteTimers(state.userKey, dash);\n                tokenManager.deleteDash(dash);\n                sessionDao.closeHardwareChannelByDashId(state.userKey, dash.id);\n            } else {\n                result.add(dash);\n            }\n        }\n\n        user.profile.dashBoards = result.toArray(new DashBoard[0]);\n        user.profile.apps = ArrayUtil.remove(user.profile.apps, existingAppIndex, App.class);\n        user.lastModifiedTs = System.currentTimeMillis();\n\n        ctx.writeAndFlush(ok(message.id), ctx.voidPromise());\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/face/MobileMailQRsLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic.face;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.TextHolder;\nimport cc.blynk.server.core.BlockingIOProcessor;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.App;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.enums.ProvisionType;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.server.db.DBManager;\nimport cc.blynk.server.db.model.FlashedToken;\nimport cc.blynk.server.notifications.mail.MailWrapper;\nimport cc.blynk.server.notifications.mail.QrHolder;\nimport cc.blynk.utils.StringUtils;\nimport cc.blynk.utils.TokenGeneratorUtil;\nimport cc.blynk.utils.properties.Placeholders;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelHandlerContext;\nimport net.glxn.qrgen.core.image.ImageType;\nimport net.glxn.qrgen.javase.QRCode;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.illegalCommandBody;\nimport static cc.blynk.server.internal.CommonByteBufUtil.notificationError;\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\n\n/**\n * Sends email from application.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic final class MobileMailQRsLogic {\n\n    private static final Logger log = LogManager.getLogger(MobileMailQRsLogic.class);\n\n    private final TextHolder textHolder;\n    private final BlockingIOProcessor blockingIOProcessor;\n    private final MailWrapper mailWrapper;\n    private final DBManager dbManager;\n\n    public MobileMailQRsLogic(Holder holder) {\n        this.blockingIOProcessor = holder.blockingIOProcessor;\n        this.mailWrapper =  holder.mailWrapper;\n        this.dbManager = holder.dbManager;\n        this.textHolder = holder.textHolder;\n    }\n\n    public void messageReceived(ChannelHandlerContext ctx, User user, StringMessage message) {\n        String[] split = message.body.split(StringUtils.BODY_SEPARATOR_STRING);\n\n        int dashId = Integer.parseInt(split[0]);\n        DashBoard dash = user.profile.getDashByIdOrThrow(dashId);\n\n        String appId = split[1];\n        App app = user.profile.getAppById(appId);\n\n        if (app == null) {\n            log.debug(\"App with passed id not found.\");\n            ctx.writeAndFlush(illegalCommandBody(message.id), ctx.voidPromise());\n            return;\n        }\n\n        if (app.provisionType == ProvisionType.STATIC && dash.devices.length == 0) {\n            log.debug(\"No devices in project.\");\n            ctx.writeAndFlush(illegalCommandBody(message.id), ctx.voidPromise());\n            return;\n        }\n\n        log.debug(\"Sending app preview email to {}, provision type {}\", user.email, app.provisionType);\n        makePublishPreviewEmail(ctx, user, dash, app.provisionType, app.name, appId, message.id);\n    }\n\n    private void makePublishPreviewEmail(ChannelHandlerContext ctx, User user, DashBoard dash,\n                                         ProvisionType provisionType,\n                                         String publishAppName, String publishAppId, int msgId) {\n        String subj = publishAppName + \" - App details\";\n        Channel channel = ctx.channel();\n        String dashName = dash.getNameOrDefault();\n        String to = user.email;\n        if (provisionType == ProvisionType.DYNAMIC) {\n            blockingIOProcessor.execute(() -> {\n                try {\n                    String newToken = TokenGeneratorUtil.generateNewToken();\n                    QrHolder qrHolder = new QrHolder(dash.id, -1, null, newToken,\n                                QRCode.from(newToken).to(ImageType.JPG).stream().toByteArray());\n                    FlashedToken flashedToken = new FlashedToken(to, newToken, publishAppId, dash.id, -1);\n\n                    if (!dbManager.insertFlashedTokens(flashedToken)) {\n                        throw new Exception(\"App Publishing Preview requires enabled DB.\");\n                    }\n\n                    String finalBody = textHolder.dynamicMailBody\n                            .replace(Placeholders.PROJECT_NAME, dashName);\n\n                    mailWrapper.sendWithAttachment(to, subj, finalBody, qrHolder);\n                    channel.writeAndFlush(ok(msgId), channel.voidPromise());\n                } catch (Exception e) {\n                    log.error(\"Error sending dynamic email from application. For user {}. Error: \", to, e);\n                    channel.writeAndFlush(notificationError(msgId), channel.voidPromise());\n                }\n            });\n        } else {\n            blockingIOProcessor.execute(() -> {\n                try {\n                    QrHolder[] qrHolders = makeQRs(user, publishAppId, dash);\n                    StringBuilder sb = new StringBuilder();\n                    for (QrHolder qrHolder : qrHolders) {\n                        qrHolder.attach(sb);\n                    }\n\n                    String finalBody = textHolder.staticMailBody\n                            .replace(Placeholders.PROJECT_NAME, dashName)\n                            .replace(Placeholders.DYNAMIC_SECTION, sb.toString());\n\n                    mailWrapper.sendWithAttachment(to, subj, finalBody, qrHolders);\n                    channel.writeAndFlush(ok(msgId), channel.voidPromise());\n                } catch (Exception e) {\n                    log.error(\"Error sending static email from application. For user {}. Reason: {}\", to, e);\n                    channel.writeAndFlush(notificationError(msgId), channel.voidPromise());\n                }\n            });\n        }\n    }\n\n\n    private QrHolder[] makeQRs(User user, String appId, DashBoard dash) throws Exception {\n        int tokensCount = dash.devices.length;\n        QrHolder[] qrHolders = new QrHolder[tokensCount];\n        FlashedToken[] flashedTokens = new FlashedToken[tokensCount];\n\n        int i = 0;\n        for (Device device : dash.devices) {\n            String newToken = TokenGeneratorUtil.generateNewToken();\n            qrHolders[i] = new QrHolder(dash.id, device.id, device.name, newToken,\n                    QRCode.from(newToken).to(ImageType.JPG).stream().toByteArray());\n            flashedTokens[i++] = new FlashedToken(user.email, newToken, appId, dash.id, device.id);\n        }\n\n        if (!dbManager.insertFlashedTokens(flashedTokens)) {\n            throw new Exception(\"App Publishing Preview requires enabled DB.\");\n        }\n\n        return qrHolders;\n    }\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/face/MobileUpdateAppLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic.face;\n\nimport cc.blynk.server.application.handlers.main.auth.MobileStateHolder;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandException;\nimport cc.blynk.server.core.protocol.exceptions.NotAllowedException;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.02.16.\n */\npublic final class MobileUpdateAppLogic {\n\n    private static final Logger log = LogManager.getLogger(MobileUpdateAppLogic.class);\n\n    private MobileUpdateAppLogic() {\n    }\n\n    public static void messageReceived(ChannelHandlerContext ctx, MobileStateHolder state,\n                                       StringMessage message, int maxWidgetSize) {\n        var appString = message.body;\n\n        if (appString == null || appString.isEmpty()) {\n            throw new IllegalCommandException(\"Income app message is empty.\");\n        }\n\n        if (appString.length() > maxWidgetSize) {\n            throw new NotAllowedException(\"App is larger then limit.\", message.id);\n        }\n\n        var newApp = JsonParser.parseApp(appString, message.id);\n\n        if (newApp.isNotValid()) {\n            throw new NotAllowedException(\"App is not valid.\", message.id);\n        }\n\n        log.debug(\"Creating new app {}.\", newApp);\n\n        var user = state.user;\n\n        var existingApp = user.profile.getAppById(newApp.id);\n\n        if (existingApp == null) {\n            throw new NotAllowedException(\"App with passed is not exists.\", message.id);\n        }\n\n        existingApp.update(newApp);\n\n        user.lastModifiedTs = System.currentTimeMillis();\n\n        ctx.writeAndFlush(ok(message.id), ctx.voidPromise());\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/face/MobileUpdateFaceLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic.face;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.core.dao.UserKey;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.App;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.utils.ArrayUtil;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.util.HashSet;\n\nimport static cc.blynk.server.core.model.serialization.CopyUtil.copyTags;\nimport static cc.blynk.server.internal.CommonByteBufUtil.notAllowed;\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\n\n/**\n * Update faces of related project.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic final class MobileUpdateFaceLogic {\n\n    private static final Logger log = LogManager.getLogger(MobileUpdateFaceLogic.class);\n\n    private MobileUpdateFaceLogic() {\n    }\n\n    public static void messageReceived(Holder holder, ChannelHandlerContext ctx,\n                                       User user, StringMessage message) {\n        int parentDashId = Integer.parseInt(message.body);\n\n        DashBoard parent = user.profile.getDashByIdOrThrow(parentDashId);\n\n        HashSet<String> appIds = new HashSet<>();\n        for (DashBoard dashBoard : user.profile.dashBoards) {\n            if (dashBoard.parentId == parentDashId) {\n                for (App app : user.profile.apps) {\n                    if (ArrayUtil.contains(app.projectIds, dashBoard.id)) {\n                        appIds.add(app.id);\n                    }\n                }\n            }\n        }\n\n        if (appIds.size() == 0) {\n            log.debug(\"Passed dash has no childs assigned to any app.\");\n            ctx.writeAndFlush(notAllowed(message.id));\n            return;\n        }\n\n        boolean hasFaces = false;\n        int count = 0;\n        log.info(\"Updating face {} for user {}-{}. App Ids : {}\", parentDashId,\n                user.email, user.appName, JsonParser.valueToJsonAsString(appIds));\n        for (User existingUser : holder.userDao.users.values()) {\n            for (DashBoard existingDash : existingUser.profile.dashBoards) {\n                if (existingDash.parentId == parentDashId && (existingUser == user\n                        || appIds.contains(existingUser.appName))) {\n                    hasFaces = true;\n                    //we found child project-face\n                    log.debug(\"Found face for {}-{}.\", existingUser.email, existingUser.appName);\n                    try {\n                        existingDash.updateFaceFields(parent);\n                        existingDash.tags = copyTags(parent.tags);\n                        existingUser.lastModifiedTs = System.currentTimeMillis();\n                        //do not close connection for initiator\n                        if (existingUser != user) {\n                            holder.sessionDao.closeAppChannelsByUser(new UserKey(existingUser));\n                        }\n                        count++;\n                    } catch (Exception e) {\n                        log.error(\"Error updating face for user {}, dashId {}.\",\n                                existingUser.email, existingDash.id, e);\n                        ctx.writeAndFlush(notAllowed(message.id));\n                    }\n                }\n            }\n        }\n\n        if (hasFaces) {\n            log.debug(\"{} faces were updated successfully.\", count);\n            ctx.writeAndFlush(ok(message.id));\n        } else {\n            log.info(\"No child faces found for update.\");\n            ctx.writeAndFlush(notAllowed(message.id));\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/graph/MobileDeleteDeviceDataLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic.graph;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.application.handlers.main.auth.MobileStateHolder;\nimport cc.blynk.server.application.handlers.sharing.auth.MobileShareStateHolder;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.widgets.ui.reporting.ReportingWidget;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandException;\nimport cc.blynk.server.core.protocol.exceptions.NotAllowedException;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.utils.StringUtils;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.illegalCommand;\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\nimport static cc.blynk.utils.StringUtils.split2Device;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic final class MobileDeleteDeviceDataLogic {\n\n    private static final Logger log = LogManager.getLogger(MobileDeleteDeviceDataLogic.class);\n\n    private MobileDeleteDeviceDataLogic() {\n    }\n\n    private static int[] getDeviceIds(Device[] devices) {\n        int[] deviceIds = new int[devices.length];\n        for (int i = 0; i < devices.length; i++) {\n            deviceIds[i] = devices[i].id;\n        }\n        return deviceIds;\n    }\n\n    public static void messageReceived(Holder holder, ChannelHandlerContext ctx,\n                                       MobileStateHolder state, StringMessage message) {\n        String[] messageParts = StringUtils.split2(message.body);\n\n        if (messageParts.length < 1) {\n            throw new IllegalCommandException(\"Wrong income message format.\");\n        }\n\n        String[] dashIdAndDeviceId = split2Device(messageParts[0]);\n        int dashId = Integer.parseInt(dashIdAndDeviceId[0]);\n        User user = state.user;\n        DashBoard dash = user.profile.getDashByIdOrThrow(dashId);\n\n        //if app is shared - check if we can remove devices\n        if (state instanceof MobileShareStateHolder) {\n            ReportingWidget reportingWidget = dash.getReportingWidget();\n            if (reportingWidget == null) {\n                throw new IllegalCommandException(\"No reporting widget.\");\n            }\n            if (!reportingWidget.allowEndUserToDeleteDataOn) {\n                throw new NotAllowedException(\"You are not allowed to delete the data.\", message.id);\n            }\n        }\n\n        if (\"*\".equals(dashIdAndDeviceId[1])) {\n            int[] deviceIds = getDeviceIds(dash.devices);\n            delete(holder, ctx.channel(), message.id, user, dash, deviceIds);\n        } else {\n            int deviceId = Integer.parseInt(dashIdAndDeviceId[1]);\n\n            //we have only deviceId\n            if (messageParts.length == 1) {\n                delete(holder, ctx.channel(), message.id, user, dash, deviceId);\n            } else {\n                //we have deviceId and datastreams to clean\n                delete(holder,  ctx.channel(), message.id, user, dash, deviceId,\n                        messageParts[1].split(StringUtils.BODY_SEPARATOR_STRING));\n            }\n        }\n    }\n\n    private static void delete(Holder holder, Channel channel, int msgId, User user, DashBoard dash, int... deviceIds) {\n        holder.blockingIOProcessor.executeHistory(() -> {\n            try {\n                for (int deviceId : deviceIds) {\n                    int removedCounter = holder.reportingDiskDao.delete(user, dash.id, deviceId);\n                    log.debug(\"Removed {} files for dashId {} and deviceId {}\", removedCounter, dash.id, deviceId);\n                }\n                channel.writeAndFlush(ok(msgId), channel.voidPromise());\n            } catch (Exception e) {\n                log.warn(\"Error removing device data. Reason : {}.\", e.getMessage());\n                channel.writeAndFlush(illegalCommand(msgId), channel.voidPromise());\n            }\n        });\n    }\n\n    private static void delete(Holder holder,  Channel channel, int msgId,\n                        User user, DashBoard dash, int deviceId, String[] pins) {\n        holder.blockingIOProcessor.executeHistory(() -> {\n            try {\n                int removedCounter = holder.reportingDiskDao.delete(user, dash.id, deviceId, pins);\n                log.debug(\"Removed {} files for dashId {} and deviceId {}\", removedCounter, dash.id, deviceId);\n                channel.writeAndFlush(ok(msgId), channel.voidPromise());\n            } catch (Exception e) {\n                log.warn(\"Error removing device data. Reason : {}.\", e.getMessage());\n                channel.writeAndFlush(illegalCommand(msgId), channel.voidPromise());\n            }\n        });\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/graph/MobileDeleteEnhancedGraphDataLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic.graph;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.DataStream;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Tag;\nimport cc.blynk.server.core.model.widgets.Target;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.outputs.graph.GraphDataStream;\nimport cc.blynk.server.core.model.widgets.outputs.graph.Superchart;\nimport cc.blynk.server.core.model.widgets.ui.DeviceSelector;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandException;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.utils.StringUtils;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.illegalCommand;\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\nimport static cc.blynk.utils.StringUtils.split2Device;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic final class MobileDeleteEnhancedGraphDataLogic {\n\n    private static final Logger log = LogManager.getLogger(MobileDeleteEnhancedGraphDataLogic.class);\n\n    private MobileDeleteEnhancedGraphDataLogic() {\n    }\n\n    public static void messageReceived(Holder holder, ChannelHandlerContext ctx,\n                                       User user, StringMessage message) {\n        String[] messageParts = StringUtils.split3(message.body);\n\n        if (messageParts.length < 2) {\n            throw new IllegalCommandException(\"Wrong income message format.\");\n        }\n\n        String[] dashIdAndDeviceId = split2Device(messageParts[0]);\n        int dashId = Integer.parseInt(dashIdAndDeviceId[0]);\n        long widgetId = Long.parseLong(messageParts[1]);\n        int streamIndex = -1;\n        if (message.body.length() == 3) {\n            streamIndex = Integer.parseInt(messageParts[2]);\n        }\n        int targetId = -1;\n        if (dashIdAndDeviceId.length == 2) {\n            targetId = Integer.parseInt(dashIdAndDeviceId[1]);\n        }\n\n        DashBoard dash = user.profile.getDashByIdOrThrow(dashId);\n\n        Widget widget = dash.getWidgetById(widgetId);\n        if (widget == null) {\n            widget = dash.getWidgetByIdInDeviceTilesOrThrow(widgetId);\n        }\n        Superchart enhancedHistoryGraph = (Superchart) widget;\n\n        if (streamIndex == -1 || streamIndex > enhancedHistoryGraph.dataStreams.length - 1) {\n            delete(holder, ctx.channel(), message.id, user, dash, targetId, enhancedHistoryGraph.dataStreams);\n        } else {\n            delete(holder, ctx.channel(),\n                    message.id, user, dash, targetId, enhancedHistoryGraph.dataStreams[streamIndex]);\n        }\n    }\n\n    private static void delete(Holder holder, Channel channel, int msgId,\n                               User user, DashBoard dash, int targetId, GraphDataStream... dataStreams) {\n        holder.blockingIOProcessor.executeHistory(() -> {\n            try {\n                for (GraphDataStream graphDataStream : dataStreams) {\n                    Target target;\n                    int targetIdUpdated = graphDataStream.getTargetId(targetId);\n                    if (targetIdUpdated < Tag.START_TAG_ID) {\n                        target = user.profile.getDeviceById(dash, targetIdUpdated);\n                    } else if (targetIdUpdated < DeviceSelector.DEVICE_SELECTOR_STARTING_ID) {\n                        target = user.profile.getTagById(dash, targetIdUpdated);\n                    } else {\n                        target = dash.getDeviceSelector(targetIdUpdated);\n                    }\n\n                    DataStream dataStream = graphDataStream.dataStream;\n                    if (target != null && dataStream != null && dataStream.pinType != null) {\n                        int deviceId = target.getDeviceId();\n                        holder.reportingDiskDao.delete(user, dash.id, deviceId, dataStream.pinType, dataStream.pin);\n                    }\n                }\n                channel.writeAndFlush(ok(msgId), channel.voidPromise());\n            } catch (Exception e) {\n                log.debug(\"Error removing enhanced graph data. Reason : {}.\", e.getMessage());\n                channel.writeAndFlush(illegalCommand(msgId), channel.voidPromise());\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/graph/MobileExportGraphDataLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic.graph;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.application.handlers.main.logic.graph.links.DeviceFileLink;\nimport cc.blynk.server.core.BlockingIOProcessor;\nimport cc.blynk.server.core.dao.ReportingDiskDao;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.DataStream;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.outputs.graph.GraphDataStream;\nimport cc.blynk.server.core.model.widgets.outputs.graph.Superchart;\nimport cc.blynk.server.core.model.widgets.ui.DeviceSelector;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandException;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.server.notifications.mail.MailWrapper;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.nio.file.Path;\nimport java.util.ArrayList;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.noData;\nimport static cc.blynk.server.internal.CommonByteBufUtil.notificationError;\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\nimport static cc.blynk.utils.StringUtils.BODY_SEPARATOR_STRING;\nimport static cc.blynk.utils.StringUtils.split2Device;\n\n/**\n * Sends graph pins data in csv format via to user email.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\n@Deprecated\npublic final class MobileExportGraphDataLogic {\n\n    private static final Logger log = LogManager.getLogger(MobileExportGraphDataLogic.class);\n\n    private final BlockingIOProcessor blockingIOProcessor;\n    private final ReportingDiskDao reportingDao;\n    private final MailWrapper mailWrapper;\n    private final String csvDownloadUrl;\n\n    public MobileExportGraphDataLogic(Holder holder) {\n        this.reportingDao = holder.reportingDiskDao;\n        this.blockingIOProcessor = holder.blockingIOProcessor;\n        this.mailWrapper = holder.mailWrapper;\n        this.csvDownloadUrl = holder.downloadUrl;\n    }\n\n    public void messageReceived(ChannelHandlerContext ctx, User user, StringMessage message) {\n        String[] messageParts = message.body.split(BODY_SEPARATOR_STRING);\n\n        if (messageParts.length < 2) {\n            throw new IllegalCommandException(\"Wrong income message format.\");\n        }\n\n        String[] dashIdAndDeviceId = split2Device(messageParts[0]);\n        int dashId = Integer.parseInt(dashIdAndDeviceId[0]);\n        int targetId = -1;\n\n        if (dashIdAndDeviceId.length == 2) {\n            targetId = Integer.parseInt(dashIdAndDeviceId[1]);\n        }\n\n        DashBoard dash = user.profile.getDashByIdOrThrow(dashId);\n\n        long widgetId = Long.parseLong(messageParts[1]);\n\n        Widget widget = dash.getWidgetById(widgetId);\n        if (widget == null) {\n            widget = dash.getWidgetByIdInDeviceTilesOrThrow(widgetId);\n        }\n\n        if (widget instanceof Superchart) {\n            Superchart enhancedHistoryGraph = (Superchart) widget;\n\n            blockingIOProcessor.executeHistory(\n                    new ExportEnhancedHistoryGraphJob(ctx, dash, targetId, enhancedHistoryGraph, message.id, user)\n            );\n        } else {\n            throw new IllegalCommandException(\"Passed wrong widget id.\");\n        }\n    }\n\n    private class ExportEnhancedHistoryGraphJob implements Runnable {\n\n        private final ChannelHandlerContext ctx;\n        private final DashBoard dash;\n        private final int targetId;\n        private final Superchart enhancedHistoryGraph;\n        private final int msgId;\n        private final User user;\n\n        ExportEnhancedHistoryGraphJob(ChannelHandlerContext ctx, DashBoard dash, int targetId,\n                                      Superchart enhancedHistoryGraph, int msgId, User user) {\n            this.ctx = ctx;\n            this.dash = dash;\n            this.targetId = targetId;\n            this.enhancedHistoryGraph = enhancedHistoryGraph;\n            this.msgId = msgId;\n            this.user = user;\n        }\n\n        @Override\n        public void run() {\n            try {\n                String dashName = dash.getNameOrEmpty();\n                ArrayList<DeviceFileLink> pinsCSVFilePath = new ArrayList<>();\n                for (GraphDataStream graphDataStream : enhancedHistoryGraph.dataStreams) {\n                    DataStream dataStream = graphDataStream.dataStream;\n                    //special case, for device tiles widget targetID may be overrided\n                    int deviceId = graphDataStream.getTargetId(targetId);\n                    if (dataStream != null) {\n                        try {\n                            int[] deviceIds = new int[] {deviceId};\n                            //special case, this is not actually a deviceId but device selector widget id\n                            //todo refactor/simplify/test\n                            if (deviceId >= DeviceSelector.DEVICE_SELECTOR_STARTING_ID) {\n                                Widget deviceSelector = dash.getWidgetById(deviceId);\n                                if (deviceSelector == null) {\n                                    deviceSelector = dash.getWidgetByIdInDeviceTilesOrThrow(deviceId);\n                                }\n                                if (deviceSelector instanceof DeviceSelector) {\n                                    deviceIds = ((DeviceSelector) deviceSelector).deviceIds;\n                                }\n                            }\n\n                            Path path = reportingDao.csvGenerator.createCSV(\n                                    user, dash.id, deviceId, dataStream.pinType, dataStream.pin, deviceIds);\n                            Device device = user.profile.getDeviceById(dash, deviceId);\n                            String name = (device == null || device.name == null) ? dashName : device.name;\n                            pinsCSVFilePath.add(new DeviceFileLink(path, name, dataStream.pinType, dataStream.pin));\n                        } catch (Exception e) {\n                            log.debug(\"Error generating csv file.\", e);\n                            //ignore any exception.\n                        }\n                    }\n                }\n\n                if (pinsCSVFilePath.size() == 0) {\n                    ctx.writeAndFlush(noData(msgId), ctx.voidPromise());\n                } else {\n                    String title = \"History graph data for project \" + dashName;\n                    String bodyWithLinks = DeviceFileLink.makeBody(csvDownloadUrl, pinsCSVFilePath);\n                    mailWrapper.sendHtml(user.email, title, bodyWithLinks);\n                    ctx.writeAndFlush(ok(msgId), ctx.voidPromise());\n                }\n\n            } catch (Exception e) {\n                log.error(\"Error making csv file for data export. Reason {}\", e.getMessage());\n                ctx.writeAndFlush(notificationError(msgId), ctx.voidPromise());\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/graph/MobileGetEnhancedGraphDataLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic.graph;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.application.handlers.main.auth.MobileStateHolder;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.Profile;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Tag;\nimport cc.blynk.server.core.model.widgets.Target;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.outputs.graph.GraphDataStream;\nimport cc.blynk.server.core.model.widgets.outputs.graph.GraphPeriod;\nimport cc.blynk.server.core.model.widgets.outputs.graph.Superchart;\nimport cc.blynk.server.core.model.widgets.ui.DeviceSelector;\nimport cc.blynk.server.core.model.widgets.ui.tiles.DeviceTiles;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandException;\nimport cc.blynk.server.core.protocol.exceptions.NoDataException;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.server.core.reporting.GraphPinRequest;\nimport cc.blynk.utils.StringUtils;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.core.protocol.enums.Command.GET_ENHANCED_GRAPH_DATA;\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeBinaryMessage;\nimport static cc.blynk.server.internal.CommonByteBufUtil.noData;\nimport static cc.blynk.server.internal.CommonByteBufUtil.serverError;\nimport static cc.blynk.utils.ByteUtils.compress;\nimport static cc.blynk.utils.StringUtils.split2Device;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic final class MobileGetEnhancedGraphDataLogic {\n\n    private static final Logger log = LogManager.getLogger(MobileGetEnhancedGraphDataLogic.class);\n\n    private MobileGetEnhancedGraphDataLogic() {\n    }\n\n    public static void messageReceived(Holder holder, ChannelHandlerContext ctx,\n                                       MobileStateHolder state, StringMessage message) {\n        String[] messageParts = message.body.split(StringUtils.BODY_SEPARATOR_STRING);\n\n        if (messageParts.length < 3) {\n            throw new IllegalCommandException(\"Wrong income message format.\");\n        }\n\n        int targetId = -1;\n        String[] dashIdAndTargetIdString = split2Device(messageParts[0]);\n        if (dashIdAndTargetIdString.length == 2) {\n            targetId = Integer.parseInt(dashIdAndTargetIdString[1]);\n        }\n        int dashId = Integer.parseInt(dashIdAndTargetIdString[0]);\n\n        long widgetId = Long.parseLong(messageParts[1]);\n        GraphPeriod graphPeriod = GraphPeriod.valueOf(messageParts[2]);\n        int page = 0;\n        if (messageParts.length == 4) {\n            page = Integer.parseInt(messageParts[3]);\n        }\n        int skipCount = graphPeriod.numberOfPoints * page;\n\n        Profile profile = state.user.profile;\n        DashBoard dash = profile.getDashByIdOrThrow(dashId);\n        Widget widget = dash.getWidgetById(widgetId);\n\n        //special case for device tiles widget.\n        if (widget == null) {\n            DeviceTiles deviceTiles = dash.getWidgetByType(DeviceTiles.class);\n            if (deviceTiles != null) {\n                widget = deviceTiles.getWidgetById(widgetId);\n            }\n        }\n\n        if (!(widget instanceof Superchart)) {\n            throw new IllegalCommandException(\"Passed wrong widget id.\");\n        }\n\n        Superchart enhancedHistoryGraph = (Superchart) widget;\n\n        int numberOfStreams = enhancedHistoryGraph.dataStreams.length;\n        if (numberOfStreams == 0) {\n            log.debug(\"No data streams for enhanced graph with id {}.\", widgetId);\n            ctx.writeAndFlush(noData(message.id), ctx.voidPromise());\n            return;\n        }\n\n        GraphPinRequest[] requestedPins = new GraphPinRequest[enhancedHistoryGraph.dataStreams.length];\n\n        int i = 0;\n        for (GraphDataStream graphDataStream : enhancedHistoryGraph.dataStreams) {\n            //special case, for device tiles widget targetID may be overrided\n            Target target;\n            int targetIdUpdated = graphDataStream.getTargetId(targetId);\n            if (targetIdUpdated < Tag.START_TAG_ID) {\n                target = profile.getDeviceById(dash, targetIdUpdated);\n            } else if (targetIdUpdated < DeviceSelector.DEVICE_SELECTOR_STARTING_ID) {\n                target = profile.getTagById(dash, targetIdUpdated);\n            } else {\n                target = dash.getDeviceSelector(targetIdUpdated);\n            }\n            if (target == null) {\n                requestedPins[i] = new GraphPinRequest(dashId, -1,\n                        graphDataStream.dataStream, graphPeriod, skipCount, graphDataStream.functionType);\n            } else {\n                if (target.isTag()) {\n                    requestedPins[i] = new GraphPinRequest(dashId, target.getDeviceIds(),\n                            graphDataStream.dataStream, graphPeriod, skipCount, graphDataStream.functionType);\n                } else {\n                    requestedPins[i] = new GraphPinRequest(dashId, target.getDeviceId(),\n                            graphDataStream.dataStream, graphPeriod, skipCount, graphDataStream.functionType);\n                }\n            }\n            i++;\n        }\n\n        readGraphData(holder, ctx.channel(), state.user, requestedPins, message.id);\n    }\n\n    private static void readGraphData(Holder holder, Channel channel, User user,\n                                      GraphPinRequest[] requestedPins, int msgId) {\n        holder.blockingIOProcessor.executeHistory(() -> {\n            try {\n                byte[][] data = holder.reportingDiskDao.getReportingData(user, requestedPins);\n                byte[] compressed = compress(requestedPins[0].dashId, data);\n\n                if (channel.isWritable()) {\n                    channel.writeAndFlush(\n                            makeBinaryMessage(GET_ENHANCED_GRAPH_DATA, msgId, compressed),\n                            channel.voidPromise()\n                    );\n                }\n            } catch (NoDataException noDataException) {\n                channel.writeAndFlush(noData(msgId), channel.voidPromise());\n            } catch (Exception e) {\n                log.error(\"Error reading reporting data. For user {}. Error: {}\", user.email, e.getMessage());\n                channel.writeAndFlush(serverError(msgId), channel.voidPromise());\n            }\n        });\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/graph/links/DeviceFileLink.java",
    "content": "package cc.blynk.server.application.handlers.main.logic.graph.links;\n\nimport cc.blynk.server.core.model.enums.PinType;\n\nimport java.nio.file.Path;\nimport java.util.List;\n\npublic class DeviceFileLink {\n\n    private final Path path;\n\n    private final String name;\n\n    private final PinType pinType;\n\n    private final short pin;\n\n    public DeviceFileLink(Path path, String name, PinType pinType, short pin) {\n        this.path = path.getFileName();\n        this.name = name;\n        this.pinType = pinType;\n        this.pin = pin;\n    }\n\n    public static String makeBody(String downLoadUrl, List<DeviceFileLink> fileUrls) {\n        var sb = new StringBuilder();\n        sb.append(\"<html><body>\");\n        for (DeviceFileLink link : fileUrls) {\n            sb.append(link.makeAHRef(downLoadUrl)).append(\"<br>\");\n        }\n        return sb.append(\"</body></html>\").toString();\n    }\n\n    private String makeAHRef(String csvDownloadUrl) {\n        return \"<a href=\\\"\" + csvDownloadUrl + path + \"\\\">\" + name + \" \" + pinType.pintTypeChar + pin + \"</a>\";\n    }\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/reporting/MobileCreateReportLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic.reporting;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.model.widgets.ui.reporting.Report;\nimport cc.blynk.server.core.model.widgets.ui.reporting.ReportScheduler;\nimport cc.blynk.server.core.model.widgets.ui.reporting.ReportingWidget;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandBodyException;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandException;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.utils.ArrayUtil;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.core.protocol.enums.Command.CREATE_REPORT;\nimport static cc.blynk.server.internal.CommonByteBufUtil.energyLimit;\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeUTF8StringMessage;\nimport static cc.blynk.utils.StringUtils.split2;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 31/05/2018.\n *\n */\npublic final class MobileCreateReportLogic {\n\n    private static final Logger log = LogManager.getLogger(MobileCreateReportLogic.class);\n\n    private MobileCreateReportLogic() {\n    }\n\n    public static void messageReceived(Holder holder, ChannelHandlerContext ctx,\n                                       User user, StringMessage message) {\n        String[] split = split2(message.body);\n\n        if (split.length < 2) {\n            throw new IllegalCommandException(\"Wrong income message format.\");\n        }\n\n        int reportsLimit = holder.limits.reportsLimit;\n        ReportScheduler reportScheduler = holder.reportScheduler;\n\n        int dashId = Integer.parseInt(split[0]);\n        String reportJson = split[1];\n\n        if (reportJson == null || reportJson.isEmpty()) {\n            throw new IllegalCommandException(\"Income report message is empty.\");\n        }\n\n        DashBoard dash = user.profile.getDashByIdOrThrow(dashId);\n        ReportingWidget reportingWidget = dash.getReportingWidget();\n\n        if (reportingWidget == null) {\n            throw new IllegalCommandException(\"Project has no reporting widget.\");\n        }\n\n        if (reportingWidget.reports.length >= reportsLimit) {\n            throw new IllegalCommandException(\"User reached reports limit.\");\n        }\n\n        Report report = JsonParser.parseReport(reportJson, message.id);\n        reportingWidget.validateId(report.id);\n\n        int price = Report.getPrice();\n        if (user.notEnoughEnergy(price)) {\n            log.debug(\"Not enough energy.\");\n            ctx.writeAndFlush(energyLimit(message.id), ctx.voidPromise());\n            return;\n        }\n\n        if (!report.isValid()) {\n            log.debug(\"Report is not valid {} for {}.\", report, user.email);\n            throw new IllegalCommandException(\"Report is not valid.\");\n        }\n\n        if (report.isPeriodic()) {\n            long initialDelaySeconds;\n            try {\n                initialDelaySeconds = report.calculateDelayInSeconds();\n            } catch (IllegalCommandBodyException e) {\n                //re throw, quick workaround\n                log.debug(\"Report has wrong configuration for {}. Report : {}\", user.email, report);\n                throw new IllegalCommandBodyException(e.getMessage(), message.id);\n            }\n\n            log.info(\"Adding periodic report for user {} with delay {} to scheduler.\",\n                    user.email, initialDelaySeconds);\n            log.debug(reportJson);\n\n            report.nextReportAt = System.currentTimeMillis() + initialDelaySeconds * 1000;\n\n            if (report.isActive) {\n                reportScheduler.schedule(user, dashId, report, initialDelaySeconds);\n            }\n        }\n\n        user.subtractEnergy(price);\n        reportingWidget.reports = ArrayUtil.add(reportingWidget.reports, report, Report.class);\n        dash.updatedAt = System.currentTimeMillis();\n\n        ctx.writeAndFlush(makeUTF8StringMessage(CREATE_REPORT, message.id, report.toString()), ctx.voidPromise());\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/reporting/MobileDeleteReportLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic.reporting;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.widgets.ui.reporting.Report;\nimport cc.blynk.server.core.model.widgets.ui.reporting.ReportScheduler;\nimport cc.blynk.server.core.model.widgets.ui.reporting.ReportingWidget;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandException;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.utils.ArrayUtil;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\nimport static cc.blynk.utils.StringUtils.split2;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 31/05/2018.\n *\n */\npublic final class MobileDeleteReportLogic {\n\n    private static final Logger log = LogManager.getLogger(MobileDeleteReportLogic.class);\n\n    private MobileDeleteReportLogic() {\n    }\n\n    public static void messageReceived(Holder holder, ChannelHandlerContext ctx,\n                                       User user, StringMessage message) {\n        String[] split = split2(message.body);\n\n        if (split.length < 2) {\n            throw new IllegalCommandException(\"Wrong income message format.\");\n        }\n\n        int dashId = Integer.parseInt(split[0]);\n        int reportId = Integer.parseInt(split[1]);\n\n        DashBoard dash = user.profile.getDashByIdOrThrow(dashId);\n        ReportingWidget reportingWidget = dash.getReportingWidget();\n\n        if (reportingWidget == null) {\n            throw new IllegalCommandException(\"Project has no reporting widget.\");\n        }\n\n        int existingReportIndex = reportingWidget.getReportIndexById(reportId);\n        if (existingReportIndex == -1) {\n            throw new IllegalCommandException(\"Cannot find report with provided id.\");\n        }\n\n        Report reportToDel = reportingWidget.reports[existingReportIndex];\n        user.addEnergy(Report.getPrice());\n        reportingWidget.reports = ArrayUtil.remove(reportingWidget.reports, existingReportIndex, Report.class);\n        dash.updatedAt = System.currentTimeMillis();\n\n        if (reportToDel.isPeriodic()) {\n            ReportScheduler reportScheduler = holder.reportScheduler;\n            boolean isRemoved = reportScheduler.cancelStoredFuture(user, dashId, reportId);\n            log.debug(\"Deleting reportId {} in scheduler for {}. Is removed: {}?.\",\n                    reportToDel.id, user.email, isRemoved);\n        }\n\n        ctx.writeAndFlush(ok(message.id), ctx.voidPromise());\n    }\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/reporting/MobileExportReportLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic.reporting;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.widgets.ui.reporting.BaseReportTask;\nimport cc.blynk.server.core.model.widgets.ui.reporting.Report;\nimport cc.blynk.server.core.model.widgets.ui.reporting.ReportScheduler;\nimport cc.blynk.server.core.model.widgets.ui.reporting.ReportingWidget;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandException;\nimport cc.blynk.server.core.protocol.exceptions.QuotaLimitException;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.util.concurrent.TimeUnit;\n\nimport static cc.blynk.server.core.protocol.enums.Command.EXPORT_REPORT;\nimport static cc.blynk.server.internal.CommonByteBufUtil.illegalCommand;\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeUTF8StringMessage;\nimport static cc.blynk.utils.StringUtils.split2;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 31/05/2018.\n *\n */\npublic final class MobileExportReportLogic {\n\n    private static final Logger log = LogManager.getLogger(MobileExportReportLogic.class);\n\n    private final static long runDelay = TimeUnit.MINUTES.toMillis(1);\n\n    private MobileExportReportLogic() {\n    }\n\n    public static void messageReceived(Holder holder, ChannelHandlerContext ctx,\n                                       User user, StringMessage message) {\n        String[] split = split2(message.body);\n\n        if (split.length < 2) {\n            throw new IllegalCommandException(\"Wrong income message format.\");\n        }\n\n        int dashId = Integer.parseInt(split[0]);\n        int reportId = Integer.parseInt(split[1]);\n\n        DashBoard dash = user.profile.getDashByIdOrThrow(dashId);\n        ReportingWidget reportingWidget = dash.getReportingWidget();\n\n        if (reportingWidget == null) {\n            throw new IllegalCommandException(\"Project has no reporting widget.\");\n        }\n\n        Report report = reportingWidget.getReportById(reportId);\n        if (report == null) {\n            throw new IllegalCommandException(\"Cannot find report with passed id.\");\n        }\n\n        if (!report.isValid()) {\n            log.debug(\"Report is not valid {} for {}.\", report, user.email);\n            throw new IllegalCommandException(\"Report is not valid.\");\n        }\n\n        long now = System.currentTimeMillis();\n        if (report.lastReportAt + runDelay > now) {\n            log.debug(\"Report {} trigger limit reached for {}.\", report.id, user.email);\n            throw new QuotaLimitException(\"Report trigger limit reached.\");\n        }\n\n        ReportScheduler reportScheduler = holder.reportScheduler;\n        reportScheduler.schedule(new BaseReportTask(user, dashId, report,\n                reportScheduler.mailWrapper, reportScheduler.reportingDao,\n                reportScheduler.downloadUrl) {\n            @Override\n            public void run() {\n                try {\n                    report.lastReportAt = generateReport();\n                    if (ctx.channel().isWritable()) {\n                        ctx.writeAndFlush(\n                                makeUTF8StringMessage(EXPORT_REPORT, message.id, report.toString()),\n                                ctx.voidPromise()\n                        );\n                    }\n                } catch (Exception e) {\n                    log.debug(\"Error generating export report {} for {}.\", report, key.user.email, e);\n                    ctx.writeAndFlush(illegalCommand(message.id), ctx.voidPromise());\n                }\n            }\n        }, 0, TimeUnit.SECONDS);\n    }\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/reporting/MobileUpdateReportLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic.reporting;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.serialization.JsonParser;\nimport cc.blynk.server.core.model.widgets.ui.reporting.Report;\nimport cc.blynk.server.core.model.widgets.ui.reporting.ReportResult;\nimport cc.blynk.server.core.model.widgets.ui.reporting.ReportScheduler;\nimport cc.blynk.server.core.model.widgets.ui.reporting.ReportingWidget;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandBodyException;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandException;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.utils.ArrayUtil;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.core.protocol.enums.Command.UPDATE_REPORT;\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeUTF8StringMessage;\nimport static cc.blynk.utils.StringUtils.split2;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 31/05/2018.\n *\n */\npublic final class MobileUpdateReportLogic {\n\n    private static final Logger log = LogManager.getLogger(MobileUpdateReportLogic.class);\n\n    private MobileUpdateReportLogic() {\n    }\n\n    public static void messageReceived(Holder holder, ChannelHandlerContext ctx,\n                                       User user, StringMessage message) {\n        String[] split = split2(message.body);\n\n        if (split.length < 2) {\n            throw new IllegalCommandException(\"Wrong income message format.\");\n        }\n\n        int dashId = Integer.parseInt(split[0]);\n        String reportJson = split[1];\n\n        if (reportJson == null || reportJson.isEmpty()) {\n            throw new IllegalCommandException(\"Income report message is empty.\");\n        }\n\n        DashBoard dash = user.profile.getDashByIdOrThrow(dashId);\n        ReportingWidget reportingWidget = dash.getReportingWidget();\n\n        if (reportingWidget == null) {\n            throw new IllegalCommandException(\"Project has no reporting widget.\");\n        }\n\n        Report report = JsonParser.parseReport(reportJson, message.id);\n\n        int existingReportIndex = reportingWidget.getReportIndexById(report.id);\n        if (existingReportIndex == -1) {\n            throw new IllegalCommandException(\"Cannot find report with provided id.\");\n        }\n\n        ReportScheduler reportScheduler = holder.reportScheduler;\n\n        //always remove prev report before any validations are done\n        if (report.isPeriodic()) {\n            boolean isRemoved = reportScheduler.cancelStoredFuture(user, dashId, report.id);\n            log.debug(\"Deleting reportId {} in scheduler for {}. Is removed: {}?.\",\n                    report.id, user.email, isRemoved);\n        }\n\n        if (!report.isValid()) {\n            log.debug(\"Report is not valid {} for {}.\", report, user.email);\n            throw new IllegalCommandException(\"Report is not valid.\");\n        }\n\n        if (report.isPeriodic()) {\n            long initialDelaySeconds;\n            try {\n                initialDelaySeconds = report.calculateDelayInSeconds();\n            } catch (IllegalCommandBodyException e) {\n                //re throw, quick workaround\n                log.debug(\"Report has wrong configuration for {}. Report : {}\", user.email, report);\n                throw new IllegalCommandBodyException(e.getMessage(), message.id);\n            }\n\n            log.info(\"Adding periodic report for user {} with delay {} to scheduler.\",\n                    user.email, initialDelaySeconds);\n            log.debug(reportJson);\n\n            report.nextReportAt = System.currentTimeMillis() + initialDelaySeconds * 1000;\n            //special case when expired report is extended\n            if (report.lastRunResult == ReportResult.EXPIRED) {\n                report.lastRunResult = null;\n            }\n\n            if (report.isActive) {\n                reportScheduler.schedule(user, dashId, report, initialDelaySeconds);\n            }\n        }\n\n        reportingWidget.reports = ArrayUtil.copyAndReplace(reportingWidget.reports, report, existingReportIndex);\n        dash.updatedAt = System.currentTimeMillis();\n\n        ctx.writeAndFlush(makeUTF8StringMessage(UPDATE_REPORT, message.id, report.toString()), ctx.voidPromise());\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/sharing/MobileGetShareTokenLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic.sharing;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.protocol.exceptions.NotAllowedException;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport io.netty.channel.ChannelHandlerContext;\n\nimport static cc.blynk.server.core.protocol.enums.Command.GET_SHARE_TOKEN;\nimport static cc.blynk.server.internal.CommonByteBufUtil.energyLimit;\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeUTF8StringMessage;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic final class MobileGetShareTokenLogic {\n\n    private static final int PRIVATE_TOKEN_PRICE = 1000;\n\n    private MobileGetShareTokenLogic() {\n    }\n\n    public static void messageReceived(Holder holder, ChannelHandlerContext ctx,\n                                       User user, StringMessage message) {\n        String dashBoardIdString = message.body;\n\n        int dashId;\n        try {\n            dashId = Integer.parseInt(dashBoardIdString);\n        } catch (NumberFormatException ex) {\n            throw new NotAllowedException(\"Dash board id not valid. Id : \" + dashBoardIdString, message.id);\n        }\n\n        DashBoard dash = user.profile.getDashByIdOrThrow(dashId);\n        String token = dash.sharedToken;\n\n        //if token not exists. generate new one\n        if (token == null) {\n            if (user.notEnoughEnergy(PRIVATE_TOKEN_PRICE)) {\n                ctx.writeAndFlush(energyLimit(message.id), ctx.voidPromise());\n                return;\n            }\n            token = holder.tokenManager.refreshSharedToken(user, dash);\n            user.subtractEnergy(PRIVATE_TOKEN_PRICE);\n            user.lastModifiedTs = System.currentTimeMillis();\n        }\n\n        if (ctx.channel().isWritable()) {\n            ctx.writeAndFlush(makeUTF8StringMessage(GET_SHARE_TOKEN, message.id, token), ctx.voidPromise());\n        }\n    }\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/sharing/MobileRefreshShareTokenLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic.sharing;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.application.handlers.main.auth.MobileStateHolder;\nimport cc.blynk.server.application.handlers.sharing.auth.MobileShareStateHolder;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.Session;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.protocol.exceptions.NotAllowedException;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelFutureListener;\nimport io.netty.channel.ChannelHandlerContext;\n\nimport static cc.blynk.server.core.protocol.enums.Command.REFRESH_SHARE_TOKEN;\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeUTF8StringMessage;\nimport static cc.blynk.server.internal.CommonByteBufUtil.notAllowed;\nimport static cc.blynk.utils.MobileStateHolderUtil.getShareState;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic final class MobileRefreshShareTokenLogic {\n\n    private MobileRefreshShareTokenLogic() {\n    }\n\n    public static void messageReceived(Holder holder, ChannelHandlerContext ctx,\n                                       MobileStateHolder state, StringMessage message) {\n        String dashBoardIdString = message.body;\n\n        int dashId;\n        try {\n            dashId = Integer.parseInt(dashBoardIdString);\n        } catch (NumberFormatException ex) {\n            throw new NotAllowedException(\"Dash board id not valid. Id : \" + dashBoardIdString, message.id);\n        }\n\n        User user = state.user;\n        DashBoard dash = user.profile.getDashByIdOrThrow(dashId);\n\n        String token = holder.tokenManager.refreshSharedToken(user, dash);\n\n        //todo move to session class?\n        Session session = holder.sessionDao.get(state.userKey);\n        for (Channel appChannel : session.appChannels) {\n            MobileShareStateHolder localState = getShareState(appChannel);\n            if (localState != null && localState.dashId == dashId) {\n                appChannel.writeAndFlush(notAllowed(message.id))\n                          .addListener(ChannelFutureListener.CLOSE);\n            }\n        }\n\n        if (ctx.channel().isWritable()) {\n            ctx.writeAndFlush(makeUTF8StringMessage(REFRESH_SHARE_TOKEN, message.id, token), ctx.voidPromise());\n        }\n    }\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/main/logic/sharing/MobileShareLogic.java",
    "content": "package cc.blynk.server.application.handlers.main.logic.sharing;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.application.handlers.main.auth.MobileStateHolder;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.Session;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.utils.StringUtils;\nimport io.netty.channel.ChannelHandlerContext;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic final class MobileShareLogic {\n\n    private MobileShareLogic() {\n    }\n\n    public static void messageReceived(Holder holder, ChannelHandlerContext ctx,\n                                       MobileStateHolder state, StringMessage message) {\n        String[] splitted = message.body.split(StringUtils.BODY_SEPARATOR_STRING);\n\n        int dashId = Integer.parseInt(splitted[0]);\n        DashBoard dash = state.user.profile.getDashByIdOrThrow(dashId);\n\n        switch (splitted[1]) {\n            case \"on\" :\n                dash.isShared = true;\n                break;\n            default :\n                dash.isShared = false;\n                break;\n        }\n\n        Session session = holder.sessionDao.get(state.userKey);\n        session.sendToSharedApps(ctx.channel(), dash.sharedToken, message.command, message.id, message.body);\n        ctx.writeAndFlush(ok(message.id), ctx.voidPromise());\n    }\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/sharing/MobileShareHandler.java",
    "content": "package cc.blynk.server.application.handlers.sharing;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.application.handlers.main.logic.LoadSharedProfileGzippedLogic;\nimport cc.blynk.server.application.handlers.main.logic.MobileAddPushLogic;\nimport cc.blynk.server.application.handlers.main.logic.MobileLogoutLogic;\nimport cc.blynk.server.application.handlers.main.logic.MobileSyncLogic;\nimport cc.blynk.server.application.handlers.main.logic.dashboard.device.MobileGetDevicesLogic;\nimport cc.blynk.server.application.handlers.main.logic.graph.MobileDeleteDeviceDataLogic;\nimport cc.blynk.server.application.handlers.main.logic.graph.MobileGetEnhancedGraphDataLogic;\nimport cc.blynk.server.application.handlers.sharing.auth.MobileShareStateHolder;\nimport cc.blynk.server.application.handlers.sharing.logic.MobileShareHardwareLogic;\nimport cc.blynk.server.common.BaseSimpleChannelInboundHandler;\nimport cc.blynk.server.common.handlers.logic.PingLogic;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.server.core.session.StateHolderBase;\nimport io.netty.channel.ChannelHandlerContext;\n\nimport static cc.blynk.server.core.protocol.enums.Command.ADD_PUSH_TOKEN;\nimport static cc.blynk.server.core.protocol.enums.Command.APP_SYNC;\nimport static cc.blynk.server.core.protocol.enums.Command.DELETE_DEVICE_DATA;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_DEVICES;\nimport static cc.blynk.server.core.protocol.enums.Command.GET_ENHANCED_GRAPH_DATA;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE;\nimport static cc.blynk.server.core.protocol.enums.Command.LOAD_PROFILE_GZIPPED;\nimport static cc.blynk.server.core.protocol.enums.Command.LOGOUT;\nimport static cc.blynk.server.core.protocol.enums.Command.PING;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic class MobileShareHandler extends BaseSimpleChannelInboundHandler<StringMessage> {\n\n    public final MobileShareStateHolder state;\n    private final Holder holder;\n    private final MobileShareHardwareLogic hardwareApp;\n    private final MobileAddPushLogic mobileAddPushLogic;\n\n    public MobileShareHandler(Holder holder, MobileShareStateHolder state) {\n        super(StringMessage.class);\n        this.state = state;\n        this.holder = holder;\n\n        this.hardwareApp = new MobileShareHardwareLogic(holder, state.userKey.email);\n        this.mobileAddPushLogic = new MobileAddPushLogic(holder);\n    }\n\n    @Override\n    public void messageReceived(ChannelHandlerContext ctx, StringMessage msg) {\n        holder.stats.incrementAppStat();\n        switch (msg.command) {\n            case HARDWARE:\n                hardwareApp.messageReceived(ctx, state, msg);\n                break;\n            case LOAD_PROFILE_GZIPPED :\n                LoadSharedProfileGzippedLogic.messageReceived(ctx, state, msg);\n                break;\n            case ADD_PUSH_TOKEN :\n                mobileAddPushLogic.messageReceived(ctx, state, msg);\n                break;\n            case GET_ENHANCED_GRAPH_DATA :\n                MobileGetEnhancedGraphDataLogic.messageReceived(holder, ctx, state, msg);\n                break;\n            case GET_DEVICES :\n                MobileGetDevicesLogic.messageReceived(ctx, state.user, msg);\n                break;\n            case PING :\n                PingLogic.messageReceived(ctx, msg.id);\n                break;\n            case APP_SYNC :\n                MobileSyncLogic.messageReceived(ctx, state, msg);\n                break;\n            case LOGOUT :\n                MobileLogoutLogic.messageReceived(ctx, state.user, msg);\n                break;\n            case DELETE_DEVICE_DATA :\n                MobileDeleteDeviceDataLogic.messageReceived(holder, ctx, state, msg);\n                break;\n        }\n    }\n\n    @Override\n    public StateHolderBase getState() {\n        return state;\n    }\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/sharing/auth/MobileShareLoginHandler.java",
    "content": "package cc.blynk.server.application.handlers.sharing.auth;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.application.handlers.main.auth.MobileGetServerHandler;\nimport cc.blynk.server.application.handlers.main.auth.MobileLoginHandler;\nimport cc.blynk.server.application.handlers.main.auth.MobileRegisterHandler;\nimport cc.blynk.server.application.handlers.main.auth.Version;\nimport cc.blynk.server.application.handlers.sharing.MobileShareHandler;\nimport cc.blynk.server.common.handlers.UserNotLoggedHandler;\nimport cc.blynk.server.core.dao.SharedTokenValue;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.Session;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.protocol.model.messages.appllication.sharing.ShareLoginMessage;\nimport cc.blynk.server.internal.ReregisterChannelUtil;\nimport cc.blynk.utils.StringUtils;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelHandler;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.ChannelPipeline;\nimport io.netty.channel.SimpleChannelInboundHandler;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.illegalCommand;\nimport static cc.blynk.server.internal.CommonByteBufUtil.notAllowed;\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\n\n/**\n * Handler responsible for managing apps sharing login messages.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\n@ChannelHandler.Sharable\npublic class MobileShareLoginHandler extends SimpleChannelInboundHandler<ShareLoginMessage> {\n\n    private static final Logger log = LogManager.getLogger(MobileShareLoginHandler.class);\n\n    private final Holder holder;\n\n    public MobileShareLoginHandler(Holder holder) {\n        this.holder = holder;\n    }\n\n    @Override\n    protected void channelRead0(ChannelHandlerContext ctx, ShareLoginMessage message) {\n        String[] messageParts = message.body.split(StringUtils.BODY_SEPARATOR_STRING);\n\n        if (messageParts.length < 2) {\n            log.error(\"Wrong income message format.\");\n            ctx.writeAndFlush(illegalCommand(message.id), ctx.voidPromise());\n        } else {\n            //var uid = messageParts.length == 5 ? messageParts[4] : null;\n            var version = messageParts.length > 3\n                    ? new Version(messageParts[2], messageParts[3])\n                    : Version.UNKNOWN_VERSION;\n            appLogin(ctx, message.id, messageParts[0], messageParts[1], version);\n        }\n    }\n\n    private void appLogin(ChannelHandlerContext ctx, int messageId, String email,\n                          String token, Version version) {\n        ///.trim() is not used for back compatibility\n        String userName = email.toLowerCase();\n\n        SharedTokenValue tokenValue = holder.tokenManager.getUserBySharedToken(token);\n\n        if (tokenValue == null || !tokenValue.user.email.equals(userName)) {\n            log.debug(\"Share token is invalid. User : {}, token {}, {}\",\n                    userName, token, ctx.channel().remoteAddress());\n            ctx.writeAndFlush(notAllowed(messageId), ctx.voidPromise());\n            return;\n        }\n\n        User user = tokenValue.user;\n        int dashId = tokenValue.dashId;\n\n        DashBoard dash = user.profile.getDashById(dashId);\n        if (!dash.isShared) {\n            log.debug(\"Dashboard is not shared. User : {}, token {}, {}\",\n                    userName, token, ctx.channel().remoteAddress());\n            ctx.writeAndFlush(notAllowed(messageId), ctx.voidPromise());\n            return;\n        }\n\n        cleanPipeline(ctx.pipeline());\n        MobileShareStateHolder mobileShareStateHolder = new MobileShareStateHolder(user, version, token, dashId);\n        ctx.pipeline().addLast(\"AAppSHareHandler\", new MobileShareHandler(holder, mobileShareStateHolder));\n\n        Session session = holder.sessionDao.getOrCreateSessionByUser(\n                mobileShareStateHolder.userKey, ctx.channel().eventLoop());\n\n        if (session.isSameEventLoop(ctx)) {\n            completeLogin(ctx.channel(), session, user.email, messageId);\n        } else {\n            log.debug(\"Re registering app channel. {}\", ctx.channel());\n            ReregisterChannelUtil.reRegisterChannel(ctx, session, channelFuture ->\n                    completeLogin(channelFuture.channel(), session, user.email, messageId));\n        }\n    }\n\n    private void completeLogin(Channel channel, Session session, String userName, int msgId) {\n        session.addAppChannel(channel);\n        channel.writeAndFlush(ok(msgId), channel.voidPromise());\n        log.info(\"Shared {} app joined.\", userName);\n    }\n\n    private void cleanPipeline(ChannelPipeline pipeline) {\n        pipeline.remove(this);\n        pipeline.remove(UserNotLoggedHandler.class);\n        pipeline.remove(MobileRegisterHandler.class);\n        pipeline.remove(MobileLoginHandler.class);\n        pipeline.remove(MobileGetServerHandler.class);\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/sharing/auth/MobileShareStateHolder.java",
    "content": "package cc.blynk.server.application.handlers.sharing.auth;\n\nimport cc.blynk.server.application.handlers.main.auth.MobileStateHolder;\nimport cc.blynk.server.application.handlers.main.auth.Version;\nimport cc.blynk.server.core.dao.SharedTokenManager;\nimport cc.blynk.server.core.model.auth.User;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 13.09.15.\n */\npublic final class MobileShareStateHolder extends MobileStateHolder {\n\n    public final String token;\n    public final int dashId;\n\n    MobileShareStateHolder(User user, Version version, String token, int dashId) {\n        super(user, version);\n        this.token = token;\n        this.dashId = dashId;\n    }\n\n    @Override\n    public boolean contains(String sharedToken) {\n        return token.equals(sharedToken) || SharedTokenManager.ALL.equals(sharedToken);\n    }\n\n    @Override\n    public boolean isSameDash(int inDashId) {\n        return this.dashId == inDashId;\n    }\n\n    @Override\n    public boolean isSameDashAndDeviceId(int inDashId, int deviceId) {\n        return isSameDash(inDashId);\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/server/application/handlers/sharing/logic/MobileShareHardwareLogic.java",
    "content": "package cc.blynk.server.application.handlers.sharing.logic;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.application.handlers.main.logic.MobileHardwareLogic;\nimport cc.blynk.server.application.handlers.sharing.auth.MobileShareStateHolder;\nimport cc.blynk.server.core.dao.SessionDao;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.Session;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Tag;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.widgets.Target;\nimport cc.blynk.server.core.model.widgets.ui.DeviceSelector;\nimport cc.blynk.server.core.processors.BaseProcessorHandler;\nimport cc.blynk.server.core.processors.WebhookProcessor;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.utils.NumberUtil;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.core.protocol.enums.Command.APP_SYNC;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE;\nimport static cc.blynk.server.internal.CommonByteBufUtil.deviceNotInNetwork;\nimport static cc.blynk.server.internal.CommonByteBufUtil.illegalCommandBody;\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeUTF8StringMessage;\nimport static cc.blynk.server.internal.CommonByteBufUtil.notAllowed;\nimport static cc.blynk.utils.StringUtils.split2;\nimport static cc.blynk.utils.StringUtils.split2Device;\nimport static cc.blynk.utils.StringUtils.split3;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic class MobileShareHardwareLogic extends BaseProcessorHandler {\n\n    private static final Logger log = LogManager.getLogger(MobileShareHardwareLogic.class);\n\n    private final SessionDao sessionDao;\n\n    public MobileShareHardwareLogic(Holder holder, String email) {\n        super(holder.eventorProcessor, new WebhookProcessor(holder.asyncHttpClient,\n                holder.limits.webhookPeriodLimitation,\n                holder.limits.webhookResponseSizeLimitBytes,\n                holder.limits.webhookFailureLimit,\n                holder.stats,\n                email));\n        this.sessionDao = holder.sessionDao;\n    }\n\n    public void messageReceived(ChannelHandlerContext ctx, MobileShareStateHolder state, StringMessage message) {\n        Session session = sessionDao.get(state.userKey);\n\n        //here expecting command in format \"1-200000 vw 88 1\"\n        String[] split = split2(message.body);\n\n        //here we have \"1-200000\"\n        String[] dashIdAndTargetIdString = split2Device(split[0]);\n        int dashId = Integer.parseInt(dashIdAndTargetIdString[0]);\n\n        User user = state.user;\n        DashBoard dash = user.profile.getDashByIdOrThrow(dashId);\n\n        //if no active dashboard - do nothing. this could happen only in case of app. bug\n        if (!dash.isActive) {\n            return;\n        }\n\n        //deviceId or tagId or device selector widget id\n        int targetId = 0;\n\n        //new logic for multi devices\n        if (dashIdAndTargetIdString.length == 2) {\n            targetId = Integer.parseInt(dashIdAndTargetIdString[1]);\n        }\n\n        if (!dash.isShared) {\n            log.debug(\"Dashboard is not shared. User : {}, {}\", user.email, ctx.channel().remoteAddress());\n            ctx.writeAndFlush(notAllowed(message.id), ctx.voidPromise());\n            return;\n        }\n\n        //sending message only if widget assigned to device or tag has assigned devices\n        Target target;\n        if (targetId < Tag.START_TAG_ID) {\n            target = user.profile.getDeviceById(dash, targetId);\n        } else if (targetId < DeviceSelector.DEVICE_SELECTOR_STARTING_ID) {\n            target = user.profile.getTagById(dash, targetId);\n        } else {\n            //means widget assigned to device selector widget.\n            target = dash.getDeviceSelector(targetId);\n        }\n        if (target == null) {\n            log.debug(\"No assigned target id for received command.\");\n            return;\n        }\n\n        int[] deviceIds = target.getDeviceIds();\n\n        if (deviceIds.length == 0) {\n            log.debug(\"No devices assigned to target.\");\n            return;\n        }\n\n        char operation = split[1].charAt(1);\n        switch (operation) {\n            case 'u' :\n                //splitting \"vu 200000 1\"\n                String[] splitBody = split3(split[1]);\n                MobileHardwareLogic.processDeviceSelectorCommand(ctx, session, user.profile, dash, message, splitBody);\n                break;\n            case 'w' :\n                splitBody = split3(split[1]);\n\n                if (splitBody.length < 3) {\n                    log.debug(\"Not valid write command.\");\n                    ctx.writeAndFlush(illegalCommandBody(message.id), ctx.voidPromise());\n                    return;\n                }\n\n                PinType pinType = PinType.getPinType(splitBody[0].charAt(0));\n                short pin = NumberUtil.parsePin(splitBody[1]);\n                String value = splitBody[2];\n                long now = System.currentTimeMillis();\n\n                for (int deviceId : deviceIds) {\n                    user.profile.update(dash, deviceId, pin, pinType, value, now);\n                }\n\n                //additional state for tag widget itself\n                if (target.isTag()) {\n                    user.profile.update(dash, targetId, pin, pinType, value, now);\n                }\n\n                String sharedToken = state.token;\n                if (sharedToken != null) {\n                    for (Channel appChannel : session.appChannels) {\n                        if (appChannel != ctx.channel() && appChannel.isWritable()\n                                && Session.needSync(appChannel, sharedToken)) {\n                            appChannel.writeAndFlush(\n                                    makeUTF8StringMessage(APP_SYNC, message.id, message.body),\n                                    appChannel.voidPromise());\n                        }\n                    }\n                }\n\n                if (session.sendMessageToHardware(dashId, HARDWARE, message.id, split[1], deviceIds)\n                        && !dash.isNotificationsOff) {\n                    log.debug(\"No device in session.\");\n                    ctx.writeAndFlush(deviceNotInNetwork(message.id), ctx.voidPromise());\n                }\n\n                processEventorAndWebhook(user, dash, targetId, session, pin, pinType, value, now);\n                break;\n        }\n    }\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/java/cc/blynk/utils/MobileStateHolderUtil.java",
    "content": "package cc.blynk.utils;\n\nimport cc.blynk.server.application.handlers.main.MobileHandler;\nimport cc.blynk.server.application.handlers.main.auth.MobileStateHolder;\nimport cc.blynk.server.application.handlers.sharing.MobileShareHandler;\nimport cc.blynk.server.application.handlers.sharing.auth.MobileShareStateHolder;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelPipeline;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 05.01.16.\n */\npublic final class MobileStateHolderUtil {\n\n    private MobileStateHolderUtil() {\n    }\n\n    public static MobileStateHolder getAppState(Channel channel) {\n        return getAppState(channel.pipeline());\n    }\n\n    private static MobileStateHolder getAppState(ChannelPipeline pipeline) {\n        MobileHandler handler = pipeline.get(MobileHandler.class);\n        if (handler == null) {\n            return getShareState(pipeline);\n        }\n        return handler.state;\n    }\n\n    public static MobileShareStateHolder getShareState(Channel channel) {\n        return getShareState(channel.pipeline());\n    }\n\n    private static MobileShareStateHolder getShareState(ChannelPipeline pipeline) {\n        MobileShareHandler handler = pipeline.get(MobileShareHandler.class);\n        return handler == null ? null : handler.state;\n    }\n\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/main/resources/static/register-email.html",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"\n        \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n    <meta name=\"viewport\" content=\"width=device-width\"/>\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"/>\n    <style type=\"text/css\">\n        * {\n            margin: 0;\n            padding: 0;\n            font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;\n            font-size: 100%;\n            line-height: 1.6;\n        }\n\n        body {\n            -webkit-font-smoothing: antialiased;\n            -webkit-text-size-adjust: none;\n            width: 100% !important;\n            height: 100%;\n        }\n\n        a {\n            color: #348eda;\n        }\n\n        .btn-primary {\n            text-decoration: none;\n            color: #FFF;\n            background-color: #24C48E;\n            border: solid #24C48E;\n            border-width: 10px 20px;\n            line-height: 1;\n            font-weight: bold;\n            text-align: center;\n            cursor: pointer;\n            display: inline-block;\n            border-radius: 25px;\n        }\n\n        .padding {\n            padding: 10px 0;\n        }\n\n        table.body-wrap {\n            width: 100%;\n            padding: 20px;\n        }\n\n        table.body-wrap .container {\n            border: 1px solid #f0f0f0;\n        }\n\n        .footer-wrap .container p {\n            font-size: 12px;\n            color: #666;\n\n        }\n\n        table.footer-wrap a {\n            color: #999;\n        }\n\n        h1, h2, h3 {\n            font-family: \"Helvetica Neue\", Helvetica, Arial, \"Lucida Grande\", sans-serif;\n            color: #000;\n            margin: 40px 0 10px;\n            line-height: 1.2;\n            font-weight: 200;\n        }\n\n        h3 {\n            font-size: 22px;\n        }\n\n        p, ul, ol {\n            margin-bottom: 10px;\n            font-weight: normal;\n            font-size: 14px;\n        }\n\n        ul li, ol li {\n            margin-left: 5px;\n            list-style-position: inside;\n        }\n\n        .container {\n            display: block !important;\n            max-width: 600px !important;\n            margin: 0 auto !important; /* makes it centered */\n            clear: both !important;\n        }\n\n        .body-wrap .container {\n            padding: 20px;\n        }\n\n        .content {\n            max-width: 600px;\n            margin: 0 auto;\n            display: block;\n        }\n\n        .content table {\n            width: 100%;\n        }\n    </style>\n</head>\n\n<body bgcolor=\"#f6f6f6\">\n\n<table class=\"body-wrap\" bgcolor=\"#f6f6f6\">\n    <tr>\n        <td></td>\n        <td class=\"container\" bgcolor=\"#FFFFFF\">\n            <div class=\"content\">\n                <table>\n                    <tr>\n                        <td>\n                            <p>Hi there,</p>\n\n                            <p>Welcome to Blynk, a platform to build your next awesome IOT project.</p>\n                            <!--\n                            <p>First of all, please confirm your email address:</p>\n                            <table>\n                                <tr>\n                                    <td class=\"padding\">\n                                        <p><a href=\"{RESET_URL}\" class=\"btn-primary\">Confirm Email</a></p>\n                                    </td>\n                                </tr>\n                            </table>\n                            -->\n                            <h3>Get Started:</h3>\n                            <ol>\n                                <li>Install Blynk Library. <a href=\"http://help.blynk.cc/getting-started-library-auth-token-code-examples/how-to-install-blynk-library-for-arduino\">Here is</a> a step-by-step guide on how to do that;</li>\n                                <li>Check <a href=\"https://examples.blynk.cc/\">Blynk examples</a> for your hardware;</li>\n                            </ol>\n\n                            <h3>Learn Blynk Basics:</h3>\n                            <ul>\n                                <li><a href=\"https://www.youtube.com/watch?v=egGs_jSIKbc\">How to read any sensor with Blynk app (for example: DHT11, DHT22, etc)</a></li>\n                                <li><a href=\"https://www.youtube.com/watch?v=iueWEkM6cuQ\">What is virtual pins and how to use it?</a></li>\n                                <li><a href=\"https://www.youtube.com/watch?v=LJ3ic8C8CcA\">Blynking LED on Raspberry Pi</a></li>\n                                <li><a href=\"https://www.youtube.com/watch?v=fgzvoan_3_w\">Control Arduino over USB with Blynk app</a></li>\n                                <li><a href=\"https://www.youtube.com/watch?v=FhS44hGk1Lc\">Control multiple devices with Blynk app (Arduino Mega and nodeMCU)</a></li>\n                                <li><a href=\"https://www.youtube.com/watch?v=33ynNkvfvWU\">How to install Local Blynk Server</a></li>\n                            </ul>\n\n                            <h3>Have Questions?</h3>\n                            <p>\n                                Post to our friendly <a href=\"https://community.blynk.cc/\">community</a> with thousands of other developers.\n                                <br>\n                                Read full Blynk documentation <a href=\"http://docs.blynk.cc/\">here</a>.\n                                <br>\n                            <br>\n                            Enjoy Blynking!\n                            </p>\n                            <p>\n                                --\n                                <br>\n                                Pavel and Blynk Team\n                            </p>\n\n                        </td>\n                    </tr>\n                </table>\n            </div>\n        </td>\n        <td></td>\n    </tr>\n</table>\n</body>\n</html>"
  },
  {
    "path": "server/tcp-app-server/src/main/resources/static/report-email.html",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"\n        \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n    <meta name=\"viewport\" content=\"width=device-width\"/>\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"/>\n    <style type=\"text/css\">\n        * {\n            margin: 0;\n            padding: 0;\n            font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;\n            font-size: 100%;\n            line-height: 1.6;\n        }\n\n        body {\n            -webkit-font-smoothing: antialiased;\n            -webkit-text-size-adjust: none;\n            width: 100% !important;\n            height: 100%;\n        }\n\n        a {\n            color: #348eda;\n        }\n\n        .btn-primary {\n            text-decoration: none;\n            color: #FFF;\n            background-color: #24C48E;\n            border: solid #24C48E;\n            border-width: 10px 20px;\n            line-height: 1;\n            font-weight: bold;\n            text-align: center;\n            cursor: pointer;\n            display: inline-block;\n            border-radius: 25px;\n        }\n\n        .padding {\n            padding: 10px 0;\n        }\n\n        table.body-wrap {\n            width: 100%;\n            padding: 20px;\n        }\n\n        table.body-wrap .container {\n            border: 1px solid #f0f0f0;\n        }\n\n        .footer-wrap .container p {\n            font-size: 12px;\n            color: #666;\n\n        }\n\n        table.footer-wrap a {\n            color: #999;\n        }\n\n        h1, h2, h3 {\n            font-family: \"Helvetica Neue\", Helvetica, Arial, \"Lucida Grande\", sans-serif;\n            color: #000;\n            margin: 40px 0 10px;\n            line-height: 1.2;\n            font-weight: 200;\n        }\n\n        p {\n            margin-bottom: 10px;\n            font-weight: normal;\n            font-size: 14px;\n        }\n\n        .container {\n            display: block !important;\n            max-width: 600px !important;\n            margin: 0 auto !important; /* makes it centered */\n            clear: both !important;\n        }\n\n        .body-wrap .container {\n            padding: 20px;\n        }\n\n        .content {\n            max-width: 600px;\n            margin: 0 auto;\n            display: block;\n        }\n\n        .content table {\n            width: 100%;\n        }\n    </style>\n</head>\n\n<body bgcolor=\"#f6f6f6\">\n\n<table class=\"body-wrap\" bgcolor=\"#f6f6f6\">\n    <tr>\n        <td></td>\n        <td class=\"container\" bgcolor=\"#FFFFFF\">\n            <div class=\"content\">\n                <table>\n                    <tr>\n                        <td>\n                            <p>\n                                {DYNAMIC_SECTION}\n                            </p>\n\n                            <table>\n                                <tr>\n                                    <td class=\"padding\">\n                                        <p><a href=\"{DOWNLOAD_URL}\" class=\"btn-primary\">Download Report</a></p>\n                                    </td>\n                                </tr>\n                            </table>\n                            <p>\n                                --\n                                <br>\n                                {PRODUCT_NAME}\n                            </p>\n\n                        </td>\n                    </tr>\n                </table>\n            </div>\n        </td>\n        <td></td>\n    </tr>\n</table>\n</body>\n</html>"
  },
  {
    "path": "server/tcp-app-server/src/test/java/cc/blynk/server/application/handlers/GetGraphDataHandlerTest.java",
    "content": "package cc.blynk.server.application.handlers;\n\nimport cc.blynk.server.core.model.graph.GraphKey;\nimport cc.blynk.utils.ByteUtils;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.ByteBuffer;\nimport java.util.zip.InflaterInputStream;\n\nimport static cc.blynk.utils.ByteUtils.REPORTING_RECORD_SIZE_BYTES;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 07.07.15.\n */\n@RunWith(MockitoJUnitRunner.class)\npublic class GetGraphDataHandlerTest {\n\n    private static byte[] decompress(byte[] bytes) {\n        InputStream in = new InflaterInputStream(new ByteArrayInputStream(bytes));\n        ByteArrayOutputStream baos = new ByteArrayOutputStream();\n        try {\n            byte[] buffer = new byte[4096];\n            int len;\n            while((len = in.read(buffer)) > 0) {\n                baos.write(buffer, 0, len);\n            }\n            return baos.toByteArray();\n        } catch (IOException e) {\n            throw new AssertionError(e);\n        }\n\n    }\n\n    private static byte[] toByteArray(GraphKey storeMessage) {\n        ByteBuffer bb = ByteBuffer.allocate(REPORTING_RECORD_SIZE_BYTES);\n        bb.putDouble(Double.valueOf(storeMessage.value));\n        bb.putLong(storeMessage.ts);\n        return bb.array();\n    }\n\n    @Test\n    public void testCompressAndDecompress() throws IOException {\n        ByteBuffer bb = ByteBuffer.allocate(1000 * REPORTING_RECORD_SIZE_BYTES);\n\n        int dataLength = 0;\n        for (int i = 0; i < 1000; i++) {\n            long ts = System.currentTimeMillis();\n            GraphKey mes = new GraphKey(1, (\"aw 1 \" + i).split(\" \"), ts);\n            bb.put(toByteArray(mes));\n            dataLength += REPORTING_RECORD_SIZE_BYTES;\n        }\n\n        System.out.println(\"Size before compression : \" + dataLength);\n        byte[][] data = new byte[1][];\n        data[0] = bb.array();\n\n        byte[] compressedData = ByteUtils.compress(data);\n        System.out.println(\"Size after compression : \" + compressedData.length + \". Compress rate \" + ((double) dataLength / compressedData.length));\n        assertNotNull(compressedData);\n        ByteBuffer result = ByteBuffer.wrap(decompress(compressedData));\n\n        assertEquals(1000 * REPORTING_RECORD_SIZE_BYTES + 4, result.capacity());\n\n        int size = result.getInt();\n        assertEquals(1000, size);\n\n        for (int i = 0; i < 1000; i++) {\n            assertEquals((double) i, result.getDouble(), 0.001);\n            result.getLong();\n        }\n\n        //System.out.println(result);\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/test/java/cc/blynk/server/application/handlers/main/auth/RegistrationLimitCheckerTest.java",
    "content": "package cc.blynk.server.application.handlers.main.auth;\n\nimport org.junit.Test;\n\nimport static org.junit.Assert.assertFalse;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 08.04.18.\n */\npublic class RegistrationLimitCheckerTest {\n\n    @Test\n    public void testBasicFlow() throws Exception {\n        LimitChecker limitChecker = new LimitChecker(10, 200);\n\n        for (int i = 0; i < 10; i++) {\n            assertFalse(limitChecker.isLimitReached());\n        }\n        assertFalse(limitChecker.isLimitReached());\n\n        Thread.sleep(200L);\n\n        for (int i = 0; i < 10; i++) {\n            assertFalse(limitChecker.isLimitReached());\n        }\n        assertFalse(limitChecker.isLimitReached());\n    }\n\n    @Test\n    public void testManyThreadsFlow() throws Exception {\n        LimitChecker limitChecker = new LimitChecker(10, 200);\n        Thread[] threads = new Thread[10];\n\n        for (int i = 0; i < 10; i++) {\n            threads[i] = new Thread(() -> assertFalse(limitChecker.isLimitReached()));\n            threads[i].run();\n        }\n        for (Thread thread : threads) {\n            thread.join();\n        }\n        assertFalse(limitChecker.isLimitReached());\n\n        Thread.sleep(200L);\n\n        for (int i = 0; i < 10; i++) {\n            threads[i] = new Thread(() -> assertFalse(limitChecker.isLimitReached()));\n            threads[i].run();\n        }\n        for (Thread thread : threads) {\n            thread.join();\n        }\n        assertFalse(limitChecker.isLimitReached());\n    }\n\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/test/java/cc/blynk/server/application/handlers/main/auth/VersionTest.java",
    "content": "package cc.blynk.server.application.handlers.main.auth;\n\nimport org.junit.Test;\n\nimport static org.junit.Assert.assertEquals;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 24.12.17.\n */\npublic class VersionTest {\n\n    @Test\n    public void testCorrectVersion() {\n        Version version = new Version(\"iOS\", \"1.2.3\");\n        assertEquals(OsType.IOS, version.osType);\n        assertEquals(10203, version.versionSingleNumber);\n    }\n\n    @Test\n    public void wrongValues() {\n        Version version = new Version(\"iOS\", \"RC13\");\n        assertEquals(OsType.IOS, version.osType);\n        assertEquals(0, version.versionSingleNumber);\n    }\n\n    @Test\n    public void wrongValues2() {\n        assertEquals(OsType.OTHER, Version.UNKNOWN_VERSION.osType);\n        assertEquals(0, Version.UNKNOWN_VERSION.versionSingleNumber);\n    }\n\n    @Test\n    public void testToString() {\n        Version version = new Version(\"iOS\", \"1.2.4\");\n        assertEquals(\"iOS-10204\", version.toString());\n\n        version = new Version(\"iOS\", \"1.1.1\");\n        assertEquals(\"iOS-10101\", version.toString());\n\n        version = Version.UNKNOWN_VERSION;\n        assertEquals(\"unknown-0\", version.toString());\n    }\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/test/java/cc/blynk/server/application/handlers/main/logic/reporting/CreateReportTestTimingTest.java",
    "content": "package cc.blynk.server.application.handlers.main.logic.reporting;\n\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.widgets.outputs.graph.GraphGranularityType;\nimport cc.blynk.server.core.model.widgets.ui.reporting.Report;\nimport cc.blynk.server.core.model.widgets.ui.reporting.source.ReportDataStream;\nimport cc.blynk.server.core.model.widgets.ui.reporting.source.ReportSource;\nimport cc.blynk.server.core.model.widgets.ui.reporting.source.TileTemplateReportSource;\nimport cc.blynk.server.core.model.widgets.ui.reporting.type.DailyReport;\nimport cc.blynk.server.core.model.widgets.ui.reporting.type.DayOfMonth;\nimport cc.blynk.server.core.model.widgets.ui.reporting.type.MonthlyReport;\nimport cc.blynk.server.core.model.widgets.ui.reporting.type.ReportDurationType;\nimport cc.blynk.server.core.model.widgets.ui.reporting.type.WeeklyReport;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandBodyException;\nimport org.junit.Test;\n\nimport java.time.Instant;\nimport java.time.ZoneId;\nimport java.time.ZonedDateTime;\nimport java.time.temporal.ChronoField;\nimport java.time.temporal.ChronoUnit;\n\nimport static cc.blynk.server.core.model.widgets.ui.reporting.ReportOutput.CSV_FILE_PER_DEVICE_PER_PIN;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertTrue;\n\npublic class CreateReportTestTimingTest {\n\n    @Test\n    public void testDailyReportStartTimeInThePast()  {\n        ReportDataStream reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, \"Temperature\", true);\n\n        ReportSource reportSource2 = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream},\n                1,\n                new int[] {0, 1}\n        );\n\n        //this time is behind a bit, so expected trigger time in the next day.\n        Instant nowInstant = Instant.now()\n                .with(ChronoField.MILLI_OF_SECOND, 0);\n        long now = nowInstant.toEpochMilli();\n\n        Report report = new Report(1, \"Daily Report\",\n                new ReportSource[] {reportSource2},\n                new DailyReport(now, ReportDurationType.INFINITE, 0, 0), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE_PER_PIN, null, ZoneId.of(\"Europe/Kiev\"), 0, 0, null);\n\n        long expectedDelayInSeconds = report.calculateDelayInSeconds();\n\n        assertEquals(expectedDelayInSeconds, nowInstant.plus(1, ChronoUnit.DAYS).minusMillis(now).getEpochSecond(), 1000);\n    }\n\n    @Test\n    public void testDailyReportStartTimeInTheFuture() {\n        ReportDataStream reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, \"Temperature\", true);\n\n        ReportSource reportSource2 = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream},\n                1,\n                new int[] {0, 1}\n        );\n\n        //this time is upfront a bit\n        Instant nowInstant = Instant.now()\n                .with(ChronoField.MILLI_OF_SECOND, 0);\n        long now = nowInstant.plusSeconds(60).toEpochMilli();\n\n        Report report = new Report(1, \"Daily Report\",\n                new ReportSource[] {reportSource2},\n                new DailyReport(now, ReportDurationType.INFINITE, 0, 0), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE_PER_PIN, null, ZoneId.of(\"Europe/Kiev\"), 0, 0, null);\n\n        long expectedDelayInSeconds = report.calculateDelayInSeconds();\n\n        assertEquals(expectedDelayInSeconds, 60, 2);\n    }\n\n    @Test\n    public void testDailyReportStartTimeInTheFutureAnotherTimezone() {\n        ReportDataStream reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, \"Temperature\", true);\n\n        ReportSource reportSource2 = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream},\n                1,\n                new int[] {0, 1}\n        );\n\n        //this time is upfront a bit\n        long now = ZonedDateTime.now(ZoneId.of(\"UTC\")).plusSeconds(60).toEpochSecond() * 1000;\n\n        Report report = new Report(1, \"Daily Report\",\n                new ReportSource[] {reportSource2},\n                new DailyReport(now, ReportDurationType.INFINITE, 0, 0), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE_PER_PIN, null, ZoneId.of(\"UTC\"), 0, 0, null);\n\n        long expectedDelayInSeconds = report.calculateDelayInSeconds();\n\n        assertEquals(expectedDelayInSeconds, 60, 2);\n    }\n\n    @Test\n    public void testDailyReportStartTimeInTheFutureAnotherTimezone2() {\n        ReportDataStream reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, \"Temperature\", true);\n\n        ReportSource reportSource2 = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream},\n                1,\n                new int[] {0, 1}\n        );\n\n        //this time is upfront a bit\n        long now = ZonedDateTime.now(ZoneId.of(\"UTC\")).plusSeconds(60).toEpochSecond() * 1000;\n\n        Report report = new Report(1, \"Daily Report\",\n                new ReportSource[] {reportSource2},\n                new DailyReport(now, ReportDurationType.INFINITE, 0, 0), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE_PER_PIN, null, ZoneId.of(\"Europe/Kiev\"), 0, 0, null);\n\n        long expectedDelayInSeconds = report.calculateDelayInSeconds();\n\n        assertEquals(expectedDelayInSeconds, 60, 2);\n    }\n\n    @Test\n    public void testWeeklyReportStartTimeInThePast() {\n        ReportDataStream reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, \"Temperature\", true);\n\n        ReportSource reportSource2 = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream},\n                1,\n                new int[] {0, 1}\n        );\n\n        //this time is behind a bit, so expected trigger time in the next day.\n        ZonedDateTime zonedNow = ZonedDateTime.now(ZoneId.of(\"UTC\"));\n        long now = zonedNow.toEpochSecond() * 1000;\n\n        Report report = new Report(1, \"Report\",\n                new ReportSource[] {reportSource2},\n                new WeeklyReport(now, ReportDurationType.INFINITE, 0, 0, 1), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE_PER_PIN, null, ZoneId.of(\"Europe/Kiev\"), 0, 0, null);\n\n        long expectedDelayInSeconds = report.calculateDelayInSeconds();\n\n        assertTrue(expectedDelayInSeconds > 0);\n    }\n\n    @Test\n    public void testStartEndDateIsSameAsNow() {\n        ReportDataStream reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, \"Temperature\", true);\n\n        ReportSource reportSource2 = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream},\n                1,\n                new int[] {0, 1}\n        );\n\n        //this time is upfront a bit, so expected trigger time today.\n        ZonedDateTime zonedNow = ZonedDateTime.now(ZoneId.of(\"UTC\")).plusSeconds(1);\n        long now = (zonedNow.toEpochSecond() + 1) * 1000;\n\n        Report report = new Report(1, \"Daily Report\",\n                new ReportSource[] {reportSource2},\n                new DailyReport(now, ReportDurationType.CUSTOM, now, now), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE_PER_PIN, null, ZoneId.of(\"Europe/Kiev\"), 0, 0, null);\n\n        long expectedDelayInSeconds = report.calculateDelayInSeconds();\n\n        assertEquals(1, expectedDelayInSeconds, 1);\n    }\n\n    @Test\n    public void testStartDateInFuture() {\n        ReportDataStream reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, \"Temperature\", true);\n\n        ReportSource reportSource2 = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream},\n                1,\n                new int[] {0, 1}\n        );\n\n        //this time is behind a bit, so expected trigger time in the next day.\n        Instant nowInstant = Instant.now()\n                .with(ChronoField.MILLI_OF_SECOND, 0);\n        long now = nowInstant.toEpochMilli();\n\n        Report report = new Report(1, \"Daily Report\",\n                new ReportSource[] {reportSource2},\n                new DailyReport(now, ReportDurationType.CUSTOM, now + 86400_000, now + 86400_000), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE_PER_PIN, null, ZoneId.of(\"Europe/Kiev\"), 0, 0, null);\n\n        long expectedDelayInSeconds = report.calculateDelayInSeconds();\n\n        assertEquals(expectedDelayInSeconds, nowInstant.plus(1, ChronoUnit.DAYS).minusMillis(now).getEpochSecond(), 1000);\n    }\n\n    @Test\n    public void testStartDateInFuture2() {\n        ReportDataStream reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, \"Temperature\", true);\n\n        ReportSource reportSource2 = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream},\n                1,\n                new int[] {0, 1}\n        );\n\n        //this time is behind a bit, so expected trigger time in the next day.\n        Instant nowInstant = Instant.now()\n                .with(ChronoField.MILLI_OF_SECOND, 0);\n        long now = nowInstant.toEpochMilli();\n\n        Report report = new Report(1, \"Daily Report\",\n                new ReportSource[] {reportSource2},\n                new DailyReport(now, ReportDurationType.CUSTOM, now + 2 * 86400_000, now + 2 * 86400_000), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE_PER_PIN, null, ZoneId.of(\"Europe/Kiev\"), 0, 0, null);\n\n        long expectedDelayInSeconds = report.calculateDelayInSeconds();\n\n        assertEquals(expectedDelayInSeconds, nowInstant.plus(2, ChronoUnit.DAYS).minusMillis(now).getEpochSecond(), 1000);\n    }\n\n    @Test(expected = IllegalCommandBodyException.class)\n    public void testEndDateInPastDailyReport() {\n        ReportDataStream reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, \"Temperature\", true);\n\n        ReportSource reportSource2 = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream},\n                1,\n                new int[] {0, 1}\n        );\n\n        //this time is behind a bit, so expected trigger time in the next day.\n        Instant nowInstant = Instant.now()\n                .with(ChronoField.MILLI_OF_SECOND, 0);\n        long now = nowInstant.toEpochMilli();\n\n        Report report = new Report(1, \"Daily Report\",\n                new ReportSource[] {reportSource2},\n                new DailyReport(now, ReportDurationType.CUSTOM, now, now - 86400_000), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE_PER_PIN, null, ZoneId.of(\"Europe/Kiev\"), 0, 0, null);\n\n        report.calculateDelayInSeconds();\n    }\n\n    @Test(expected = IllegalCommandBodyException.class)\n    public void testEndDateInPastMonthlyReport() {\n        ReportDataStream reportDataStream = new ReportDataStream((short) 1, PinType.VIRTUAL, \"Temperature\", true);\n\n        ReportSource reportSource2 = new TileTemplateReportSource(\n                new ReportDataStream[] {reportDataStream},\n                1,\n                new int[] {0, 1}\n        );\n\n        ZonedDateTime zonedNow = ZonedDateTime.now(ZoneId.of(\"UTC\"));\n        long now = zonedNow.toEpochSecond() * 1000;\n\n        if (zonedNow.getDayOfMonth() == 1) {\n            now += 86400_000;\n        }\n\n        Report report = new Report(1, \"Daily Report\",\n                new ReportSource[] {reportSource2},\n                new MonthlyReport(now, ReportDurationType.CUSTOM, now, now, DayOfMonth.FIRST), \"test@gmail.com\",\n                GraphGranularityType.MINUTE, true, CSV_FILE_PER_DEVICE_PER_PIN, null, ZoneId.of(\"Europe/Kiev\"), 0, 0, null);\n\n        report.calculateDelayInSeconds();\n    }\n\n\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/test/java/cc/blynk/server/application/handlers/main/logic/reporting/ExportGraphDataLogicTest.java",
    "content": "package cc.blynk.server.application.handlers.main.logic.reporting;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 09.07.16.\n */\npublic class ExportGraphDataLogicTest {\n}\n"
  },
  {
    "path": "server/tcp-app-server/src/test/java/cc/blynk/utils/EMailValidationTest.java",
    "content": "package cc.blynk.utils;\n\nimport cc.blynk.utils.validators.BlynkEmailValidator;\nimport org.junit.Test;\n\nimport static org.junit.Assert.assertFalse;\nimport static org.junit.Assert.assertTrue;\n\n/**\n * User: ddumanskiy\n * Date: 8/11/13\n * Time: 6:43 PM\n */\npublic class EMailValidationTest {\n\n    @Test\n    public void testAllValid() {\n        String[] mailList = new String[] {\n                \"xxxx.yyy@rsa.rohde-schwarz.com\",\n                \"1@mail.ru\",\n                \"google@gmail.com\",\n                \"dsasd234e021-0+@mail.ua\",\n                \"ddd@yahoo.com\",\n                \"mmmm@yahoo.com\",\n                \"mmmmm-100@yahoo.com\",\n                \"mmmmm.100@yahoo.com\",\n                \"mmmm111@mmmm.com\",\n                \"mmmm-100@mmmm.net\",\n                \"mmmm.100@mmmm.com.au\",\n                \"mmmm@1.com\",\n                \"mmmm@gmail.com.com\",\n                \"mmmm+100@gmail.com\",\n                \"bla@bla.com.ua\",\n                \"bla@bla.cc\",\n                \"mmmm-100@yahoo-test.com\"\n        };\n\n        for (String email : mailList) {\n            assertFalse(email, BlynkEmailValidator.isNotValidEmail(email));\n        }\n    }\n\n    @Test\n    public void testAllInValid() {\n        String[] mailList = new String[] {\n                \"mmmm\",\n                \"mmmm@.com.my\",\n                \"mmm@hey.con\",\n                \"mmm@hey.cpm\",\n                \"mmm@hey.comcom\",\n                \"mmm@hey.fe\",\n                \"mmm@hey.hshs\",\n                \"mmm@hey.aa\",\n                \"mmm@hey.cim\",\n                \"mmmm123@.com.com\",\n                \".mmmm@mmmm.com\",\n                \"mmmm()*@gmail.com\",\n                \"mmmm..2002@gmail.com\",\n                \"mmmm.@gmail.com\",\n                \"mmmm@mmmm@gmail.com\",\n                \"ji?pui@gmail.com\",\n                \"bla@bla\"\n        };\n\n        for (String email : mailList) {\n            assertTrue(email, BlynkEmailValidator.isNotValidEmail(email));\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-hardware-server/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <artifactId>server</artifactId>\n        <groupId>cc.blynk.server</groupId>\n        <version>0.41.17-SNAPSHOT</version>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n\n    <groupId>cc.blynk.server.hardware</groupId>\n    <artifactId>tcp-hardware-server</artifactId>\n\n    <dependencies>\n\n        <dependency>\n            <groupId>cc.blynk.server</groupId>\n            <artifactId>core</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n\n        <dependency>\n            <groupId>io.netty</groupId>\n            <artifactId>netty-codec-mqtt</artifactId>\n            <version>${netty.version}</version>\n        </dependency>\n\n        <dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-lang3</artifactId>\n            <version>${commons-lang3.version}</version>\n            <scope>test</scope>\n        </dependency>\n\n    </dependencies>\n\n</project>"
  },
  {
    "path": "server/tcp-hardware-server/src/main/java/cc/blynk/server/hardware/handlers/hardware/HardwareChannelStateHandler.java",
    "content": "package cc.blynk.server.hardware.handlers.hardware;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.core.dao.SessionDao;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.Session;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.device.Status;\nimport cc.blynk.server.core.model.widgets.notifications.Notification;\nimport cc.blynk.server.notifications.push.GCMWrapper;\nimport cc.blynk.utils.properties.Placeholders;\nimport io.netty.channel.ChannelHandler;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.ChannelInboundHandlerAdapter;\nimport io.netty.handler.timeout.IdleStateEvent;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.util.concurrent.TimeUnit;\n\nimport static cc.blynk.server.internal.StateHolderUtil.getHardState;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/20/2015.\n *\n * Removes channel from session in case it became inactive (closed from client side).\n */\n@ChannelHandler.Sharable\npublic class HardwareChannelStateHandler extends ChannelInboundHandlerAdapter {\n\n    private static final Logger log = LogManager.getLogger(HardwareChannelStateHandler.class);\n\n    private final SessionDao sessionDao;\n    private final GCMWrapper gcmWrapper;\n    private final String pushNotificationBody;\n\n    public HardwareChannelStateHandler(Holder holder) {\n        this.sessionDao = holder.sessionDao;\n        this.gcmWrapper = holder.gcmWrapper;\n        this.pushNotificationBody = holder.textHolder.pushNotificationBody;\n    }\n\n    @Override\n    public void channelInactive(ChannelHandlerContext ctx) {\n        var hardwareChannel = ctx.channel();\n        var state = getHardState(hardwareChannel);\n        if (state != null) {\n            var session = sessionDao.get(state.userKey);\n            if (session != null) {\n                var device = state.device;\n                log.trace(\"Hardware channel disconnect for {}, dashId {}, deviceId {}, token {}.\",\n                        state.userKey, state.dash.id, device.id, device.token);\n                sentOfflineMessage(ctx, session, state.dash, device);\n            }\n        }\n    }\n\n    @Override\n    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {\n        if (evt instanceof IdleStateEvent) {\n            log.trace(\"State handler. Hardware timeout disconnect. Event : {}. Closing.\",\n                    ((IdleStateEvent) evt).state());\n            ctx.close();\n        } else {\n            ctx.fireUserEventTriggered(evt);\n        }\n    }\n\n    private void sentOfflineMessage(ChannelHandlerContext ctx, Session session, DashBoard dashBoard, Device device) {\n        //this is special case.\n        //in case hardware quickly reconnects we do not mark it as disconnected\n        //as it is already online after quick disconnect.\n        //https://github.com/blynkkk/blynk-server/issues/403\n        boolean isHardwareConnected = session.isHardwareConnected(dashBoard.id, device.id);\n        if (!isHardwareConnected) {\n            log.trace(\"Changing device status. DeviceId {}, dashId {}\", device.id, dashBoard.id);\n            device.disconnected();\n        }\n\n        if (!dashBoard.isActive) {\n            return;\n        }\n\n        Notification notification = dashBoard.getNotificationWidget();\n\n        if (notification != null && notification.notifyWhenOffline) {\n            sendPushNotification(ctx, session, notification, dashBoard, device);\n        } else if (!dashBoard.isNotificationsOff) {\n            session.sendOfflineMessageToApps(dashBoard.id, device.id);\n        }\n    }\n\n    private void sendPushNotification(ChannelHandlerContext ctx, Session session,\n                                      Notification notification, DashBoard dash, Device device) {\n        var deviceName = ((device == null || device.name == null) ? \"device\" : device.name);\n        var message = pushNotificationBody.replace(Placeholders.DEVICE_NAME, deviceName);\n        if (notification.notifyWhenOfflineIgnorePeriod == 0 || device == null) {\n            if (!dash.isNotificationsOff && device != null) {\n                session.sendOfflineMessageToApps(dash.id, device.id);\n            }\n            notification.push(gcmWrapper,\n                    message,\n                    dash.id\n            );\n        } else {\n            //delayed notification\n            //https://github.com/blynkkk/blynk-server/issues/493\n            ctx.executor().schedule(new DelayedPush(session, device, notification, message, dash),\n                                    notification.notifyWhenOfflineIgnorePeriod, TimeUnit.MILLISECONDS);\n        }\n    }\n\n    private final class DelayedPush implements Runnable {\n\n        private final Session session;\n        private final Device device;\n        private final Notification notification;\n        private final String message;\n        private final DashBoard dash;\n\n        DelayedPush(Session session, Device device, Notification notification, String message, DashBoard dash) {\n            this.session = session;\n            this.device = device;\n            this.notification = notification;\n            this.message = message;\n            this.dash = dash;\n        }\n\n        @Override\n        public void run() {\n            if (device.status == Status.OFFLINE) {\n                if (!dash.isNotificationsOff) {\n                    session.sendOfflineMessageToApps(dash.id, device.id);\n                }\n                long now = System.currentTimeMillis();\n                if (now - device.disconnectTime >= notification.notifyWhenOfflineIgnorePeriod) {\n                    notification.push(gcmWrapper,\n                                      message,\n                                      dash.id\n                    );\n                }\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-hardware-server/src/main/java/cc/blynk/server/hardware/handlers/hardware/HardwareHandler.java",
    "content": "package cc.blynk.server.hardware.handlers.hardware;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.common.BaseSimpleChannelInboundHandler;\nimport cc.blynk.server.common.handlers.logic.PingLogic;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.server.core.session.HardwareStateHolder;\nimport cc.blynk.server.core.session.StateHolderBase;\nimport cc.blynk.server.hardware.handlers.hardware.logic.BlynkInternalLogic;\nimport cc.blynk.server.hardware.handlers.hardware.logic.BridgeLogic;\nimport cc.blynk.server.hardware.handlers.hardware.logic.HardwareLogic;\nimport cc.blynk.server.hardware.handlers.hardware.logic.HardwareSyncLogic;\nimport cc.blynk.server.hardware.handlers.hardware.logic.MailLogic;\nimport cc.blynk.server.hardware.handlers.hardware.logic.PushLogic;\nimport cc.blynk.server.hardware.handlers.hardware.logic.SetWidgetPropertyLogic;\nimport cc.blynk.server.hardware.handlers.hardware.logic.SmsLogic;\nimport cc.blynk.server.hardware.handlers.hardware.logic.TwitLogic;\nimport cc.blynk.server.hardware.internal.BridgeForwardMessage;\nimport io.netty.channel.ChannelHandlerContext;\n\nimport static cc.blynk.server.core.protocol.enums.Command.BLYNK_INTERNAL;\nimport static cc.blynk.server.core.protocol.enums.Command.BRIDGE;\nimport static cc.blynk.server.core.protocol.enums.Command.EMAIL;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE_LOGIN;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE_SYNC;\nimport static cc.blynk.server.core.protocol.enums.Command.LOGIN;\nimport static cc.blynk.server.core.protocol.enums.Command.PING;\nimport static cc.blynk.server.core.protocol.enums.Command.PUSH_NOTIFICATION;\nimport static cc.blynk.server.core.protocol.enums.Command.SET_WIDGET_PROPERTY;\nimport static cc.blynk.server.core.protocol.enums.Command.SMS;\nimport static cc.blynk.server.core.protocol.enums.Command.TWEET;\nimport static cc.blynk.server.internal.CommonByteBufUtil.alreadyRegistered;\nimport static cc.blynk.server.internal.CommonByteBufUtil.illegalCommand;\n\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 29.07.15.\n */\npublic class HardwareHandler extends BaseSimpleChannelInboundHandler<StringMessage> {\n\n    private final HardwareStateHolder state;\n    private final Holder holder;\n    private final HardwareLogic hardware;\n    private final MailLogic email;\n    private final PushLogic push;\n\n    //this is rare handlers, most of users don't use them, so lazy init it.\n    private BridgeLogic bridge;\n    private TwitLogic tweet;\n    private SmsLogic sms;\n\n    public HardwareHandler(Holder holder, HardwareStateHolder stateHolder) {\n        super(StringMessage.class);\n        this.state = stateHolder;\n        this.holder = holder;\n\n        this.hardware = new HardwareLogic(holder, stateHolder.user.email);\n        this.email = new MailLogic(holder);\n        this.push = new PushLogic(holder.gcmWrapper, holder.limits.notificationPeriodLimitSec);\n    }\n\n    @Override\n    public void messageReceived(ChannelHandlerContext ctx, StringMessage msg) {\n        switch (msg.command) {\n            case HARDWARE:\n                hardware.messageReceived(ctx, state, msg);\n                break;\n            case PING:\n                PingLogic.messageReceived(ctx, msg.id);\n                break;\n            case BRIDGE:\n                if (bridge == null) {\n                    this.bridge = new BridgeLogic(holder.sessionDao, holder.tokenManager);\n                }\n                bridge.messageReceived(ctx, state, msg);\n                break;\n            case EMAIL:\n                email.messageReceived(ctx, state, msg);\n                break;\n            case PUSH_NOTIFICATION:\n                push.messageReceived(ctx, state, msg);\n                break;\n            case TWEET:\n                if (tweet == null) {\n                    this.tweet = new TwitLogic(holder.twitterWrapper, holder.limits.notificationPeriodLimitSec);\n                }\n                tweet.messageReceived(ctx, state, msg);\n                break;\n            case SMS:\n                if (sms == null) {\n                    this.sms = new SmsLogic(holder.smsWrapper, holder.limits.notificationPeriodLimitSec);\n                }\n                sms.messageReceived(ctx, state, msg);\n                break;\n            case HARDWARE_SYNC:\n                HardwareSyncLogic.messageReceived(ctx, state, msg);\n                break;\n            case BLYNK_INTERNAL:\n                BlynkInternalLogic.messageReceived(holder, ctx, state, msg);\n                break;\n            case SET_WIDGET_PROPERTY:\n                SetWidgetPropertyLogic.messageReceived(holder, ctx, state, msg);\n                break;\n            //may when firmware is bad written\n            case LOGIN:\n            case HARDWARE_LOGIN:\n                if (ctx.channel().isWritable()) {\n                    ctx.writeAndFlush(alreadyRegistered(msg.id), ctx.voidPromise());\n                }\n                break;\n        }\n    }\n\n    @Override\n    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {\n        if (evt instanceof BridgeForwardMessage) {\n            var bridgeForwardMessage = (BridgeForwardMessage) evt;\n            var tokenValue = bridgeForwardMessage.tokenValue;\n            try {\n                hardware.messageReceived(ctx, bridgeForwardMessage.message,\n                        bridgeForwardMessage.userKey, tokenValue.user, tokenValue.dash, tokenValue.device);\n            } catch (NumberFormatException nfe) {\n                log.debug(\"Error parsing number. {}\", nfe.getMessage());\n                ctx.writeAndFlush(illegalCommand(bridgeForwardMessage.message.id), ctx.voidPromise());\n            }\n        } else {\n            ctx.fireUserEventTriggered(evt);\n        }\n    }\n\n    @Override\n    public StateHolderBase getState() {\n        return state;\n    }\n}\n"
  },
  {
    "path": "server/tcp-hardware-server/src/main/java/cc/blynk/server/hardware/handlers/hardware/MqttHardwareHandler.java",
    "content": "package cc.blynk.server.hardware.handlers.hardware;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.common.BaseSimpleChannelInboundHandler;\nimport cc.blynk.server.core.session.HardwareStateHolder;\nimport cc.blynk.server.core.session.StateHolderBase;\nimport cc.blynk.server.core.stats.GlobalStats;\nimport cc.blynk.server.hardware.handlers.hardware.mqtt.logic.MqttHardwareLogic;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.handler.codec.mqtt.MqttMessage;\nimport io.netty.handler.codec.mqtt.MqttMessageFactory;\nimport io.netty.handler.codec.mqtt.MqttMessageType;\nimport io.netty.handler.codec.mqtt.MqttPublishMessage;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 29.07.15.\n */\npublic class MqttHardwareHandler extends BaseSimpleChannelInboundHandler<MqttMessage> {\n\n    public final HardwareStateHolder state;\n    private final MqttHardwareLogic hardware;\n    private final GlobalStats stats;\n\n    public MqttHardwareHandler(Holder holder, HardwareStateHolder stateHolder) {\n        super(MqttMessage.class);\n        this.hardware = new MqttHardwareLogic(holder.sessionDao, holder.reportingDiskDao);\n        this.state = stateHolder;\n        this.stats = holder.stats;\n    }\n\n    @Override\n    public void messageReceived(ChannelHandlerContext ctx, MqttMessage msg) {\n        this.stats.incrementMqttStat();\n        MqttMessageType messageType = msg.fixedHeader().messageType();\n\n        switch (messageType) {\n            case PUBLISH :\n                MqttPublishMessage publishMessage = (MqttPublishMessage) msg;\n                String topic = publishMessage.variableHeader().topicName();\n\n                switch (topic.toLowerCase()) {\n                    case \"hardware\" :\n                        hardware.messageReceived(state, publishMessage);\n                        break;\n                }\n\n                break;\n\n            case PINGREQ :\n                ctx.writeAndFlush(\n                        MqttMessageFactory.newMessage(msg.fixedHeader(), msg.variableHeader(), null),\n                        ctx.voidPromise());\n                break;\n\n            case DISCONNECT :\n                log.trace(\"Got disconnect. Closing...\");\n                ctx.close();\n                break;\n        }\n    }\n\n    @Override\n    public StateHolderBase getState() {\n        return state;\n    }\n}\n"
  },
  {
    "path": "server/tcp-hardware-server/src/main/java/cc/blynk/server/hardware/handlers/hardware/auth/HardwareLoginHandler.java",
    "content": "package cc.blynk.server.hardware.handlers.hardware.auth;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.core.BlockingIOProcessor;\nimport cc.blynk.server.core.dao.TokenValue;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.Session;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.protocol.model.messages.MessageBase;\nimport cc.blynk.server.core.protocol.model.messages.appllication.LoginMessage;\nimport cc.blynk.server.core.session.HardwareStateHolder;\nimport cc.blynk.server.db.DBManager;\nimport cc.blynk.server.hardware.handlers.hardware.HardwareHandler;\nimport cc.blynk.server.internal.ReregisterChannelUtil;\nimport cc.blynk.utils.IPUtils;\nimport cc.blynk.utils.StringUtils;\nimport cc.blynk.utils.structure.LRUCache;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelHandler;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.ChannelPipeline;\nimport io.netty.channel.SimpleChannelInboundHandler;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.util.concurrent.RejectedExecutionException;\n\nimport static cc.blynk.server.core.protocol.enums.Command.CONNECT_REDIRECT;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE;\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE_CONNECTED;\nimport static cc.blynk.server.internal.CommonByteBufUtil.invalidToken;\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeASCIIStringMessage;\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\nimport static cc.blynk.server.internal.CommonByteBufUtil.serverError;\nimport static cc.blynk.utils.StringUtils.BODY_SEPARATOR;\nimport static cc.blynk.utils.StringUtils.DEVICE_SEPARATOR;\n\n/**\n * Handler responsible for managing hardware and apps login messages.\n * Initializes netty channel with a state tied with user.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\n@ChannelHandler.Sharable\npublic class HardwareLoginHandler extends SimpleChannelInboundHandler<LoginMessage> {\n\n    private static final Logger log = LogManager.getLogger(HardwareLoginHandler.class);\n\n    private static final int HARDWARE_PIN_MODE_MSG_ID = 1;\n\n    private final Holder holder;\n    private final DBManager dbManager;\n    private final BlockingIOProcessor blockingIOProcessor;\n    private final String listenPort;\n    private final boolean allowStoreIp;\n\n    public HardwareLoginHandler(Holder holder, int listenPort) {\n        this.holder = holder;\n        this.dbManager = holder.dbManager;\n        this.blockingIOProcessor = holder.blockingIOProcessor;\n        boolean isForce80ForRedirect = holder.props.getBoolProperty(\"force.port.80.for.redirect\");\n        this.listenPort = isForce80ForRedirect ? \"80\" : String.valueOf(listenPort);\n        this.allowStoreIp = holder.props.getAllowStoreIp();\n    }\n\n    private void completeLogin(Channel channel, Session session, User user,\n                                      DashBoard dash, Device device, int msgId) {\n        log.debug(\"completeLogin. {}\", channel);\n\n        session.addHardChannel(channel);\n        channel.write(ok(msgId));\n\n        String body = dash.buildPMMessage(device.id);\n        if (dash.isActive && body != null) {\n            channel.write(makeASCIIStringMessage(HARDWARE, HARDWARE_PIN_MODE_MSG_ID, body));\n        }\n\n        channel.flush();\n\n        String responseBody = String.valueOf(dash.id) + DEVICE_SEPARATOR + device.id;\n        session.sendToApps(HARDWARE_CONNECTED, msgId, dash.id, responseBody);\n        log.trace(\"Connected device id {}, dash id {}\", device.id, dash.id);\n        device.connected();\n        if (device.firstConnectTime == 0) {\n            device.firstConnectTime = device.connectTime;\n        }\n        if (allowStoreIp) {\n            device.lastLoggedIP = IPUtils.getIp(channel.remoteAddress());\n        }\n\n        log.info(\"{} hardware joined.\", user.email);\n    }\n\n    @Override\n    protected void channelRead0(ChannelHandlerContext ctx, LoginMessage message) {\n        String token = message.body.trim();\n        TokenValue tokenValue = holder.tokenManager.getTokenValueByToken(token);\n\n        if (tokenValue == null) {\n            //token should always be 32 chars and shouldn't contain invalid nil char\n            if (token.length() != 32 || token.contains(StringUtils.BODY_SEPARATOR_STRING)) {\n                log.debug(\"HardwareLogic token is invalid. Token '{}', '{}'\", token, ctx.channel().remoteAddress());\n                ctx.writeAndFlush(invalidToken(message.id), ctx.voidPromise());\n            } else {\n                //no user on current server, trying to find server that user belongs to.\n                checkTokenOnOtherServer(ctx, token, message.id);\n            }\n            return;\n        }\n\n        User user = tokenValue.user;\n        Device device = tokenValue.device;\n        DashBoard dash = tokenValue.dash;\n\n        if (tokenValue.isTemporary()) {\n            holder.tokenManager.updateRegularCache(token, tokenValue);\n            user.profile.addDevice(dash, device);\n            user.lastModifiedTs = System.currentTimeMillis();\n        }\n\n        createSessionAndReregister(ctx, user, dash, device, message.id);\n    }\n\n    private void createSessionAndReregister(ChannelHandlerContext ctx,\n                                            User user, DashBoard dash, Device device, int msgId) {\n        HardwareStateHolder hardwareStateHolder = new HardwareStateHolder(user, dash, device);\n\n        ChannelPipeline pipeline = ctx.pipeline();\n        pipeline.replace(this, \"HHArdwareHandler\", new HardwareHandler(holder, hardwareStateHolder));\n\n        Session session = holder.sessionDao.getOrCreateSessionByUser(\n                hardwareStateHolder.userKey, ctx.channel().eventLoop());\n\n        if (session.isSameEventLoop(ctx)) {\n            completeLogin(ctx.channel(), session, user, dash, device, msgId);\n        } else {\n            log.debug(\"Re registering hard channel. {}\", ctx.channel());\n            ReregisterChannelUtil.reRegisterChannel(ctx, session, channelFuture ->\n                    completeLogin(channelFuture.channel(), session, user, dash, device, msgId));\n        }\n    }\n\n    private void checkTokenOnOtherServer(ChannelHandlerContext ctx, String token, int msgId) {\n        //check cache first\n        LRUCache.CacheEntry cacheEntry = LRUCache.LOGIN_TOKENS_CACHE.get(token);\n        if (cacheEntry == null) {\n            try {\n                blockingIOProcessor.executeDBGetServer(() -> {\n                    String server;\n                    log.debug(\"Checking invalid token in DB.\");\n                    server = dbManager.getServerByToken(token);\n                    LRUCache.LOGIN_TOKENS_CACHE.put(token, new LRUCache.CacheEntry(server));\n                    // no server found, that's means token is wrong.\n                    sendRedirectResponse(ctx, token, server, msgId);\n                });\n            } catch (RejectedExecutionException ree) {\n                log.warn(\"Error in getServerByToken handler. Limit of tasks reached.\");\n                ctx.writeAndFlush(serverError(msgId), ctx.voidPromise());\n            }\n        } else {\n            log.debug(\"Taking token from cache.\");\n            sendRedirectResponse(ctx, token, cacheEntry.value, msgId);\n        }\n    }\n\n    private void sendRedirectResponse(ChannelHandlerContext ctx, String token, String server, int msgId) {\n        MessageBase response;\n        if (server == null || server.equals(holder.props.host)) {\n            log.debug(\"HardwareLogic token is invalid. Token '{}', '{}'\", token, ctx.channel().remoteAddress());\n            response = invalidToken(msgId);\n        } else {\n            log.debug(\"Redirecting token '{}' to {}\", token, server);\n            response = makeASCIIStringMessage(CONNECT_REDIRECT, msgId, server + BODY_SEPARATOR + listenPort);\n        }\n        ctx.writeAndFlush(response, ctx.voidPromise());\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-hardware-server/src/main/java/cc/blynk/server/hardware/handlers/hardware/logic/BlynkInternalLogic.java",
    "content": "package cc.blynk.server.hardware.handlers.hardware.logic;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.core.dao.ota.OTAManager;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.device.HardwareInfo;\nimport cc.blynk.server.core.model.widgets.others.rtc.RTC;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.server.core.session.HardwareStateHolder;\nimport cc.blynk.utils.NumberUtil;\nimport cc.blynk.utils.StringUtils;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.handler.timeout.IdleStateHandler;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.core.protocol.enums.Command.BLYNK_INTERNAL;\nimport static cc.blynk.server.internal.CommonByteBufUtil.illegalCommand;\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeASCIIStringMessage;\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\nimport static cc.blynk.utils.StringUtils.BODY_SEPARATOR;\n\n/**\n *\n * Simple handler that accepts info command from hardware.\n * At the moment only 1 param is used \"h-beat\".\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic final class BlynkInternalLogic {\n\n    private static final Logger log = LogManager.getLogger(BlynkInternalLogic.class);\n\n    private BlynkInternalLogic() {\n    }\n\n    public static void messageReceived(Holder holder, ChannelHandlerContext ctx,\n                                       HardwareStateHolder state, StringMessage message) {\n        String[] messageParts = message.body.split(StringUtils.BODY_SEPARATOR_STRING);\n\n        if (messageParts.length == 0 || messageParts[0].length() == 0) {\n            ctx.writeAndFlush(illegalCommand(message.id), ctx.voidPromise());\n            return;\n        }\n\n        String cmd = messageParts[0];\n\n        switch (cmd.charAt(0)) {\n            case 'v' : //ver\n            case 'f' : //fw\n            case 'h' : //h-beat\n            case 'b' : //buff-in\n            case 'd' : //dev\n            case 'c' : //cpu\n            case 't' : //tmpl\n                parseHardwareInfo(holder, ctx, messageParts, state, message.id);\n                break;\n            case 'r' : //rtc\n                sendRTC(ctx, state, message.id);\n                break;\n            case 'a' :\n            case 'o' :\n                break;\n        }\n\n    }\n\n    private static void sendRTC(ChannelHandlerContext ctx, HardwareStateHolder state, int msgId) {\n        DashBoard dashBoard = state.dash;\n        RTC rtc = dashBoard.getWidgetByType(RTC.class);\n        if (rtc != null && ctx.channel().isWritable()) {\n            ctx.writeAndFlush(makeASCIIStringMessage(BLYNK_INTERNAL, msgId, \"rtc\" + BODY_SEPARATOR + rtc.getTime()),\n                    ctx.voidPromise());\n        }\n    }\n\n    private static void parseHardwareInfo(Holder holder, ChannelHandlerContext ctx,\n                                          String[] messageParts,\n                                          HardwareStateHolder state, int msgId) {\n        HardwareInfo hardwareInfo = new HardwareInfo(messageParts);\n        int newHardwareInterval = hardwareInfo.heartbeatInterval;\n\n        log.trace(\"Info command. heartbeat interval {}\", newHardwareInterval);\n        OTAManager otaManager = holder.otaManager;\n        int hardwareIdleTimeout = holder.limits.hardwareIdleTimeout;\n\n        //no need to change IdleStateHandler if heartbeat interval wasn't changed or wasn't provided\n        if (hardwareIdleTimeout != 0 && newHardwareInterval > 0 && newHardwareInterval != hardwareIdleTimeout) {\n            int newReadTimeout = NumberUtil.calcHeartbeatTimeout(newHardwareInterval);\n            log.debug(\"Changing read timeout interval to {}\", newReadTimeout);\n            ctx.pipeline().replace(IdleStateHandler.class,\n                    \"H_IdleStateHandler_Replaced\", new IdleStateHandler(newReadTimeout, 0, 0));\n        }\n\n        DashBoard dashBoard = state.dash;\n        Device device = state.device;\n\n        if (device != null) {\n            otaManager.initiateHardwareUpdate(ctx, state.userKey, hardwareInfo, dashBoard, device);\n            device.hardwareInfo = hardwareInfo;\n            dashBoard.updatedAt = System.currentTimeMillis();\n        }\n\n        ctx.writeAndFlush(ok(msgId), ctx.voidPromise());\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-hardware-server/src/main/java/cc/blynk/server/hardware/handlers/hardware/logic/BridgeLogic.java",
    "content": "package cc.blynk.server.hardware.handlers.hardware.logic;\n\nimport cc.blynk.server.core.dao.SessionDao;\nimport cc.blynk.server.core.dao.TokenManager;\nimport cc.blynk.server.core.dao.TokenValue;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.server.core.session.HardwareStateHolder;\nimport cc.blynk.server.hardware.internal.BridgeForwardMessage;\nimport cc.blynk.utils.StringUtils;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static cc.blynk.server.core.protocol.enums.Command.BRIDGE;\nimport static cc.blynk.server.internal.CommonByteBufUtil.deviceNotInNetwork;\nimport static cc.blynk.server.internal.CommonByteBufUtil.illegalCommand;\nimport static cc.blynk.server.internal.CommonByteBufUtil.notAllowed;\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\nimport static cc.blynk.server.internal.StateHolderUtil.getHardState;\nimport static cc.blynk.utils.StringUtils.split3;\n\n/**\n * Bridge handler responsible for forwarding messages between different hardware via Blynk Server.\n * SendTo device defined by Auth Token.\n *\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic class BridgeLogic {\n\n    private static final Logger log = LogManager.getLogger(BridgeLogic.class);\n    private final SessionDao sessionDao;\n    private final TokenManager tokenManager;\n    private Map<String, TokenValue> sendToMap;\n\n    public BridgeLogic(SessionDao sessionDao, TokenManager tokenManager) {\n        this.sessionDao = sessionDao;\n        this.tokenManager = tokenManager;\n    }\n\n    private static boolean isInit(String body) {\n        return body.length() > 0 && body.charAt(0) == 'i';\n    }\n\n    public void messageReceived(ChannelHandlerContext ctx, HardwareStateHolder state, StringMessage message) {\n        var session = sessionDao.get(state.userKey);\n        var split = split3(message.body);\n\n        if (split.length < 3) {\n            log.error(\"Wrong bridge body. '{}'\", message.body);\n            ctx.writeAndFlush(illegalCommand(message.id), ctx.voidPromise());\n            return;\n        }\n\n        var bridgePin = split[0];\n\n        if (isInit(split[1])) {\n            var token = split[2];\n            if (sendToMap == null) {\n                sendToMap = new HashMap<>();\n            }\n\n            if (sendToMap.size() > 100 || token.length() != 32) {\n                ctx.writeAndFlush(notAllowed(message.id), ctx.voidPromise());\n                return;\n            }\n\n            //sendToMap may be already initialized, so checking it first.\n            var tokenValue = sendToMap.get(token);\n            if (tokenValue == null) {\n                tokenValue = tokenManager.getTokenValueByToken(token);\n                if (tokenValue == null) {\n                    log.debug(\"Token {} for bridge command does not exists.\", token);\n                    ctx.writeAndFlush(notAllowed(message.id), ctx.voidPromise());\n                    return;\n                }\n            }\n\n            if (!tokenValue.user.equals(state.user)) {\n                log.debug(\"User {} allowed to access devices only within own account.\", state.user);\n                ctx.writeAndFlush(notAllowed(message.id), ctx.voidPromise());\n                return;\n            }\n\n            sendToMap.put(bridgePin, tokenValue);\n            ctx.writeAndFlush(ok(message.id), ctx.voidPromise());\n        } else {\n            if (sendToMap == null || sendToMap.size() == 0) {\n                log.debug(\"Bridge not initialized. {}\", state.user.email);\n                ctx.writeAndFlush(notAllowed(message.id), ctx.voidPromise());\n                return;\n            }\n\n            var tokenvalue = sendToMap.get(bridgePin);\n            if (tokenvalue == null) {\n                log.debug(\"No token. Bridge not initialized. {}\", state.user.email);\n                ctx.writeAndFlush(notAllowed(message.id), ctx.voidPromise());\n                return;\n            }\n\n            var body = message.body.substring(message.body.indexOf(StringUtils.BODY_SEPARATOR_STRING) + 1);\n            var bridgeMessage = new StringMessage(message.id, BRIDGE, body);\n\n            var targetDeviceId = tokenvalue.device.id;\n            var targetDashId = tokenvalue.dash.id;\n\n            if (session.hardwareChannels.size() > 1) {\n                var messageWasSent = false;\n                for (Channel channel : session.hardwareChannels) {\n                    if (channel != ctx.channel() && channel.isWritable()) {\n                        HardwareStateHolder hardwareState = getHardState(channel);\n                        if (hardwareState != null\n                                && hardwareState.isSameDashAndDeviceId(targetDashId, targetDeviceId)) {\n                            messageWasSent = true;\n                            channel.writeAndFlush(bridgeMessage, channel.voidPromise());\n                        }\n                    }\n                }\n                if (!messageWasSent) {\n                    ctx.writeAndFlush(deviceNotInNetwork(message.id), ctx.voidPromise());\n                }\n            } else {\n                ctx.writeAndFlush(deviceNotInNetwork(message.id), ctx.voidPromise());\n            }\n\n            ctx.pipeline().fireUserEventTriggered(new BridgeForwardMessage(bridgeMessage, tokenvalue, state.userKey));\n        }\n    }\n}\n\n"
  },
  {
    "path": "server/tcp-hardware-server/src/main/java/cc/blynk/server/hardware/handlers/hardware/logic/HardwareLogic.java",
    "content": "package cc.blynk.server.hardware.handlers.hardware.logic;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.core.dao.ReportingDiskDao;\nimport cc.blynk.server.core.dao.SessionDao;\nimport cc.blynk.server.core.dao.UserKey;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.Session;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.processors.BaseProcessorHandler;\nimport cc.blynk.server.core.processors.WebhookProcessor;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.server.core.session.HardwareStateHolder;\nimport cc.blynk.utils.NumberUtil;\nimport io.netty.channel.ChannelHandlerContext;\n\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE;\nimport static cc.blynk.server.internal.CommonByteBufUtil.illegalCommand;\nimport static cc.blynk.utils.StringUtils.split3;\n\n/**\n * Handler responsible for forwarding messages from hardware to applications.\n * Also handler stores all incoming hardware commands to disk in order to export and\n * analyze data.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic class HardwareLogic extends BaseProcessorHandler {\n\n    private final ReportingDiskDao reportingDao;\n    private final SessionDao sessionDao;\n\n    public HardwareLogic(Holder holder, String email) {\n        super(holder.eventorProcessor, new WebhookProcessor(holder.asyncHttpClient,\n                holder.limits.webhookPeriodLimitation,\n                holder.limits.webhookResponseSizeLimitBytes,\n                holder.limits.webhookFailureLimit,\n                holder.stats,\n                email));\n        this.sessionDao = holder.sessionDao;\n        this.reportingDao = holder.reportingDiskDao;\n    }\n\n    private static boolean isWriteOperation(String body) {\n        return body.charAt(1) == 'w';\n    }\n\n    public void messageReceived(ChannelHandlerContext ctx, HardwareStateHolder state, StringMessage message) {\n        messageReceived(ctx, message, state.userKey, state.user, state.dash, state.device);\n    }\n\n    public void messageReceived(ChannelHandlerContext ctx, StringMessage message,\n                                UserKey userKey, User user, DashBoard dash, Device device) {\n        String body = message.body;\n\n        //minimum command - \"ar 1\"\n        if (body.length() < 4) {\n            log.debug(\"HardwareLogic command body too short.\");\n            ctx.writeAndFlush(illegalCommand(message.id), ctx.voidPromise());\n            return;\n        }\n\n        if (isWriteOperation(body)) {\n            String[] splitBody = split3(body);\n\n            if (splitBody.length < 3 || splitBody[0].length() == 0 || splitBody[2].length() == 0) {\n                log.debug(\"Write command is wrong {} for {} and deviceId {}.\", body, user.email, device.id);\n                ctx.writeAndFlush(illegalCommand(message.id), ctx.voidPromise());\n                return;\n            }\n\n            PinType pinType = PinType.getPinType(splitBody[0].charAt(0));\n            short pin = NumberUtil.parsePin(splitBody[1]);\n            String value = splitBody[2];\n            long now = System.currentTimeMillis();\n            int deviceId = device.id;\n\n            reportingDao.process(user, dash, deviceId, pin, pinType, value, now);\n            user.profile.update(dash, deviceId, pin, pinType, value, now);\n            device.dataReceivedAt = now;\n\n            Session session = sessionDao.get(userKey);\n            processEventorAndWebhook(user, dash, deviceId, session, pin, pinType, value, now);\n\n            if (dash.isActive) {\n                session.sendToApps(HARDWARE, message.id, dash.id, deviceId, body);\n            } else {\n                log.trace(\"No active dashboard.\");\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-hardware-server/src/main/java/cc/blynk/server/hardware/handlers/hardware/logic/HardwareSyncLogic.java",
    "content": "package cc.blynk.server.hardware.handlers.hardware.logic;\n\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.DataStream;\nimport cc.blynk.server.core.model.Profile;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.storage.key.DashPinPropertyStorageKey;\nimport cc.blynk.server.core.model.storage.key.DashPinStorageKey;\nimport cc.blynk.server.core.model.storage.value.PinStorageValue;\nimport cc.blynk.server.core.model.widgets.HardwareSyncWidget;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.others.rtc.RTC;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.server.core.session.HardwareStateHolder;\nimport cc.blynk.utils.NumberUtil;\nimport cc.blynk.utils.StringUtils;\nimport io.netty.channel.ChannelHandlerContext;\n\nimport java.util.Map;\n\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE;\nimport static cc.blynk.server.internal.CommonByteBufUtil.illegalCommand;\nimport static cc.blynk.server.internal.CommonByteBufUtil.makeUTF8StringMessage;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic final class HardwareSyncLogic {\n\n    private HardwareSyncLogic() {\n    }\n\n    public static void messageReceived(ChannelHandlerContext ctx, HardwareStateHolder state, StringMessage message) {\n        int deviceId = state.device.id;\n        DashBoard dash = state.dash;\n\n        if (message.body.length() == 0) {\n            syncAll(ctx, message.id, state.user.profile, dash, deviceId);\n        } else {\n            syncSpecificPins(ctx, message.body, message.id, state.user.profile, dash, deviceId);\n        }\n    }\n\n    private static void syncAll(ChannelHandlerContext ctx, int msgId, Profile profile, DashBoard dash, int deviceId) {\n        //return all widgets state\n        for (Widget widget : dash.widgets) {\n            //one exclusion, no need to sync RTC\n            if (widget instanceof HardwareSyncWidget && !(widget instanceof RTC) && ctx.channel().isWritable()) {\n                ((HardwareSyncWidget) widget).sendHardSync(ctx, msgId, deviceId);\n            }\n        }\n\n        for (Map.Entry<DashPinStorageKey, PinStorageValue> entry : profile.pinsStorage.entrySet()) {\n            DashPinStorageKey key = entry.getKey();\n            if (deviceId == key.deviceId\n                    && dash.id == key.dashId\n                    && !(key instanceof DashPinPropertyStorageKey)\n                    && ctx.channel().isWritable()) {\n                for (String value : entry.getValue().values()) {\n                    String body = key.makeHardwareBody(value);\n                    ctx.write(makeUTF8StringMessage(HARDWARE, msgId, body), ctx.voidPromise());\n                }\n            }\n        }\n\n        ctx.flush();\n    }\n\n    //message format is \"vr 22 33\"\n    //return specific widget state\n    private static void syncSpecificPins(ChannelHandlerContext ctx, String messageBody,\n                                         int msgId, Profile profile, DashBoard dash, int deviceId) {\n        String[] bodyParts = messageBody.split(StringUtils.BODY_SEPARATOR_STRING);\n\n        if (bodyParts.length < 2 || bodyParts[0].isEmpty()) {\n            ctx.writeAndFlush(illegalCommand(msgId), ctx.voidPromise());\n            return;\n        }\n\n        PinType pinType = PinType.getPinType(bodyParts[0].charAt(0));\n\n        if (StringUtils.isReadOperation(bodyParts[0])) {\n            for (int i = 1; i < bodyParts.length; i++) {\n                short pin = NumberUtil.parsePin(bodyParts[i]);\n                Widget widget = dash.findWidgetByPin(deviceId, pin, pinType);\n                if (ctx.channel().isWritable()) {\n                    if (widget == null) {\n                        PinStorageValue pinStorageValue =\n                                profile.pinsStorage.get(new DashPinStorageKey(dash.id, deviceId, pinType, pin));\n                        if (pinStorageValue != null) {\n                            for (String value : pinStorageValue.values()) {\n                                String body = DataStream.makeHardwareBody(pinType, pin, value);\n                                ctx.write(makeUTF8StringMessage(HARDWARE, msgId, body), ctx.voidPromise());\n                            }\n                        }\n                    } else if (widget instanceof HardwareSyncWidget) {\n                        ((HardwareSyncWidget) widget).sendHardSync(ctx, msgId, deviceId);\n                    }\n                }\n            }\n            ctx.flush();\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-hardware-server/src/main/java/cc/blynk/server/hardware/handlers/hardware/logic/MailLogic.java",
    "content": "package cc.blynk.server.hardware.handlers.hardware.logic;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.core.BlockingIOProcessor;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.widgets.notifications.Mail;\nimport cc.blynk.server.core.processors.NotificationBase;\nimport cc.blynk.server.core.protocol.exceptions.IllegalCommandException;\nimport cc.blynk.server.core.protocol.exceptions.NotAllowedException;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.server.core.session.HardwareStateHolder;\nimport cc.blynk.server.notifications.mail.MailWrapper;\nimport cc.blynk.utils.properties.Placeholders;\nimport cc.blynk.utils.validators.BlynkEmailValidator;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.notificationError;\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\n\n/**\n * Sends email from received from hardware. Via google smtp server.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic class MailLogic extends NotificationBase {\n\n    private static final Logger log = LogManager.getLogger(MailLogic.class);\n\n    private final BlockingIOProcessor blockingIOProcessor;\n    private final MailWrapper mailWrapper;\n    private final String vendorEmail;\n\n    public MailLogic(Holder holder) {\n        super(holder.limits.notificationPeriodLimitSec);\n        this.blockingIOProcessor = holder.blockingIOProcessor;\n        this.mailWrapper = holder.mailWrapper;\n        String tmp = holder.props.vendorEmail;\n        this.vendorEmail = tmp == null ? \"\" : tmp;\n    }\n\n    public void messageReceived(ChannelHandlerContext ctx, HardwareStateHolder state, StringMessage message) {\n        User user = state.user;\n        DashBoard dash = state.dash;\n\n        Mail mail = dash.getMailWidget();\n\n        if (mail == null) {\n            throw new NotAllowedException(\"User has no mail widget.\", message.id);\n        }\n\n        if (!dash.isActive) {\n            throw new NotAllowedException(\"User has no active dashboard.\", message.id);\n        }\n\n        if (message.body.isEmpty()) {\n            throw new IllegalCommandException(\"Invalid mail notification body.\");\n        }\n\n        user.checkDailyEmailLimit();\n\n        String[] bodyParts = message.body.split(\"\\0\");\n\n        if (bodyParts.length < 2) {\n            throw new IllegalCommandException(\"Invalid mail notification body.\");\n        }\n\n        String to;\n        String subj;\n        String body;\n\n        if (bodyParts.length == 3) {\n            //if widget has no TO field\n            if (mail.to == null || mail.to.isEmpty()) {\n                to = bodyParts[0]\n                        .replace(Placeholders.VENDOR_EMAIL, vendorEmail)\n                        .replace(Placeholders.DEVICE_OWNER_EMAIL, user.email);\n            } else {\n                //if widget has to field it has priority over hardware field\n                to = mail.to;\n            }\n            subj = bodyParts[1];\n            body = bodyParts[2];\n        } else {\n            to = (mail.to == null || mail.to.isEmpty()) ? user.email : mail.to;\n            subj = bodyParts[0];\n            body = bodyParts[1];\n        }\n\n        checkIfNotificationQuotaLimitIsNotReached();\n\n        //minimal validation for receiver.\n        if (BlynkEmailValidator.isNotValidEmail(to)) {\n            throw new IllegalCommandException(\"Invalid mail receiver.\");\n        }\n\n        String deviceName = state.device.name == null ? \"\" : state.device.name;\n\n        String updatedSubj = subj.replace(Placeholders.DEVICE_NAME, deviceName)\n                                 .replace(Placeholders.VENDOR_EMAIL, vendorEmail)\n                                 .replace(Placeholders.DEVICE_OWNER_EMAIL, user.email);\n\n        String updatedBody = body.replace(Placeholders.DEVICE_NAME, deviceName)\n                                 .replace(Placeholders.VENDOR_EMAIL, vendorEmail)\n                                 .replace(Placeholders.DEVICE_OWNER_EMAIL, user.email);\n\n        log.trace(\"Sending Mail for user {}, with message : '{}'.\", user.email, updatedBody);\n        mail(ctx.channel(), user.email, to, updatedSubj, updatedBody, message.id, mail.isText());\n        user.emailMessages++;\n    }\n\n    private void mail(Channel channel, String email, String to, String subj, String body, int msgId, boolean isText) {\n        blockingIOProcessor.execute(() -> {\n            try {\n                if (isText) {\n                    mailWrapper.sendText(to, subj, body);\n                } else {\n                    mailWrapper.sendHtml(to, subj, body);\n                }\n                channel.writeAndFlush(ok(msgId), channel.voidPromise());\n            } catch (Exception e) {\n                log.error(\"Error sending email from hardware. From user {}, to : {}. Reason : {}\",\n                        email, to, e.getMessage());\n                if (channel.isActive() && channel.isWritable()) {\n                    channel.writeAndFlush(notificationError(msgId), channel.voidPromise());\n                }\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "server/tcp-hardware-server/src/main/java/cc/blynk/server/hardware/handlers/hardware/logic/PushLogic.java",
    "content": "package cc.blynk.server.hardware.handlers.hardware.logic;\n\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.widgets.notifications.Notification;\nimport cc.blynk.server.core.processors.NotificationBase;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.server.core.session.HardwareStateHolder;\nimport cc.blynk.server.notifications.push.GCMWrapper;\nimport cc.blynk.utils.properties.Placeholders;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.noActiveDash;\nimport static cc.blynk.server.internal.CommonByteBufUtil.notificationInvalidBody;\nimport static cc.blynk.server.internal.CommonByteBufUtil.notificationNotAuthorized;\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\n\n/**\n * Handler sends push notifications to Applications. Initiation is on hardware side.\n * Sends both to iOS and Android via Google Cloud Messaging service.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic class PushLogic extends NotificationBase {\n\n    private static final Logger log = LogManager.getLogger(PushLogic.class);\n\n    private final GCMWrapper gcmWrapper;\n\n    public PushLogic(GCMWrapper gcmWrapper, long notificationQuotaLimit) {\n        super(notificationQuotaLimit);\n        this.gcmWrapper = gcmWrapper;\n    }\n\n    public void messageReceived(ChannelHandlerContext ctx, HardwareStateHolder state, StringMessage message) {\n        if (Notification.isWrongBody(message.body)) {\n            log.debug(\"Notification message is empty or larger than limit.\");\n            ctx.writeAndFlush(notificationInvalidBody(message.id), ctx.voidPromise());\n            return;\n        }\n\n        DashBoard dash = state.dash;\n\n        if (!dash.isActive) {\n            log.debug(\"No active dashboard.\");\n            ctx.writeAndFlush(noActiveDash(message.id), ctx.voidPromise());\n            return;\n        }\n\n        Notification widget = dash.getNotificationWidget();\n\n        if (widget == null) {\n            log.debug(\"User has no notifications widget.\");\n            ctx.writeAndFlush(notificationNotAuthorized(message.id), ctx.voidPromise());\n            return;\n        }\n\n        if (widget.hasNoToken()) {\n            log.debug(\"User has no access token provided for push widget.\");\n            ctx.writeAndFlush(notificationNotAuthorized(message.id), ctx.voidPromise());\n            return;\n        }\n\n        long now = System.currentTimeMillis();\n        checkIfNotificationQuotaLimitIsNotReached(now);\n\n        String deviceName = state.device.name == null ? \"\" : state.device.name;\n        String updatedBody = message.body.replace(Placeholders.DEVICE_NAME, deviceName);\n\n        if (Notification.isWrongBody(updatedBody)) {\n            log.debug(\"Notification message is larger than limit.\");\n            ctx.writeAndFlush(notificationInvalidBody(message.id), ctx.voidPromise());\n            return;\n        }\n\n        log.trace(\"Sending push for user {}, with message : '{}'.\", state.user.email, message.body);\n        widget.push(gcmWrapper, updatedBody, state.dash.id);\n        ctx.writeAndFlush(ok(message.id), ctx.voidPromise());\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-hardware-server/src/main/java/cc/blynk/server/hardware/handlers/hardware/logic/SetWidgetPropertyLogic.java",
    "content": "package cc.blynk.server.hardware.handlers.hardware.logic;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.core.dao.SessionDao;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.Session;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.model.enums.WidgetProperty;\nimport cc.blynk.server.core.model.widgets.Widget;\nimport cc.blynk.server.core.model.widgets.ui.tiles.DeviceTiles;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.server.core.session.HardwareStateHolder;\nimport cc.blynk.utils.NumberUtil;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.core.protocol.enums.Command.SET_WIDGET_PROPERTY;\nimport static cc.blynk.server.internal.CommonByteBufUtil.illegalCommandBody;\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\nimport static cc.blynk.utils.StringUtils.split3;\n\n/**\n * Handler that allows to change widget properties from hardware side.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic final class SetWidgetPropertyLogic {\n\n    private static final Logger log = LogManager.getLogger(SetWidgetPropertyLogic.class);\n\n    private SetWidgetPropertyLogic() {\n    }\n\n    public static void messageReceived(Holder holder, ChannelHandlerContext ctx,\n                                       HardwareStateHolder state, StringMessage message) {\n        SessionDao sessionDao = holder.sessionDao;\n\n        String[] bodyParts = split3(message.body);\n\n        if (bodyParts.length != 3) {\n            log.debug(\"SetWidgetProperty command body has wrong format. {}\", message.body);\n            ctx.writeAndFlush(illegalCommandBody(message.id), ctx.voidPromise());\n            return;\n        }\n\n        String property = bodyParts[1];\n        String propertyValue = bodyParts[2];\n\n        if (property.length() == 0 || propertyValue.length() == 0) {\n            log.debug(\"SetWidgetProperty command body has wrong format. {}\", message.body);\n            ctx.writeAndFlush(illegalCommandBody(message.id), ctx.voidPromise());\n            return;\n        }\n\n        DashBoard dash = state.dash;\n\n        if (!dash.isActive) {\n            return;\n        }\n\n        WidgetProperty widgetProperty = WidgetProperty.getProperty(property);\n\n        if (widgetProperty == null) {\n            log.debug(\"Unsupported set property {}.\", property);\n            ctx.writeAndFlush(illegalCommandBody(message.id), ctx.voidPromise());\n            return;\n        }\n\n        int deviceId = state.device.id;\n        short pin = NumberUtil.parsePin(bodyParts[0]);\n\n        Widget widget = dash.updateProperty(deviceId, pin, widgetProperty, propertyValue);\n        //this is possible case for device selector\n        if (widget == null || widget instanceof DeviceTiles) {\n            state.user.profile.putPinPropertyStorageValue(dash,\n                    deviceId, PinType.VIRTUAL, pin, widgetProperty, propertyValue);\n        }\n\n        dash.updatedAt = System.currentTimeMillis();\n\n        Session session = sessionDao.get(state.userKey);\n        session.sendToApps(SET_WIDGET_PROPERTY, message.id, dash.id, deviceId, message.body);\n        ctx.writeAndFlush(ok(message.id), ctx.voidPromise());\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-hardware-server/src/main/java/cc/blynk/server/hardware/handlers/hardware/logic/SmsLogic.java",
    "content": "package cc.blynk.server.hardware.handlers.hardware.logic;\n\nimport cc.blynk.server.core.model.widgets.notifications.SMS;\nimport cc.blynk.server.core.processors.NotificationBase;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.server.core.session.HardwareStateHolder;\nimport cc.blynk.server.notifications.sms.SMSWrapper;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.notificationError;\nimport static cc.blynk.server.internal.CommonByteBufUtil.notificationInvalidBody;\nimport static cc.blynk.server.internal.CommonByteBufUtil.notificationNotAuthorized;\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\n\n/**\n * Sends tweets from hardware.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic class SmsLogic extends NotificationBase {\n\n    private static final Logger log = LogManager.getLogger(SmsLogic.class);\n\n    private static final int MAX_SMS_BODY_SIZE = 160;\n\n    private final SMSWrapper smsWrapper;\n\n    public SmsLogic(SMSWrapper smsWrapper, long notificationQuotaLimit) {\n        super(notificationQuotaLimit);\n        this.smsWrapper = smsWrapper;\n    }\n\n    public void messageReceived(ChannelHandlerContext ctx, HardwareStateHolder state, StringMessage message) {\n        if (message.body == null || message.body.isEmpty() || message.body.length() > MAX_SMS_BODY_SIZE) {\n            log.debug(\"Notification message is empty or larger than limit.\");\n            ctx.writeAndFlush(notificationInvalidBody(message.id), ctx.voidPromise());\n            return;\n        }\n\n        var dash = state.dash;\n        var smsWidget = dash.getWidgetByType(SMS.class);\n\n        if (smsWidget == null || !dash.isActive\n                || smsWidget.to == null || smsWidget.to.isEmpty()) {\n            log.debug(\"User has no access phone number provided.\");\n            ctx.writeAndFlush(notificationNotAuthorized(message.id), ctx.voidPromise());\n            return;\n        }\n\n        checkIfNotificationQuotaLimitIsNotReached();\n\n        log.trace(\"Sending sms for user {}, with message : '{}'.\", state.user.email, message.body);\n        sms(ctx.channel(), state.user.email, smsWidget.to, message.body, message.id);\n    }\n\n    private void sms(Channel channel, String email, String to, String body, int msgId) {\n        try {\n            smsWrapper.send(to, body);\n            channel.writeAndFlush(ok(msgId), channel.voidPromise());\n        } catch (Exception e) {\n            log.error(\"Error sending sms for user {}. Reason : {}\",  email, e.getMessage());\n            channel.writeAndFlush(notificationError(msgId), channel.voidPromise());\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-hardware-server/src/main/java/cc/blynk/server/hardware/handlers/hardware/logic/TwitLogic.java",
    "content": "package cc.blynk.server.hardware.handlers.hardware.logic;\n\nimport cc.blynk.server.core.model.widgets.notifications.Twitter;\nimport cc.blynk.server.core.processors.NotificationBase;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.server.core.session.HardwareStateHolder;\nimport cc.blynk.server.notifications.twitter.TwitterWrapper;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.handler.codec.http.HttpResponseStatus;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\nimport org.asynchttpclient.AsyncCompletionHandler;\nimport org.asynchttpclient.Response;\n\nimport static cc.blynk.server.internal.CommonByteBufUtil.notificationError;\nimport static cc.blynk.server.internal.CommonByteBufUtil.notificationInvalidBody;\nimport static cc.blynk.server.internal.CommonByteBufUtil.notificationNotAuthorized;\nimport static cc.blynk.server.internal.CommonByteBufUtil.ok;\n\n/**\n * Sends tweets from hardware.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic class TwitLogic extends NotificationBase {\n\n    private static final Logger log = LogManager.getLogger(TwitLogic.class);\n\n    private final TwitterWrapper twitterWrapper;\n\n    public TwitLogic(TwitterWrapper twitterWrapper, long notificationQuotaLimit) {\n        super(notificationQuotaLimit);\n        this.twitterWrapper = twitterWrapper;\n    }\n\n    public void messageReceived(ChannelHandlerContext ctx, HardwareStateHolder state, StringMessage message) {\n        if (Twitter.isWrongBody(message.body)) {\n            log.debug(\"Notification message is empty or larger than limit.\");\n            ctx.writeAndFlush(notificationInvalidBody(message.id), ctx.voidPromise());\n            return;\n        }\n\n        var dash = state.dash;\n        var twitterWidget = dash.getTwitterWidget();\n\n        if (twitterWidget == null || !dash.isActive\n                || twitterWidget.token == null || twitterWidget.token.isEmpty()\n                || twitterWidget.secret == null || twitterWidget.secret.isEmpty()) {\n            log.debug(\"User has no access token provided for twit widget.\");\n            ctx.writeAndFlush(notificationNotAuthorized(message.id), ctx.voidPromise());\n            return;\n        }\n\n        checkIfNotificationQuotaLimitIsNotReached();\n\n        log.trace(\"Sending Twit for user {}, with message : '{}'.\", state.user.email, message.body);\n        twit(ctx.channel(), state.user.email, twitterWidget.token, twitterWidget.secret, message.body, message.id);\n    }\n\n    private void twit(Channel channel, String email, String token, String secret, String body, int msgId) {\n        twitterWrapper.send(token, secret, body,\n                new AsyncCompletionHandler<>() {\n                    @Override\n                    public Response onCompleted(Response response) {\n                        if (response.getStatusCode() == HttpResponseStatus.OK.code()) {\n                            channel.writeAndFlush(ok(msgId), channel.voidPromise());\n                        }\n                        return response;\n                    }\n\n                    @Override\n                    public void onThrowable(Throwable t) {\n                        logError(t.getMessage(), email);\n                        channel.writeAndFlush(notificationError(msgId), channel.voidPromise());\n                    }\n                }\n        );\n    }\n\n    private static void logError(String errorMessage, String email) {\n        if (errorMessage != null) {\n            if (errorMessage.contains(\"Status is a duplicate\")) {\n                log.warn(\"Duplicate twit status for user {}.\", email);\n            } else if (errorMessage.contains(\"Authentication credentials\")) {\n                log.warn(\"Tweet authentication failure for {}.\", email);\n            } else if (errorMessage.contains(\"The request is understood, but it has been refused.\")) {\n                log.warn(\"User twit account is banned by twitter. {}.\", email);\n            } else {\n                log.error(\"Error sending twit for user {}. Reason : {}\", email, errorMessage);\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-hardware-server/src/main/java/cc/blynk/server/hardware/handlers/hardware/mqtt/auth/MqttHardwareLoginHandler.java",
    "content": "package cc.blynk.server.hardware.handlers.hardware.mqtt.auth;\n\nimport cc.blynk.server.Holder;\nimport cc.blynk.server.core.dao.TokenValue;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.Session;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.protocol.handlers.DefaultExceptionHandler;\nimport cc.blynk.server.core.session.HardwareStateHolder;\nimport cc.blynk.server.hardware.handlers.hardware.MqttHardwareHandler;\nimport cc.blynk.server.internal.ReregisterChannelUtil;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelHandler;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.ChannelPipeline;\nimport io.netty.channel.SimpleChannelInboundHandler;\nimport io.netty.handler.codec.mqtt.MqttConnAckMessage;\nimport io.netty.handler.codec.mqtt.MqttConnAckVariableHeader;\nimport io.netty.handler.codec.mqtt.MqttConnectMessage;\nimport io.netty.handler.codec.mqtt.MqttConnectPayload;\nimport io.netty.handler.codec.mqtt.MqttConnectReturnCode;\nimport io.netty.handler.codec.mqtt.MqttFixedHeader;\nimport io.netty.handler.codec.mqtt.MqttMessageType;\nimport io.netty.handler.codec.mqtt.MqttQoS;\nimport io.netty.util.CharsetUtil;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE_CONNECTED;\nimport static cc.blynk.utils.StringUtils.DEVICE_SEPARATOR;\nimport static io.netty.handler.codec.mqtt.MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD;\n\n/**\n * Handler responsible for managing hardware and apps login messages.\n * Initializes netty channel with a state tied with user.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\n@ChannelHandler.Sharable\npublic class MqttHardwareLoginHandler extends SimpleChannelInboundHandler<MqttConnectMessage> {\n\n    private static final Logger log = LogManager.getLogger(MqttHardwareLoginHandler.class);\n\n    private static final MqttConnAckMessage ACCEPTED = createConnAckMessage(MqttConnectReturnCode.CONNECTION_ACCEPTED);\n\n    private final Holder holder;\n\n    public MqttHardwareLoginHandler(Holder holder) {\n        this.holder = holder;\n    }\n\n    private static void completeLogin(Channel channel, Session session, User user,\n                                      DashBoard dash, Device device, int msgId) {\n        log.debug(\"completeLogin. {}\", channel);\n\n        session.addHardChannel(channel);\n        channel.writeAndFlush(ACCEPTED);\n\n        String responseBody = String.valueOf(dash.id) + DEVICE_SEPARATOR + device.id;\n        session.sendToApps(HARDWARE_CONNECTED, msgId, dash.id, responseBody);\n\n        log.info(\"{} mqtt hardware joined.\", user.email);\n    }\n\n    private static MqttConnAckMessage createConnAckMessage(MqttConnectReturnCode code) {\n        MqttFixedHeader mqttFixedHeader =\n                new MqttFixedHeader(MqttMessageType.CONNACK, false, MqttQoS.AT_MOST_ONCE, false, 2);\n        MqttConnAckVariableHeader mqttConnAckVariableHeader = new MqttConnAckVariableHeader(code, true);\n        return new MqttConnAckMessage(mqttFixedHeader, mqttConnAckVariableHeader);\n    }\n\n    @Override\n    protected void channelRead0(ChannelHandlerContext ctx, MqttConnectMessage message) {\n        MqttConnectPayload mqttConnectPayload = message.payload();\n        if (mqttConnectPayload == null) {\n            ctx.writeAndFlush(createConnAckMessage(CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD), ctx.voidPromise());\n            return;\n        }\n\n        String username = mqttConnectPayload.userName();\n        if (username == null) {\n            ctx.writeAndFlush(createConnAckMessage(CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD), ctx.voidPromise());\n            return;\n        }\n\n        username = username.trim().toLowerCase();\n        byte[] password = mqttConnectPayload.passwordInBytes();\n        if (password == null) {\n            ctx.writeAndFlush(createConnAckMessage(CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD), ctx.voidPromise());\n            return;\n        }\n\n        String token = new String(password, CharsetUtil.UTF_8);\n\n        TokenValue tokenValue = holder.tokenManager.getTokenValueByToken(token);\n\n        if (tokenValue == null || !tokenValue.user.email.equalsIgnoreCase(username)) {\n            log.debug(\"MqttHardwareLogic token is invalid. Token '{}', '{}'\", token, ctx.channel().remoteAddress());\n            ctx.writeAndFlush(createConnAckMessage(CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD), ctx.voidPromise());\n            return;\n        }\n\n        User user = tokenValue.user;\n        Device device = tokenValue.device;\n        DashBoard dash = tokenValue.dash;\n\n        ChannelPipeline pipeline = ctx.pipeline();\n        HardwareStateHolder hardwareStateHolder = new HardwareStateHolder(user, tokenValue.dash, device);\n        pipeline.replace(this, \"HHArdwareMqttHandler\", new MqttHardwareHandler(holder, hardwareStateHolder));\n\n        Session session = holder.sessionDao.getOrCreateSessionByUser(\n                hardwareStateHolder.userKey, ctx.channel().eventLoop());\n\n        if (session.isSameEventLoop(ctx)) {\n            completeLogin(ctx.channel(), session, user, dash, device, -1);\n        } else {\n            log.debug(\"Re registering hard channel. {}\", ctx.channel());\n            ReregisterChannelUtil.reRegisterChannel(ctx, session, channelFuture ->\n                    completeLogin(channelFuture.channel(), session, user, dash, device, -1));\n        }\n    }\n\n    @Override\n    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {\n        DefaultExceptionHandler.handleGeneralException(ctx, cause);\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-hardware-server/src/main/java/cc/blynk/server/hardware/handlers/hardware/mqtt/logic/MqttHardwareLogic.java",
    "content": "package cc.blynk.server.hardware.handlers.hardware.mqtt.logic;\n\nimport cc.blynk.server.core.dao.ReportingDiskDao;\nimport cc.blynk.server.core.dao.SessionDao;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.Session;\nimport cc.blynk.server.core.model.enums.PinType;\nimport cc.blynk.server.core.session.HardwareStateHolder;\nimport cc.blynk.utils.NumberUtil;\nimport io.netty.handler.codec.mqtt.MqttPublishMessage;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.nio.charset.StandardCharsets;\n\nimport static cc.blynk.server.core.protocol.enums.Command.HARDWARE;\nimport static cc.blynk.utils.StringUtils.split3;\n\n/**\n * Handler responsible for forwarding messages from hardware to applications.\n * Also handler stores all incoming hardware commands to disk in order to export and\n * analyze data.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/1/2015.\n *\n */\npublic class MqttHardwareLogic {\n\n    private static final Logger log = LogManager.getLogger(MqttHardwareLogic.class);\n\n    private final ReportingDiskDao reportingDao;\n    private final SessionDao sessionDao;\n\n    public MqttHardwareLogic(SessionDao sessionDao, ReportingDiskDao reportingDao) {\n        this.sessionDao = sessionDao;\n        this.reportingDao = reportingDao;\n    }\n\n    private static boolean isWriteOperation(String body) {\n        return body.charAt(1) == 'w';\n    }\n\n    public void messageReceived(HardwareStateHolder state, MqttPublishMessage msg) {\n        Session session = sessionDao.get(state.userKey);\n\n        String body = msg.payload().readSlice(msg.payload().capacity()).toString(StandardCharsets.UTF_8);\n\n        //just temp solution to simplify demo\n        body = body.replace(\" \", \"\\0\").replace(\" \", \"\\0\");\n\n        //minimum command - \"aw 1\"\n        if (body.length() < 4) {\n            //throw new IllegalCommandException(\"HardwareLogic command body too short.\");\n            return;\n        }\n\n        int dashId = state.dash.id;\n        int deviceId = state.device.id;\n\n        DashBoard dash = state.dash;\n\n        if (isWriteOperation(body)) {\n            //\" |\\0\" - to simplify demonstration\n            String[] splitBody = split3(body);\n\n            if (splitBody.length < 3 || splitBody[0].length() == 0) {\n                //throw new IllegalCommandException(\"Write command is wrong.\");\n                return;\n            }\n\n            PinType pinType = PinType.getPinType(splitBody[0].charAt(0));\n            short pin = NumberUtil.parsePin(splitBody[1]);\n            String value = splitBody[2];\n\n            if (value.length() == 0) {\n                //throw new IllegalCommandException(\"Hardware write command doesn't have value for pin.\");\n                return;\n            }\n\n            long now = System.currentTimeMillis();\n\n            reportingDao.process(state.user, dash, deviceId, pin, pinType, value, now);\n\n            state.user.profile.update(dash, 0, pin, pinType, value, now);\n        }\n\n        if (dash.isActive) {\n            session.sendToApps(HARDWARE, msg.variableHeader().packetId(), dashId, deviceId, body);\n        } else {\n            log.debug(\"No active dashboard.\");\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/tcp-hardware-server/src/main/java/cc/blynk/server/hardware/internal/BridgeForwardMessage.java",
    "content": "package cc.blynk.server.hardware.internal;\n\nimport cc.blynk.server.core.dao.TokenValue;\nimport cc.blynk.server.core.dao.UserKey;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 01.02.18.\n */\npublic class BridgeForwardMessage {\n\n    public final StringMessage message;\n\n    public final TokenValue tokenValue;\n\n    public final UserKey userKey;\n\n    public BridgeForwardMessage(StringMessage bridgeMessage, TokenValue tokenValue, UserKey userKey) {\n        this.message = bridgeMessage;\n        this.tokenValue = tokenValue;\n        this.userKey = userKey;\n    }\n}\n\n"
  },
  {
    "path": "server/tcp-hardware-server/src/test/java/cc/blynk/server/hardware/handlers/TwitHandlerTest.java",
    "content": "package cc.blynk.server.hardware.handlers;\n\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.Profile;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.core.model.widgets.notifications.Twitter;\nimport cc.blynk.server.core.protocol.enums.Command;\nimport cc.blynk.server.core.protocol.exceptions.QuotaLimitException;\nimport cc.blynk.server.core.protocol.model.messages.MessageFactory;\nimport cc.blynk.server.core.protocol.model.messages.StringMessage;\nimport cc.blynk.server.core.session.HardwareStateHolder;\nimport cc.blynk.server.hardware.handlers.hardware.logic.TwitLogic;\nimport cc.blynk.server.notifications.twitter.TwitterWrapper;\nimport cc.blynk.utils.properties.ServerProperties;\nimport io.netty.channel.ChannelHandlerContext;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport java.util.Collections;\nimport java.util.concurrent.TimeUnit;\n\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.when;\n\n/**\n * The Blynk Project.\n * Created by Andrew Zakordonets.\n * Created on 26.04.15.\n */\n@RunWith(MockitoJUnitRunner.Silent.class)\npublic class TwitHandlerTest {\n\n\t@Mock\n\tprivate TwitterWrapper twitterWrapper;\n\n\t@Mock\n\tprivate ChannelHandlerContext ctx;\n\n\t@Mock\n\tprivate User user;\n\n\t@Mock\n\tprivate Profile profile;\n\n\t@Mock\n\tprivate DashBoard dash;\n\n\t@Mock\n\tprivate Device device;\n\n    private HardwareStateHolder state;\n\n    @Before\n    public void setup() {\n\t\tstate = new HardwareStateHolder(user, dash, device);\n    }\n\n\t@Test(expected = QuotaLimitException.class)\n\tpublic void testSendQuotaLimitationException() {\n\t\tStringMessage twitMessage = (StringMessage) MessageFactory.produce(1, Command.TWEET, \"this is a test tweet\");\n\t\tTwitLogic tweetHandler = spy(new TwitLogic(twitterWrapper, 60));\n        state.user.profile = profile;\n\t\tTwitter twitter = new Twitter();\n\t\ttwitter.token = \"token\";\n\t\ttwitter.secret = \"secret_token\";\n\t\twhen(state.user.profile.getDashByIdOrThrow(1)).thenReturn(dash);\n\t\twhen(dash.getTwitterWidget()).thenReturn(twitter);\n\t\tdash.isActive = true;\n\n\t\ttweetHandler.messageReceived(ctx, state, twitMessage);\n\t\ttweetHandler.messageReceived(ctx, state, twitMessage);\n\t}\n\n\t@Test\n\tpublic void testSendQuotaLimitationIsWorking() throws InterruptedException {\n\t\tStringMessage twitMessage = (StringMessage) MessageFactory.produce(1, Command.TWEET, \"this is a test tweet\");\n\t\tServerProperties props = new ServerProperties(Collections.emptyMap());\n\t\tprops.setProperty(\"notifications.frequency.user.quota.limit\", \"1\");\n\t\tfinal long defaultQuotaTime = props.getLongProperty(\"notifications.frequency.user.quota.limit\") * 1000;\n\t\tTwitLogic tweetHandler = spy(new TwitLogic(twitterWrapper, 60));\n\t\tstate.user.profile = profile;\n\t\tTwitter twitter = new Twitter();\n\t\ttwitter.token = \"token\";\n\t\ttwitter.secret = \"secret_token\";\n\t\twhen(state.user.profile.getDashByIdOrThrow(1)).thenReturn(dash);\n\t\twhen(dash.getTwitterWidget()).thenReturn(twitter);\n\t\tdash.isActive = true;\n\n\t\ttweetHandler.messageReceived(ctx, state, twitMessage);\n\t\tTimeUnit.MILLISECONDS.sleep(defaultQuotaTime);\n\t\ttweetHandler.messageReceived(ctx, state, twitMessage);\n\t}\n\n}\n"
  },
  {
    "path": "server/tcp-web-server/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <artifactId>server</artifactId>\n        <groupId>cc.blynk.server</groupId>\n        <version>0.41.17-SNAPSHOT</version>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n\n    <groupId>cc.blynk.server.web</groupId>\n    <artifactId>tcp-web-server</artifactId>\n\n    <dependencies>\n\n        <dependency>\n            <groupId>cc.blynk.server</groupId>\n            <artifactId>core</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n\n    </dependencies>\n</project>"
  },
  {
    "path": "server/tools/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <artifactId>server</artifactId>\n        <groupId>cc.blynk.server</groupId>\n        <version>0.41.17-SNAPSHOT</version>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n\n    <groupId>cc.blynk.server.tools</groupId>\n    <artifactId>tools</artifactId>\n\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-shade-plugin</artifactId>\n                <version>${maven-shade-plugin.version}</version>\n                <executions>\n                    <execution>\n                        <phase>package</phase>\n                        <goals>\n                            <goal>shade</goal>\n                        </goals>\n                        <configuration>\n                            <finalName>tools-${project.version}</finalName>\n                            <transformers>\n                                <transformer implementation=\"org.apache.maven.plugins.shade.resource.ManifestResourceTransformer\">\n                                    <manifestEntries>\n                                        <Main-Class>cc.blynk.server.tools.ReportingDataCleaner</Main-Class>\n                                        <Build-Number>${project.version}</Build-Number>\n                                        <Build-By>Blynk Inc.</Build-By>\n                                    </manifestEntries>\n                                </transformer>\n                            </transformers>\n                            <filters>\n                                <filter>\n                                    <artifact>*:*</artifact>\n                                    <excludes>\n                                        <exclude>META-INF/maven/**</exclude>\n                                    </excludes>\n                                </filter>\n                                <filter>\n                                    <artifact>*:*</artifact>\n                                    <excludes>\n                                        <exclude>META-INF/*.SF</exclude>\n                                        <exclude>META-INF/*.DSA</exclude>\n                                        <exclude>META-INF/*.RSA</exclude>\n                                    </excludes>\n                                </filter>\n                            </filters>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n\n    <dependencies>\n        <dependency>\n            <groupId>cc.blynk.server</groupId>\n            <artifactId>core</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n    </dependencies>\n</project>"
  },
  {
    "path": "server/tools/src/main/java/cc/blynk/server/tools/ForwardingTokenGenerator.java",
    "content": "package cc.blynk.server.tools;\n\nimport cc.blynk.server.core.BlockingIOProcessor;\nimport cc.blynk.server.core.dao.UserKey;\nimport cc.blynk.server.core.model.DashBoard;\nimport cc.blynk.server.core.model.auth.User;\nimport cc.blynk.server.core.model.device.Device;\nimport cc.blynk.server.db.DBManager;\nimport cc.blynk.server.db.dao.ForwardingTokenEntry;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.ConcurrentMap;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 17.11.17.\n */\npublic final class ForwardingTokenGenerator {\n\n    private ForwardingTokenGenerator() {\n\n    }\n\n    public static void main(String[] args) throws Exception {\n        BlockingIOProcessor blockingIOProcessor = new BlockingIOProcessor(6, 100);\n\n        try (DBManager dbManager = new DBManager(blockingIOProcessor, true)) {\n            process(dbManager, \"singapore\", \"188.166.206.43\");\n            process(dbManager, \"frankfurt\", \"139.59.206.133\");\n            process(dbManager, \"ny3\", \"45.55.96.146\");\n        }\n    }\n\n    private static void process(DBManager dbManager, String region, String ip) throws Exception {\n        System.out.println(\"Reading all users from region : \" + region + \". Forward to \" + ip);\n        ConcurrentMap<UserKey, User> users = dbManager.userDBDao.getAllUsers(region);\n        System.out.println(\"Read \" + users.size() + \" users.\");\n        int count = 0;\n        List<ForwardingTokenEntry> entryList = new ArrayList<>(1100);\n        for (User user : users.values()) {\n            for (DashBoard dashBoard : user.profile.dashBoards) {\n                for (Device device : dashBoard.devices) {\n                    if (device != null && device.token != null) {\n                        count++;\n                        entryList.add(new ForwardingTokenEntry(device.token, ip, user.email, dashBoard.id, device.id));\n                    }\n                }\n            }\n            if (entryList.size() > 1000) {\n                dbManager.forwardingTokenDBDao.insertTokenHostBatch(entryList);\n                System.out.println(entryList.size() + \" tokens inserted.\");\n                entryList = new ArrayList<>(1100);\n            }\n        }\n\n        if (entryList.size() > 0) {\n            dbManager.forwardingTokenDBDao.insertTokenHostBatch(entryList);\n            System.out.println(entryList.size() + \" tokens inserted.\");\n        }\n\n        System.out.println(\"Total entries : \" + count);\n    }\n\n}\n"
  },
  {
    "path": "server/tools/src/main/java/cc/blynk/server/tools/ReportingDataCleaner.java",
    "content": "package cc.blynk.server.tools;\n\nimport cc.blynk.utils.FileUtils;\n\nimport java.io.File;\nimport java.io.FileOutputStream;\nimport java.nio.ByteBuffer;\nimport java.nio.channels.FileChannel;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 05.02.17.\n */\npublic final class ReportingDataCleaner {\n\n    private ReportingDataCleaner() {\n    }\n\n    public static void main(String[] args) {\n        String reportingFolder = args[0];\n        Path reportingPath = Paths.get(reportingFolder);\n        if (Files.exists(reportingPath)) {\n            System.out.println(\"Starting processing \" + reportingPath.toString());\n            start(reportingPath);\n        } else {\n            System.out.println(reportingPath.toString() + \" not exists.\");\n        }\n    }\n\n    private static void start(Path reportingPath) {\n        File[] allReporting = reportingPath.toFile().listFiles();\n        if (allReporting == null || allReporting.length == 0) {\n            System.out.println(\"No files.\");\n            return;\n        }\n\n        System.out.println(\"Directories number : \" + allReporting.length);\n\n        int count = 24 * 60 * 7; //1 storing minute points only for 1 week\n        int filesCount = 0;\n        int overrideCount = 0;\n\n        for (File userDirectory : allReporting) {\n            if (userDirectory.isDirectory()) {\n                File[] userFiles = userDirectory.listFiles();\n                if (userFiles == null || userFiles.length == 0) {\n                    System.out.println(userDirectory + \" is empty.\");\n                    try {\n                        if (userDirectory.delete()) {\n                            System.out.println(userDirectory + \" deleted.\");\n                        }\n                    } catch (Exception e) {\n                        //ignore\n                    }\n                    continue;\n                }\n                for (File file : userFiles) {\n                    if (filesCount != 0 && filesCount % 1000 == 0) {\n                        System.out.println(\"Visited \" + filesCount + \" files.\");\n                    }\n                    long fileSize = file.length();\n                    if (file.getPath().endsWith(\"minute.bin\") && fileSize > count * 16) {\n                        System.out.println(\"Found \" + file.getPath() + \". Size : \" + fileSize);\n                        try {\n                            Path path = file.toPath();\n                            ByteBuffer userReportingData = FileUtils.read(path, count);\n                            write(file, userReportingData);\n                            System.out.println(\"Successfully copied. Truncated : \"\n                                    + (fileSize - userReportingData.position()));\n                            overrideCount++;\n                        } catch (Exception e) {\n                            System.out.println(\"Error reading file \" + file.getAbsolutePath());\n                            System.out.println(\"Skipping.\");\n                        }\n                    }\n\n                    filesCount++;\n                }\n            }\n        }\n\n        System.out.println(\"Visited : \" + filesCount + \". Overrided : \" + overrideCount);\n    }\n\n    private static void write(File file, ByteBuffer data) throws Exception {\n        try (FileChannel channel = new FileOutputStream(file, false).getChannel()) {\n            channel.write(data);\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/tools/src/test/java/cc/blynk/server/tools/ReportingDataCleanerTest.java",
    "content": "package cc.blynk.server.tools;\n\nimport cc.blynk.utils.FileUtils;\nimport org.junit.Before;\nimport org.junit.Test;\n\nimport java.io.File;\nimport java.nio.ByteBuffer;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\n\nimport static org.junit.Assert.assertEquals;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 05.02.17.\n */\npublic class ReportingDataCleanerTest {\n\n    private static Path reportingPath = Paths.get(System.getProperty(\"java.io.tmpdir\"), \"test_reporting\");\n    private static Path userPath = Paths.get(System.getProperty(\"java.io.tmpdir\"), \"test_reporting/test@test.gmail.com\");\n\n    @Before\n    public void init() throws Exception {\n        deleteDirectory(userPath.toFile());\n        Files.createDirectories(userPath);\n    }\n\n    @Test\n    public void testDoNotOverrideSmallFile() throws Exception {\n        Path userFile = Paths.get(userPath.toString(), \"123_minute.bin\");\n        int count = 1;\n        fillWithData(userFile, count);\n\n        ReportingDataCleaner.main(new String[] {reportingPath.toString()});\n\n        assertEquals(16 * count, Files.size(userFile));\n    }\n\n    @Test\n    public void testDoNotOverrideSmallFile2() throws Exception {\n        Path userFile = Paths.get(userPath.toString(), \"123_minute.bin\");\n        int count = 360;\n        fillWithData(userFile, count);\n\n        ReportingDataCleaner.main(new String[] {reportingPath.toString()});\n\n        assertEquals(16 * count, Files.size(userFile));\n    }\n\n    @Test\n    public void testOverrideCorrectFlowAndCheckContent() throws Exception {\n        Path userFile = Paths.get(userPath.toString(), \"123_minute.bin\");\n        int count = 360;\n        fillWithData(userFile, count);\n\n        ReportingDataCleaner.main(new String[] {reportingPath.toString()});\n\n        assertEquals(16 * 360, Files.size(userFile));\n\n        ByteBuffer userReportingData = FileUtils.read(Paths.get(userPath.toString(), \"123_minute.bin\"), 360);\n        for (int i = 0; i < 360; i++) {\n            double value = userReportingData.getDouble();\n            long ts = userReportingData.getLong();\n            assertEquals(i, (int) value);\n            assertEquals(ts, i);\n        }\n    }\n\n    @Test\n    public void testOverrideCorrectFlowAndCheckContent2() throws Exception {\n        Path userFile = Paths.get(userPath.toString(), \"123_minute.bin\");\n        int count = 720;\n        fillWithData(userFile, count);\n\n        ReportingDataCleaner.main(new String[] {reportingPath.toString()});\n\n        assertEquals(11520, Files.size(userFile));\n\n        ByteBuffer userReportingData = FileUtils.read(Paths.get(userPath.toString(), \"123_minute.bin\"), 360);\n        for (int i = 360; i < 720; i++) {\n            double value = userReportingData.getDouble();\n            long ts = userReportingData.getLong();\n            assertEquals(i, (int) value);\n            assertEquals(ts, i);\n        }\n    }\n\n\n    private static void fillWithData(Path path, int count) throws Exception {\n        for (int i = 0; i < count; i++) {\n            FileUtils.write(path, (double) i, (long) i);\n        }\n    }\n\n    public static boolean deleteDirectory(File dir) {\n        if (dir.isDirectory()) {\n            File[] children = dir.listFiles();\n\n            if (children != null) {\n                for (File aChildren : children) {\n                    boolean success = deleteDirectory(aChildren);\n                    if (!success) {\n                        return false;\n                    }\n                }\n            }\n        }\n\n        return dir.delete();\n    }\n\n}\n"
  },
  {
    "path": "server/utils/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <artifactId>server</artifactId>\n        <groupId>cc.blynk.server</groupId>\n        <version>0.41.17-SNAPSHOT</version>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n\n    <groupId>cc.blynk.utils</groupId>\n    <artifactId>utils</artifactId>\n\n</project>"
  },
  {
    "path": "server/utils/src/main/java/cc/blynk/utils/AppNameUtil.java",
    "content": "package cc.blynk.utils;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 5/17/16.\n */\npublic final class AppNameUtil {\n\n    private AppNameUtil() {\n    }\n\n    public static final String BLYNK = \"Blynk\";\n    public static final String FACEBOOK = \"facebook\";\n\n    public static String generateAppId() {\n        return AppNameUtil.BLYNK.toLowerCase() + StringUtils.randomString(8);\n    }\n\n}\n"
  },
  {
    "path": "server/utils/src/main/java/cc/blynk/utils/ArrayUtil.java",
    "content": "package cc.blynk.utils;\n\nimport java.lang.reflect.Array;\nimport java.util.Arrays;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 04.01.16.\n */\npublic final class ArrayUtil {\n\n    private static final int[] EMPTY_INTS = {};\n\n    private ArrayUtil() {\n    }\n\n    public static <T> T[] add(T[] array, T element, Class<T> type) {\n        T[] newArray = copyArrayGrow1(array, type);\n        newArray[newArray.length - 1] = element;\n        return newArray;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static <T> T[] copyArrayGrow1(final T[] array, Class<T> type) {\n        T[] newArray = (T[]) Array.newInstance(type, array.length + 1);\n        System.arraycopy(array, 0, newArray, 0, array.length);\n        return newArray;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static <T> T[] remove(final T[] array, final int index, Class<T> type) {\n        T[] result = (T[]) Array.newInstance(type, array.length - 1);\n        System.arraycopy(array, 0, result, 0, index);\n        if (index < array.length - 1) {\n            System.arraycopy(array, index + 1, result, index, array.length - index - 1);\n        }\n\n        return result;\n    }\n\n    public static <T> T[] copyAndReplace(T[] array, T element, int index) {\n        T[] newArray = Arrays.copyOf(array, array.length);\n        newArray[index] = element;\n        return newArray;\n    }\n\n    public static int getIndexByVal(int[] array, int val) {\n        for (int i = 0; i < array.length; i++) {\n            if (array[i] == val) {\n                return i;\n            }\n        }\n        return -1;\n    }\n\n    public static int[] deleteFromArray(int[] ids, int id) {\n        int index = getIndexByVal(ids, id);\n        if (index == -1) {\n            return ids;\n        }\n        return ids.length == 1 ? EMPTY_INTS : remove(ids, index);\n    }\n\n    public static int[] remove(int[] array, int index) {\n        int[] result = new int[array.length - 1];\n        System.arraycopy(array, 0, result, 0, index);\n        if (index < array.length - 1) {\n            System.arraycopy(array, index + 1, result, index, array.length - index - 1);\n        }\n\n        return result;\n    }\n\n    public static boolean contains(final int[] ar, final int val) {\n        for (int arVal : ar) {\n            if (arVal == val) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n}\n"
  },
  {
    "path": "server/utils/src/main/java/cc/blynk/utils/BlynkTPFactory.java",
    "content": "package cc.blynk.utils;\n\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.ThreadFactory;\n\npublic final class BlynkTPFactory implements ThreadFactory  {\n\n    private final ThreadFactory defaultThreadFactory;\n    private final String name;\n\n    private BlynkTPFactory(String name) {\n        this.defaultThreadFactory = Executors.defaultThreadFactory();\n        this.name = name;\n    }\n\n    public static ThreadFactory build(String name) {\n        return new BlynkTPFactory(name);\n    }\n\n    @Override\n    public Thread newThread(Runnable r) {\n        final Thread thread = defaultThreadFactory.newThread(r);\n        thread.setName(name + \"-\" + thread.getName());\n        return thread;\n    }\n}\n"
  },
  {
    "path": "server/utils/src/main/java/cc/blynk/utils/ByteUtils.java",
    "content": "package cc.blynk.utils;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.nio.ByteBuffer;\nimport java.nio.charset.StandardCharsets;\nimport java.util.zip.DeflaterOutputStream;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 07.09.15.\n */\npublic final class ByteUtils {\n\n    public static final int REPORTING_RECORD_SIZE_BYTES = 16;\n\n    private ByteUtils() {\n    }\n\n    public static byte[] compress(String value) throws IOException {\n        byte[] stringData = value.getBytes(StandardCharsets.UTF_8);\n        var baos = new ByteArrayOutputStream(stringData.length);\n\n        try (var out = new DeflaterOutputStream(baos)) {\n            out.write(stringData);\n        }\n\n        return baos.toByteArray();\n    }\n\n    public static byte[] compress(byte[][] values) throws IOException {\n        var baos = new ByteArrayOutputStream(8192);\n\n        try (var out = new DeflaterOutputStream(baos)) {\n            for (var data : values) {\n                var bb = ByteBuffer.allocate(4);\n                bb.putInt(data.length / REPORTING_RECORD_SIZE_BYTES);\n                out.write(bb.array());\n                out.write(data);\n            }\n        }\n\n        return baos.toByteArray();\n    }\n\n    public static byte[] compress(int dashId, byte[][] values) throws IOException {\n        var baos = new ByteArrayOutputStream(8192);\n\n        try (var out = new DeflaterOutputStream(baos)) {\n            writeInt(out, dashId);\n            for (var data : values) {\n                writeInt(out, data.length / REPORTING_RECORD_SIZE_BYTES);\n                out.write(data);\n            }\n        }\n        return baos.toByteArray();\n    }\n\n    private static void writeInt(OutputStream out, int value) throws IOException {\n        out.write((value >>> 24) & 0xFF);\n        out.write((value >>> 16) & 0xFF);\n        out.write((value >>>  8) & 0xFF);\n        out.write((value) & 0xFF);\n    }\n\n    public static int parseColor(String fieldValue) {\n        var decodedColor = Integer.decode(fieldValue);\n        return convertARGBtoRGBA(setAlphaComponent(decodedColor, 255));\n    }\n\n    private static int convertARGBtoRGBA(int color) {\n        var a = (color & 0xff000000) >> 24;\n        var r = (color & 0x00ff0000) >> 16;\n        var g = (color & 0x0000ff00) >> 8;\n        var b = (color & 0x000000ff);\n\n        return (r << 24) | (g << 16) | (b << 8) | (a & 0xff);\n    }\n\n    private static int setAlphaComponent(int color, int alpha) {\n        return (color & 0x00ffffff) | (alpha << 24);\n    }\n}\n"
  },
  {
    "path": "server/utils/src/main/java/cc/blynk/utils/DateTimeUtils.java",
    "content": "package cc.blynk.utils;\n\nimport java.time.ZoneId;\nimport java.util.Calendar;\nimport java.util.TimeZone;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 04.09.16.\n */\npublic final class DateTimeUtils {\n\n    private DateTimeUtils() {\n    }\n\n    public static final ZoneId UTC = ZoneId.of(\"UTC\");\n    public static final ZoneId AMERICA_REGINA = ZoneId.of(\"America/Regina\");\n    public static final ZoneId ASIA_HO_CHI = ZoneId.of(\"Asia/Ho_Chi_Minh\");\n\n    public static final Calendar UTC_CALENDAR = Calendar.getInstance(TimeZone.getTimeZone(\"UTC\"));\n\n}\n"
  },
  {
    "path": "server/utils/src/main/java/cc/blynk/utils/FileLoaderUtil.java",
    "content": "package cc.blynk.utils;\n\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.List;\n\n/**\n * Simply reads full file from disk as String\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/12/2015.\n */\npublic final class FileLoaderUtil {\n\n    public static final String TOKEN_MAIL_BODY = \"single_token_mail_body.txt\";\n\n    private static final String NEW_LINE = System.getProperty(\"line.separator\");\n\n    private FileLoaderUtil() {\n    }\n\n    private static Path getFileInCurrentDir(String filename) {\n        return Paths.get(System.getProperty(\"user.dir\"), filename);\n    }\n\n    public static String readTokenMailBody() {\n        return readFileAsString(TOKEN_MAIL_BODY);\n    }\n\n    public static String readDynamicMailBody() {\n        return readFileAsString(\"dynamic_provisioning_mail.html\");\n    }\n\n    public static String readStaticMailBody() {\n        return readFileAsString(\"static_provisioning_mail.html\");\n    }\n\n    public static String readTemplateIdMailBody() {\n        return readFileAsString(\"template_id_mail.html\");\n    }\n\n    public static String readResetEmailTemplateAsString() {\n        return readFileAsString(\"static/reset/reset-email.html\");\n    }\n\n    public static String readAppResetEmailConfirmationTemplateAsString() {\n        return readFileAsString(\"static/reset/reset-email-app-confirmation.html\");\n    }\n\n    public static String readAppResetEmailTemplateAsString() {\n        return readFileAsString(\"static/reset/reset-email-app.html\");\n    }\n\n    public static String readResetPassLandingTemplateAsString() {\n        return readFileAsString(\"static/reset/enterNewPassword.html\");\n    }\n\n    public static String readRegisterEmailTemplate() {\n        return readFileAsString(\"static/register-email.html\");\n    }\n\n    public static String readReportEmailTemplate() {\n        return readFileAsString(\"static/report-email.html\");\n    }\n\n    /**\n     * First loads file from class path after that from current folder.\n     * So file in current folder is always overrides properties in classpath.\n     *\n     * @param fileName - name of properties file, for example \"twitter4j.properties\"\n     */\n    private static String readFileAsString(String fileName) {\n        if (!fileName.startsWith(\"/\")) {\n            fileName = \"/\" + fileName;\n        }\n\n        String result = null;\n\n        try (InputStream classPath = FileLoaderUtil.class.getResourceAsStream(fileName)) {\n            if (classPath != null) {\n                result = load(classPath);\n            }\n        } catch (Exception e) {\n            throw new RuntimeException(\"Error getting properties file : \" + fileName, e);\n        }\n\n        Path curDirPath = getFileInCurrentDir(fileName);\n        if (Files.exists(curDirPath)) {\n            try {\n                List<String> stringList = Files.readAllLines(curDirPath);\n                StringBuilder responseData = new StringBuilder();\n                for (String s : stringList) {\n                    responseData.append(s).append(NEW_LINE);\n                }\n                result = responseData.toString();\n            } catch (Exception e) {\n                throw new RuntimeException(\"Error getting properties file : \" + fileName, e);\n            }\n        }\n\n        return result;\n    }\n\n    private static String load(InputStream is) throws IOException {\n        try (BufferedReader in = new BufferedReader(new InputStreamReader(is))) {\n            String line;\n            StringBuilder responseData = new StringBuilder();\n            while ((line = in.readLine()) != null) {\n                responseData.append(line).append(NEW_LINE);\n            }\n            return responseData.toString();\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/utils/src/main/java/cc/blynk/utils/FileUtils.java",
    "content": "package cc.blynk.utils;\n\nimport java.io.BufferedWriter;\nimport java.io.DataOutputStream;\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.Buffer;\nimport java.nio.ByteBuffer;\nimport java.nio.channels.SeekableByteChannel;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.nio.file.StandardCopyOption;\nimport java.nio.file.attribute.BasicFileAttributes;\nimport java.nio.file.attribute.FileTime;\nimport java.time.Instant;\nimport java.time.format.DateTimeFormatter;\nimport java.util.Arrays;\nimport java.util.EnumSet;\n\nimport static java.nio.file.StandardOpenOption.APPEND;\nimport static java.nio.file.StandardOpenOption.CREATE;\nimport static java.nio.file.StandardOpenOption.READ;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 04.01.16.\n */\npublic final class FileUtils {\n\n    private static final String BLYNK_FOLDER = \"blynk\";\n    public static final String CSV_DIR = Paths.get(\n            System.getProperty(\"java.io.tmpdir\"), BLYNK_FOLDER)\n            .toString();\n\n    private FileUtils() {\n    }\n\n    private static final String[] POSSIBLE_LOCAL_PATHS = new String[] {\n            \"./server/http-dashboard/target/classes\",\n            \"./server/http-api/target/classes\",\n            \"./server/http-admin/target/classes\",\n            \"./server/http-core/target/classes\",\n            \"./server/core/target\",\n            \"../server/http-admin/target/classes\",\n            \"../server/http-dashboard/target/classes\",\n            \"../server/http-core/target/classes\",\n            \"../server/core/target\",\n            \"./server/utils/target\",\n            \"../server/utils/target\",\n            Paths.get(System.getProperty(\"java.io.tmpdir\"), BLYNK_FOLDER).toString()\n    };\n\n    //reporting entry is long value (8 bytes) + timestamp (8 bytes)\n    public static final int SIZE_OF_REPORT_ENTRY = 16;\n\n    public static void deleteQuietly(Path path) {\n        try {\n            Files.deleteIfExists(path);\n        } catch (Exception ignored) {\n            //ignore\n        }\n    }\n\n    public static void move(Path source, Path target) throws IOException {\n        Path targetFile = Paths.get(target.toString(), source.getFileName().toString());\n        Files.move(source, targetFile, StandardCopyOption.REPLACE_EXISTING);\n    }\n\n    /**\n     * Simply writes single reporting entry to disk (16 bytes).\n     * Reporting entry is value (double) and timestamp (long)\n     *\n     * @param reportingPath - path to user specific reporting file\n     * @param value         - sensor data\n     * @param ts            - time when entry was created\n     */\n    public static void write(Path reportingPath, double value, long ts) throws IOException {\n        try (DataOutputStream dos = new DataOutputStream(\n                Files.newOutputStream(reportingPath, CREATE, APPEND))) {\n            dos.writeDouble(value);\n            dos.writeLong(ts);\n            dos.flush();\n        }\n    }\n\n    /**\n     * Read bunch of last records from file.\n     *\n     * @param userDataFile - file to read\n     * @param count = number of records to read\n     *\n     * @return - byte buffer with data\n     */\n    public static ByteBuffer read(Path userDataFile, int count) throws IOException {\n        return read(userDataFile, count, 0);\n    }\n\n    /**\n     * Read bunch of last records from file.\n     *\n     * @param userDataFile - file to read\n     * @param count        - number of records to read\n     * @param skip         - number of entries to skip from the end\n     * @return - byte buffer with data\n     */\n    public static ByteBuffer read(Path userDataFile, int count, int skip) throws IOException {\n        int size = (int) Files.size(userDataFile);\n        int expectedMinimumLength = (count + skip) * SIZE_OF_REPORT_ENTRY;\n        int diff = size - expectedMinimumLength;\n        int startReadIndex = Math.max(0, diff);\n        int bufferSize = diff < 0 ? count * SIZE_OF_REPORT_ENTRY + diff : count * SIZE_OF_REPORT_ENTRY;\n        if (bufferSize <= 0) {\n            return null;\n        }\n\n        ByteBuffer buf = ByteBuffer.allocate(bufferSize);\n\n        try (SeekableByteChannel channel = Files.newByteChannel(userDataFile, EnumSet.of(READ))) {\n            channel.position(startReadIndex)\n                    .read(buf);\n            ((Buffer) buf).flip();\n            return buf;\n        }\n    }\n\n    public static boolean writeBufToCsvFilterAndFormat(BufferedWriter writer, ByteBuffer onePinData,\n                                                      String pin, String deviceName,\n                                                      long startFrom, DateTimeFormatter formatter) throws IOException {\n        boolean hasData = false;\n        while (onePinData.remaining() > 0) {\n            double value = onePinData.getDouble();\n            long ts = onePinData.getLong();\n\n            if (startFrom <= ts) {\n                String formattedTs = formatTS(formatter, ts);\n                writer.write(formattedTs + ',' + pin + ',' + deviceName + ',' + value + '\\n');\n                hasData = true;\n            }\n        }\n        if (hasData) {\n            writer.flush();\n        }\n        return hasData;\n    }\n\n    public static boolean writeBufToCsvFilterAndFormat(BufferedWriter writer, ByteBuffer onePinData, String pin,\n                                                      long startFrom, DateTimeFormatter formatter) throws IOException {\n        boolean hasData = false;\n        while (onePinData.remaining() > 0) {\n            double value = onePinData.getDouble();\n            long ts = onePinData.getLong();\n\n            if (startFrom <= ts) {\n                String formattedTs = formatTS(formatter, ts);\n                writer.write(formattedTs + ',' + pin + ',' + value + '\\n');\n                hasData = true;\n            }\n        }\n        if (hasData) {\n            writer.flush();\n        }\n        return hasData;\n    }\n\n    public static String writeBufToCsvFilterAndFormat(ByteBuffer onePinData,\n                                                    long startFrom, DateTimeFormatter formatter) {\n        StringBuilder sb = new StringBuilder(onePinData.capacity() * 3);\n        while (onePinData.remaining() > 0) {\n            double value = onePinData.getDouble();\n            long ts = onePinData.getLong();\n\n            if (startFrom <= ts) {\n                String formattedTs = formatTS(formatter, ts);\n                sb.append(formattedTs).append(',')\n                        .append(value).append('\\n');\n            }\n        }\n        return sb.toString();\n    }\n\n    private static String formatTS(DateTimeFormatter formatter, long ts) {\n        if (formatter == null) {\n            return String.valueOf(ts);\n        }\n        return formatter.format(Instant.ofEpochMilli(ts));\n    }\n\n    public static void writeBufToCsv(BufferedWriter writer, ByteBuffer onePinData, int deviceId) throws Exception {\n        while (onePinData.remaining() > 0) {\n            double value = onePinData.getDouble();\n            long ts = onePinData.getLong();\n\n            writer.write(\"\" + value + ',' + ts + ',' + deviceId + '\\n');\n        }\n    }\n\n    public static Path getUserReportDir(String email, String appName, int reportId, String date) {\n        return Paths.get(FileUtils.CSV_DIR, email + \"_\" + appName + \"_\" + reportId + \"_\" + date);\n    }\n\n    public static String getUserStorageDir(String email, String appName) {\n        if (AppNameUtil.BLYNK.equals(appName)) {\n            return email;\n        }\n        return email + \"_\" + appName;\n    }\n\n    public static String downloadUrl(String host, String httpPort, boolean forcePort80) {\n        if (forcePort80) {\n            return \"http://\" + host + \"/\";\n        }\n        return \"http://\" + host + \":\" + httpPort + \"/\";\n    }\n\n    public static File getLatestFile(File[] files) {\n        if (files == null || files.length == 0) {\n            return null;\n        }\n        File lastModifiedFile = files[0];\n        for (int i = 1; i < files.length; i++) {\n            if (lastModifiedFile.lastModified() < files[i].lastModified()) {\n                lastModifiedFile = files[i];\n            }\n        }\n        return lastModifiedFile;\n    }\n\n    public static String getPatternFromString(Path path, String pattern) throws IOException {\n        byte[] data = Files.readAllBytes(path);\n\n        int index = KMPMatch.indexOf(data, pattern.getBytes());\n\n        if (index != -1) {\n            int start = index + pattern.length();\n            int end = 0;\n            byte b = -1;\n\n            while (b != '\\0') {\n                end++;\n                b = data[start + end];\n            }\n\n            byte[] copy = Arrays.copyOfRange(data, start, start + end);\n            return new String(copy);\n        }\n        throw new RuntimeException(\"Unable to read build number fro firmware.\");\n    }\n\n    public static Path getPathForLocalRun(String uri) {\n        for (String possiblePath : POSSIBLE_LOCAL_PATHS) {\n            Path path = Paths.get(possiblePath, uri);\n            if (Files.exists(path)) {\n                return path;\n            }\n        }\n        return null;\n    }\n\n    public static long getLastModified(Path filePath) throws IOException {\n        BasicFileAttributes attr = Files.readAttributes(filePath, BasicFileAttributes.class);\n        FileTime modifiedTime = attr.lastModifiedTime();\n        return modifiedTime.toMillis();\n    }\n}\n"
  },
  {
    "path": "server/utils/src/main/java/cc/blynk/utils/IPUtils.java",
    "content": "package cc.blynk.utils;\n\nimport java.net.Inet4Address;\nimport java.net.InetAddress;\nimport java.net.InetSocketAddress;\nimport java.net.NetworkInterface;\nimport java.net.SocketAddress;\nimport java.util.Enumeration;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 13.03.16.\n */\npublic final class IPUtils {\n\n    private IPUtils() {\n    }\n\n    public static String resolveHostIP(String netInterface) {\n        try {\n            Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();\n            while (interfaces.hasMoreElements()) {\n                NetworkInterface networkInterface = interfaces.nextElement();\n                if (networkInterface.getDisplayName().startsWith(netInterface)) {\n                    Enumeration<InetAddress> ips = networkInterface.getInetAddresses();\n                    while (ips.hasMoreElements()) {\n                        InetAddress inetAddress = ips.nextElement();\n                        if (inetAddress instanceof Inet4Address) {\n                            return inetAddress.getHostAddress();\n                        }\n                    }\n                    return networkInterface.getDisplayName();\n                }\n            }\n            return InetAddress.getLocalHost().getHostAddress();\n        } catch (Exception e) {\n            return \"127.0.0.1\";\n        }\n    }\n\n    public static String getIp(SocketAddress remoteSocketAddress) {\n        try {\n            InetSocketAddress socketAddress = (InetSocketAddress) remoteSocketAddress;\n            return socketAddress.getAddress().getHostAddress();\n        } catch (Exception e) {\n            //ignoring\n        }\n        return null;\n    }\n\n}\n"
  },
  {
    "path": "server/utils/src/main/java/cc/blynk/utils/IntArray.java",
    "content": "package cc.blynk.utils;\n\nimport java.util.Arrays;\n\n/**\n * Dynamic primitive array. Mostly copied from ArrayList.\n * It is used to avoid Integer values within array and optimize toArray() operation\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 07.09.15.\n */\npublic class IntArray {\n\n    private static final int[] EMPTY = {};\n\n    private static final int DEFAULT_CAPACITY = 10;\n    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;\n\n    private int[] elementData;\n\n    private int size;\n\n    public IntArray() {\n        this.elementData = EMPTY;\n    }\n\n    private static int hugeCapacity(int minCapacity) {\n        if (minCapacity < 0) { // overflow\n            throw new OutOfMemoryError();\n        }\n        return (minCapacity > MAX_ARRAY_SIZE)\n                ? Integer.MAX_VALUE\n                : MAX_ARRAY_SIZE;\n    }\n\n    public int size() {\n        return size;\n    }\n\n    public void add(int e) {\n        add(e, elementData, size);\n    }\n\n    private void add(int e, int[] elementData, int s) {\n        if (s == elementData.length) {\n            elementData = grow();\n        }\n        elementData[s] = e;\n        size = s + 1;\n    }\n\n    private int[] grow(int minCapacity) {\n        return elementData = Arrays.copyOf(elementData, newCapacity(minCapacity));\n    }\n\n    private int[] grow() {\n        return grow(size + 1);\n    }\n\n    private int newCapacity(int minCapacity) {\n        // overflow-conscious code\n        int oldCapacity = elementData.length;\n        int newCapacity = oldCapacity + (oldCapacity >> 1);\n        if (newCapacity - minCapacity <= 0) {\n            if (elementData == EMPTY) {\n                return Math.max(DEFAULT_CAPACITY, minCapacity);\n            }\n            if (minCapacity < 0) { // overflow\n                throw new OutOfMemoryError();\n            }\n            return minCapacity;\n        }\n        return (newCapacity - MAX_ARRAY_SIZE <= 0)\n                ? newCapacity\n                : hugeCapacity(minCapacity);\n    }\n\n    public int[] toArray() {\n        if (size == 0) {\n            return EMPTY;\n        }\n        return Arrays.copyOf(elementData, size);\n    }\n}\n"
  },
  {
    "path": "server/utils/src/main/java/cc/blynk/utils/JarUtil.java",
    "content": "package cc.blynk.utils;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.net.URL;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.security.CodeSource;\nimport java.util.ArrayList;\nimport java.util.Properties;\nimport java.util.stream.Stream;\nimport java.util.zip.ZipEntry;\nimport java.util.zip.ZipInputStream;\n\nimport static java.nio.file.StandardCopyOption.REPLACE_EXISTING;\n\n/**\n * Utility class to work with jar file. Used in order to find all static resources\n * within jar file and helps extract them into file system.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 11.12.15.\n */\npublic final class JarUtil {\n\n    private JarUtil() {\n    }\n\n    /**\n     * Unpacks all files from staticFolder of jar and puts them to current folder within staticFolder path.\n     *\n     * @param staticFolder - path to resources\n     */\n    public static boolean unpackStaticFiles(String jarPath, String staticFolder) {\n        try {\n            ArrayList<String> staticResources = find(staticFolder);\n\n            if (staticResources.size() == 0) {\n                return false;\n            }\n\n            for (String staticFile : staticResources) {\n                try (InputStream is = JarUtil.class.getResourceAsStream(\"/\" + staticFile)) {\n                    Path newStaticFile = Paths.get(jarPath, staticFile);\n\n                    Files.deleteIfExists(newStaticFile);\n                    Files.createDirectories(newStaticFile);\n\n                    Files.copy(is, newStaticFile, REPLACE_EXISTING);\n                }\n            }\n\n            // Now override the static files with installation-specific override files.\n            File overrides = new File(\"static-override\");\n            if (overrides.exists()) {\n                File staticDir = new File(\"static\");\n                copyFolder(overrides.toPath(), staticDir.toPath());\n            }\n            return true;\n        } catch (Exception e) {\n            throw new RuntimeException(\"Error unpacking static files.\", e);\n        }\n    }\n\n    private static void copyFolder(Path src, Path dest) throws IOException {\n        try (Stream<Path> stream = Files.walk(src)) {\n            stream.forEach(from -> {\n                try {\n                    Path to = dest.resolve(src.relativize(from));\n                    if (!Files.isDirectory(from)) {\n                        Files.copy(from, to, REPLACE_EXISTING);\n                    }\n                } catch (Exception e) {\n                    System.out.println(\"Error overriding static file. \" + e.getMessage());\n                }\n            });\n        }\n    }\n\n    /**\n     * Returns list of resources that were found in staticResourcesFolder\n     *\n     * @param staticResourcesFolder - resource folder\n     * @return - absolute path to resources within staticResourcesFolder\n     */\n    private static ArrayList<String> find(String staticResourcesFolder) throws Exception {\n        if (!staticResourcesFolder.endsWith(\"/\")) {\n            staticResourcesFolder = staticResourcesFolder + \"/\";\n        }\n        CodeSource src = JarUtil.class.getProtectionDomain().getCodeSource();\n        ArrayList<String> staticResources = new ArrayList<>();\n\n        if (src != null) {\n            URL jar = src.getLocation();\n            try (ZipInputStream zip = new ZipInputStream(jar.openStream())) {\n                ZipEntry ze;\n\n                while ((ze = zip.getNextEntry()) != null) {\n                    String entryName = ze.getName();\n                    if (!ze.isDirectory() && entryName.startsWith(staticResourcesFolder)) {\n                        staticResources.add(entryName);\n                    }\n                }\n            }\n        }\n\n        return staticResources;\n    }\n\n    /**\n     * Gets server version from jar file.\n     *\n     * @return server version\n     */\n    public static String getServerVersion() {\n        try (InputStream is = JarUtil.class.getResourceAsStream(\"/META-INF/MANIFEST.MF\")) {\n            Properties properties = new Properties();\n            properties.load(is);\n            return properties.getProperty(\"Build-Number\", \"\");\n        } catch (Exception e) {\n            return \"\";\n        }\n    }\n}\n"
  },
  {
    "path": "server/utils/src/main/java/cc/blynk/utils/KMPMatch.java",
    "content": "package cc.blynk.utils;\n\n/**\n * Knuth-Morris-Pratt Algorithm for Pattern Matching\n */\nfinal class KMPMatch {\n\n    private KMPMatch() {\n    }\n\n    /**\n     * Finds the first occurrence of the pattern in the text.\n     */\n    static int indexOf(byte[] data, byte[] pattern) {\n        int[] failure = computeFailure(pattern);\n\n        int j = 0;\n        if (data.length == 0) {\n            return -1;\n        }\n\n        for (int i = 0; i < data.length; i++) {\n            while (j > 0 && pattern[j] != data[i]) {\n                j = failure[j - 1];\n            }\n            if (pattern[j] == data[i]) {\n                j++;\n            }\n            if (j == pattern.length) {\n                return i - pattern.length + 1;\n            }\n        }\n        return -1;\n    }\n\n    /**\n     * Computes the failure function using a boot-strapping process,\n     * where the pattern is matched against itself.\n     */\n    private static int[] computeFailure(byte[] pattern) {\n        int[] failure = new int[pattern.length];\n\n        int j = 0;\n        for (int i = 1; i < pattern.length; i++) {\n            while (j > 0 && pattern[j] != pattern[i]) {\n                j = failure[j - 1];\n            }\n            if (pattern[j] == pattern[i]) {\n                j++;\n            }\n            failure[i] = j;\n        }\n\n        return failure;\n    }\n\n}\n"
  },
  {
    "path": "server/utils/src/main/java/cc/blynk/utils/NumberUtil.java",
    "content": "package cc.blynk.utils;\n\n/**\n * Optimized but less precise double parsing method. It is also doesn't spam objects.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 23.01.17.\n */\npublic final class NumberUtil {\n\n    public static final double NO_RESULT = Double.MIN_VALUE;\n    private static final NumberFormatException cachedNumberFormatException =\n            new NumberFormatException(\"Not a valid double number.\");\n    private static final NumberFormatException cachedNumberFormatExceptionForPin =\n            new NumberFormatException(\"Not a valid pin number.\");\n\n    private NumberUtil() {\n    }\n\n    // Precompute Math.pow(10, n) as table:\n    private final static int POW_RANGE = 256;\n    private final static double[] POS_EXPS = new double[POW_RANGE];\n    private final static double[] NEG_EXPS = new double[POW_RANGE];\n\n    static {\n        for (int i = 0; i < POW_RANGE; i++) {\n            POS_EXPS[i] = Math.pow(10., i);\n            NEG_EXPS[i] = Math.pow(10., -i);\n        }\n    }\n\n    // Calculate the value of the specified exponent - reuse a precalculated value if possible\n    private static double getPow10(final int exp) {\n        if (exp > -POW_RANGE) {\n            if (exp <= 0) {\n                return NEG_EXPS[-exp];\n            } else if (exp < POW_RANGE) {\n                return POS_EXPS[exp];\n            }\n        }\n        return Math.pow(10., exp);\n    }\n\n    public static double parseDoubleOrThrow(final String s) {\n        double result = parseDouble(s);\n        if (result == NO_RESULT) {\n            throw cachedNumberFormatException;\n        }\n        return result;\n    }\n\n    public static double parseDouble(final String s) {\n\n        int off = 0;\n        int len = s.length();\n\n        if (len == 0) {\n            return NO_RESULT;\n        }\n\n        char ch;\n        boolean numSign = true;\n\n        ch = s.charAt(off);\n        if (ch == '+') {\n            off++;\n            len--;\n        } else if (ch == '-') {\n            numSign = false;\n            off++;\n            len--;\n        }\n\n        double number;\n\n        boolean error = true;\n\n        int startOffset = off;\n        double dval;\n\n        for (dval = 0d; (len > 0) && ((ch = s.charAt(off)) >= '0') && (ch <= '9');) {\n            dval *= 10d;\n            dval += ch - '0';\n            off++;\n            len--;\n        }\n        int numberLength = off - startOffset;\n\n        number = dval;\n\n        if (numberLength > 0) {\n            error = false;\n        }\n\n        // Check for fractional values after decimal\n        if ((len > 0) && (s.charAt(off) == '.')) {\n\n            off++;\n            len--;\n\n            startOffset = off;\n\n            for (dval = 0d; (len > 0) && ((ch = s.charAt(off)) >= '0') && (ch <= '9');) {\n                dval *= 10d;\n                dval += ch - '0';\n                off++;\n                len--;\n            }\n            numberLength = off - startOffset;\n\n            if (numberLength > 0) {\n                number += getPow10(-numberLength) * dval;\n                error = false;\n            }\n        }\n\n        if (error) {\n            return NO_RESULT;\n        }\n\n        // Look for an exponent\n        if (len > 0) {\n            // note: ignore any non-digit character at end:\n\n            if ((ch = s.charAt(off)) == 'e' || ch == 'E') {\n\n                off++;\n                len--;\n\n                if (len > 0) {\n                    boolean expSign = true;\n\n                    ch = s.charAt(off);\n                    if (ch == '+') {\n                        off++;\n                        len--;\n                    } else if (ch == '-') {\n                        expSign = false;\n                        off++;\n                        len--;\n                    }\n\n                    int exponent;\n\n                    // note: ignore any non-digit character at end:\n                    for (exponent = 0; (len > 0) && ((ch = s.charAt(off)) >= '0') && (ch <= '9');) {\n                        exponent *= 10;\n                        exponent += ch - '0';\n                        off++;\n                        len--;\n                    }\n\n                    if (!expSign) {\n                        exponent = -exponent;\n                    }\n\n                    // For very small numbers we try to miminize\n                    // effects of denormalization.\n                    if (exponent > -300) {\n                        number *= getPow10(exponent);\n                    } else {\n                        number = 1.0E-300 * (number * getPow10(exponent + 300));\n                    }\n                }\n            }\n        }\n        // check other characters:\n        if (len > 0) {\n            return NO_RESULT;\n        }\n\n\n        return (numSign) ? number : -number;\n    }\n\n    public static int calcHeartbeatTimeout(int heartbeatInterval) {\n        return (int) Math.ceil(heartbeatInterval * 2.3D);\n    }\n\n    public static short parsePin(String pinString) throws NumberFormatException {\n        int i = Integer.parseInt(pinString, 10);\n        if (i < 0 || i > 255) {\n            throw cachedNumberFormatExceptionForPin;\n        }\n        return (short) i;\n    }\n}\n"
  },
  {
    "path": "server/utils/src/main/java/cc/blynk/utils/ReflectionUtil.java",
    "content": "package cc.blynk.utils;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/4/2015.\n */\npublic final class ReflectionUtil {\n\n    private ReflectionUtil() {\n    }\n\n    /**\n     * Used to generate map of class fields where key is field value and value is field name.\n     */\n    public static Map<Integer, String> generateMapOfValueNameInteger(Class<?> clazz) {\n        var valuesName = new HashMap<Integer, String>();\n        try {\n            for (var field : clazz.getFields()) {\n                valuesName.put((Integer) field.get(int.class), field.getName());\n            }\n        } catch (IllegalAccessException e) {\n            e.printStackTrace();\n        }\n        return valuesName;\n    }\n\n    public static Object castTo(Class type, String value) {\n        if (type == byte.class || type == Byte.class) {\n            return Byte.valueOf(value);\n        }\n        if (type == short.class || type == Short.class) {\n            return Short.valueOf(value);\n        }\n        if (type == int.class || type == Integer.class) {\n            return Integer.valueOf(value);\n        }\n        if (type == long.class || type == Long.class) {\n            return Long.valueOf(value);\n        }\n        if (type == boolean.class || type == Boolean.class) {\n            return Boolean.valueOf(value);\n        }\n        return value;\n    }\n}\n"
  },
  {
    "path": "server/utils/src/main/java/cc/blynk/utils/ReportingUtil.java",
    "content": "package cc.blynk.utils;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 04.09.15.\n */\npublic final class ReportingUtil {\n\n    public final static int REPORTING_RECORD_SIZE = 16;\n\n    private ReportingUtil() {\n    }\n\n}\n"
  },
  {
    "path": "server/utils/src/main/java/cc/blynk/utils/SHA256Util.java",
    "content": "package cc.blynk.utils;\n\nimport java.nio.charset.StandardCharsets;\nimport java.security.MessageDigest;\nimport java.security.NoSuchAlgorithmException;\nimport java.util.Base64;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 22.04.15.\n */\npublic final class SHA256Util {\n\n    private static final String SHA_256 = \"SHA-256\";\n\n    private SHA256Util() {\n        try {\n            MessageDigest.getInstance(SHA_256);\n        } catch (NoSuchAlgorithmException e) {\n            throw new RuntimeException(\"SHA-256 not supported.\");\n        }\n    }\n\n    public static String makeHash(String password, String salt) {\n        try {\n            MessageDigest md = MessageDigest.getInstance(SHA_256);\n            md.update(password.getBytes(StandardCharsets.UTF_8));\n            byte[] byteData = md.digest(makeHash(salt.toLowerCase()));\n            return Base64.getEncoder().encodeToString(byteData);\n        } catch (NoSuchAlgorithmException e) {\n            //ignore, will never happen.\n        }\n        return password;\n    }\n\n    private static byte[] makeHash(String val) throws NoSuchAlgorithmException {\n        return MessageDigest.getInstance(SHA_256).digest(val.getBytes(StandardCharsets.UTF_8));\n    }\n\n}\n"
  },
  {
    "path": "server/utils/src/main/java/cc/blynk/utils/StringUtils.java",
    "content": "package cc.blynk.utils;\n\nimport java.net.URLEncoder;\nimport java.nio.charset.StandardCharsets;\nimport java.security.SecureRandom;\nimport java.util.regex.Pattern;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/18/2015.\n */\npublic final class StringUtils {\n\n    public final static String BLYNK_LANDING = \"https://www.blynk.cc\";\n\n    public static final char BODY_SEPARATOR = '\\0';\n    public static final String BODY_SEPARATOR_STRING = String.valueOf(BODY_SEPARATOR);\n    public static final char DEVICE_SEPARATOR = '-';\n    public static final String DEVICE_SEPARATOR_STRING = \"-\";\n\n    public static final Pattern PIN_PATTERN =  Pattern.compile(\"/pin/\", Pattern.LITERAL);\n    public static final Pattern PIN_PATTERN_0 =  Pattern.compile(\"/pin[0]/\", Pattern.LITERAL);\n    public static final Pattern PIN_PATTERN_1 =  Pattern.compile(\"/pin[1]/\", Pattern.LITERAL);\n    public static final Pattern PIN_PATTERN_2 =  Pattern.compile(\"/pin[2]/\", Pattern.LITERAL);\n    public static final Pattern PIN_PATTERN_3 =  Pattern.compile(\"/pin[3]/\", Pattern.LITERAL);\n    public static final Pattern PIN_PATTERN_4 =  Pattern.compile(\"/pin[4]/\", Pattern.LITERAL);\n    public static final Pattern PIN_PATTERN_5 =  Pattern.compile(\"/pin[5]/\", Pattern.LITERAL);\n    public static final Pattern PIN_PATTERN_6 =  Pattern.compile(\"/pin[6]/\", Pattern.LITERAL);\n    public static final Pattern PIN_PATTERN_7 =  Pattern.compile(\"/pin[7]/\", Pattern.LITERAL);\n    public static final Pattern PIN_PATTERN_8 =  Pattern.compile(\"/pin[8]/\", Pattern.LITERAL);\n    public static final Pattern PIN_PATTERN_9 =  Pattern.compile(\"/pin[9]/\", Pattern.LITERAL);\n    public static final Pattern GENERIC_PLACEHOLDER = Pattern.compile(\"%s\", Pattern.LITERAL);\n    private static final Pattern NOT_SUPPORTED_CHARS = Pattern.compile(\"[\\\\\\\\/:*?\\\"<>| ]\");\n\n    public static final Pattern DATETIME_PATTERN =  Pattern.compile(\"/datetime_iso/\", Pattern.LITERAL);\n    public static final Pattern DEVICE_OWNER_EMAIL =  Pattern.compile(\"device_owner_email\",\n                                                                      Pattern.LITERAL | Pattern.CASE_INSENSITIVE);\n    public static final String WEBSOCKET_PATH = \"/websocket\";\n    public static final String WEBSOCKETS_PATH = \"/websockets\";\n    public static final String WEBSOCKET_WEB_PATH = \"/dashws\";\n\n    private StringUtils() {\n    }\n\n    /**\n     * Parses string similar to this : \"xw 1 xxxx\"\n     * Every hard message has at least 3 starting chars we don't need.\n     */\n    private static final int START_INDEX = 3;\n\n    private static final String IN_DATA = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\";\n    private static final SecureRandom SECURE_RANDOM = new SecureRandom();\n\n    /**\n     * Efficient split method (instead of String.split).\n     *\n     * Returns pin from hardware body. For instance\n     *\n     * \"aw 11 32\" - is body. Where 11 is pin Number.\n     *\n     * @throws java.lang.NumberFormatException in case parsed pin not a Number.\n     *\n     */\n    public static String fetchPin(String body) {\n        int i = START_INDEX;\n        while (i < body.length()) {\n            if (body.charAt(i) == BODY_SEPARATOR) {\n                return body.substring(START_INDEX, i);\n            }\n            i++;\n        }\n\n        return body.substring(START_INDEX, i);\n    }\n\n    /**\n     * Optimized method for splitting. It is uses knowledge of Blynk message structure. So it is 2-3 times faster\n     * and produces less garbage.\n     *\n     * Does same as String.split(BODY_SEPARATOR_STRING, 3);\n     *\n     * See StringUtilPerfTest\n     *\n     Benchmark                                        Mode  Cnt    Score    Error  Units\n     StringUtilPerfTest.customSplit3_aw_100_900       avgt    5   46.806 ±  6.927  ns/op\n     StringUtilPerfTest.customSplit3_aw_10_long_text  avgt    5   52.334 ±  9.231  ns/op\n     StringUtilPerfTest.customSplit3_aw_1_2           avgt    5   48.483 ± 14.347  ns/op\n     StringUtilPerfTest.customSplit3_vw_1             avgt    5   34.943 ±  9.918  ns/op\n     StringUtilPerfTest.customSplit3_vw_99_22222      avgt    5   46.511 ± 15.401  ns/op\n     StringUtilPerfTest.customSplit3_vw_99_900        avgt    5   48.025 ± 18.951  ns/op\n     StringUtilPerfTest.split3_aw_100_900             avgt    5  113.358 ± 26.606  ns/op\n     StringUtilPerfTest.split3_aw_10_long_text        avgt    5  117.831 ± 39.682  ns/op\n     StringUtilPerfTest.split3_aw_1_2                 avgt    5  106.119 ±  1.289  ns/op\n     StringUtilPerfTest.split3_vw_1                   avgt    5   87.868 ± 11.709  ns/op\n     StringUtilPerfTest.split3_vw_99_22222            avgt    5  115.280 ±  8.697  ns/op\n     StringUtilPerfTest.split3_vw_99_900              avgt    5  123.085 ± 20.625  ns/op\n     *\n     *\n     */\n    public static String[] split3(char separator, String body) {\n        final int i1 = body.indexOf(separator, 1);\n        if (i1 == -1) {\n            return new String[] {body};\n        }\n\n        final int i2 = body.indexOf(separator, i1 + 1);\n        if (i2 == -1) {\n            return new String[] {body.substring(0, i1), body.substring(i1 + 1)};\n        }\n\n        return new String[] {body.substring(0, i1), body.substring(i1 + 1, i2), body.substring(i2 + 1)};\n    }\n\n    public static String[] split3(String body) {\n        return split3(BODY_SEPARATOR, body);\n    }\n\n    public static String[] split2Device(String body) {\n        return split2(DEVICE_SEPARATOR, body);\n    }\n\n    public static String[] split2(char separator, String body) {\n        final int i1 = body.indexOf(separator, 1);\n        if (i1 == -1) {\n            return new String[] {body};\n        }\n\n        return new String[] {body.substring(0, i1), body.substring(i1 + 1)};\n    }\n\n    public static String[] split2(String body) {\n        return split2(BODY_SEPARATOR, body);\n    }\n\n    public static String prependDashIdAndDeviceId(int dashId, int deviceId, String body) {\n        return \"\" + dashId + DEVICE_SEPARATOR + deviceId + BODY_SEPARATOR + body;\n    }\n\n    public static String randomPassword(int len) {\n        return randomString(IN_DATA, len);\n    }\n\n    public static String randomString(int len) {\n        //using only lowercase chars for app id.\n        String dataForId = IN_DATA.substring(0, 26);\n        return randomString(dataForId, len);\n    }\n\n    private static String randomString(String inData, int len) {\n        StringBuilder sb = new StringBuilder(len);\n        int inDataLength = inData.length();\n        for (int i = 0; i < len; i++) {\n            sb.append(inData.charAt(SECURE_RANDOM.nextInt(inDataLength)));\n        }\n        return sb.toString();\n    }\n\n    private static String removeUnsupportedChars(String name) {\n        return NOT_SUPPORTED_CHARS.matcher(name).replaceAll(\"\");\n    }\n\n    public static String truncate(String name, int size) {\n        return name.length() <= size ? name : name.substring(0, size);\n    }\n\n    public static String escapeCSV(String name) {\n        name = name.replace(\"\\\"\", \"\\\"\\\"\");\n        if (name.contains(\",\") || name.contains(\";\") || name.contains(\"\\\"\")) {\n            return \"\\\"\" + name + \"\\\"\";\n        }\n        return name;\n    }\n\n    public static String truncateFileName(String name) {\n        if (name == null) {\n            return \"\";\n        }\n\n        String truncated = removeUnsupportedChars(name);\n        return truncate(truncated, 16);\n    }\n\n    public static boolean isReadOperation(String body) {\n        return body.length() > 1 && body.charAt(1) == 'r';\n    }\n\n    public static String encode(String s) {\n        try {\n            return URLEncoder.encode(s, StandardCharsets.UTF_8);\n        } catch (Exception e) {\n            return s;\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/utils/src/main/java/cc/blynk/utils/TokenGeneratorUtil.java",
    "content": "package cc.blynk.utils;\n\nimport java.security.SecureRandom;\nimport java.util.Base64;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 12.05.16.\n */\npublic final class TokenGeneratorUtil {\n\n    private static final SecureRandom secureRandom = new SecureRandom();\n    private static final Base64.Encoder base64Encoder = Base64.getUrlEncoder();\n\n    private TokenGeneratorUtil() {\n    }\n\n    public static String generateNewToken() {\n        byte[] randomBytes = new byte[24];\n        secureRandom.nextBytes(randomBytes);\n        return base64Encoder.encodeToString(randomBytes);\n    }\n\n    public static boolean isNotValidResetToken(String token) {\n        if (token.length() != 64) {\n            return true;\n        }\n        for (int i = 0; i < 64; i++) {\n            char c = token.charAt(i);\n            if (!(Character.isLetterOrDigit(c) || c == '-' || c == '_')) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n}\n"
  },
  {
    "path": "server/utils/src/main/java/cc/blynk/utils/http/ContentType.java",
    "content": "package cc.blynk.utils.http;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 10.01.18.\n */\npublic enum ContentType {\n\n    TEXT_HTML,\n    TEXT_PLAIN\n\n}\n"
  },
  {
    "path": "server/utils/src/main/java/cc/blynk/utils/http/MediaType.java",
    "content": "package cc.blynk.utils.http;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 23.09.16.\n */\npublic final class MediaType {\n\n    private MediaType() {\n    }\n\n    public final static String APPLICATION_JSON = \"application/json\";\n    public final static String APPLICATION_FORM_URLENCODED = \"application/x-www-form-urlencoded\";\n    public final static String TEXT_PLAIN = \"text/plain\";\n    public final static String TEXT_HTML = \"text/html\";\n\n}\n"
  },
  {
    "path": "server/utils/src/main/java/cc/blynk/utils/properties/BaseProperties.java",
    "content": "package cc.blynk.utils.properties;\n\nimport java.io.File;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.Map;\nimport java.util.Properties;\n\nimport static cc.blynk.utils.properties.ServerProperties.SERVER_PROPERTIES_FILENAME;\n\n/**\n * Java properties class wrapper.\n * Loads properties file from class path. After that loads properties\n * from dir where jar file is. On every stage properties override previous.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/12/2015.\n */\npublic abstract class BaseProperties extends Properties {\n\n    public final String jarPath;\n\n    BaseProperties(Map<String, String> cmdProperties, String serverConfig) {\n        this.jarPath = getJarPath();\n        var propertiesFileName = cmdProperties.get(serverConfig);\n        if (propertiesFileName == null) {\n            initProperties(serverConfig);\n        } else {\n            initProperties(Paths.get(propertiesFileName));\n        }\n        putAll(cmdProperties);\n    }\n\n    private static String getJarPath() {\n        try {\n            var codeSource = BaseProperties.class.getProtectionDomain().getCodeSource();\n            var jarFile = new File(codeSource.getLocation().toURI().getPath());\n            return jarFile.getParentFile().getPath();\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    /**\n     * First loads properties file from class path after that from current folder.\n     * So properties file in current folder is always overrides properties in classpath.\n     *\n     * @param filePropertiesName - name of properties file, for example \"twitter4j.properties\"\n     */\n    private void initProperties(String filePropertiesName) {\n        readFromClassPath(filePropertiesName);\n\n        var curDirPath = Paths.get(jarPath, filePropertiesName);\n        if (Files.exists(curDirPath)) {\n            try (var curFolder = Files.newInputStream(curDirPath)) {\n                load(curFolder);\n            } catch (Exception e) {\n                throw new RuntimeException(\"Error getting properties file : \" + filePropertiesName, e);\n            }\n        }\n\n    }\n\n    private void readFromClassPath(String filePropertiesName) {\n        if (!filePropertiesName.startsWith(\"/\")) {\n            filePropertiesName = \"/\" + filePropertiesName;\n        }\n\n        try (var classPath = BaseProperties.class.getResourceAsStream(filePropertiesName)) {\n            if (classPath != null) {\n                load(classPath);\n            }\n        } catch (Exception e) {\n            throw new RuntimeException(\"Error getting properties file : \" + filePropertiesName, e);\n        }\n    }\n\n    private void initProperties(Path path) {\n        if (!Files.exists(path)) {\n            System.out.println(\"Path \" + path + \" not found.\");\n            System.exit(1);\n        }\n\n        readFromClassPath(SERVER_PROPERTIES_FILENAME);\n\n        try (var curFolder = Files.newInputStream(path)) {\n            load(curFolder);\n        } catch (Exception e) {\n            System.out.println(\"Error reading properties file : '\" + path + \"'. Reason : \" + e.getMessage());\n            System.exit(1);\n        }\n    }\n\n    public int getIntProperty(String propertyName) {\n        return Integer.parseInt(getProperty(propertyName));\n    }\n\n    public int getIntProperty(String propertyName, int defaultValue) {\n        var prop = getProperty(propertyName);\n        if (prop == null || prop.isEmpty()) {\n            return defaultValue;\n        }\n        return Integer.parseInt(prop);\n    }\n\n    public boolean getBoolProperty(String propertyName) {\n        return Boolean.parseBoolean(getProperty(propertyName));\n    }\n\n    public long getLongProperty(String propertyName) {\n        return Long.parseLong(getProperty(propertyName));\n    }\n\n    public String getAdminRootPath() {\n        return getProperty(\"admin.rootPath\", \"/admin\");\n    }\n\n    public long getLongProperty(String propertyName, long defaultValue) {\n        var prop = getProperty(propertyName);\n        if (prop == null || prop.isEmpty()) {\n            return defaultValue;\n        }\n        return Long.parseLong(prop);\n    }\n\n    public String[] getCommaSeparatedValueAsArray(String propertyName) {\n        var val = getProperty(propertyName);\n        if (val == null) {\n            return null;\n        }\n        return val.trim().toLowerCase().split(\",\");\n    }\n\n    public boolean getAllowWithoutActiveApp() {\n        return getBoolProperty(\"allow.reading.widget.without.active.app\");\n    }\n\n}\n"
  },
  {
    "path": "server/utils/src/main/java/cc/blynk/utils/properties/DBProperties.java",
    "content": "package cc.blynk.utils.properties;\n\nimport java.util.Collections;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 03.01.17.\n */\npublic class DBProperties extends BaseProperties {\n\n    public static final String DB_PROPERTIES_FILENAME = \"db.properties\";\n\n    public DBProperties(String fileName) {\n        super(Collections.emptyMap(), fileName);\n    }\n\n    public DBProperties() {\n        super(Collections.emptyMap(), DB_PROPERTIES_FILENAME);\n    }\n\n    public boolean cleanReporting() {\n        return getBoolProperty(\"clean.reporting\");\n    }\n}\n"
  },
  {
    "path": "server/utils/src/main/java/cc/blynk/utils/properties/GCMProperties.java",
    "content": "package cc.blynk.utils.properties;\n\nimport java.util.Map;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 03.01.17.\n */\npublic class GCMProperties extends BaseProperties {\n\n    public static final String GCM_PROPERTIES_FILENAME = \"gcm.properties\";\n\n    public GCMProperties(Map<String, String> cmdProperties) {\n        super(cmdProperties, GCM_PROPERTIES_FILENAME);\n    }\n\n    public String getNotificationTitle() {\n        return getProperty(\"notification.title\", Placeholders.PRODUCT_NAME + \" Notification\");\n    }\n\n    public String getNotificationBody() {\n        return getProperty(\"notification.body\", \"Your \" + Placeholders.DEVICE_NAME + \" went offline.\");\n    }\n\n    public String getGCMApiKey() {\n        return getProperty(\"gcm.api.key\");\n    }\n\n    public String getGCMServer() {\n        return getProperty(\"gcm.server\");\n    }\n}\n"
  },
  {
    "path": "server/utils/src/main/java/cc/blynk/utils/properties/MailProperties.java",
    "content": "package cc.blynk.utils.properties;\n\nimport java.util.Map;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 03.01.17.\n */\npublic class MailProperties extends BaseProperties {\n\n    public static final String MAIL_PROPERTIES_FILENAME = \"mail.properties\";\n\n    public MailProperties(Map<String, String> cmdProperties) {\n        super(cmdProperties, MAIL_PROPERTIES_FILENAME);\n    }\n\n    public String getSMTPUsername() {\n        return getProperty(\"mail.smtp.username\");\n    }\n\n    public String getSMTPPassword() {\n        return getProperty(\"mail.smtp.password\");\n    }\n\n    public String getSMTPHost() {\n        return getProperty(\"mail.smtp.host\");\n    }\n\n    public String getSMTPort() {\n        return getProperty(\"mail.smtp.port\");\n    }\n}\n"
  },
  {
    "path": "server/utils/src/main/java/cc/blynk/utils/properties/Placeholders.java",
    "content": "package cc.blynk.utils.properties;\n\n/**\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 19/06/2018.\n */\npublic final class Placeholders {\n\n    public static final String VENDOR_EMAIL = \"{VENDOR_EMAIL}\";\n    public static final String DEVICE_OWNER_EMAIL = \"{DEVICE_OWNER_EMAIL}\";\n    public static final String PRODUCT_NAME = \"{PRODUCT_NAME}\";\n    public static final String DEVICE_NAME = \"{DEVICE_NAME}\";\n    public static final String RESET_URL = \"{RESET_URL}\";\n    public static final String DOWNLOAD_URL = \"{DOWNLOAD_URL}\";\n    public static final String DYNAMIC_SECTION = \"{DYNAMIC_SECTION}\";\n    public static final String EMAIL = \"{EMAIL}\";\n    public static final String TOKEN = \"{TOKEN}\";\n    public static final String TEMPLATE_NAME = \"{template_name}\";\n    public static final String TEMPLATE_ID = \"{template_id}\";\n    public static final String PROJECT_NAME = \"{project_name}\";\n\n    private Placeholders() {\n    }\n}\n"
  },
  {
    "path": "server/utils/src/main/java/cc/blynk/utils/properties/ServerProperties.java",
    "content": "package cc.blynk.utils.properties;\n\nimport cc.blynk.utils.AppNameUtil;\nimport cc.blynk.utils.IPUtils;\nimport cc.blynk.utils.JarUtil;\n\nimport java.nio.file.Paths;\nimport java.util.Map;\n\n/**\n * Java properties class wrapper.\n * Loads properties file from class path. After that loads properties\n * from dir where jar file is. On every stage properties override previous.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 2/12/2015.\n */\npublic class ServerProperties extends BaseProperties {\n\n    public static final String SERVER_PROPERTIES_FILENAME = \"server.properties\";\n\n    private static final String STATIC_FILES_FOLDER = \"static\";\n\n    //this is reusable properties so we want to fetch them only once\n    public final boolean isUnpacked;\n    public final String vendorEmail;\n    public final String productName;\n    public final String region;\n    public final String host;\n\n    public ServerProperties(Map<String, String> cmdProperties, String serverConfig) {\n        super(cmdProperties, serverConfig);\n        this.isUnpacked = JarUtil.unpackStaticFiles(jarPath, STATIC_FILES_FOLDER);\n        this.vendorEmail = getVendorEmail();\n        this.productName = getProductName();\n        this.region = getRegion();\n        this.host = getServerHost();\n    }\n\n    public ServerProperties(Map<String, String> cmdProperties) {\n        this(cmdProperties, SERVER_PROPERTIES_FILENAME);\n    }\n\n    public boolean isLocalRegion() {\n        return region.equals(\"local\");\n    }\n\n    private String getProductName() {\n        return getProperty(\"product.name\", AppNameUtil.BLYNK);\n    }\n\n    private String getVendorEmail() {\n        return getProperty(\"vendor.email\");\n    }\n\n    private String getRegion() {\n        return getProperty(\"region\", \"local\");\n    }\n\n    public String getDataFolder() {\n        return getProperty(\"data.folder\");\n    }\n\n    public String getReportingFolder() {\n        return Paths.get(getDataFolder(), \"data\").toString();\n    }\n\n    public int getHttpPort() {\n        return getIntProperty(\"http.port\");\n    }\n\n    public int getHttpsPort() {\n        return getIntProperty(\"https.port\");\n    }\n\n    public boolean isRenewalDisabled() {\n        return getBoolProperty(\"renewal.disabled\");\n    }\n\n    private String getServerHost() {\n        String host = getHostProperty();\n        if (host == null || host.isEmpty()) {\n            var netInterface = getProperty(\"net.interface\", \"eth\");\n            return IPUtils.resolveHostIP(netInterface);\n        } else {\n            return host;\n        }\n    }\n\n    public String getRestoreHost() {\n        return getProperty(\"restore.host\");\n    }\n\n    private String getHostProperty() {\n        return getProperty(\"server.host\");\n    }\n\n    public String getAdminUrl(String host) {\n        String httpsPort = getHttpsPortOrBlankIfDefaultAsString();\n        return \"https://\" + host + httpsPort + getAdminRootPath();\n    }\n\n    public boolean isDBEnabled() {\n        return getBoolProperty(\"enable.db\");\n    }\n\n    private String getHttpsPortOrBlankIfDefaultAsString() {\n        if (force80Port()) {\n            //means default port 443 is used, so no need to attach it\n            return \"\";\n        }\n        String httpsPort = getProperty(\"https.port\");\n        if (httpsPort == null || httpsPort.equals(\"443\")) {\n            return \"\";\n        }\n        return \":\" + httpsPort;\n    }\n\n    private boolean force80Port() {\n        return getBoolProperty(\"force.port.80.for.csv\");\n    }\n\n    public String getHttpsPortAsString() {\n        return force80Port() ? \"443\" : getProperty(\"https.port\");\n    }\n\n    public boolean getAllowStoreIp() {\n        return getBoolProperty(\"allow.store.ip\");\n    }\n\n    public boolean isRawDBEnabled() {\n        return getBoolProperty(\"enable.raw.db.data.store\");\n    }\n}\n"
  },
  {
    "path": "server/utils/src/main/java/cc/blynk/utils/properties/SmsProperties.java",
    "content": "package cc.blynk.utils.properties;\n\nimport java.util.Map;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 03.01.17.\n */\npublic class SmsProperties extends BaseProperties {\n\n    public static final String SMS_PROPERTIES_FILENAME = \"sms.properties\";\n\n    public SmsProperties(Map<String, String> cmdProperties) {\n        super(cmdProperties, SMS_PROPERTIES_FILENAME);\n    }\n}\n"
  },
  {
    "path": "server/utils/src/main/java/cc/blynk/utils/properties/TwitterProperties.java",
    "content": "package cc.blynk.utils.properties;\n\nimport java.util.Map;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 03.01.17.\n */\npublic class TwitterProperties extends BaseProperties {\n\n    public static final String TWITTER_PROPERTIES_FILENAME = \"twitter4j.properties\";\n\n    public TwitterProperties(Map<String, String> cmdProperties) {\n        super(cmdProperties, TWITTER_PROPERTIES_FILENAME);\n    }\n\n    public String getConsumerKey() {\n        return getProperty(\"oauth.consumerKey\");\n    }\n\n    public String getConsumerSecret() {\n        return getProperty(\"oauth.consumerSecret\");\n    }\n}\n"
  },
  {
    "path": "server/utils/src/main/java/cc/blynk/utils/structure/BaseLimitedQueue.java",
    "content": "package cc.blynk.utils.structure;\n\nimport java.util.concurrent.LinkedBlockingQueue;\n\n/**\n *\n * FIFO limited array.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 07.09.16.\n */\npublic class BaseLimitedQueue<T> extends LinkedBlockingQueue<T> {\n\n    private final int limit;\n\n    BaseLimitedQueue(int limit) {\n        super(limit);\n        this.limit = limit;\n    }\n\n    @Override\n    public boolean add(T o) {\n        if (size() > limit - 1) {\n            super.poll();\n        }\n        super.add(o);\n        return true;\n    }\n\n}\n"
  },
  {
    "path": "server/utils/src/main/java/cc/blynk/utils/structure/LCDLimitedQueue.java",
    "content": "package cc.blynk.utils.structure;\n\n/**\n *\n * FIFO limited array.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 07.09.16.\n */\npublic class LCDLimitedQueue<T> extends BaseLimitedQueue<T> {\n\n    public static final int POOL_SIZE = Integer.parseInt(System.getProperty(\"lcd.strings.pool.size\", \"6\"));\n\n    public LCDLimitedQueue() {\n        super(POOL_SIZE);\n    }\n\n}\n"
  },
  {
    "path": "server/utils/src/main/java/cc/blynk/utils/structure/LRUCache.java",
    "content": "package cc.blynk.utils.structure;\n\nimport java.util.Collections;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 17.11.17.\n */\npublic class LRUCache<K, V> extends LinkedHashMap<K, V> {\n\n    private final int maxSize;\n\n    //size hardcoded for now\n    public static final Map<String, CacheEntry> LOGIN_TOKENS_CACHE =\n            Collections.synchronizedMap(new LRUCache<>(1000));\n\n    public LRUCache(int maxSize) {\n        this.maxSize = maxSize;\n    }\n\n    @Override\n    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {\n        return size() > maxSize;\n    }\n\n    public final static class CacheEntry {\n        public final String value;\n        public CacheEntry(String value) {\n            this.value = value;\n        }\n    }\n}\n"
  },
  {
    "path": "server/utils/src/main/java/cc/blynk/utils/structure/LimitedArrayDeque.java",
    "content": "package cc.blynk.utils.structure;\n\nimport java.util.ArrayDeque;\n\n/**\n *\n * FIFO limited array.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 07.09.16.\n */\npublic class LimitedArrayDeque<T> extends ArrayDeque<T> {\n\n    private final int limit;\n\n    public LimitedArrayDeque(int capacity) {\n        super(capacity);\n        this.limit = capacity - 1;\n    }\n\n    @Override\n    public boolean add(T element) {\n        while (size() > limit) {\n            removeFirst();\n        }\n\n        return super.add(element);\n    }\n\n}\n"
  },
  {
    "path": "server/utils/src/main/java/cc/blynk/utils/structure/MapLimitedQueue.java",
    "content": "package cc.blynk.utils.structure;\n\npublic class MapLimitedQueue<T> extends BaseLimitedQueue<T> {\n\n    public static final int POOL_SIZE = Integer.parseInt(System.getProperty(\"map.strings.pool.size\", \"25\"));\n\n    public MapLimitedQueue() {\n        super(POOL_SIZE);\n    }\n\n}\n"
  },
  {
    "path": "server/utils/src/main/java/cc/blynk/utils/structure/TableLimitedQueue.java",
    "content": "package cc.blynk.utils.structure;\n\n/**\n *\n * FIFO limited array.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 07.09.16.\n */\npublic class TableLimitedQueue<T> extends BaseLimitedQueue<T> {\n\n    private static final int POOL_SIZE = Integer.parseInt(System.getProperty(\"table.rows.pool.size\", \"100\"));\n\n    public TableLimitedQueue() {\n        super(POOL_SIZE);\n    }\n\n}\n"
  },
  {
    "path": "server/utils/src/main/java/cc/blynk/utils/structure/TerminalLimitedQueue.java",
    "content": "package cc.blynk.utils.structure;\n\n/**\n *\n * FIFO limited array.\n *\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 07.09.16.\n */\npublic class TerminalLimitedQueue<T> extends BaseLimitedQueue<T> {\n\n    public static final int POOL_SIZE = Integer.parseInt(System.getProperty(\"terminal.strings.pool.size\", \"25\"));\n\n    public TerminalLimitedQueue() {\n        super(POOL_SIZE);\n    }\n\n}\n"
  },
  {
    "path": "server/utils/src/main/java/cc/blynk/utils/validators/BlynkEmailValidator.java",
    "content": "package cc.blynk.utils.validators;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 09.03.17.\n */\npublic final class BlynkEmailValidator {\n\n    private BlynkEmailValidator() {\n    }\n\n    public static boolean isNotValidEmail(String email) {\n        return email == null || email.isEmpty() || email.length() > 255\n                || email.contains(\"?\") || !email.contains(\"@\")\n                || !EmailValidator.getInstance().isValid(email);\n    }\n\n    public static boolean isValidEmails(String emails) {\n        if (emails == null || emails.isEmpty()) {\n            return false;\n        }\n        String[] emailsSplit = emails.split(\",\");\n        if (emailsSplit.length > 5) {\n            return false;\n        }\n        for (String email : emailsSplit) {\n            if (isNotValidEmail(email)) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n}\n"
  },
  {
    "path": "server/utils/src/main/java/cc/blynk/utils/validators/DomainValidator.java",
    "content": "package cc.blynk.utils.validators;\n\nimport java.net.IDN;\nimport java.util.Arrays;\nimport java.util.Locale;\n\n/**\n * <p><b>Domain name</b> validation routines.</p>\n *\n * <p>\n * This validator provides methods for validating Internet domain names\n * and top-level domains.\n * </p>\n *\n * <p>Domain names are evaluated according\n * to the standards <a href=\"http://www.ietf.org/rfc/rfc1034.txt\">RFC1034</a>,\n * section 3, and <a href=\"http://www.ietf.org/rfc/rfc1123.txt\">RFC1123</a>,\n * section 2.1. No accommodation is provided for the specialized needs of\n * other applications; if the domain name has been URL-encoded, for example,\n * validation will fail even though the equivalent plaintext version of the\n * same name would have passed.\n * </p>\n *\n * <p>\n * Validation is also provided for top-level domains (TLDs) as defined and\n * maintained by the Internet Assigned Numbers Authority (IANA):\n * </p>\n *\n *   <ul>\n *     <li>{@link #isValidInfrastructureTld} - validates infrastructure TLDs\n *         (<code>.arpa</code>, etc.)</li>\n *     <li>{@link #isValidGenericTld} - validates generic TLDs\n *         (<code>.com, .org</code>, etc.)</li>\n *     <li>{@link #isValidCountryCodeTld} - validates country code TLDs\n *         (<code>.us, .uk, .cn</code>, etc.)</li>\n *   </ul>\n *\n * <p>\n * (<b>NOTE</b>: This class does not provide IP address lookup for domain names or\n * methods to ensure that a given domain name matches a specific IP; see\n * {@link java.net.InetAddress} for that functionality.)\n * </p>\n *\n * @version $Revision: 1781829 $\n * @since Validator 1.4\n */\npublic final class DomainValidator {\n\n    private static final int MAX_DOMAIN_LENGTH = 253;\n\n    private static final String[] EMPTY_STRING_ARRAY = new String[0];\n\n    // Regular expression strings for hostnames (derived from RFC2396 and RFC 1123)\n\n    // RFC2396: domainlabel   = alphanum | alphanum *( alphanum | \"-\" ) alphanum\n    // Max 63 characters\n    private static final String DOMAIN_LABEL_REGEX = \"\\\\p{Alnum}(?>[\\\\p{Alnum}-]{0,61}\\\\p{Alnum})?\";\n\n    // RFC2396 toplabel = alpha | alpha *( alphanum | \"-\" ) alphanum\n    // Max 63 characters\n    private static final String TOP_LABEL_REGEX = \"\\\\p{Alpha}(?>[\\\\p{Alnum}-]{0,61}\\\\p{Alnum})?\";\n\n    // RFC2396 hostname = *( domainlabel \".\" ) toplabel [ \".\" ]\n    // Note that the regex currently requires both a domain label and a top level label, whereas\n    // the RFC does not. This is because the regex is used to detect if a TLD is present.\n    // If the match fails, input is checked against DOMAIN_LABEL_REGEX (hostnameRegex)\n    // RFC1123 sec 2.1 allows hostnames to start with a digit\n    private static final String DOMAIN_NAME_REGEX =\n            \"^(?:\" + DOMAIN_LABEL_REGEX + \"\\\\.)+\" + \"(\" + TOP_LABEL_REGEX + \")\\\\.?$\";\n\n    private final boolean allowLocal;\n\n    /**\n     * Singleton instance of this validator, which\n     *  doesn't consider local addresses as valid.\n     */\n    private static final DomainValidator DOMAIN_VALIDATOR = new DomainValidator(false);\n\n    /**\n     * Singleton instance of this validator, which does\n     *  consider local addresses valid.\n     */\n    private static final DomainValidator DOMAIN_VALIDATOR_WITH_LOCAL = new DomainValidator(true);\n\n    /**\n     * RegexValidator for matching domains.\n     */\n    private final RegexValidator domainRegex =\n            new RegexValidator(DOMAIN_NAME_REGEX);\n    /**\n     * RegexValidator for matching a local hostname\n     */\n    // RFC1123 sec 2.1 allows hostnames to start with a digit\n    private final RegexValidator hostnameRegex =\n            new RegexValidator(DOMAIN_LABEL_REGEX);\n\n    /**\n     * Returns the singleton instance of this validator. It\n     *  will not consider local addresses as valid.\n     * @return the singleton instance of this validator\n     */\n    public static synchronized DomainValidator getInstance() {\n        inUse = true;\n        return DOMAIN_VALIDATOR;\n    }\n\n    /**\n     * Returns the singleton instance of this validator,\n     *  with local validation as required.\n     * @param allowLocal Should local addresses be considered valid?\n     * @return the singleton instance of this validator\n     */\n    public static synchronized DomainValidator getInstance(boolean allowLocal) {\n        inUse = true;\n        if (allowLocal) {\n            return DOMAIN_VALIDATOR_WITH_LOCAL;\n        }\n        return DOMAIN_VALIDATOR;\n    }\n\n    /** Private constructor. */\n    private DomainValidator(boolean allowLocal) {\n        this.allowLocal = allowLocal;\n    }\n\n    /**\n     * Returns true if the specified <code>String</code> parses\n     * as a valid domain name with a recognized top-level domain.\n     * The parsing is case-insensitive.\n     * @param domain the parameter to check for domain name syntax\n     * @return true if the parameter is a valid domain name\n     */\n    public boolean isValid(String domain) {\n        if (domain == null) {\n            return false;\n        }\n        domain = unicodeToASCII(domain);\n        // hosts must be equally reachable via punycode and Unicode;\n        // Unicode is never shorter than punycode, so check punycode\n        // if domain did not convert, then it will be caught by ASCII\n        // checks in the regexes below\n        if (domain.length() > MAX_DOMAIN_LENGTH) {\n            return false;\n        }\n        String[] groups = domainRegex.match(domain);\n        if (groups != null && groups.length > 0) {\n            return isValidTld(groups[0]);\n        }\n        return allowLocal && hostnameRegex.isValid(domain);\n    }\n\n    /**\n     * Returns true if the specified <code>String</code> matches any\n     * IANA-defined top-level domain. Leading dots are ignored if present.\n     * The search is case-insensitive.\n     * @param tld the parameter to check for TLD status, not null\n     * @return true if the parameter is a TLD\n     */\n    public boolean isValidTld(String tld) {\n        tld = unicodeToASCII(tld);\n        if (allowLocal && isValidLocalTld(tld)) {\n            return true;\n        }\n        return isValidInfrastructureTld(tld)\n                || isValidGenericTld(tld)\n                || isValidCountryCodeTld(tld);\n    }\n\n    /**\n     * Returns true if the specified <code>String</code> matches any\n     * IANA-defined infrastructure top-level domain. Leading dots are\n     * ignored if present. The search is case-insensitive.\n     * @param iTld the parameter to check for infrastructure TLD status, not null\n     * @return true if the parameter is an infrastructure TLD\n     */\n    public boolean isValidInfrastructureTld(String iTld) {\n        final String key = chompLeadingDot(unicodeToASCII(iTld).toLowerCase(Locale.ENGLISH));\n        return arrayContains(INFRASTRUCTURE_TLDS, key);\n    }\n\n    /**\n     * Returns true if the specified <code>String</code> matches any\n     * IANA-defined generic top-level domain. Leading dots are ignored\n     * if present. The search is case-insensitive.\n     * @param gTld the parameter to check for generic TLD status, not null\n     * @return true if the parameter is a generic TLD\n     */\n    public boolean isValidGenericTld(String gTld) {\n        final String key = chompLeadingDot(unicodeToASCII(gTld).toLowerCase(Locale.ENGLISH));\n        return (arrayContains(GENERIC_TLDS, key) || arrayContains(genericTLDsPlus, key))\n                && !arrayContains(genericTLDsMinus, key);\n    }\n\n    /**\n     * Returns true if the specified <code>String</code> matches any\n     * IANA-defined country code top-level domain. Leading dots are\n     * ignored if present. The search is case-insensitive.\n     * @param ccTld the parameter to check for country code TLD status, not null\n     * @return true if the parameter is a country code TLD\n     */\n    public boolean isValidCountryCodeTld(String ccTld) {\n        final String key = chompLeadingDot(unicodeToASCII(ccTld).toLowerCase(Locale.ENGLISH));\n        return (arrayContains(COUNTRY_CODE_TLDS, key) || arrayContains(countryCodeTLDsPlus, key))\n                && !arrayContains(countryCodeTLDsMinus, key);\n    }\n\n    /**\n     * Returns true if the specified <code>String</code> matches any\n     * widely used \"local\" domains (localhost or localdomain). Leading dots are\n     * ignored if present. The search is case-insensitive.\n     * @param lTld the parameter to check for local TLD status, not null\n     * @return true if the parameter is an local TLD\n     */\n    public boolean isValidLocalTld(String lTld) {\n        final String key = chompLeadingDot(unicodeToASCII(lTld).toLowerCase(Locale.ENGLISH));\n        return arrayContains(LOCAL_TLDS, key);\n    }\n\n    private String chompLeadingDot(String str) {\n        if (str.startsWith(\".\")) {\n            return str.substring(1);\n        }\n        return str;\n    }\n\n    // ---------------------------------------------\n    // ----- TLDs defined by IANA\n    // ----- Authoritative and comprehensive list at:\n    // ----- http://data.iana.org/TLD/tlds-alpha-by-domain.txt\n\n    // Note that the above list is in UPPER case.\n    // The code currently converts strings to lower case (as per the tables below)\n\n    // IANA also provide an HTML list at http://www.iana.org/domains/root/db\n    // Note that this contains several country code entries which are NOT in\n    // the text file. These all have the \"Not assigned\" in the \"Sponsoring Organisation\" column\n    // For example (as of 2015-01-02):\n    // .bl  country-code    Not assigned\n    // .um  country-code    Not assigned\n\n    // WARNING: this array MUST be sorted, otherwise it cannot be searched reliably using binary search\n    private static final String[] INFRASTRUCTURE_TLDS = new String[] {\n        \"arpa\",               // internet infrastructure\n    };\n\n    // WARNING: this array MUST be sorted, otherwise it cannot be searched reliably using binary search\n    private static final String[] GENERIC_TLDS = new String[] {\n        // Taken from Version 2017020400, Last Updated Sat Feb  4 07:07:01 2017 UTC\n        \"aaa\", // aaa American Automobile Association, Inc.\n        \"aarp\", // aarp AARP\n        \"abarth\", // abarth Fiat Chrysler Automobiles N.V.\n        \"abb\", // abb ABB Ltd\n        \"abbott\", // abbott Abbott Laboratories, Inc.\n        \"abbvie\", // abbvie AbbVie Inc.\n        \"abc\", // abc Disney Enterprises, Inc.\n        \"able\", // able Able Inc.\n        \"abogado\", // abogado Top Level Domain Holdings Limited\n        \"abudhabi\", // abudhabi Abu Dhabi Systems and Information Centre\n        \"academy\", // academy Half Oaks, LLC\n        \"accenture\", // accenture Accenture plc\n        \"accountant\", // accountant dot Accountant Limited\n        \"accountants\", // accountants Knob Town, LLC\n        \"aco\", // aco ACO Severin Ahlmann GmbH &amp; Co. KG\n        \"active\", // active The Active Network, Inc\n        \"actor\", // actor United TLD Holdco Ltd.\n        \"adac\", // adac Allgemeiner Deutscher Automobil-Club e.V. (ADAC)\n        \"ads\", // ads Charleston Road Registry Inc.\n        \"adult\", // adult ICM Registry AD LLC\n        \"aeg\", // aeg Aktiebolaget Electrolux\n        \"aero\", // aero Societe Internationale de Telecommunications Aeronautique (SITA INC USA)\n        \"aetna\", // aetna Aetna Life Insurance Company\n        \"afamilycompany\", // afamilycompany Johnson Shareholdings, Inc.\n        \"afl\", // afl Australian Football League\n        \"agakhan\", // agakhan Fondation Aga Khan (Aga Khan Foundation)\n        \"agency\", // agency Steel Falls, LLC\n        \"aig\", // aig American International Group, Inc.\n        \"aigo\", // aigo aigo Digital Technology Co,Ltd.\n        \"airbus\", // airbus Airbus S.A.S.\n        \"airforce\", // airforce United TLD Holdco Ltd.\n        \"airtel\", // airtel Bharti Airtel Limited\n        \"akdn\", // akdn Fondation Aga Khan (Aga Khan Foundation)\n        \"alfaromeo\", // alfaromeo Fiat Chrysler Automobiles N.V.\n        \"alibaba\", // alibaba Alibaba Group Holding Limited\n        \"alipay\", // alipay Alibaba Group Holding Limited\n        \"allfinanz\", // allfinanz Allfinanz Deutsche Vermögensberatung Aktiengesellschaft\n        \"allstate\", // allstate Allstate Fire and Casualty Insurance Company\n        \"ally\", // ally Ally Financial Inc.\n        \"alsace\", // alsace REGION D ALSACE\n        \"alstom\", // alstom ALSTOM\n        \"americanexpress\", // americanexpress American Express Travel Related Services Company, Inc.\n        \"americanfamily\", // americanfamily AmFam, Inc.\n        \"amex\", // amex American Express Travel Related Services Company, Inc.\n        \"amfam\", // amfam AmFam, Inc.\n        \"amica\", // amica Amica Mutual Insurance Company\n        \"amsterdam\", // amsterdam Gemeente Amsterdam\n        \"analytics\", // analytics Campus IP LLC\n        \"android\", // android Charleston Road Registry Inc.\n        \"anquan\", // anquan QIHOO 360 TECHNOLOGY CO. LTD.\n        \"anz\", // anz Australia and New Zealand Banking Group Limited\n        \"aol\", // aol AOL Inc.\n        \"apartments\", // apartments June Maple, LLC\n        \"app\", // app Charleston Road Registry Inc.\n        \"apple\", // apple Apple Inc.\n        \"aquarelle\", // aquarelle Aquarelle.com\n        \"aramco\", // aramco Aramco Services Company\n        \"archi\", // archi STARTING DOT LIMITED\n        \"army\", // army United TLD Holdco Ltd.\n        \"art\", // art UK Creative Ideas Limited\n        \"arte\", // arte Association Relative à la Télévision Européenne G.E.I.E.\n        \"asda\", // asda Wal-Mart Stores, Inc.\n        \"asia\", // asia DotAsia Organisation Ltd.\n        \"associates\", // associates Baxter Hill, LLC\n        \"athleta\", // athleta The Gap, Inc.\n        \"attorney\", // attorney United TLD Holdco, Ltd\n        \"auction\", // auction United TLD HoldCo, Ltd.\n        \"audi\", // audi AUDI Aktiengesellschaft\n        \"audible\", // audible Amazon Registry Services, Inc.\n        \"audio\", // audio Uniregistry, Corp.\n        \"auspost\", // auspost Australian Postal Corporation\n        \"author\", // author Amazon Registry Services, Inc.\n        \"auto\", // auto Uniregistry, Corp.\n        \"autos\", // autos DERAutos, LLC\n        \"avianca\", // avianca Aerovias del Continente Americano S.A. Avianca\n        \"aws\", // aws Amazon Registry Services, Inc.\n        \"axa\", // axa AXA SA\n        \"azure\", // azure Microsoft Corporation\n        \"baby\", // baby Johnson &amp; Johnson Services, Inc.\n        \"baidu\", // baidu Baidu, Inc.\n        \"banamex\", // banamex Citigroup Inc.\n        \"bananarepublic\", // bananarepublic The Gap, Inc.\n        \"band\", // band United TLD Holdco, Ltd\n        \"bank\", // bank fTLD Registry Services, LLC\n        \"bar\", // bar Punto 2012 Sociedad Anonima Promotora de Inversion de Capital Variable\n        \"barcelona\", // barcelona Municipi de Barcelona\n        \"barclaycard\", // barclaycard Barclays Bank PLC\n        \"barclays\", // barclays Barclays Bank PLC\n        \"barefoot\", // barefoot Gallo Vineyards, Inc.\n        \"bargains\", // bargains Half Hallow, LLC\n        \"baseball\", // baseball MLB Advanced Media DH, LLC\n        \"basketball\", // basketball Fédération Internationale de Basketball (FIBA)\n        \"bauhaus\", // bauhaus Werkhaus GmbH\n        \"bayern\", // bayern Bayern Connect GmbH\n        \"bbc\", // bbc British Broadcasting Corporation\n        \"bbt\", // bbt BB&amp;T Corporation\n        \"bbva\", // bbva BANCO BILBAO VIZCAYA ARGENTARIA, S.A.\n        \"bcg\", // bcg The Boston Consulting Group, Inc.\n        \"bcn\", // bcn Municipi de Barcelona\n        \"beats\", // beats Beats Electronics, LLC\n        \"beauty\", // beauty L&#39;Oréal\n        \"beer\", // beer Top Level Domain Holdings Limited\n        \"bentley\", // bentley Bentley Motors Limited\n        \"berlin\", // berlin dotBERLIN GmbH &amp; Co. KG\n        \"best\", // best BestTLD Pty Ltd\n        \"bestbuy\", // bestbuy BBY Solutions, Inc.\n        \"bet\", // bet Afilias plc\n        \"bharti\", // bharti Bharti Enterprises (Holding) Private Limited\n        \"bible\", // bible American Bible Society\n        \"bid\", // bid dot Bid Limited\n        \"bike\", // bike Grand Hollow, LLC\n        \"bing\", // bing Microsoft Corporation\n        \"bingo\", // bingo Sand Cedar, LLC\n        \"bio\", // bio STARTING DOT LIMITED\n        \"biz\", // biz Neustar, Inc.\n        \"black\", // black Afilias Limited\n        \"blackfriday\", // blackfriday Uniregistry, Corp.\n        \"blanco\", // blanco BLANCO GmbH + Co KG\n        \"blockbuster\", // blockbuster Dish DBS Corporation\n        \"blog\", // blog Knock Knock WHOIS There, LLC\n        \"bloomberg\", // bloomberg Bloomberg IP Holdings LLC\n        \"blue\", // blue Afilias Limited\n        \"bms\", // bms Bristol-Myers Squibb Company\n        \"bmw\", // bmw Bayerische Motoren Werke Aktiengesellschaft\n        \"bnl\", // bnl Banca Nazionale del Lavoro\n        \"bnpparibas\", // bnpparibas BNP Paribas\n        \"boats\", // boats DERBoats, LLC\n        \"boehringer\", // boehringer Boehringer Ingelheim International GmbH\n        \"bofa\", // bofa NMS Services, Inc.\n        \"bom\", // bom Núcleo de Informação e Coordenação do Ponto BR - NIC.br\n        \"bond\", // bond Bond University Limited\n        \"boo\", // boo Charleston Road Registry Inc.\n        \"book\", // book Amazon Registry Services, Inc.\n        \"booking\", // booking Booking.com B.V.\n        \"boots\", // boots THE BOOTS COMPANY PLC\n        \"bosch\", // bosch Robert Bosch GMBH\n        \"bostik\", // bostik Bostik SA\n        \"boston\", // boston Boston TLD Management, LLC\n        \"bot\", // bot Amazon Registry Services, Inc.\n        \"boutique\", // boutique Over Galley, LLC\n        \"box\", // box NS1 Limited\n        \"bradesco\", // bradesco Banco Bradesco S.A.\n        \"bridgestone\", // bridgestone Bridgestone Corporation\n        \"broadway\", // broadway Celebrate Broadway, Inc.\n        \"broker\", // broker DOTBROKER REGISTRY LTD\n        \"brother\", // brother Brother Industries, Ltd.\n        \"brussels\", // brussels DNS.be vzw\n        \"budapest\", // budapest Top Level Domain Holdings Limited\n        \"bugatti\", // bugatti Bugatti International SA\n        \"build\", // build Plan Bee LLC\n        \"builders\", // builders Atomic Madison, LLC\n        \"business\", // business Spring Cross, LLC\n        \"buy\", // buy Amazon Registry Services, INC\n        \"buzz\", // buzz DOTSTRATEGY CO.\n        \"bzh\", // bzh Association www.bzh\n        \"cab\", // cab Half Sunset, LLC\n        \"cafe\", // cafe Pioneer Canyon, LLC\n        \"cal\", // cal Charleston Road Registry Inc.\n        \"call\", // call Amazon Registry Services, Inc.\n        \"calvinklein\", // calvinklein PVH gTLD Holdings LLC\n        \"cam\", // cam AC Webconnecting Holding B.V.\n        \"camera\", // camera Atomic Maple, LLC\n        \"camp\", // camp Delta Dynamite, LLC\n        \"cancerresearch\", // cancerresearch Australian Cancer Research Foundation\n        \"canon\", // canon Canon Inc.\n        \"capetown\", // capetown ZA Central Registry NPC trading as ZA Central Registry\n        \"capital\", // capital Delta Mill, LLC\n        \"capitalone\", // capitalone Capital One Financial Corporation\n        \"car\", // car Cars Registry Limited\n        \"caravan\", // caravan Caravan International, Inc.\n        \"cards\", // cards Foggy Hollow, LLC\n        \"care\", // care Goose Cross, LLC\n        \"career\", // career dotCareer LLC\n        \"careers\", // careers Wild Corner, LLC\n        \"cars\", // cars Uniregistry, Corp.\n        \"cartier\", // cartier Richemont DNS Inc.\n        \"casa\", // casa Top Level Domain Holdings Limited\n        \"case\", // case CNH Industrial N.V.\n        \"caseih\", // caseih CNH Industrial N.V.\n        \"cash\", // cash Delta Lake, LLC\n        \"casino\", // casino Binky Sky, LLC\n        \"cat\", // cat Fundacio puntCAT\n        \"catering\", // catering New Falls. LLC\n        \"catholic\", // catholic Pontificium Consilium de Comunicationibus Socialibus (PCCS)\n        \"cba\", // cba COMMONWEALTH BANK OF AUSTRALIA\n        \"cbn\", // cbn The Christian Broadcasting Network, Inc.\n        \"cbre\", // cbre CBRE, Inc.\n        \"cbs\", // cbs CBS Domains Inc.\n        \"ceb\", // ceb The Corporate Executive Board Company\n        \"center\", // center Tin Mill, LLC\n        \"ceo\", // ceo CEOTLD Pty Ltd\n        \"cern\", // cern European Organization for Nuclear Research (&quot;CERN&quot;)\n        \"cfa\", // cfa CFA Institute\n        \"cfd\", // cfd DOTCFD REGISTRY LTD\n        \"chanel\", // chanel Chanel International B.V.\n        \"channel\", // channel Charleston Road Registry Inc.\n        \"chase\", // chase JPMorgan Chase &amp; Co.\n        \"chat\", // chat Sand Fields, LLC\n        \"cheap\", // cheap Sand Cover, LLC\n        \"chintai\", // chintai CHINTAI Corporation\n        \"chloe\", // chloe Richemont DNS Inc.\n        \"christmas\", // christmas Uniregistry, Corp.\n        \"chrome\", // chrome Charleston Road Registry Inc.\n        \"chrysler\", // chrysler FCA US LLC.\n        \"church\", // church Holly Fileds, LLC\n        \"cipriani\", // cipriani Hotel Cipriani Srl\n        \"circle\", // circle Amazon Registry Services, Inc.\n        \"cisco\", // cisco Cisco Technology, Inc.\n        \"citadel\", // citadel Citadel Domain LLC\n        \"citi\", // citi Citigroup Inc.\n        \"citic\", // citic CITIC Group Corporation\n        \"city\", // city Snow Sky, LLC\n        \"cityeats\", // cityeats Lifestyle Domain Holdings, Inc.\n        \"claims\", // claims Black Corner, LLC\n        \"cleaning\", // cleaning Fox Shadow, LLC\n        \"click\", // click Uniregistry, Corp.\n        \"clinic\", // clinic Goose Park, LLC\n        \"clinique\", // clinique The Estée Lauder Companies Inc.\n        \"clothing\", // clothing Steel Lake, LLC\n        \"cloud\", // cloud ARUBA S.p.A.\n        \"club\", // club .CLUB DOMAINS, LLC\n        \"clubmed\", // clubmed Club Méditerranée S.A.\n        \"coach\", // coach Koko Island, LLC\n        \"codes\", // codes Puff Willow, LLC\n        \"coffee\", // coffee Trixy Cover, LLC\n        \"college\", // college XYZ.COM LLC\n        \"cologne\", // cologne NetCologne Gesellschaft für Telekommunikation mbH\n        \"com\", // com VeriSign Global Registry Services\n        \"comcast\", // comcast Comcast IP Holdings I, LLC\n        \"commbank\", // commbank COMMONWEALTH BANK OF AUSTRALIA\n        \"community\", // community Fox Orchard, LLC\n        \"company\", // company Silver Avenue, LLC\n        \"compare\", // compare iSelect Ltd\n        \"computer\", // computer Pine Mill, LLC\n        \"comsec\", // comsec VeriSign, Inc.\n        \"condos\", // condos Pine House, LLC\n        \"construction\", // construction Fox Dynamite, LLC\n        \"consulting\", // consulting United TLD Holdco, LTD.\n        \"contact\", // contact Top Level Spectrum, Inc.\n        \"contractors\", // contractors Magic Woods, LLC\n        \"cooking\", // cooking Top Level Domain Holdings Limited\n        \"cookingchannel\", // cookingchannel Lifestyle Domain Holdings, Inc.\n        \"cool\", // cool Koko Lake, LLC\n        \"coop\", // coop DotCooperation LLC\n        \"corsica\", // corsica Collectivité Territoriale de Corse\n        \"country\", // country Top Level Domain Holdings Limited\n        \"coupon\", // coupon Amazon Registry Services, Inc.\n        \"coupons\", // coupons Black Island, LLC\n        \"courses\", // courses OPEN UNIVERSITIES AUSTRALIA PTY LTD\n        \"credit\", // credit Snow Shadow, LLC\n        \"creditcard\", // creditcard Binky Frostbite, LLC\n        \"creditunion\", // creditunion CUNA Performance Resources, LLC\n        \"cricket\", // cricket dot Cricket Limited\n        \"crown\", // crown Crown Equipment Corporation\n        \"crs\", // crs Federated Co-operatives Limited\n        \"cruise\", // cruise Viking River Cruises (Bermuda) Ltd.\n        \"cruises\", // cruises Spring Way, LLC\n        \"csc\", // csc Alliance-One Services, Inc.\n        \"cuisinella\", // cuisinella SALM S.A.S.\n        \"cymru\", // cymru Nominet UK\n        \"cyou\", // cyou Beijing Gamease Age Digital Technology Co., Ltd.\n        \"dabur\", // dabur Dabur India Limited\n        \"dad\", // dad Charleston Road Registry Inc.\n        \"dance\", // dance United TLD Holdco Ltd.\n        \"data\", // data Dish DBS Corporation\n        \"date\", // date dot Date Limited\n        \"dating\", // dating Pine Fest, LLC\n        \"datsun\", // datsun NISSAN MOTOR CO., LTD.\n        \"day\", // day Charleston Road Registry Inc.\n        \"dclk\", // dclk Charleston Road Registry Inc.\n        \"dds\", // dds Minds + Machines Group Limited\n        \"deal\", // deal Amazon Registry Services, Inc.\n        \"dealer\", // dealer Dealer Dot Com, Inc.\n        \"deals\", // deals Sand Sunset, LLC\n        \"degree\", // degree United TLD Holdco, Ltd\n        \"delivery\", // delivery Steel Station, LLC\n        \"dell\", // dell Dell Inc.\n        \"deloitte\", // deloitte Deloitte Touche Tohmatsu\n        \"delta\", // delta Delta Air Lines, Inc.\n        \"democrat\", // democrat United TLD Holdco Ltd.\n        \"dental\", // dental Tin Birch, LLC\n        \"dentist\", // dentist United TLD Holdco, Ltd\n        \"desi\", // desi Desi Networks LLC\n        \"design\", // design Top Level Design, LLC\n        \"dev\", // dev Charleston Road Registry Inc.\n        \"dhl\", // dhl Deutsche Post AG\n        \"diamonds\", // diamonds John Edge, LLC\n        \"diet\", // diet Uniregistry, Corp.\n        \"digital\", // digital Dash Park, LLC\n        \"direct\", // direct Half Trail, LLC\n        \"directory\", // directory Extra Madison, LLC\n        \"discount\", // discount Holly Hill, LLC\n        \"discover\", // discover Discover Financial Services\n        \"dish\", // dish Dish DBS Corporation\n        \"diy\", // diy Lifestyle Domain Holdings, Inc.\n        \"dnp\", // dnp Dai Nippon Printing Co., Ltd.\n        \"docs\", // docs Charleston Road Registry Inc.\n        \"doctor\", // doctor Brice Trail, LLC\n        \"dodge\", // dodge FCA US LLC.\n        \"dog\", // dog Koko Mill, LLC\n        \"doha\", // doha Communications Regulatory Authority (CRA)\n        \"domains\", // domains Sugar Cross, LLC\n//            \"doosan\", // doosan Doosan Corporation (retired)\n        \"dot\", // dot Dish DBS Corporation\n        \"download\", // download dot Support Limited\n        \"drive\", // drive Charleston Road Registry Inc.\n        \"dtv\", // dtv Dish DBS Corporation\n        \"dubai\", // dubai Dubai Smart Government Department\n        \"duck\", // duck Johnson Shareholdings, Inc.\n        \"dunlop\", // dunlop The Goodyear Tire &amp; Rubber Company\n        \"duns\", // duns The Dun &amp; Bradstreet Corporation\n        \"dupont\", // dupont E. I. du Pont de Nemours and Company\n        \"durban\", // durban ZA Central Registry NPC trading as ZA Central Registry\n        \"dvag\", // dvag Deutsche Vermögensberatung Aktiengesellschaft DVAG\n        \"dvr\", // dvr Hughes Satellite Systems Corporation\n        \"earth\", // earth Interlink Co., Ltd.\n        \"eat\", // eat Charleston Road Registry Inc.\n        \"eco\", // eco Big Room Inc.\n        \"edeka\", // edeka EDEKA Verband kaufmännischer Genossenschaften e.V.\n        \"edu\", // edu EDUCAUSE\n        \"education\", // education Brice Way, LLC\n        \"email\", // email Spring Madison, LLC\n        \"emerck\", // emerck Merck KGaA\n        \"energy\", // energy Binky Birch, LLC\n        \"engineer\", // engineer United TLD Holdco Ltd.\n        \"engineering\", // engineering Romeo Canyon\n        \"enterprises\", // enterprises Snow Oaks, LLC\n        \"epost\", // epost Deutsche Post AG\n        \"epson\", // epson Seiko Epson Corporation\n        \"equipment\", // equipment Corn Station, LLC\n        \"ericsson\", // ericsson Telefonaktiebolaget L M Ericsson\n        \"erni\", // erni ERNI Group Holding AG\n        \"esq\", // esq Charleston Road Registry Inc.\n        \"estate\", // estate Trixy Park, LLC\n        \"esurance\", // esurance Esurance Insurance Company\n        \"eurovision\", // eurovision European Broadcasting Union (EBU)\n        \"eus\", // eus Puntueus Fundazioa\n        \"events\", // events Pioneer Maple, LLC\n        \"everbank\", // everbank EverBank\n        \"exchange\", // exchange Spring Falls, LLC\n        \"expert\", // expert Magic Pass, LLC\n        \"exposed\", // exposed Victor Beach, LLC\n        \"express\", // express Sea Sunset, LLC\n        \"extraspace\", // extraspace Extra Space Storage LLC\n        \"fage\", // fage Fage International S.A.\n        \"fail\", // fail Atomic Pipe, LLC\n        \"fairwinds\", // fairwinds FairWinds Partners, LLC\n        \"faith\", // faith dot Faith Limited\n        \"family\", // family United TLD Holdco Ltd.\n        \"fan\", // fan Asiamix Digital Ltd\n        \"fans\", // fans Asiamix Digital Limited\n        \"farm\", // farm Just Maple, LLC\n        \"farmers\", // farmers Farmers Insurance Exchange\n        \"fashion\", // fashion Top Level Domain Holdings Limited\n        \"fast\", // fast Amazon Registry Services, Inc.\n        \"fedex\", // fedex Federal Express Corporation\n        \"feedback\", // feedback Top Level Spectrum, Inc.\n        \"ferrari\", // ferrari Fiat Chrysler Automobiles N.V.\n        \"ferrero\", // ferrero Ferrero Trading Lux S.A.\n        \"fiat\", // fiat Fiat Chrysler Automobiles N.V.\n        \"fidelity\", // fidelity Fidelity Brokerage Services LLC\n        \"fido\", // fido Rogers Communications Canada Inc.\n        \"film\", // film Motion Picture Domain Registry Pty Ltd\n        \"final\", // final Núcleo de Informação e Coordenação do Ponto BR - NIC.br\n        \"finance\", // finance Cotton Cypress, LLC\n        \"financial\", // financial Just Cover, LLC\n        \"fire\", // fire Amazon Registry Services, Inc.\n        \"firestone\", // firestone Bridgestone Corporation\n        \"firmdale\", // firmdale Firmdale Holdings Limited\n        \"fish\", // fish Fox Woods, LLC\n        \"fishing\", // fishing Top Level Domain Holdings Limited\n        \"fit\", // fit Minds + Machines Group Limited\n        \"fitness\", // fitness Brice Orchard, LLC\n        \"flickr\", // flickr Yahoo! Domain Services Inc.\n        \"flights\", // flights Fox Station, LLC\n        \"flir\", // flir FLIR Systems, Inc.\n        \"florist\", // florist Half Cypress, LLC\n        \"flowers\", // flowers Uniregistry, Corp.\n//        \"flsmidth\", // flsmidth FLSmidth A/S retired 2016-07-22\n        \"fly\", // fly Charleston Road Registry Inc.\n        \"foo\", // foo Charleston Road Registry Inc.\n        \"food\", // food Lifestyle Domain Holdings, Inc.\n        \"foodnetwork\", // foodnetwork Lifestyle Domain Holdings, Inc.\n        \"football\", // football Foggy Farms, LLC\n        \"ford\", // ford Ford Motor Company\n        \"forex\", // forex DOTFOREX REGISTRY LTD\n        \"forsale\", // forsale United TLD Holdco, LLC\n        \"forum\", // forum Fegistry, LLC\n        \"foundation\", // foundation John Dale, LLC\n        \"fox\", // fox FOX Registry, LLC\n        \"free\", // free Amazon Registry Services, Inc.\n        \"fresenius\", // fresenius Fresenius Immobilien-Verwaltungs-GmbH\n        \"frl\", // frl FRLregistry B.V.\n        \"frogans\", // frogans OP3FT\n        \"frontdoor\", // frontdoor Lifestyle Domain Holdings, Inc.\n        \"frontier\", // frontier Frontier Communications Corporation\n        \"ftr\", // ftr Frontier Communications Corporation\n        \"fujitsu\", // fujitsu Fujitsu Limited\n        \"fujixerox\", // fujixerox Xerox DNHC LLC\n        \"fun\", // fun DotSpace, Inc.\n        \"fund\", // fund John Castle, LLC\n        \"furniture\", // furniture Lone Fields, LLC\n        \"futbol\", // futbol United TLD Holdco, Ltd.\n        \"fyi\", // fyi Silver Tigers, LLC\n        \"gal\", // gal Asociación puntoGAL\n        \"gallery\", // gallery Sugar House, LLC\n        \"gallo\", // gallo Gallo Vineyards, Inc.\n        \"gallup\", // gallup Gallup, Inc.\n        \"game\", // game Uniregistry, Corp.\n        \"games\", // games United TLD Holdco Ltd.\n        \"gap\", // gap The Gap, Inc.\n        \"garden\", // garden Top Level Domain Holdings Limited\n        \"gbiz\", // gbiz Charleston Road Registry Inc.\n        \"gdn\", // gdn Joint Stock Company \"Navigation-information systems\"\n        \"gea\", // gea GEA Group Aktiengesellschaft\n        \"gent\", // gent COMBELL GROUP NV/SA\n        \"genting\", // genting Resorts World Inc. Pte. Ltd.\n        \"george\", // george Wal-Mart Stores, Inc.\n        \"ggee\", // ggee GMO Internet, Inc.\n        \"gift\", // gift Uniregistry, Corp.\n        \"gifts\", // gifts Goose Sky, LLC\n        \"gives\", // gives United TLD Holdco Ltd.\n        \"giving\", // giving Giving Limited\n        \"glade\", // glade Johnson Shareholdings, Inc.\n        \"glass\", // glass Black Cover, LLC\n        \"gle\", // gle Charleston Road Registry Inc.\n        \"global\", // global Dot Global Domain Registry Limited\n        \"globo\", // globo Globo Comunicação e Participações S.A\n        \"gmail\", // gmail Charleston Road Registry Inc.\n        \"gmbh\", // gmbh Extra Dynamite, LLC\n        \"gmo\", // gmo GMO Internet, Inc.\n        \"gmx\", // gmx 1&amp;1 Mail &amp; Media GmbH\n        \"godaddy\", // godaddy Go Daddy East, LLC\n        \"gold\", // gold June Edge, LLC\n        \"goldpoint\", // goldpoint YODOBASHI CAMERA CO.,LTD.\n        \"golf\", // golf Lone Falls, LLC\n        \"goo\", // goo NTT Resonant Inc.\n        \"goodhands\", // goodhands Allstate Fire and Casualty Insurance Company\n        \"goodyear\", // goodyear The Goodyear Tire &amp; Rubber Company\n        \"goog\", // goog Charleston Road Registry Inc.\n        \"google\", // google Charleston Road Registry Inc.\n        \"gop\", // gop Republican State Leadership Committee, Inc.\n        \"got\", // got Amazon Registry Services, Inc.\n        \"gov\", // gov General Services Administration Attn: QTDC, 2E08 (.gov Domain Registration)\n        \"grainger\", // grainger Grainger Registry Services, LLC\n        \"graphics\", // graphics Over Madison, LLC\n        \"gratis\", // gratis Pioneer Tigers, LLC\n        \"green\", // green Afilias Limited\n        \"gripe\", // gripe Corn Sunset, LLC\n        \"group\", // group Romeo Town, LLC\n        \"guardian\", // guardian The Guardian Life Insurance Company of America\n        \"gucci\", // gucci Guccio Gucci S.p.a.\n        \"guge\", // guge Charleston Road Registry Inc.\n        \"guide\", // guide Snow Moon, LLC\n        \"guitars\", // guitars Uniregistry, Corp.\n        \"guru\", // guru Pioneer Cypress, LLC\n        \"hair\", // hair L&#39;Oreal\n        \"hamburg\", // hamburg Hamburg Top-Level-Domain GmbH\n        \"hangout\", // hangout Charleston Road Registry Inc.\n        \"haus\", // haus United TLD Holdco, LTD.\n        \"hbo\", // hbo HBO Registry Services, Inc.\n        \"hdfc\", // hdfc HOUSING DEVELOPMENT FINANCE CORPORATION LIMITED\n        \"hdfcbank\", // hdfcbank HDFC Bank Limited\n        \"health\", // health DotHealth, LLC\n        \"healthcare\", // healthcare Silver Glen, LLC\n        \"help\", // help Uniregistry, Corp.\n        \"helsinki\", // helsinki City of Helsinki\n        \"here\", // here Charleston Road Registry Inc.\n        \"hermes\", // hermes Hermes International\n        \"hgtv\", // hgtv Lifestyle Domain Holdings, Inc.\n        \"hiphop\", // hiphop Uniregistry, Corp.\n        \"hisamitsu\", // hisamitsu Hisamitsu Pharmaceutical Co.,Inc.\n        \"hitachi\", // hitachi Hitachi, Ltd.\n        \"hiv\", // hiv dotHIV gemeinnuetziger e.V.\n        \"hkt\", // hkt PCCW-HKT DataCom Services Limited\n        \"hockey\", // hockey Half Willow, LLC\n        \"holdings\", // holdings John Madison, LLC\n        \"holiday\", // holiday Goose Woods, LLC\n        \"homedepot\", // homedepot Homer TLC, Inc.\n        \"homegoods\", // homegoods The TJX Companies, Inc.\n        \"homes\", // homes DERHomes, LLC\n        \"homesense\", // homesense The TJX Companies, Inc.\n        \"honda\", // honda Honda Motor Co., Ltd.\n        \"honeywell\", // honeywell Honeywell GTLD LLC\n        \"horse\", // horse Top Level Domain Holdings Limited\n        \"hospital\", // hospital Ruby Pike, LLC\n        \"host\", // host DotHost Inc.\n        \"hosting\", // hosting Uniregistry, Corp.\n        \"hot\", // hot Amazon Registry Services, Inc.\n        \"hoteles\", // hoteles Travel Reservations SRL\n        \"hotmail\", // hotmail Microsoft Corporation\n        \"house\", // house Sugar Park, LLC\n        \"how\", // how Charleston Road Registry Inc.\n        \"hsbc\", // hsbc HSBC Holdings PLC\n        \"htc\", // htc HTC corporation\n        \"hughes\", // hughes Hughes Satellite Systems Corporation\n        \"hyatt\", // hyatt Hyatt GTLD, L.L.C.\n        \"hyundai\", // hyundai Hyundai Motor Company\n        \"ibm\", // ibm International Business Machines Corporation\n        \"icbc\", // icbc Industrial and Commercial Bank of China Limited\n        \"ice\", // ice IntercontinentalExchange, Inc.\n        \"icu\", // icu One.com A/S\n        \"ieee\", // ieee IEEE Global LLC\n        \"ifm\", // ifm ifm electronic gmbh\n//        \"iinet\", // iinet Connect West Pty. Ltd. (Retired)\n        \"ikano\", // ikano Ikano S.A.\n        \"imamat\", // imamat Fondation Aga Khan (Aga Khan Foundation)\n        \"imdb\", // imdb Amazon Registry Services, Inc.\n        \"immo\", // immo Auburn Bloom, LLC\n        \"immobilien\", // immobilien United TLD Holdco Ltd.\n        \"industries\", // industries Outer House, LLC\n        \"infiniti\", // infiniti NISSAN MOTOR CO., LTD.\n        \"info\", // info Afilias Limited\n        \"ing\", // ing Charleston Road Registry Inc.\n        \"ink\", // ink Top Level Design, LLC\n        \"institute\", // institute Outer Maple, LLC\n        \"insurance\", // insurance fTLD Registry Services LLC\n        \"insure\", // insure Pioneer Willow, LLC\n        \"int\", // int Internet Assigned Numbers Authority\n        \"intel\", // intel Intel Corporation\n        \"international\", // international Wild Way, LLC\n        \"intuit\", // intuit Intuit Administrative Services, Inc.\n        \"investments\", // investments Holly Glen, LLC\n        \"ipiranga\", // ipiranga Ipiranga Produtos de Petroleo S.A.\n        \"irish\", // irish Dot-Irish LLC\n        \"iselect\", // iselect iSelect Ltd\n        \"ismaili\", // ismaili Fondation Aga Khan (Aga Khan Foundation)\n        \"ist\", // ist Istanbul Metropolitan Municipality\n        \"istanbul\", // istanbul Istanbul Metropolitan Municipality / Medya A.S.\n        \"itau\", // itau Itau Unibanco Holding S.A.\n        \"itv\", // itv ITV Services Limited\n        \"iveco\", // iveco CNH Industrial N.V.\n        \"iwc\", // iwc Richemont DNS Inc.\n        \"jaguar\", // jaguar Jaguar Land Rover Ltd\n        \"java\", // java Oracle Corporation\n        \"jcb\", // jcb JCB Co., Ltd.\n        \"jcp\", // jcp JCP Media, Inc.\n        \"jeep\", // jeep FCA US LLC.\n        \"jetzt\", // jetzt New TLD Company AB\n        \"jewelry\", // jewelry Wild Bloom, LLC\n        \"jio\", // jio Affinity Names, Inc.\n        \"jlc\", // jlc Richemont DNS Inc.\n        \"jll\", // jll Jones Lang LaSalle Incorporated\n        \"jmp\", // jmp Matrix IP LLC\n        \"jnj\", // jnj Johnson &amp; Johnson Services, Inc.\n        \"jobs\", // jobs Employ Media LLC\n        \"joburg\", // joburg ZA Central Registry NPC trading as ZA Central Registry\n        \"jot\", // jot Amazon Registry Services, Inc.\n        \"joy\", // joy Amazon Registry Services, Inc.\n        \"jpmorgan\", // jpmorgan JPMorgan Chase &amp; Co.\n        \"jprs\", // jprs Japan Registry Services Co., Ltd.\n        \"juegos\", // juegos Uniregistry, Corp.\n        \"juniper\", // juniper JUNIPER NETWORKS, INC.\n        \"kaufen\", // kaufen United TLD Holdco Ltd.\n        \"kddi\", // kddi KDDI CORPORATION\n        \"kerryhotels\", // kerryhotels Kerry Trading Co. Limited\n        \"kerrylogistics\", // kerrylogistics Kerry Trading Co. Limited\n        \"kerryproperties\", // kerryproperties Kerry Trading Co. Limited\n        \"kfh\", // kfh Kuwait Finance House\n        \"kia\", // kia KIA MOTORS CORPORATION\n        \"kim\", // kim Afilias Limited\n        \"kinder\", // kinder Ferrero Trading Lux S.A.\n        \"kindle\", // kindle Amazon Registry Services, Inc.\n        \"kitchen\", // kitchen Just Goodbye, LLC\n        \"kiwi\", // kiwi DOT KIWI LIMITED\n        \"koeln\", // koeln NetCologne Gesellschaft für Telekommunikation mbH\n        \"komatsu\", // komatsu Komatsu Ltd.\n        \"kosher\", // kosher Kosher Marketing Assets LLC\n        \"kpmg\", // kpmg KPMG International Cooperative (KPMG International Genossenschaft)\n        \"kpn\", // kpn Koninklijke KPN N.V.\n        \"krd\", // krd KRG Department of Information Technology\n        \"kred\", // kred KredTLD Pty Ltd\n        \"kuokgroup\", // kuokgroup Kerry Trading Co. Limited\n        \"kyoto\", // kyoto Academic Institution: Kyoto Jyoho Gakuen\n        \"lacaixa\", // lacaixa CAIXA D&#39;ESTALVIS I PENSIONS DE BARCELONA\n        \"ladbrokes\", // ladbrokes LADBROKES INTERNATIONAL PLC\n        \"lamborghini\", // lamborghini Automobili Lamborghini S.p.A.\n        \"lamer\", // lamer The Estée Lauder Companies Inc.\n        \"lancaster\", // lancaster LANCASTER\n        \"lancia\", // lancia Fiat Chrysler Automobiles N.V.\n        \"lancome\", // lancome L&#39;Oréal\n        \"land\", // land Pine Moon, LLC\n        \"landrover\", // landrover Jaguar Land Rover Ltd\n        \"lanxess\", // lanxess LANXESS Corporation\n        \"lasalle\", // lasalle Jones Lang LaSalle Incorporated\n        \"lat\", // lat ECOM-LAC Federación de Latinoamérica y el Caribe para Internet y el Comercio Electrónico\n        \"latino\", // latino Dish DBS Corporation\n        \"latrobe\", // latrobe La Trobe University\n        \"law\", // law Minds + Machines Group Limited\n        \"lawyer\", // lawyer United TLD Holdco, Ltd\n        \"lds\", // lds IRI Domain Management, LLC\n        \"lease\", // lease Victor Trail, LLC\n        \"leclerc\", // leclerc A.C.D. LEC Association des Centres Distributeurs Edouard Leclerc\n        \"lefrak\", // lefrak LeFrak Organization, Inc.\n        \"legal\", // legal Blue Falls, LLC\n        \"lego\", // lego LEGO Juris A/S\n        \"lexus\", // lexus TOYOTA MOTOR CORPORATION\n        \"lgbt\", // lgbt Afilias Limited\n        \"liaison\", // liaison Liaison Technologies, Incorporated\n        \"lidl\", // lidl Schwarz Domains und Services GmbH &amp; Co. KG\n        \"life\", // life Trixy Oaks, LLC\n        \"lifeinsurance\", // lifeinsurance American Council of Life Insurers\n        \"lifestyle\", // lifestyle Lifestyle Domain Holdings, Inc.\n        \"lighting\", // lighting John McCook, LLC\n        \"like\", // like Amazon Registry Services, Inc.\n        \"lilly\", // lilly Eli Lilly and Company\n        \"limited\", // limited Big Fest, LLC\n        \"limo\", // limo Hidden Frostbite, LLC\n        \"lincoln\", // lincoln Ford Motor Company\n        \"linde\", // linde Linde Aktiengesellschaft\n        \"link\", // link Uniregistry, Corp.\n        \"lipsy\", // lipsy Lipsy Ltd\n        \"live\", // live United TLD Holdco Ltd.\n        \"living\", // living Lifestyle Domain Holdings, Inc.\n        \"lixil\", // lixil LIXIL Group Corporation\n        \"loan\", // loan dot Loan Limited\n        \"loans\", // loans June Woods, LLC\n        \"locker\", // locker Dish DBS Corporation\n        \"locus\", // locus Locus Analytics LLC\n        \"loft\", // loft Annco, Inc.\n        \"lol\", // lol Uniregistry, Corp.\n        \"london\", // london Dot London Domains Limited\n        \"lotte\", // lotte Lotte Holdings Co., Ltd.\n        \"lotto\", // lotto Afilias Limited\n        \"love\", // love Merchant Law Group LLP\n        \"lpl\", // lpl LPL Holdings, Inc.\n        \"lplfinancial\", // lplfinancial LPL Holdings, Inc.\n        \"ltd\", // ltd Over Corner, LLC\n        \"ltda\", // ltda InterNetX Corp.\n        \"lundbeck\", // lundbeck H. Lundbeck A/S\n        \"lupin\", // lupin LUPIN LIMITED\n        \"luxe\", // luxe Top Level Domain Holdings Limited\n        \"luxury\", // luxury Luxury Partners LLC\n        \"macys\", // macys Macys, Inc.\n        \"madrid\", // madrid Comunidad de Madrid\n        \"maif\", // maif Mutuelle Assurance Instituteur France (MAIF)\n        \"maison\", // maison Victor Frostbite, LLC\n        \"makeup\", // makeup L&#39;Oréal\n        \"man\", // man MAN SE\n        \"management\", // management John Goodbye, LLC\n        \"mango\", // mango PUNTO FA S.L.\n        \"market\", // market Unitied TLD Holdco, Ltd\n        \"marketing\", // marketing Fern Pass, LLC\n        \"markets\", // markets DOTMARKETS REGISTRY LTD\n        \"marriott\", // marriott Marriott Worldwide Corporation\n        \"marshalls\", // marshalls The TJX Companies, Inc.\n        \"maserati\", // maserati Fiat Chrysler Automobiles N.V.\n        \"mattel\", // mattel Mattel Sites, Inc.\n        \"mba\", // mba Lone Hollow, LLC\n        \"mcd\", // mcd McDonald’s Corporation\n        \"mcdonalds\", // mcdonalds McDonald’s Corporation\n        \"mckinsey\", // mckinsey McKinsey Holdings, Inc.\n        \"med\", // med Medistry LLC\n        \"media\", // media Grand Glen, LLC\n        \"meet\", // meet Afilias Limited\n        \"melbourne\", // melbourne The Crown in right of the State of Victoria\n        \"meme\", // meme Charleston Road Registry Inc.\n        \"memorial\", // memorial Dog Beach, LLC\n        \"men\", // men Exclusive Registry Limited\n        \"menu\", // menu Wedding TLD2, LLC\n        \"meo\", // meo PT Comunicacoes S.A.\n        \"metlife\", // metlife MetLife Services and Solutions, LLC\n        \"miami\", // miami Top Level Domain Holdings Limited\n        \"microsoft\", // microsoft Microsoft Corporation\n        \"mil\", // mil DoD Network Information Center\n        \"mini\", // mini Bayerische Motoren Werke Aktiengesellschaft\n        \"mint\", // mint Intuit Administrative Services, Inc.\n        \"mit\", // mit Massachusetts Institute of Technology\n        \"mitsubishi\", // mitsubishi Mitsubishi Corporation\n        \"mlb\", // mlb MLB Advanced Media DH, LLC\n        \"mls\", // mls The Canadian Real Estate Association\n        \"mma\", // mma MMA IARD\n        \"mobi\", // mobi Afilias Technologies Limited dba dotMobi\n        \"mobile\", // mobile Dish DBS Corporation\n        \"mobily\", // mobily GreenTech Consultancy Company W.L.L.\n        \"moda\", // moda United TLD Holdco Ltd.\n        \"moe\", // moe Interlink Co., Ltd.\n        \"moi\", // moi Amazon Registry Services, Inc.\n        \"mom\", // mom Uniregistry, Corp.\n        \"monash\", // monash Monash University\n        \"money\", // money Outer McCook, LLC\n        \"monster\", // monster Monster Worldwide, Inc.\n        \"montblanc\", // montblanc Richemont DNS Inc.\n        \"mopar\", // mopar FCA US LLC.\n        \"mormon\", // mormon IRI Domain Management, LLC (&quot;Applicant&quot;)\n        \"mortgage\", // mortgage United TLD Holdco, Ltd\n        \"moscow\", // moscow Foundation for Assistance for Internet Technologies and Infrastructure Development (FAITID)\n        \"moto\", // moto Motorola Trademark Holdings, LLC\n        \"motorcycles\", // motorcycles DERMotorcycles, LLC\n        \"mov\", // mov Charleston Road Registry Inc.\n        \"movie\", // movie New Frostbite, LLC\n        \"movistar\", // movistar Telefónica S.A.\n        \"msd\", // msd MSD Registry Holdings, Inc.\n        \"mtn\", // mtn MTN Dubai Limited\n        \"mtpc\", // mtpc Mitsubishi Tanabe Pharma Corporation\n        \"mtr\", // mtr MTR Corporation Limited\n        \"museum\", // museum Museum Domain Management Association\n        \"mutual\", // mutual Northwestern Mutual MU TLD Registry, LLC\n//        \"mutuelle\", // mutuelle Fédération Nationale de la Mutualité Française (Retired)\n        \"nab\", // nab National Australia Bank Limited\n        \"nadex\", // nadex Nadex Domains, Inc\n        \"nagoya\", // nagoya GMO Registry, Inc.\n        \"name\", // name VeriSign Information Services, Inc.\n        \"nationwide\", // nationwide Nationwide Mutual Insurance Company\n        \"natura\", // natura NATURA COSMÉTICOS S.A.\n        \"navy\", // navy United TLD Holdco Ltd.\n        \"nba\", // nba NBA REGISTRY, LLC\n        \"nec\", // nec NEC Corporation\n        \"net\", // net VeriSign Global Registry Services\n        \"netbank\", // netbank COMMONWEALTH BANK OF AUSTRALIA\n        \"netflix\", // netflix Netflix, Inc.\n        \"network\", // network Trixy Manor, LLC\n        \"neustar\", // neustar NeuStar, Inc.\n        \"new\", // new Charleston Road Registry Inc.\n        \"newholland\", // newholland CNH Industrial N.V.\n        \"news\", // news United TLD Holdco Ltd.\n        \"next\", // next Next plc\n        \"nextdirect\", // nextdirect Next plc\n        \"nexus\", // nexus Charleston Road Registry Inc.\n        \"nfl\", // nfl NFL Reg Ops LLC\n        \"ngo\", // ngo Public Interest Registry\n        \"nhk\", // nhk Japan Broadcasting Corporation (NHK)\n        \"nico\", // nico DWANGO Co., Ltd.\n        \"nike\", // nike NIKE, Inc.\n        \"nikon\", // nikon NIKON CORPORATION\n        \"ninja\", // ninja United TLD Holdco Ltd.\n        \"nissan\", // nissan NISSAN MOTOR CO., LTD.\n        \"nissay\", // nissay Nippon Life Insurance Company\n        \"nokia\", // nokia Nokia Corporation\n        \"northwesternmutual\", // northwesternmutual Northwestern Mutual Registry, LLC\n        \"norton\", // norton Symantec Corporation\n        \"now\", // now Amazon Registry Services, Inc.\n        \"nowruz\", // nowruz Asia Green IT System Bilgisayar San. ve Tic. Ltd. Sti.\n        \"nowtv\", // nowtv Starbucks (HK) Limited\n        \"nra\", // nra NRA Holdings Company, INC.\n        \"nrw\", // nrw Minds + Machines GmbH\n        \"ntt\", // ntt NIPPON TELEGRAPH AND TELEPHONE CORPORATION\n        \"nyc\", // nyc The City of New York by\n        \"obi\", // obi OBI Group Holding SE &amp; Co. KGaA\n        \"observer\", // observer Top Level Spectrum, Inc.\n        \"off\", // off Johnson Shareholdings, Inc.\n        \"office\", // office Microsoft Corporation\n        \"okinawa\", // okinawa BusinessRalliart inc.\n        \"olayan\", // olayan Crescent Holding GmbH\n        \"olayangroup\", // olayangroup Crescent Holding GmbH\n        \"oldnavy\", // oldnavy The Gap, Inc.\n        \"ollo\", // ollo Dish DBS Corporation\n        \"omega\", // omega The Swatch Group Ltd\n        \"one\", // one One.com A/S\n        \"ong\", // ong Public Interest Registry\n        \"onl\", // onl I-REGISTRY Ltd., Niederlassung Deutschland\n        \"online\", // online DotOnline Inc.\n        \"onyourside\", // onyourside Nationwide Mutual Insurance Company\n        \"ooo\", // ooo INFIBEAM INCORPORATION LIMITED\n        \"open\", // open American Express Travel Related Services Company, Inc.\n        \"oracle\", // oracle Oracle Corporation\n        \"orange\", // orange Orange Brand Services Limited\n        \"org\", // org Public Interest Registry (PIR)\n        \"organic\", // organic Afilias Limited\n        \"orientexpress\", // orientexpress Orient Express\n        \"origins\", // origins The Estée Lauder Companies Inc.\n        \"osaka\", // osaka Interlink Co., Ltd.\n        \"otsuka\", // otsuka Otsuka Holdings Co., Ltd.\n        \"ott\", // ott Dish DBS Corporation\n        \"ovh\", // ovh OVH SAS\n        \"page\", // page Charleston Road Registry Inc.\n        \"pamperedchef\", // pamperedchef The Pampered Chef, Ltd.\n        \"panasonic\", // panasonic Panasonic Corporation\n        \"panerai\", // panerai Richemont DNS Inc.\n        \"paris\", // paris City of Paris\n        \"pars\", // pars Asia Green IT System Bilgisayar San. ve Tic. Ltd. Sti.\n        \"partners\", // partners Magic Glen, LLC\n        \"parts\", // parts Sea Goodbye, LLC\n        \"party\", // party Blue Sky Registry Limited\n        \"passagens\", // passagens Travel Reservations SRL\n        \"pay\", // pay Amazon Registry Services, Inc.\n        \"pccw\", // pccw PCCW Enterprises Limited\n        \"pet\", // pet Afilias plc\n        \"pfizer\", // pfizer Pfizer Inc.\n        \"pharmacy\", // pharmacy National Association of Boards of Pharmacy\n        \"philips\", // philips Koninklijke Philips N.V.\n        \"phone\", // phone Dish DBS Corporation\n        \"photo\", // photo Uniregistry, Corp.\n        \"photography\", // photography Sugar Glen, LLC\n        \"photos\", // photos Sea Corner, LLC\n        \"physio\", // physio PhysBiz Pty Ltd\n        \"piaget\", // piaget Richemont DNS Inc.\n        \"pics\", // pics Uniregistry, Corp.\n        \"pictet\", // pictet Pictet Europe S.A.\n        \"pictures\", // pictures Foggy Sky, LLC\n        \"pid\", // pid Top Level Spectrum, Inc.\n        \"pin\", // pin Amazon Registry Services, Inc.\n        \"ping\", // ping Ping Registry Provider, Inc.\n        \"pink\", // pink Afilias Limited\n        \"pioneer\", // pioneer Pioneer Corporation\n        \"pizza\", // pizza Foggy Moon, LLC\n        \"place\", // place Snow Galley, LLC\n        \"play\", // play Charleston Road Registry Inc.\n        \"playstation\", // playstation Sony Computer Entertainment Inc.\n        \"plumbing\", // plumbing Spring Tigers, LLC\n        \"plus\", // plus Sugar Mill, LLC\n        \"pnc\", // pnc PNC Domain Co., LLC\n        \"pohl\", // pohl Deutsche Vermögensberatung Aktiengesellschaft DVAG\n        \"poker\", // poker Afilias Domains No. 5 Limited\n        \"politie\", // politie Politie Nederland\n        \"porn\", // porn ICM Registry PN LLC\n        \"post\", // post Universal Postal Union\n        \"pramerica\", // pramerica Prudential Financial, Inc.\n        \"praxi\", // praxi Praxi S.p.A.\n        \"press\", // press DotPress Inc.\n        \"prime\", // prime Amazon Registry Services, Inc.\n        \"pro\", // pro Registry Services Corporation dba RegistryPro\n        \"prod\", // prod Charleston Road Registry Inc.\n        \"productions\", // productions Magic Birch, LLC\n        \"prof\", // prof Charleston Road Registry Inc.\n        \"progressive\", // progressive Progressive Casualty Insurance Company\n        \"promo\", // promo Afilias plc\n        \"properties\", // properties Big Pass, LLC\n        \"property\", // property Uniregistry, Corp.\n        \"protection\", // protection XYZ.COM LLC\n        \"pru\", // pru Prudential Financial, Inc.\n        \"prudential\", // prudential Prudential Financial, Inc.\n        \"pub\", // pub United TLD Holdco Ltd.\n        \"pwc\", // pwc PricewaterhouseCoopers LLP\n        \"qpon\", // qpon dotCOOL, Inc.\n        \"quebec\", // quebec PointQuébec Inc\n        \"quest\", // quest Quest ION Limited\n        \"qvc\", // qvc QVC, Inc.\n        \"racing\", // racing Premier Registry Limited\n        \"radio\", // radio European Broadcasting Union (EBU)\n        \"raid\", // raid Johnson Shareholdings, Inc.\n        \"read\", // read Amazon Registry Services, Inc.\n        \"realestate\", // realestate dotRealEstate LLC\n        \"realtor\", // realtor Real Estate Domains LLC\n        \"realty\", // realty Fegistry, LLC\n        \"recipes\", // recipes Grand Island, LLC\n        \"red\", // red Afilias Limited\n        \"redstone\", // redstone Redstone Haute Couture Co., Ltd.\n        \"redumbrella\", // redumbrella Travelers TLD, LLC\n        \"rehab\", // rehab United TLD Holdco Ltd.\n        \"reise\", // reise Foggy Way, LLC\n        \"reisen\", // reisen New Cypress, LLC\n        \"reit\", // reit National Association of Real Estate Investment Trusts, Inc.\n        \"reliance\", // reliance Reliance Industries Limited\n        \"ren\", // ren Beijing Qianxiang Wangjing Technology Development Co., Ltd.\n        \"rent\", // rent XYZ.COM LLC\n        \"rentals\", // rentals Big Hollow,LLC\n        \"repair\", // repair Lone Sunset, LLC\n        \"report\", // report Binky Glen, LLC\n        \"republican\", // republican United TLD Holdco Ltd.\n        \"rest\", // rest Punto 2012 Sociedad Anonima Promotora de Inversion de Capital Variable\n        \"restaurant\", // restaurant Snow Avenue, LLC\n        \"review\", // review dot Review Limited\n        \"reviews\", // reviews United TLD Holdco, Ltd.\n        \"rexroth\", // rexroth Robert Bosch GMBH\n        \"rich\", // rich I-REGISTRY Ltd., Niederlassung Deutschland\n        \"richardli\", // richardli Pacific Century Asset Management (HK) Limited\n        \"ricoh\", // ricoh Ricoh Company, Ltd.\n        \"rightathome\", // rightathome Johnson Shareholdings, Inc.\n        \"ril\", // ril Reliance Industries Limited\n        \"rio\", // rio Empresa Municipal de Informática SA - IPLANRIO\n        \"rip\", // rip United TLD Holdco Ltd.\n        \"rmit\", // rmit Royal Melbourne Institute of Technology\n        \"rocher\", // rocher Ferrero Trading Lux S.A.\n        \"rocks\", // rocks United TLD Holdco, LTD.\n        \"rodeo\", // rodeo Top Level Domain Holdings Limited\n        \"rogers\", // rogers Rogers Communications Canada Inc.\n        \"room\", // room Amazon Registry Services, Inc.\n        \"rsvp\", // rsvp Charleston Road Registry Inc.\n        \"ruhr\", // ruhr regiodot GmbH &amp; Co. KG\n        \"run\", // run Snow Park, LLC\n        \"rwe\", // rwe RWE AG\n        \"ryukyu\", // ryukyu BusinessRalliart inc.\n        \"saarland\", // saarland dotSaarland GmbH\n        \"safe\", // safe Amazon Registry Services, Inc.\n        \"safety\", // safety Safety Registry Services, LLC.\n        \"sakura\", // sakura SAKURA Internet Inc.\n        \"sale\", // sale United TLD Holdco, Ltd\n        \"salon\", // salon Outer Orchard, LLC\n        \"samsclub\", // samsclub Wal-Mart Stores, Inc.\n        \"samsung\", // samsung SAMSUNG SDS CO., LTD\n        \"sandvik\", // sandvik Sandvik AB\n        \"sandvikcoromant\", // sandvikcoromant Sandvik AB\n        \"sanofi\", // sanofi Sanofi\n        \"sap\", // sap SAP AG\n        \"sapo\", // sapo PT Comunicacoes S.A.\n        \"sarl\", // sarl Delta Orchard, LLC\n        \"sas\", // sas Research IP LLC\n        \"save\", // save Amazon Registry Services, Inc.\n        \"saxo\", // saxo Saxo Bank A/S\n        \"sbi\", // sbi STATE BANK OF INDIA\n        \"sbs\", // sbs SPECIAL BROADCASTING SERVICE CORPORATION\n        \"sca\", // sca SVENSKA CELLULOSA AKTIEBOLAGET SCA (publ)\n        \"scb\", // scb The Siam Commercial Bank Public Company Limited (&quot;SCB&quot;)\n        \"schaeffler\", // schaeffler Schaeffler Technologies AG &amp; Co. KG\n        \"schmidt\", // schmidt SALM S.A.S.\n        \"scholarships\", // scholarships Scholarships.com, LLC\n        \"school\", // school Little Galley, LLC\n        \"schule\", // schule Outer Moon, LLC\n        \"schwarz\", // schwarz Schwarz Domains und Services GmbH &amp; Co. KG\n        \"science\", // science dot Science Limited\n        \"scjohnson\", // scjohnson Johnson Shareholdings, Inc.\n        \"scor\", // scor SCOR SE\n        \"scot\", // scot Dot Scot Registry Limited\n        \"seat\", // seat SEAT, S.A. (Sociedad Unipersonal)\n        \"secure\", // secure Amazon Registry Services, Inc.\n        \"security\", // security XYZ.COM LLC\n        \"seek\", // seek Seek Limited\n        \"select\", // select iSelect Ltd\n        \"sener\", // sener Sener Ingeniería y Sistemas, S.A.\n        \"services\", // services Fox Castle, LLC\n        \"ses\", // ses SES\n        \"seven\", // seven Seven West Media Ltd\n        \"sew\", // sew SEW-EURODRIVE GmbH &amp; Co KG\n        \"sex\", // sex ICM Registry SX LLC\n        \"sexy\", // sexy Uniregistry, Corp.\n        \"sfr\", // sfr Societe Francaise du Radiotelephone - SFR\n        \"shangrila\", // shangrila Shangri‐La International Hotel Management Limited\n        \"sharp\", // sharp Sharp Corporation\n        \"shaw\", // shaw Shaw Cablesystems G.P.\n        \"shell\", // shell Shell Information Technology International Inc\n        \"shia\", // shia Asia Green IT System Bilgisayar San. ve Tic. Ltd. Sti.\n        \"shiksha\", // shiksha Afilias Limited\n        \"shoes\", // shoes Binky Galley, LLC\n        \"shop\", // shop GMO Registry, Inc.\n        \"shopping\", // shopping Over Keep, LLC\n        \"shouji\", // shouji QIHOO 360 TECHNOLOGY CO. LTD.\n        \"show\", // show Snow Beach, LLC\n        \"showtime\", // showtime CBS Domains Inc.\n        \"shriram\", // shriram Shriram Capital Ltd.\n        \"silk\", // silk Amazon Registry Services, Inc.\n        \"sina\", // sina Sina Corporation\n        \"singles\", // singles Fern Madison, LLC\n        \"site\", // site DotSite Inc.\n        \"ski\", // ski STARTING DOT LIMITED\n        \"skin\", // skin L&#39;Oréal\n        \"sky\", // sky Sky International AG\n        \"skype\", // skype Microsoft Corporation\n        \"sling\", // sling Hughes Satellite Systems Corporation\n        \"smart\", // smart Smart Communications, Inc. (SMART)\n        \"smile\", // smile Amazon Registry Services, Inc.\n        \"sncf\", // sncf SNCF (Société Nationale des Chemins de fer Francais)\n        \"soccer\", // soccer Foggy Shadow, LLC\n        \"social\", // social United TLD Holdco Ltd.\n        \"softbank\", // softbank SoftBank Group Corp.\n        \"software\", // software United TLD Holdco, Ltd\n        \"sohu\", // sohu Sohu.com Limited\n        \"solar\", // solar Ruby Town, LLC\n        \"solutions\", // solutions Silver Cover, LLC\n        \"song\", // song Amazon Registry Services, Inc.\n        \"sony\", // sony Sony Corporation\n        \"soy\", // soy Charleston Road Registry Inc.\n        \"space\", // space DotSpace Inc.\n        \"spiegel\", // spiegel SPIEGEL-Verlag Rudolf Augstein GmbH &amp; Co. KG\n        \"spot\", // spot Amazon Registry Services, Inc.\n        \"spreadbetting\", // spreadbetting DOTSPREADBETTING REGISTRY LTD\n        \"srl\", // srl InterNetX Corp.\n        \"srt\", // srt FCA US LLC.\n        \"stada\", // stada STADA Arzneimittel AG\n        \"staples\", // staples Staples, Inc.\n        \"star\", // star Star India Private Limited\n        \"starhub\", // starhub StarHub Limited\n        \"statebank\", // statebank STATE BANK OF INDIA\n        \"statefarm\", // statefarm State Farm Mutual Automobile Insurance Company\n        \"statoil\", // statoil Statoil ASA\n        \"stc\", // stc Saudi Telecom Company\n        \"stcgroup\", // stcgroup Saudi Telecom Company\n        \"stockholm\", // stockholm Stockholms kommun\n        \"storage\", // storage Self Storage Company LLC\n        \"store\", // store DotStore Inc.\n        \"stream\", // stream dot Stream Limited\n        \"studio\", // studio United TLD Holdco Ltd.\n        \"study\", // study OPEN UNIVERSITIES AUSTRALIA PTY LTD\n        \"style\", // style Binky Moon, LLC\n        \"sucks\", // sucks Vox Populi Registry Ltd.\n        \"supplies\", // supplies Atomic Fields, LLC\n        \"supply\", // supply Half Falls, LLC\n        \"support\", // support Grand Orchard, LLC\n        \"surf\", // surf Top Level Domain Holdings Limited\n        \"surgery\", // surgery Tin Avenue, LLC\n        \"suzuki\", // suzuki SUZUKI MOTOR CORPORATION\n        \"swatch\", // swatch The Swatch Group Ltd\n        \"swiftcover\", // swiftcover Swiftcover Insurance Services Limited\n        \"swiss\", // swiss Swiss Confederation\n        \"sydney\", // sydney State of New South Wales, Department of Premier and Cabinet\n        \"symantec\", // symantec Symantec Corporation\n        \"systems\", // systems Dash Cypress, LLC\n        \"tab\", // tab Tabcorp Holdings Limited\n        \"taipei\", // taipei Taipei City Government\n        \"talk\", // talk Amazon Registry Services, Inc.\n        \"taobao\", // taobao Alibaba Group Holding Limited\n        \"target\", // target Target Domain Holdings, LLC\n        \"tatamotors\", // tatamotors Tata Motors Ltd\n        \"tatar\", // tatar Limited Liability Company &quot;\n        \"tattoo\", // tattoo Uniregistry, Corp.\n        \"tax\", // tax Storm Orchard, LLC\n        \"taxi\", // taxi Pine Falls, LLC\n        \"tci\", // tci Asia Green IT System Bilgisayar San. ve Tic. Ltd. Sti.\n        \"tdk\", // tdk TDK Corporation\n        \"team\", // team Atomic Lake, LLC\n        \"tech\", // tech Dot Tech LLC\n        \"technology\", // technology Auburn Falls, LLC\n        \"tel\", // tel Telnic Ltd.\n        \"telecity\", // telecity TelecityGroup International Limited\n        \"telefonica\", // telefonica Telefónica S.A.\n        \"temasek\", // temasek Temasek Holdings (Private) Limited\n        \"tennis\", // tennis Cotton Bloom, LLC\n        \"teva\", // teva Teva Pharmaceutical Industries Limited\n        \"thd\", // thd Homer TLC, Inc.\n        \"theater\", // theater Blue Tigers, LLC\n        \"theatre\", // theatre XYZ.COM LLC\n        \"tiaa\", // tiaa Teachers Insurance and Annuity Association of America\n        \"tickets\", // tickets Accent Media Limited\n        \"tienda\", // tienda Victor Manor, LLC\n        \"tiffany\", // tiffany Tiffany and Company\n        \"tips\", // tips Corn Willow, LLC\n        \"tires\", // tires Dog Edge, LLC\n        \"tirol\", // tirol punkt Tirol GmbH\n        \"tjmaxx\", // tjmaxx The TJX Companies, Inc.\n        \"tjx\", // tjx The TJX Companies, Inc.\n        \"tkmaxx\", // tkmaxx The TJX Companies, Inc.\n        \"tmall\", // tmall Alibaba Group Holding Limited\n        \"today\", // today Pearl Woods, LLC\n        \"tokyo\", // tokyo GMO Registry, Inc.\n        \"tools\", // tools Pioneer North, LLC\n        \"top\", // top Jiangsu Bangning Science &amp; Technology Co.,Ltd.\n        \"toray\", // toray Toray Industries, Inc.\n        \"toshiba\", // toshiba TOSHIBA Corporation\n        \"total\", // total Total SA\n        \"tours\", // tours Sugar Station, LLC\n        \"town\", // town Koko Moon, LLC\n        \"toyota\", // toyota TOYOTA MOTOR CORPORATION\n        \"toys\", // toys Pioneer Orchard, LLC\n        \"trade\", // trade Elite Registry Limited\n        \"trading\", // trading DOTTRADING REGISTRY LTD\n        \"training\", // training Wild Willow, LLC\n        \"travel\", // travel Tralliance Registry Management Company, LLC.\n        \"travelchannel\", // travelchannel Lifestyle Domain Holdings, Inc.\n        \"travelers\", // travelers Travelers TLD, LLC\n        \"travelersinsurance\", // travelersinsurance Travelers TLD, LLC\n        \"trust\", // trust Artemis Internet Inc\n        \"trv\", // trv Travelers TLD, LLC\n        \"tube\", // tube Latin American Telecom LLC\n        \"tui\", // tui TUI AG\n        \"tunes\", // tunes Amazon Registry Services, Inc.\n        \"tushu\", // tushu Amazon Registry Services, Inc.\n        \"tvs\", // tvs T V SUNDRAM IYENGAR  &amp; SONS PRIVATE LIMITED\n        \"ubank\", // ubank National Australia Bank Limited\n        \"ubs\", // ubs UBS AG\n        \"uconnect\", // uconnect FCA US LLC.\n        \"unicom\", // unicom China United Network Communications Corporation Limited\n        \"university\", // university Little Station, LLC\n        \"uno\", // uno Dot Latin LLC\n        \"uol\", // uol UBN INTERNET LTDA.\n        \"ups\", // ups UPS Market Driver, Inc.\n        \"vacations\", // vacations Atomic Tigers, LLC\n        \"vana\", // vana Lifestyle Domain Holdings, Inc.\n        \"vanguard\", // vanguard The Vanguard Group, Inc.\n        \"vegas\", // vegas Dot Vegas, Inc.\n        \"ventures\", // ventures Binky Lake, LLC\n        \"verisign\", // verisign VeriSign, Inc.\n        \"versicherung\", // versicherung dotversicherung-registry GmbH\n        \"vet\", // vet United TLD Holdco, Ltd\n        \"viajes\", // viajes Black Madison, LLC\n        \"video\", // video United TLD Holdco, Ltd\n        \"vig\", // vig VIENNA INSURANCE GROUP AG Wiener Versicherung Gruppe\n        \"viking\", // viking Viking River Cruises (Bermuda) Ltd.\n        \"villas\", // villas New Sky, LLC\n        \"vin\", // vin Holly Shadow, LLC\n        \"vip\", // vip Minds + Machines Group Limited\n        \"virgin\", // virgin Virgin Enterprises Limited\n        \"visa\", // visa Visa Worldwide Pte. Limited\n        \"vision\", // vision Koko Station, LLC\n        \"vista\", // vista Vistaprint Limited\n        \"vistaprint\", // vistaprint Vistaprint Limited\n        \"viva\", // viva Saudi Telecom Company\n        \"vivo\", // vivo Telefonica Brasil S.A.\n        \"vlaanderen\", // vlaanderen DNS.be vzw\n        \"vodka\", // vodka Top Level Domain Holdings Limited\n        \"volkswagen\", // volkswagen Volkswagen Group of America Inc.\n        \"volvo\", // volvo Volvo Holding Sverige Aktiebolag\n        \"vote\", // vote Monolith Registry LLC\n        \"voting\", // voting Valuetainment Corp.\n        \"voto\", // voto Monolith Registry LLC\n        \"voyage\", // voyage Ruby House, LLC\n        \"vuelos\", // vuelos Travel Reservations SRL\n        \"wales\", // wales Nominet UK\n        \"walmart\", // walmart Wal-Mart Stores, Inc.\n        \"walter\", // walter Sandvik AB\n        \"wang\", // wang Zodiac Registry Limited\n        \"wanggou\", // wanggou Amazon Registry Services, Inc.\n        \"warman\", // warman Weir Group IP Limited\n        \"watch\", // watch Sand Shadow, LLC\n        \"watches\", // watches Richemont DNS Inc.\n        \"weather\", // weather The Weather Channel, LLC\n        \"weatherchannel\", // weatherchannel The Weather Channel, LLC\n        \"webcam\", // webcam dot Webcam Limited\n        \"weber\", // weber Saint-Gobain Weber SA\n        \"website\", // website DotWebsite Inc.\n        \"wed\", // wed Atgron, Inc.\n        \"wedding\", // wedding Top Level Domain Holdings Limited\n        \"weibo\", // weibo Sina Corporation\n        \"weir\", // weir Weir Group IP Limited\n        \"whoswho\", // whoswho Who&#39;s Who Registry\n        \"wien\", // wien punkt.wien GmbH\n        \"wiki\", // wiki Top Level Design, LLC\n        \"williamhill\", // williamhill William Hill Organization Limited\n        \"win\", // win First Registry Limited\n        \"windows\", // windows Microsoft Corporation\n        \"wine\", // wine June Station, LLC\n        \"winners\", // winners The TJX Companies, Inc.\n        \"wme\", // wme William Morris Endeavor Entertainment, LLC\n        \"wolterskluwer\", // wolterskluwer Wolters Kluwer N.V.\n        \"woodside\", // woodside Woodside Petroleum Limited\n        \"work\", // work Top Level Domain Holdings Limited\n        \"works\", // works Little Dynamite, LLC\n        \"world\", // world Bitter Fields, LLC\n        \"wow\", // wow Amazon Registry Services, Inc.\n        \"wtc\", // wtc World Trade Centers Association, Inc.\n        \"wtf\", // wtf Hidden Way, LLC\n        \"xbox\", // xbox Microsoft Corporation\n        \"xerox\", // xerox Xerox DNHC LLC\n        \"xfinity\", // xfinity Comcast IP Holdings I, LLC\n        \"xihuan\", // xihuan QIHOO 360 TECHNOLOGY CO. LTD.\n        \"xin\", // xin Elegant Leader Limited\n        \"xn--11b4c3d\", // कॉम VeriSign Sarl\n        \"xn--1ck2e1b\", // セール Amazon Registry Services, Inc.\n        \"xn--1qqw23a\", // 佛山 Guangzhou YU Wei Information Technology Co., Ltd.\n        \"xn--30rr7y\", // 慈善 Excellent First Limited\n        \"xn--3bst00m\", // 集团 Eagle Horizon Limited\n        \"xn--3ds443g\", // 在线 TLD REGISTRY LIMITED\n        \"xn--3oq18vl8pn36a\", // 大众汽车 Volkswagen (China) Investment Co., Ltd.\n        \"xn--3pxu8k\", // 点看 VeriSign Sarl\n        \"xn--42c2d9a\", // คอม VeriSign Sarl\n        \"xn--45q11c\", // 八卦 Zodiac Scorpio Limited\n        \"xn--4gbrim\", // موقع Suhub Electronic Establishment\n        \"xn--55qw42g\", // 公益 China Organizational Name Administration Center\n        \"xn--55qx5d\", // 公司 Computer Network Information\n        \"xn--5su34j936bgsg\", // 香格里拉 Shangri‐La International Hotel Management Limited\n        \"xn--5tzm5g\", // 网站 Global Website TLD Asia Limited\n        \"xn--6frz82g\", // 移动 Afilias Limited\n        \"xn--6qq986b3xl\", // 我爱你 Tycoon Treasure Limited\n        \"xn--80adxhks\", // москва Foundation for Assistance\n        \"xn--80aqecdr1a\", // католик Pontificium Consilium\n        \"xn--80asehdb\", // онлайн CORE Association\n        \"xn--80aswg\", // сайт CORE Association\n        \"xn--8y0a063a\", // 联通 China United Network Communications Corporation Limited\n        \"xn--90ae\", // бг Imena.BG Plc (NAMES.BG Plc)\n        \"xn--9dbq2a\", // קום VeriSign Sarl\n        \"xn--9et52u\", // 时尚 RISE VICTORY LIMITED\n        \"xn--9krt00a\", // 微博 Sina Corporation\n        \"xn--b4w605ferd\", // 淡马锡 Temasek Holdings (Private) Limited\n        \"xn--bck1b9a5dre4c\", // ファッション Amazon Registry Services, Inc.\n        \"xn--c1avg\", // орг Public Interest Registry\n        \"xn--c2br7g\", // नेट VeriSign Sarl\n        \"xn--cck2b3b\", // ストア Amazon Registry Services, Inc.\n        \"xn--cg4bki\", // 삼성 SAMSUNG SDS CO., LTD\n        \"xn--czr694b\", // 商标 HU YI GLOBAL INFORMATION RESOURCES(HOLDING) COMPANY.HONGKONG LIMITED\n        \"xn--czrs0t\", // 商店 Wild Island, LLC\n        \"xn--czru2d\", // 商城 Zodiac Aquarius Limited\n        \"xn--d1acj3b\", // дети The Foundation for Network Initiatives “The Smart Internet”\n        \"xn--eckvdtc9d\", // ポイント Amazon Registry Services, Inc.\n        \"xn--efvy88h\", // 新闻 Xinhua News Agency Guangdong Branch 新华通讯社广东分社\n        \"xn--estv75g\", // 工行 Industrial and Commercial Bank of China Limited\n        \"xn--fct429k\", // 家電 Amazon Registry Services, Inc.\n        \"xn--fhbei\", // كوم VeriSign Sarl\n        \"xn--fiq228c5hs\", // 中文网 TLD REGISTRY LIMITED\n        \"xn--fiq64b\", // 中信 CITIC Group Corporation\n        \"xn--fjq720a\", // 娱乐 Will Bloom, LLC\n        \"xn--flw351e\", // 谷歌 Charleston Road Registry Inc.\n        \"xn--fzys8d69uvgm\", // 電訊盈科 PCCW Enterprises Limited\n        \"xn--g2xx48c\", // 购物 Minds + Machines Group Limited\n        \"xn--gckr3f0f\", // クラウド Amazon Registry Services, Inc.\n        \"xn--gk3at1e\", // 通販 Amazon Registry Services, Inc.\n        \"xn--hxt814e\", // 网店 Zodiac Libra Limited\n        \"xn--i1b6b1a6a2e\", // संगठन Public Interest Registry\n        \"xn--imr513n\", // 餐厅 HU YI GLOBAL INFORMATION RESOURCES (HOLDING) COMPANY. HONGKONG LIMITED\n        \"xn--io0a7i\", // 网络 Computer Network Information Center of Chinese Academy of Sciences\n        \"xn--j1aef\", // ком VeriSign Sarl\n        \"xn--jlq61u9w7b\", // 诺基亚 Nokia Corporation\n        \"xn--jvr189m\", // 食品 Amazon Registry Services, Inc.\n        \"xn--kcrx77d1x4a\", // 飞利浦 Koninklijke Philips N.V.\n        \"xn--kpu716f\", // 手表 Richemont DNS Inc.\n        \"xn--kput3i\", // 手机 Beijing RITT-Net Technology Development Co., Ltd\n        \"xn--mgba3a3ejt\", // ارامكو Aramco Services Company\n        \"xn--mgba7c0bbn0a\", // العليان Crescent Holding GmbH\n        \"xn--mgbab2bd\", // بازار CORE Association\n        \"xn--mgbb9fbpob\", // موبايلي GreenTech Consultancy Company W.L.L.\n        \"xn--mgbca7dzdo\", // ابوظبي Abu Dhabi Systems and Information Centre\n        \"xn--mgbi4ecexp\", // كاثوليك Pontificium Consilium de Comunicationibus Socialibus (PCCS)\n        \"xn--mgbt3dhd\", // همراه Asia Green IT System Bilgisayar San. ve Tic. Ltd. Sti.\n        \"xn--mk1bu44c\", // 닷컴 VeriSign Sarl\n        \"xn--mxtq1m\", // 政府 Net-Chinese Co., Ltd.\n        \"xn--ngbc5azd\", // شبكة International Domain Registry Pty. Ltd.\n        \"xn--ngbe9e0a\", // بيتك Kuwait Finance House\n        \"xn--nqv7f\", // 机构 Public Interest Registry\n        \"xn--nqv7fs00ema\", // 组织机构 Public Interest Registry\n        \"xn--nyqy26a\", // 健康 Stable Tone Limited\n        \"xn--p1acf\", // рус Rusnames Limited\n        \"xn--pbt977c\", // 珠宝 Richemont DNS Inc.\n        \"xn--pssy2u\", // 大拿 VeriSign Sarl\n        \"xn--q9jyb4c\", // みんな Charleston Road Registry Inc.\n        \"xn--qcka1pmc\", // グーグル Charleston Road Registry Inc.\n        \"xn--rhqv96g\", // 世界 Stable Tone Limited\n        \"xn--rovu88b\", // 書籍 Amazon EU S.à r.l.\n        \"xn--ses554g\", // 网址 KNET Co., Ltd\n        \"xn--t60b56a\", // 닷넷 VeriSign Sarl\n        \"xn--tckwe\", // コム VeriSign Sarl\n        \"xn--tiq49xqyj\", // 天主教 Pontificium Consilium\n        \"xn--unup4y\", // 游戏 Spring Fields, LLC\n        \"xn--vermgensberater-ctb\", // VERMöGENSBERATER Deutsche Vermögensberatung Aktiengesellschaft DVAG\n        \"xn--vermgensberatung-pwb\", // VERMöGENSBERATUNG Deutsche Vermögensberatung Aktiengesellschaft DVAG\n        \"xn--vhquv\", // 企业 Dash McCook, LLC\n        \"xn--vuq861b\", // 信息 Beijing Tele-info Network Technology Co., Ltd.\n        \"xn--w4r85el8fhu5dnra\", // 嘉里大酒店 Kerry Trading Co. Limited\n        \"xn--w4rs40l\", // 嘉里 Kerry Trading Co. Limited\n        \"xn--xhq521b\", // 广东 Guangzhou YU Wei Information Technology Co., Ltd.\n        \"xn--zfr164b\", // 政务 China Organizational Name Administration Center\n        \"xperia\", // xperia Sony Mobile Communications AB\n        \"xxx\", // xxx ICM Registry LLC\n        \"xyz\", // xyz XYZ.COM LLC\n        \"yachts\", // yachts DERYachts, LLC\n        \"yahoo\", // yahoo Yahoo! Domain Services Inc.\n        \"yamaxun\", // yamaxun Amazon Registry Services, Inc.\n        \"yandex\", // yandex YANDEX, LLC\n        \"yodobashi\", // yodobashi YODOBASHI CAMERA CO.,LTD.\n        \"yoga\", // yoga Top Level Domain Holdings Limited\n        \"yokohama\", // yokohama GMO Registry, Inc.\n        \"you\", // you Amazon Registry Services, Inc.\n        \"youtube\", // youtube Charleston Road Registry Inc.\n        \"yun\", // yun QIHOO 360 TECHNOLOGY CO. LTD.\n        \"zappos\", // zappos Amazon Registry Services, Inc.\n        \"zara\", // zara Industria de Diseño Textil, S.A. (INDITEX, S.A.)\n        \"zero\", // zero Amazon Registry Services, Inc.\n        \"zip\", // zip Charleston Road Registry Inc.\n        \"zippo\", // zippo Zadco Company\n        \"zone\", // zone Outer Falls, LLC\n        \"zuerich\", // zuerich Kanton Zürich (Canton of Zurich)\n};\n\n    // WARNING: this array MUST be sorted, otherwise it cannot be searched reliably using binary search\n    private static final String[] COUNTRY_CODE_TLDS = new String[] {\n        \"ac\",                 // Ascension Island\n        \"ad\",                 // Andorra\n        \"ae\",                 // United Arab Emirates\n        \"af\",                 // Afghanistan\n        \"ag\",                 // Antigua and Barbuda\n        \"ai\",                 // Anguilla\n        \"al\",                 // Albania\n        \"am\",                 // Armenia\n//        \"an\",                 // Netherlands Antilles (retired)\n        \"ao\",                 // Angola\n        \"aq\",                 // Antarctica\n        \"ar\",                 // Argentina\n        \"as\",                 // American Samoa\n        \"at\",                 // Austria\n        \"au\",                 // Australia (includes Ashmore and Cartier Islands and Coral Sea Islands)\n        \"aw\",                 // Aruba\n        \"ax\",                 // Åland\n        \"az\",                 // Azerbaijan\n        \"ba\",                 // Bosnia and Herzegovina\n        \"bb\",                 // Barbados\n        \"bd\",                 // Bangladesh\n        \"be\",                 // Belgium\n        \"bf\",                 // Burkina Faso\n        \"bg\",                 // Bulgaria\n        \"bh\",                 // Bahrain\n        \"bi\",                 // Burundi\n        \"bj\",                 // Benin\n        \"bm\",                 // Bermuda\n        \"bn\",                 // Brunei Darussalam\n        \"bo\",                 // Bolivia\n        \"br\",                 // Brazil\n        \"bs\",                 // Bahamas\n        \"bt\",                 // Bhutan\n        \"bv\",                 // Bouvet Island\n        \"bw\",                 // Botswana\n        \"by\",                 // Belarus\n        \"bz\",                 // Belize\n        \"ca\",                 // Canada\n        \"cc\",                 // Cocos (Keeling) Islands\n        \"cd\",                 // Democratic Republic of the Congo (formerly Zaire)\n        \"cf\",                 // Central African Republic\n        \"cg\",                 // Republic of the Congo\n        \"ch\",                 // Switzerland\n        \"ci\",                 // Côte d'Ivoire\n        \"ck\",                 // Cook Islands\n        \"cl\",                 // Chile\n        \"cm\",                 // Cameroon\n        \"cn\",                 // China, mainland\n        \"co\",                 // Colombia\n        \"cr\",                 // Costa Rica\n        \"cu\",                 // Cuba\n        \"cv\",                 // Cape Verde\n        \"cw\",                 // Curaçao\n        \"cx\",                 // Christmas Island\n        \"cy\",                 // Cyprus\n        \"cz\",                 // Czech Republic\n        \"de\",                 // Germany\n        \"dj\",                 // Djibouti\n        \"dk\",                 // Denmark\n        \"dm\",                 // Dominica\n        \"do\",                 // Dominican Republic\n        \"dz\",                 // Algeria\n        \"ec\",                 // Ecuador\n        \"ee\",                 // Estonia\n        \"eg\",                 // Egypt\n        \"er\",                 // Eritrea\n        \"es\",                 // Spain\n        \"et\",                 // Ethiopia\n        \"eu\",                 // European Union\n        \"fi\",                 // Finland\n        \"fj\",                 // Fiji\n        \"fk\",                 // Falkland Islands\n        \"fm\",                 // Federated States of Micronesia\n        \"fo\",                 // Faroe Islands\n        \"fr\",                 // France\n        \"ga\",                 // Gabon\n        \"gb\",                 // Great Britain (United Kingdom)\n        \"gd\",                 // Grenada\n        \"ge\",                 // Georgia\n        \"gf\",                 // French Guiana\n        \"gg\",                 // Guernsey\n        \"gh\",                 // Ghana\n        \"gi\",                 // Gibraltar\n        \"gl\",                 // Greenland\n        \"gm\",                 // The Gambia\n        \"gn\",                 // Guinea\n        \"gp\",                 // Guadeloupe\n        \"gq\",                 // Equatorial Guinea\n        \"gr\",                 // Greece\n        \"gs\",                 // South Georgia and the South Sandwich Islands\n        \"gt\",                 // Guatemala\n        \"gu\",                 // Guam\n        \"gw\",                 // Guinea-Bissau\n        \"gy\",                 // Guyana\n        \"hk\",                 // Hong Kong\n        \"hm\",                 // Heard Island and McDonald Islands\n        \"hn\",                 // Honduras\n        \"hr\",                 // Croatia (Hrvatska)\n        \"ht\",                 // Haiti\n        \"hu\",                 // Hungary\n        \"id\",                 // Indonesia\n        \"ie\",                 // Ireland (Éire)\n        \"il\",                 // Israel\n        \"im\",                 // Isle of Man\n        \"in\",                 // India\n        \"io\",                 // British Indian Ocean Territory\n        \"iq\",                 // Iraq\n        \"ir\",                 // Iran\n        \"is\",                 // Iceland\n        \"it\",                 // Italy\n        \"je\",                 // Jersey\n        \"jm\",                 // Jamaica\n        \"jo\",                 // Jordan\n        \"jp\",                 // Japan\n        \"ke\",                 // Kenya\n        \"kg\",                 // Kyrgyzstan\n        \"kh\",                 // Cambodia (Khmer)\n        \"ki\",                 // Kiribati\n        \"km\",                 // Comoros\n        \"kn\",                 // Saint Kitts and Nevis\n        \"kp\",                 // North Korea\n        \"kr\",                 // South Korea\n        \"kw\",                 // Kuwait\n        \"ky\",                 // Cayman Islands\n        \"kz\",                 // Kazakhstan\n        \"la\",                 // Laos (currently being marketed as the official domain for Los Angeles)\n        \"lb\",                 // Lebanon\n        \"lc\",                 // Saint Lucia\n        \"li\",                 // Liechtenstein\n        \"lk\",                 // Sri Lanka\n        \"lr\",                 // Liberia\n        \"ls\",                 // Lesotho\n        \"lt\",                 // Lithuania\n        \"lu\",                 // Luxembourg\n        \"lv\",                 // Latvia\n        \"ly\",                 // Libya\n        \"ma\",                 // Morocco\n        \"mc\",                 // Monaco\n        \"md\",                 // Moldova\n        \"me\",                 // Montenegro\n        \"mg\",                 // Madagascar\n        \"mh\",                 // Marshall Islands\n        \"mk\",                 // Republic of Macedonia\n        \"ml\",                 // Mali\n        \"mm\",                 // Myanmar\n        \"mn\",                 // Mongolia\n        \"mo\",                 // Macau\n        \"mp\",                 // Northern Mariana Islands\n        \"mq\",                 // Martinique\n        \"mr\",                 // Mauritania\n        \"ms\",                 // Montserrat\n        \"mt\",                 // Malta\n        \"mu\",                 // Mauritius\n        \"mv\",                 // Maldives\n        \"mw\",                 // Malawi\n        \"mx\",                 // Mexico\n        \"my\",                 // Malaysia\n        \"mz\",                 // Mozambique\n        \"na\",                 // Namibia\n        \"nc\",                 // New Caledonia\n        \"ne\",                 // Niger\n        \"nf\",                 // Norfolk Island\n        \"ng\",                 // Nigeria\n        \"ni\",                 // Nicaragua\n        \"nl\",                 // Netherlands\n        \"no\",                 // Norway\n        \"np\",                 // Nepal\n        \"nr\",                 // Nauru\n        \"nu\",                 // Niue\n        \"nz\",                 // New Zealand\n        \"om\",                 // Oman\n        \"pa\",                 // Panama\n        \"pe\",                 // Peru\n        \"pf\",                 // French Polynesia With Clipperton Island\n        \"pg\",                 // Papua New Guinea\n        \"ph\",                 // Philippines\n        \"pk\",                 // Pakistan\n        \"pl\",                 // Poland\n        \"pm\",                 // Saint-Pierre and Miquelon\n        \"pn\",                 // Pitcairn Islands\n        \"pr\",                 // Puerto Rico\n        \"ps\",                 // Palestinian territories (PA-controlled West Bank and Gaza Strip)\n        \"pt\",                 // Portugal\n        \"pw\",                 // Palau\n        \"py\",                 // Paraguay\n        \"qa\",                 // Qatar\n        \"re\",                 // Réunion\n        \"ro\",                 // Romania\n        \"rs\",                 // Serbia\n        \"ru\",                 // Russia\n        \"rw\",                 // Rwanda\n        \"sa\",                 // Saudi Arabia\n        \"sb\",                 // Solomon Islands\n        \"sc\",                 // Seychelles\n        \"sd\",                 // Sudan\n        \"se\",                 // Sweden\n        \"sg\",                 // Singapore\n        \"sh\",                 // Saint Helena\n        \"si\",                 // Slovenia\n        \"sj\",                 // Svalbard and Jan Mayen Islands Not in use (Norwegian dependencies; see .no)\n        \"sk\",                 // Slovakia\n        \"sl\",                 // Sierra Leone\n        \"sm\",                 // San Marino\n        \"sn\",                 // Senegal\n        \"so\",                 // Somalia\n        \"sr\",                 // Suriname\n        \"st\",                 // São Tomé and Príncipe\n        \"su\",                 // Soviet Union (deprecated)\n        \"sv\",                 // El Salvador\n        \"sx\",                 // Sint Maarten\n        \"sy\",                 // Syria\n        \"sz\",                 // Swaziland\n        \"tc\",                 // Turks and Caicos Islands\n        \"td\",                 // Chad\n        \"tf\",                 // French Southern and Antarctic Lands\n        \"tg\",                 // Togo\n        \"th\",                 // Thailand\n        \"tj\",                 // Tajikistan\n        \"tk\",                 // Tokelau\n        \"tl\",                 // East Timor (deprecated old code)\n        \"tm\",                 // Turkmenistan\n        \"tn\",                 // Tunisia\n        \"to\",                 // Tonga\n//        \"tp\",                 // East Timor (Retired)\n        \"tr\",                 // Turkey\n        \"tt\",                 // Trinidad and Tobago\n        \"tv\",                 // Tuvalu\n        \"tw\",                 // Taiwan, Republic of China\n        \"tz\",                 // Tanzania\n        \"ua\",                 // Ukraine\n        \"ug\",                 // Uganda\n        \"uk\",                 // United Kingdom\n        \"us\",                 // United States of America\n        \"uy\",                 // Uruguay\n        \"uz\",                 // Uzbekistan\n        \"va\",                 // Vatican City State\n        \"vc\",                 // Saint Vincent and the Grenadines\n        \"ve\",                 // Venezuela\n        \"vg\",                 // British Virgin Islands\n        \"vi\",                 // U.S. Virgin Islands\n        \"vn\",                 // Vietnam\n        \"vu\",                 // Vanuatu\n        \"wf\",                 // Wallis and Futuna\n        \"ws\",                 // Samoa (formerly Western Samoa)\n        \"xn--3e0b707e\", // 한국 KISA (Korea Internet &amp; Security Agency)\n        \"xn--45brj9c\", // ভারত National Internet Exchange of India\n        \"xn--54b7fta0cc\", // বাংলা Posts and Telecommunications Division\n        \"xn--80ao21a\", // қаз Association of IT Companies of Kazakhstan\n        \"xn--90a3ac\", // срб Serbian National Internet Domain Registry (RNIDS)\n        \"xn--90ais\", // ??? Reliable Software Inc.\n        \"xn--clchc0ea0b2g2a9gcd\", // சிங்கப்பூர் Singapore Network Information Centre (SGNIC) Pte Ltd\n        \"xn--d1alf\", // мкд Macedonian Academic Research Network Skopje\n        \"xn--e1a4c\", // ею EURid vzw/asbl\n        \"xn--fiqs8s\", // 中国 China Internet Network Information Center\n        \"xn--fiqz9s\", // 中國 China Internet Network Information Center\n        \"xn--fpcrj9c3d\", // భారత్ National Internet Exchange of India\n        \"xn--fzc2c9e2c\", // ලංකා LK Domain Registry\n        \"xn--gecrj9c\", // ભારત National Internet Exchange of India\n        \"xn--h2brj9c\", // भारत National Internet Exchange of India\n        \"xn--j1amh\", // укр Ukrainian Network Information Centre (UANIC), Inc.\n        \"xn--j6w193g\", // 香港 Hong Kong Internet Registration Corporation Ltd.\n        \"xn--kprw13d\", // 台湾 Taiwan Network Information Center (TWNIC)\n        \"xn--kpry57d\", // 台灣 Taiwan Network Information Center (TWNIC)\n        \"xn--l1acc\", // мон Datacom Co.,Ltd\n        \"xn--lgbbat1ad8j\", // الجزائر CERIST\n        \"xn--mgb9awbf\", // عمان Telecommunications Regulatory Authority (TRA)\n        \"xn--mgba3a4f16a\", // ایران Institute for Research in Fundamental Sciences (IPM)\n        \"xn--mgbaam7a8h\", // امارات Telecommunications Regulatory Authority (TRA)\n        \"xn--mgbayh7gpa\", // الاردن National Information Technology Center (NITC)\n        \"xn--mgbbh1a71e\", // بھارت National Internet Exchange of India\n        \"xn--mgbc0a9azcg\", // المغرب Agence Nationale de Réglementation des Télécommunications (ANRT)\n        \"xn--mgberp4a5d4ar\", // السعودية Communications and Information Technology Commission\n        \"xn--mgbpl2fh\", // ????? Sudan Internet Society\n        \"xn--mgbtx2b\", // عراق Communications and Media Commission (CMC)\n        \"xn--mgbx4cd0ab\", // مليسيا MYNIC Berhad\n        \"xn--mix891f\", // 澳門 Bureau of Telecommunications Regulation (DSRT)\n        \"xn--node\", // გე Information Technologies Development Center (ITDC)\n        \"xn--o3cw4h\", // ไทย Thai Network Information Center Foundation\n        \"xn--ogbpf8fl\", // سورية National Agency for Network Services (NANS)\n        \"xn--p1ai\", // рф Coordination Center for TLD RU\n        \"xn--pgbs0dh\", // تونس Agence Tunisienne d&#39;Internet\n        \"xn--qxam\", // ελ ICS-FORTH GR\n        \"xn--s9brj9c\", // ਭਾਰਤ National Internet Exchange of India\n        \"xn--wgbh1c\", // مصر National Telecommunication Regulatory Authority - NTRA\n        \"xn--wgbl6a\", // قطر Communications Regulatory Authority\n        \"xn--xkc2al3hye2a\", // இலங்கை LK Domain Registry\n        \"xn--xkc2dl3a5ee0h\", // இந்தியா National Internet Exchange of India\n        \"xn--y9a3aq\", // ??? Internet Society\n        \"xn--yfro4i67o\", // 新加坡 Singapore Network Information Centre (SGNIC) Pte Ltd\n        \"xn--ygbi2ammx\", // فلسطين Ministry of Telecom &amp; Information Technology (MTIT)\n        \"ye\",                 // Yemen\n        \"yt\",                 // Mayotte\n        \"za\",                 // South Africa\n        \"zm\",                 // Zambia\n        \"zw\",                 // Zimbabwe\n    };\n\n    // WARNING: this array MUST be sorted, otherwise it cannot be searched reliably using binary search\n    private static final String[] LOCAL_TLDS = new String[] {\n       \"localdomain\",         // Also widely used as localhost.localdomain\n       \"localhost\",           // RFC2606 defined\n    };\n\n    // Additional arrays to supplement or override the built in ones.\n    // The PLUS arrays are valid keys, the MINUS arrays are invalid keys\n\n    /*\n     * This field is used to detect whether the getInstance has been called.\n     * After this, the method updateTLDOverride is not allowed to be called.\n     * This field does not need to be volatile since it is only accessed from\n     * synchronized methods.\n     */\n    private static boolean inUse = false;\n\n    /*\n     * These arrays are mutable, but they don't need to be volatile.\n     * They can only be updated by the updateTLDOverride method, and any readers must get an instance\n     * using the getInstance methods which are all (now) synchronised.\n     */\n    // WARNING: this array MUST be sorted, otherwise it cannot be searched reliably using binary search\n    private static volatile String[] countryCodeTLDsPlus = EMPTY_STRING_ARRAY;\n\n    // WARNING: this array MUST be sorted, otherwise it cannot be searched reliably using binary search\n    private static volatile String[] genericTLDsPlus = EMPTY_STRING_ARRAY;\n\n    // WARNING: this array MUST be sorted, otherwise it cannot be searched reliably using binary search\n    private static volatile String[] countryCodeTLDsMinus = EMPTY_STRING_ARRAY;\n\n    // WARNING: this array MUST be sorted, otherwise it cannot be searched reliably using binary search\n    private static volatile String[] genericTLDsMinus = EMPTY_STRING_ARRAY;\n\n    /**\n     * Converts potentially Unicode input to punycode.\n     * If conversion fails, returns the original input.\n     *\n     * @param input the string to convert, not null\n     * @return converted input, or original input if conversion fails\n     */\n    // Needed by UrlValidator\n    static String unicodeToASCII(String input) {\n        if (isOnlyASCII(input)) { // skip possibly expensive processing\n            return input;\n        }\n        try {\n            final String ascii = IDN.toASCII(input);\n            if (IDNBUGHOLDER.IDN_TOASCII_PRESERVES_TRAILING_DOTS) {\n                return ascii;\n            }\n            final int length = input.length();\n            if (length == 0) { // check there is a last character\n                return input;\n            }\n            // RFC3490 3.1. 1)\n            //            Whenever dots are used as label separators, the following\n            //            characters MUST be recognized as dots: U+002E (full stop), U+3002\n            //            (ideographic full stop), U+FF0E (fullwidth full stop), U+FF61\n            //            (halfwidth ideographic full stop).\n            char lastChar = input.charAt(length - 1); // fetch original last char\n            switch (lastChar) {\n                case '\\u002E': // \".\" full stop\n                case '\\u3002': // ideographic full stop\n                case '\\uFF0E': // fullwidth full stop\n                case '\\uFF61': // halfwidth ideographic full stop\n                    return ascii + \".\"; // restore the missing stop\n                default:\n                    return ascii;\n            }\n        } catch (IllegalArgumentException e) { // input is not valid\n            return input;\n        }\n    }\n\n    private static class IDNBUGHOLDER {\n        private static boolean keepsTrailingDot() {\n            final String input = \"a.\"; // must be a valid name\n            return input.equals(IDN.toASCII(input));\n        }\n        private static final boolean IDN_TOASCII_PRESERVES_TRAILING_DOTS = keepsTrailingDot();\n    }\n\n    /*\n     * Check if input contains only ASCII\n     * Treats null as all ASCII\n     */\n    private static boolean isOnlyASCII(String input) {\n        if (input == null) {\n            return true;\n        }\n        for (int i = 0; i < input.length(); i++) {\n            if (input.charAt(i) > 0x7F) { // CHECKSTYLE IGNORE MagicNumber\n                return false;\n            }\n        }\n        return true;\n    }\n\n    /**\n     * Check if a sorted array contains the specified key\n     *\n     * @param sortedArray the array to search\n     * @param key the key to find\n     * @return {@code true} if the array contains the key\n     */\n    private static boolean arrayContains(String[] sortedArray, String key) {\n        return Arrays.binarySearch(sortedArray, key) >= 0;\n    }\n}\n"
  },
  {
    "path": "server/utils/src/main/java/cc/blynk/utils/validators/EmailValidator.java",
    "content": "package cc.blynk.utils.validators;\n\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * <p>Perform email validations.</p>\n * <p>\n * Based on a script by <a href=\"mailto:stamhankar@hotmail.com\">Sandeep V. Tamhankar</a>\n * http://javascript.internet.com\n * </p>\n * <p>\n * This implementation is not guaranteed to catch all possible errors in an email address.\n * </p>.\n *\n * @version $Revision: 1723573 $\n * @since Validator 1.4\n */\npublic class EmailValidator {\n\n    private static final String SPECIAL_CHARS = \"\\\\p{Cntrl}\\\\(\\\\)<>@,;:'\\\\\\\\\\\\\\\"\\\\.\\\\[\\\\]\";\n    private static final String VALID_CHARS = \"(\\\\\\\\.)|[^\\\\s\" + SPECIAL_CHARS + \"]\";\n    private static final String QUOTED_USER = \"(\\\"(\\\\\\\\\\\"|[^\\\"])*\\\")\";\n    private static final String WORD = \"((\" + VALID_CHARS + \"|')+|\" + QUOTED_USER + \")\";\n\n    private static final String EMAIL_REGEX = \"^\\\\s*?(.+)@(.+?)\\\\s*$\";\n    private static final String IP_DOMAIN_REGEX = \"^\\\\[(.*)\\\\]$\";\n    private static final String USER_REGEX = \"^\\\\s*\" + WORD + \"(\\\\.\" + WORD + \")*$\";\n\n    private static final Pattern EMAIL_PATTERN = Pattern.compile(EMAIL_REGEX);\n    private static final Pattern IP_DOMAIN_PATTERN = Pattern.compile(IP_DOMAIN_REGEX);\n    private static final Pattern USER_PATTERN = Pattern.compile(USER_REGEX);\n\n    private static final int MAX_USERNAME_LEN = 64;\n\n    private final boolean allowLocal;\n    private final boolean allowTld;\n\n    /**\n     * Singleton instance of this class, which\n     *  doesn't consider local addresses as valid.\n     */\n    private static final EmailValidator EMAIL_VALIDATOR = new EmailValidator(false, false);\n\n\n    /**\n     * Returns the Singleton instance of this validator.\n     *\n     * @return singleton instance of this validator.\n     */\n    public static EmailValidator getInstance() {\n        return EMAIL_VALIDATOR;\n    }\n\n    /**\n     * Protected constructor for subclasses to use.\n     *\n     * @param allowLocal Should local addresses be considered valid?\n     * @param allowTld Should TLDs be allowed?\n     */\n    protected EmailValidator(boolean allowLocal, boolean allowTld) {\n        super();\n        this.allowLocal = allowLocal;\n        this.allowTld = allowTld;\n    }\n\n    /**\n     * <p>Checks if a field has a valid e-mail address.</p>\n     *\n     * @param email The value validation is being performed on.  A <code>null</code>\n     *              value is considered invalid.\n     * @return true if the email address is valid.\n     */\n    public boolean isValid(String email) {\n        if (email.endsWith(\".\")) { // check this first - it's cheap!\n            return false;\n        }\n\n        // Check the whole email address structure\n        Matcher emailMatcher = EMAIL_PATTERN.matcher(email);\n        if (!emailMatcher.matches()) {\n            return false;\n        }\n\n        if (!isValidUser(emailMatcher.group(1))) {\n            return false;\n        }\n\n        return isValidDomain(emailMatcher.group(2));\n\n    }\n\n    /**\n     * Returns true if the domain component of an email address is valid.\n     *\n     * @param domain being validated, may be in IDN format\n     * @return true if the email address's domain is valid.\n     */\n    protected boolean isValidDomain(String domain) {\n        // see if domain is an IP address in brackets\n        Matcher ipDomainMatcher = IP_DOMAIN_PATTERN.matcher(domain);\n\n        if (ipDomainMatcher.matches()) {\n            InetAddressValidator inetAddressValidator =\n                    InetAddressValidator.getInstance();\n            return inetAddressValidator.isValid(ipDomainMatcher.group(1));\n        }\n        // Domain is symbolic name\n        DomainValidator domainValidator =\n                DomainValidator.getInstance(allowLocal);\n        if (allowTld) {\n            return domainValidator.isValid(domain) || (!domain.startsWith(\".\") && domainValidator.isValidTld(domain));\n        } else {\n            return domainValidator.isValid(domain);\n        }\n    }\n\n    /**\n     * Returns true if the user component of an email address is valid.\n     *\n     * @param user being validated\n     * @return true if the user name is valid.\n     */\n    protected boolean isValidUser(String user) {\n        return !(user == null || user.length() > MAX_USERNAME_LEN) && USER_PATTERN.matcher(user).matches();\n    }\n\n}\n"
  },
  {
    "path": "server/utils/src/main/java/cc/blynk/utils/validators/InetAddressValidator.java",
    "content": "package cc.blynk.utils.validators;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\n/**\n * <p><b>InetAddress</b> validation and conversion routines (<code>java.net.InetAddress</code>).</p>\n *\n * <p>This class provides methods to validate a candidate IP address.\n *\n * <p>\n * This class is a Singleton; you can retrieve the instance via the {@link #getInstance()} method.\n * </p>\n *\n * @version $Revision: 1783032 $\n * @since Validator 1.4\n */\npublic class InetAddressValidator {\n\n    private static final int IPV4_MAX_OCTET_VALUE = 255;\n\n    private static final int MAX_UNSIGNED_SHORT = 0xffff;\n\n    private static final int BASE_16 = 16;\n\n    private static final String IPV4_REGEX =\n            \"^(\\\\d{1,3})\\\\.(\\\\d{1,3})\\\\.(\\\\d{1,3})\\\\.(\\\\d{1,3})$\";\n\n    // Max number of hex groups (separated by :) in an IPV6 address\n    private static final int IPV6_MAX_HEX_GROUPS = 8;\n\n    // Max hex digits in each IPv6 group\n    private static final int IPV6_MAX_HEX_DIGITS_PER_GROUP = 4;\n\n    /**\n     * Singleton instance of this class.\n     */\n    private static final InetAddressValidator VALIDATOR = new InetAddressValidator();\n\n    /** IPv4 RegexValidator */\n    private final RegexValidator ipv4Validator = new RegexValidator(IPV4_REGEX);\n\n    /**\n     * Returns the singleton instance of this validator.\n     * @return the singleton instance of this validator\n     */\n    public static InetAddressValidator getInstance() {\n        return VALIDATOR;\n    }\n\n    /**\n     * Checks if the specified string is a valid IP address.\n     * @param inetAddress the string to validate\n     * @return true if the string validates as an IP address\n     */\n    public boolean isValid(String inetAddress) {\n        return isValidInet4Address(inetAddress) || isValidInet6Address(inetAddress);\n    }\n\n    /**\n     * Validates an IPv4 address. Returns true if valid.\n     * @param inet4Address the IPv4 address to validate\n     * @return true if the argument contains a valid IPv4 address\n     */\n    public boolean isValidInet4Address(String inet4Address) {\n        // verify that address conforms to generic IPv4 format\n        String[] groups = ipv4Validator.match(inet4Address);\n\n        if (groups == null) {\n            return false;\n        }\n\n        // verify that address subgroups are legal\n        for (String ipSegment : groups) {\n            if (ipSegment == null || ipSegment.length() == 0) {\n                return false;\n            }\n\n            int iIpSegment;\n\n            try {\n                iIpSegment = Integer.parseInt(ipSegment);\n            } catch (NumberFormatException e) {\n                return false;\n            }\n\n            if (iIpSegment > IPV4_MAX_OCTET_VALUE) {\n                return false;\n            }\n\n            if (ipSegment.length() > 1 && ipSegment.startsWith(\"0\")) {\n                return false;\n            }\n\n        }\n\n        return true;\n    }\n\n    /**\n     * Validates an IPv6 address. Returns true if valid.\n     * @param inet6Address the IPv6 address to validate\n     * @return true if the argument contains a valid IPv6 address\n     *\n     * @since 1.4.1\n     */\n    public boolean isValidInet6Address(String inet6Address) {\n        boolean containsCompressedZeroes = inet6Address.contains(\"::\");\n        if (containsCompressedZeroes && (inet6Address.indexOf(\"::\") != inet6Address.lastIndexOf(\"::\"))) {\n            return false;\n        }\n        if ((inet6Address.startsWith(\":\") && !inet6Address.startsWith(\"::\"))\n                || (inet6Address.endsWith(\":\") && !inet6Address.endsWith(\"::\"))) {\n            return false;\n        }\n        String[] octets = inet6Address.split(\":\");\n        if (containsCompressedZeroes) {\n            List<String> octetList = new ArrayList<>(Arrays.asList(octets));\n            if (inet6Address.endsWith(\"::\")) {\n                // String.split() drops ending empty segments\n                octetList.add(\"\");\n            } else if (inet6Address.startsWith(\"::\") && !octetList.isEmpty()) {\n                octetList.remove(0);\n            }\n            octets = octetList.toArray(new String[0]);\n        }\n        if (octets.length > IPV6_MAX_HEX_GROUPS) {\n            return false;\n        }\n        int validOctets = 0;\n        int emptyOctets = 0; // consecutive empty chunks\n        for (int index = 0; index < octets.length; index++) {\n            String octet = octets[index];\n            if (octet.length() == 0) {\n                emptyOctets++;\n                if (emptyOctets > 1) {\n                    return false;\n                }\n            } else {\n                emptyOctets = 0;\n                // Is last chunk an IPv4 address?\n                if (index == octets.length - 1 && octet.contains(\".\")) {\n                    if (!isValidInet4Address(octet)) {\n                        return false;\n                    }\n                    validOctets += 2;\n                    continue;\n                }\n                if (octet.length() > IPV6_MAX_HEX_DIGITS_PER_GROUP) {\n                    return false;\n                }\n                int octetInt;\n                try {\n                    octetInt = Integer.parseInt(octet, BASE_16);\n                } catch (NumberFormatException e) {\n                    return false;\n                }\n                if (octetInt < 0 || octetInt > MAX_UNSIGNED_SHORT) {\n                    return false;\n                }\n            }\n            validOctets++;\n        }\n        return !(validOctets > IPV6_MAX_HEX_GROUPS || (validOctets < IPV6_MAX_HEX_GROUPS && !containsCompressedZeroes));\n    }\n}\n"
  },
  {
    "path": "server/utils/src/main/java/cc/blynk/utils/validators/RegexValidator.java",
    "content": "package cc.blynk.utils.validators;\n\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * <b>Regular Expression</b> validation (using JDK 1.4+ regex support).\n * <p>\n * Construct the validator either for a single regular expression or a set (array) of\n * regular expressions. By default validation is <i>case sensitive</i> but constructors\n * are provided to allow  <i>case in-sensitive</i> validation. For example to create\n * a validator which does <i>case in-sensitive</i> validation for a set of regular\n * expressions:\n * </p>\n * <pre>\n * <code>\n * String[] regexs = new String[] {...};\n * RegexValidator validator = new RegexValidator(regexs, false);\n * </code>\n * </pre>\n *\n * <ul>\n *   <li>Validate <code>true</code> or <code>false</code>:</li>\n *   <li>\n *     <ul>\n *       <li><code>boolean valid = validator.isValid(value);</code></li>\n *     </ul>\n *   </li>\n *   <li>Validate returning an aggregated String of the matched groups:</li>\n *   <li>\n *     <ul>\n *       <li><code>String result = validator.validate(value);</code></li>\n *     </ul>\n *   </li>\n *   <li>Validate returning the matched groups:</li>\n *   <li>\n *     <ul>\n *       <li><code>String[] result = validator.match(value);</code></li>\n *     </ul>\n *   </li>\n * </ul>\n *\n * <b>Note that patterns are matched against the entire input.</b>\n *\n * <p>\n * Cached instances pre-compile and re-use {@link Pattern}(s) - which according\n * to the {@link Pattern} API are safe to use in a multi-threaded environment.\n * </p>\n *\n * @version $Revision: 1739356 $\n * @since Validator 1.4\n */\npublic class RegexValidator {\n\n    private final Pattern[] patterns;\n\n    /**\n     * Construct a <i>case sensitive</i> validator for a single\n     * regular expression.\n     *\n     * @param regex The regular expression this validator will\n     * validate against\n     */\n    public RegexValidator(String regex) {\n        this(regex, true);\n    }\n\n    /**\n     * Construct a validator for a single regular expression\n     * with the specified case sensitivity.\n     *\n     * @param regex The regular expression this validator will\n     * validate against\n     * @param caseSensitive when <code>true</code> matching is <i>case\n     * sensitive</i>, otherwise matching is <i>case in-sensitive</i>\n     */\n    public RegexValidator(String regex, boolean caseSensitive) {\n        this(new String[] {regex}, caseSensitive);\n    }\n\n    /**\n     * Construct a validator that matches any one of the set of regular\n     * expressions with the specified case sensitivity.\n     *\n     * @param regexs The set of regular expressions this validator will\n     * validate against\n     * @param caseSensitive when <code>true</code> matching is <i>case\n     * sensitive</i>, otherwise matching is <i>case in-sensitive</i>\n     */\n    public RegexValidator(String[] regexs, boolean caseSensitive) {\n        if (regexs == null || regexs.length == 0) {\n            throw new IllegalArgumentException(\"Regular expressions are missing\");\n        }\n        patterns = new Pattern[regexs.length];\n        int flags =  (caseSensitive ? 0 : Pattern.CASE_INSENSITIVE);\n        for (int i = 0; i < regexs.length; i++) {\n            if (regexs[i] == null || regexs[i].length() == 0) {\n                throw new IllegalArgumentException(\"Regular expression[\" + i + \"] is missing\");\n            }\n            patterns[i] =  Pattern.compile(regexs[i], flags);\n        }\n    }\n\n    /**\n     * Validate a value against the set of regular expressions.\n     *\n     * @param value The value to validate.\n     * @return <code>true</code> if the value is valid\n     * otherwise <code>false</code>.\n     */\n    public boolean isValid(String value) {\n        if (value == null) {\n            return false;\n        }\n        for (Pattern pattern : patterns) {\n            if (pattern.matcher(value).matches()) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    /**\n     * Validate a value against the set of regular expressions\n     * returning the array of matched groups.\n     *\n     * @param value The value to validate.\n     * @return String array of the <i>groups</i> matched if\n     * valid or <code>null</code> if invalid\n     */\n    public String[] match(String value) {\n        if (value == null) {\n            return null;\n        }\n        for (Pattern pattern : patterns) {\n            Matcher matcher = pattern.matcher(value);\n            if (matcher.matches()) {\n                int count = matcher.groupCount();\n                String[] groups = new String[count];\n                for (int j = 0; j < count; j++) {\n                    groups[j] = matcher.group(j + 1);\n                }\n                return groups;\n            }\n        }\n        return null;\n    }\n\n    /**\n     * Provide a String representation of this validator.\n     * @return A String representation of this validator\n     */\n    @Override\n    public String toString() {\n        StringBuilder buffer = new StringBuilder();\n        buffer.append(\"RegexValidator{\");\n        for (int i = 0; i < patterns.length; i++) {\n            if (i > 0) {\n                buffer.append(\",\");\n            }\n            buffer.append(patterns[i].pattern());\n        }\n        buffer.append(\"}\");\n        return buffer.toString();\n    }\n\n}\n"
  },
  {
    "path": "server/utils/src/main/java/module-info.java",
    "content": "/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 30.09.17.\n */\nmodule cc.blynk.utils {\n    exports cc.blynk.utils;\n    exports cc.blynk.utils.http;\n    exports cc.blynk.utils.properties;\n    exports cc.blynk.utils.structure;\n    exports cc.blynk.utils.validators;\n}"
  },
  {
    "path": "server/utils/src/test/java/cc/blynk/utils/structure/IntArrayTest.java",
    "content": "package cc.blynk.utils.structure;\n\nimport cc.blynk.utils.IntArray;\nimport org.junit.Test;\n\nimport static org.junit.Assert.assertEquals;\n\npublic class IntArrayTest {\n\n    @Test\n    public void test() {\n        IntArray intArray = new IntArray();\n        assertEquals(0, intArray.toArray().length);\n\n        intArray.add(10);\n        int[] result = intArray.toArray();\n        assertEquals(1, result.length);\n        assertEquals(10, result[0]);\n\n        intArray = new IntArray();\n        for (int i = 0; i < 1000; i++) {\n            intArray.add(i);\n        }\n\n        result = intArray.toArray();\n        assertEquals(1000, result.length);\n\n        for (int i = 0; i < 1000; i++) {\n            assertEquals(i, result[i]);\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "server/utils/src/test/java/cc/blynk/utils/structure/LimitedArrayDequeTest.java",
    "content": "package cc.blynk.utils.structure;\n\nimport org.junit.Test;\n\nimport static org.junit.Assert.assertEquals;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 07.09.16.\n */\npublic class LimitedArrayDequeTest {\n\n    @Test\n    public void testDeque() {\n        LimitedArrayDeque<String> limitedArrayDeque = new LimitedArrayDeque<>(4);\n        limitedArrayDeque.add(\"1\");\n        limitedArrayDeque.add(\"2\");\n        limitedArrayDeque.add(\"3\");\n        limitedArrayDeque.add(\"4\");\n        limitedArrayDeque.add(\"5\");\n\n        assertEquals(4, limitedArrayDeque.size());\n        int i = 2;\n        for (String s : limitedArrayDeque) {\n            assertEquals(\"\" + i++, s);\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/utils/src/test/java/cc/blynk/utils/structure/LimitedQueueTest.java",
    "content": "package cc.blynk.utils.structure;\n\nimport org.junit.Test;\n\nimport static org.junit.Assert.assertEquals;\n\n/**\n * The Blynk Project.\n * Created by Dmitriy Dumanskiy.\n * Created on 24.05.16.\n */\npublic class LimitedQueueTest {\n\n    @Test\n    public void addLimitTest() {\n        BaseLimitedQueue<String> list = new BaseLimitedQueue<>(2);\n        list.add(\"1\");\n        list.add(\"2\");\n        list.add(\"3\");\n        assertEquals(2, list.size());\n        assertEquals(\"2\", list.poll());\n        assertEquals(\"3\", list.poll());\n    }\n\n    private static BaseLimitedQueue<String> makeList() {\n        return new BaseLimitedQueue<>(3) {{\n            add(\"1\");\n            add(\"2\");\n            add(\"3\");\n        }\n        };\n    }\n\n}\n"
  }
]