diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e62bf85
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,14 @@
+# Vagrant
+.vagrant/
+
+# OSX
+.DS_store
+
+# Idea
+build/
+.gradle/
+.idea
+*.iml
+
+# Redash
+src/test/resources/redash_dynamic.properties
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..5c53b8d
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,30 @@
+sudo: required
+language: java
+
+services:
+ - docker
+
+env:
+ matrix:
+ - DOCKER_COMPOSE_VERSION=1.20.0
+ global:
+ - secure: dAf7IBdUmw93M2w8CfbkMyH1VMGn46vIYCOhNI2vjWgK3yli8qOrcVnGGKegeXwr1oLO0e1ZPdVV2DgnktRdveFJag+IWfhcP4S3ChFrC7/cYzvcziRCVjVbBVcjgQxlRBRvY48FAD7u8erTteXeOnZcKLE6jGVP1JZjzgoLtVLT34yYYH6Mdfk6iQjJruYrT466zLwfv/0VCV033+17PjCWBP8PDKBx59ouluGLsWLq9NfaKW8gL10Pmg3Pzpri0BFl94o5J0y0/0hKiTI6Evyf4XB0PVI3tMbwIukzx7vQG7NY4qtDwCbQ9044TlwawhqOx8fTWOnuvO83wzmRYpwdOc3lGQZAmJM6nYfGrbXxwbwWAgegqHoZ/5XgY06QmOKnr5xtFMiEnw6g5fY7XXC7L9QOCGBIm3dsL/SZQ52nyGmW7qNGbuqz1fG7rs+JCbNO02fugNTmVJPbDorVe0AtzTxvBxTBQkmqmSa8TqBj6NVvL3+GJ8wBrumq9VxBlYvpmSjFhFhXWGTegdcAAX/BaiuykLww2SB8EvICJNCoSREy00R5NlYrAFtsurMaNrKiy5+ri2EBDRjNN1Uj4wfLmN6wpGJlPTlDXsXZheJsScKGl9ESzJS8V7slb6RpK0P2y1/ZDcyWDMGxYZ/ov2ENziJh19jbQYrqZ96te4I=
+ - secure: e+37F4FwSbbZh4b2zh9cMkD2gGAuJSsDrBm3IRtTeo8WDRfqyov/PnO7sphS4icxZG2T2w3Tfl5ghJW40ohMWHjmratz16PFZ1HSYR8ZEhmuFQuTeGroGH5z8skP5VZiVjix5b2ilm/7PP3n11thx4bXJNzaNeKcgS/dAjlko9XjlUXnuRav1SgBzZGavA3pVx4rJgR9SO9g4TVaWZ4uPtS2fXV2OFPANmgIKWOcB4hlP7RkBNpw5+NEeC0S5lEh7CxsxcdamVtSqSsQtNkwXUywsIL+WAUk+JJzDX7WI0yk7HPkjw8nzjF4O2Z9MqFf2v7DwH3tqKwbfSqxURoikoQzj+qEI/GlWw+J0jkSN+hBoMXcLd+QUKBJVX3xsYPfGj5hyrpSlkPeiJAz/0bbXagrWkmx8v122+5QBQ1NZmw3YHjWsC8nPpbd34v0i0jSc5nhgAEFHApLB5EEAb0BfdUhyUtCFtFw/2JO0S9FlI3CXBFH9udNrdpo7U0+WPh7MX6VWcMeO6sHgFBIvGaI0NBJXMxG7UVFMe8GWBzxDclCAAbHvdS+VcTMNZMH6Vjv7MdHVfoabPaD6kQ8wFr1AmghWNcye3KZXnqkkoVabzCJALG1ovkJC8Q6dwQ6uQKTFFzWZ513zxBgjdCGXtG+qG9Ulth0r7YqWZZuHQjoANE=
+
+before_install:
+ - sudo apt-get update
+ - sudo apt-get -y -o Dpkg::Options::="--force-confnew" install docker-ce
+ - sudo rm /usr/local/bin/docker-compose
+ - curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose
+ - chmod +x docker-compose
+ - sudo mv docker-compose /usr/local/bin
+
+script:
+ - ./gradlew test jacocoTestReport coveralls
+
+deploy:
+ skip_cleanup: true
+ provider: script
+ script: "./gradlew bintrayUpload"
+ on:
+ tags: true
diff --git a/CHANGELOG b/CHANGELOG
new file mode 100644
index 0000000..8092b2e
--- /dev/null
+++ b/CHANGELOG
@@ -0,0 +1,3 @@
+Version 0.1.0 (2018-03-xx)
+--------------------------
+Initial release
diff --git a/LICENSE-2.0.txt b/LICENSE-2.0.txt
new file mode 100644
index 0000000..5b24426
--- /dev/null
+++ b/LICENSE-2.0.txt
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2012-2018 Snowplow Analytics Ltd.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+limitations under the License.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..228151b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,90 @@
+# redash-java-sdk
+
+[![Build Status][travis-image]][travis] [![Coveralls][coveralls-image]][coveralls] [![Release][release-image]][releases] [![License][license-image]][license]
+
+## Overview
+
+This library can be used to interact with your Redash server on a programatic level. The API functions it currently exposes are:
+
+1. Create new data-source
+2. Update specific data-source
+3. List all data-sources
+4. Delete specific data-source
+5. Create new user-group
+6. Add user to specific user-group(s)
+7. Add data-source to specific user-group(s)
+8. Remove user from specific user-group(s)
+9. Remove data-source from specific user-groups(s)
+10. List all user-groups
+11. Delete specific user-group
+12. List all users
+13. Get specific user
+14. Get specific data-source
+
+Let us know if there are other endpoints you would like to see added here!
+
+### Documentation
+
+Please see the [Javadoc][techdocs] for help in understanding the API or the [Setup Guide][setup] for help on integrating the library.
+
+### Compatibility
+
+This library has been tested with the following versions of Redash:
+
+* 3.0.0+b3134
+
+## Quickstart
+
+Assuming git, **[Vagrant][vagrant-install]** and **[VirtualBox][virtualbox-install]** installed:
+
+```bash
+ host$ git clone https://github.com/snowplow-incubator/redash-java-sdk.git
+ host$ cd redash-java-sdk
+ host$ vagrant up && vagrant ssh
+guest$ cd /vagrant
+```
+
+The tests require a local Redash server be setup and configured in a certain way. This setup has been fully automated and added as a custom gradle task `redashSetup` which is always run before running `test`. This task does several things:
+
+1. Launches the 5 docker containers required for redash
+2. Configures an admin and default user
+3. Extracts the admin users API key
+4. Populates a dynamic properties resource within `src/test/resources/redash_dynamic.properties`
+
+__Note__: If there are any issues with this setup you can first try resetting the setup via `./gradlew redashDestroy` and then rerunning the `./gradlew redashSetup`.
+
+```
+guest$ ./gradlew clean build
+guest$ ./gradlew test
+```
+
+## Copyright and license
+
+The Redash Java SDK is copyright 2018 Snowplow Analytics Ltd.
+
+Licensed under the **[Apache License, Version 2.0][license]** (the "License");
+you may not use this software except in compliance with the License.
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+[travis]: https://travis-ci.org/snowplow-incubator/redash-java-sdk
+[travis-image]: https://travis-ci.org/snowplow-incubator/redash-java-sdk.svg?branch=master
+
+[release-image]: http://img.shields.io/badge/release-0.1.0-6ad7e5.svg?style=flat
+[releases]: https://github.com/snowplow-incubator/redash-java-sdk/releases
+
+[license-image]: http://img.shields.io/badge/license-Apache--2-blue.svg?style=flat
+[license]: http://www.apache.org/licenses/LICENSE-2.0
+
+[coveralls-image]: https://coveralls.io/repos/github/snowplow-incubator/redash-java-sdk/badge.svg?branch=master
+[coveralls]: https://coveralls.io/github/snowplow-incubator/redash-java-sdk?branch=master
+
+[vagrant-install]: http://docs.vagrantup.com/v2/installation/index.html
+[virtualbox-install]: https://www.virtualbox.org/wiki/Downloads
+
+[techdocs]: https://snowplow-incubator.github.io/redash-java-sdk/
+[setup]: https://github.com/snowplow-incubator/redash-java-sdk/wiki/Redash-Java-SDK-Setup
diff --git a/VERSION b/VERSION
new file mode 100644
index 0000000..6c6aa7c
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+0.1.0
\ No newline at end of file
diff --git a/Vagrantfile b/Vagrantfile
new file mode 100644
index 0000000..577e640
--- /dev/null
+++ b/Vagrantfile
@@ -0,0 +1,26 @@
+Vagrant.configure("2") do |config|
+
+ config.vm.box = "ubuntu/trusty64"
+ config.vm.hostname = "redash-java-sdk"
+ config.ssh.forward_agent = true
+
+ # Required for NFS to work, pick any local IP
+ # Use NFS for shared folders for better performance
+ config.vm.network :private_network, ip: '192.168.50.50' # Uncomment to use NFS
+ config.vm.synced_folder '.', '/vagrant', nfs: true # Uncomment to use NFS
+ config.vm.network "forwarded_port", guest: 80, host: 2000
+
+ config.vm.provider :virtualbox do |vb|
+ vb.name = Dir.pwd().split("/")[-1] + "-" + Time.now.to_f.to_i.to_s
+ vb.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
+ vb.customize [ "guestproperty", "set", :id, "--timesync-threshold", 10000 ]
+ # Java is memory-hungry
+ vb.memory = 5120
+ vb.cpus = 4 # Uncomment to use more cores
+ end
+
+ config.vm.provision :shell do |sh|
+ sh.path = "vagrant/up.bash"
+ end
+
+end
diff --git a/build.gradle b/build.gradle
new file mode 100755
index 0000000..b2a2cd9
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,131 @@
+// --- Configure Plugins and Repositories
+
+buildscript {
+ repositories {
+ jcenter()
+ mavenCentral()
+ }
+ dependencies {
+ classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7'
+ }
+}
+
+plugins {
+ id 'jacoco'
+ id 'com.github.kt3k.coveralls' version '2.6.3'
+}
+
+apply plugin: 'java'
+apply plugin: 'maven-publish'
+apply plugin: 'jacoco'
+apply plugin: 'com.jfrog.bintray'
+
+wrapper.gradleVersion = '2.14'
+
+group 'com.snowplowanalytics.redash'
+version '0.1.0'
+
+sourceCompatibility = 1.8
+
+jacoco {
+ toolVersion = "0.7.7.201606060606"
+ reportsDir = file("$buildDir/reports/")
+}
+
+repositories {
+ jcenter()
+ mavenCentral()
+}
+
+dependencies {
+ compile group: 'com.squareup.okhttp3', name: 'okhttp', version: '3.9.1'
+ compile group: 'org.json', name: 'json', version: '20171018'
+ compile group: 'com.google.code.gson', name: 'gson', version: '2.8.2'
+
+ testCompile group: 'org.mockito', name: 'mockito-all', version: '1.10.19'
+ testCompile group: 'junit', name: 'junit', version: '4.12'
+}
+
+// --- Configure Tasks
+
+task redashSetup(type: Exec) {
+ commandLine 'sh', './integration/setup_redash.bash'
+}
+
+task redashDestroy(type: Exec) {
+ commandLine 'sh', './integration/remove_redash.bash'
+}
+
+jacocoTestReport {
+ reports {
+ xml.enabled = true
+ xml.destination = "${buildDir}/reports/jacoco/test/jacocoTestReport.xml"
+ html.enabled = true
+ html.destination = "${buildDir}/reports/jacoco/test/html"
+ }
+}
+
+test {
+ dependsOn redashSetup
+ finalizedBy jacocoTestReport
+}
+
+// --- Deployment
+
+task sourcesJar(type: Jar, dependsOn: classes) {
+ classifier = 'sources'
+ from sourceSets.main.allSource
+}
+
+task javadocJar(type: Jar, dependsOn: javadoc) {
+ classifier = 'javadoc'
+ from javadoc.destinationDir
+}
+
+task cleanDocs(type: Exec) {
+ commandLine "rm", "-rf", "docs/"
+}
+
+task commitDocs(type: Exec) {
+ dependsOn clean
+ dependsOn javadoc
+ dependsOn cleanDocs
+ commandLine "cp", "-a", "${buildDir}/docs/javadoc/.", "docs/"
+}
+
+artifacts {
+ archives sourcesJar, javadocJar
+}
+
+publishing {
+ publications {
+ mavenJava(MavenPublication) {
+ from components.java
+ artifact sourcesJar
+ artifact javadocJar
+ }
+ }
+}
+
+bintray {
+ user = System.getenv('BINTRAY_SNOWPLOW_MAVEN_USER')
+ key = System.getenv('BINTRAY_SNOWPLOW_MAVEN_API_KEY')
+
+ publish = true
+ override = false
+
+ publications = ['mavenJava']
+
+ pkg {
+ repo = 'snowplow-maven'
+ name = 'redash-java-sdk'
+ group = "${project.group}"
+ userOrg = 'snowplow'
+
+ websiteUrl = 'https://github.com/snowplow-incubator/redash-java-sdk'
+ issueTrackerUrl = 'https://github.com/snowplow-incubator/redash-java-sdk/issues'
+ vcsUrl = 'https://github.com/snowplow-incubator/redash-java-sdk'
+
+ version.name = "${project.version}"
+ }
+}
diff --git a/docs/allclasses-frame.html b/docs/allclasses-frame.html
new file mode 100644
index 0000000..0861dd1
--- /dev/null
+++ b/docs/allclasses-frame.html
@@ -0,0 +1,27 @@
+
+
+
+
Returns an array containing the constants of this enum type, in
+the order they are declared. This method may be used to iterate
+over the constants as follows:
+
+for (CheckResponseStatus c : CheckResponseStatus.values())
+ System.out.println(c);
+
+
+
Returns:
+
an array containing the constants of this enum type, in the order they are declared
Returns the enum constant of this type with the specified name.
+The string must match exactly an identifier used to declare an
+enum constant in this type. (Extraneous whitespace characters are
+not permitted.)
+
+
Parameters:
+
name - the name of the enum constant to be returned.
+
Returns:
+
the enum constant with the specified name
+
Throws:
+
java.lang.IllegalArgumentException - if this enum type has no constant with the specified name
+
java.lang.NullPointerException - if the argument is null
public boolean updateDataSource(DataSource dataSource)
+ throws java.io.IOException
+
Updates an already existing data-source. Please note that all the argument's fields must be present and have correct values.
+ This function uses the data-source name to discover what needs to be updated.
+
+
Parameters:
+
dataSource - A data-source object which should contain all necessary information.
+
Returns:
+
boolean False if any of the argument's fields have null or empty value and True if entity successfully updated.
+
Throws:
+
java.io.IOException - If a data-source with this name could not be found or if the server is unavailable due to a connection error
+ or if API key is invalid or if url is not a valid HTTP or HTTPS URL.
This API (Application Programming Interface) document has pages corresponding to the items in the navigation bar, described as follows.
+
+
+
+
+
Overview
+
The Overview page is the front page of this API document and provides a list of all packages with a summary for each. This page can also contain an overall description of the set of packages.
+
+
+
Package
+
Each package has a page that contains a list of its classes and interfaces, with a summary for each. This page can contain six categories:
+
+
Interfaces (italic)
+
Classes
+
Enums
+
Exceptions
+
Errors
+
Annotation Types
+
+
+
+
Class/Interface
+
Each class, interface, nested class and nested interface has its own separate page. Each of these pages has three sections consisting of a class/interface description, summary tables, and detailed member descriptions:
+
+
Class inheritance diagram
+
Direct Subclasses
+
All Known Subinterfaces
+
All Known Implementing Classes
+
Class/interface declaration
+
Class/interface description
+
+
+
Nested Class Summary
+
Field Summary
+
Constructor Summary
+
Method Summary
+
+
+
Field Detail
+
Constructor Detail
+
Method Detail
+
+
Each summary entry contains the first sentence from the detailed description for that item. The summary entries are alphabetical, while the detailed descriptions are in the order they appear in the source code. This preserves the logical groupings established by the programmer.
+
+
+
Annotation Type
+
Each annotation type has its own separate page with the following sections:
+
+
Annotation Type declaration
+
Annotation Type description
+
Required Element Summary
+
Optional Element Summary
+
Element Detail
+
+
+
+
Enum
+
Each enum has its own separate page with the following sections:
+
+
Enum declaration
+
Enum description
+
Enum Constant Summary
+
Enum Constant Detail
+
+
+
+
Tree (Class Hierarchy)
+
There is a Class Hierarchy page for all packages, plus a hierarchy for each package. Each hierarchy page contains a list of classes and a list of interfaces. The classes are organized by inheritance structure starting with java.lang.Object. The interfaces do not inherit from java.lang.Object.
+
+
When viewing the Overview page, clicking on "Tree" displays the hierarchy for all packages.
+
When viewing a particular package, class or interface page, clicking "Tree" displays the hierarchy for only that package.
+
+
+
+
Deprecated API
+
The Deprecated API page lists all of the API that have been deprecated. A deprecated API is not recommended for use, generally due to improvements, and a replacement API is usually given. Deprecated APIs may be removed in future implementations.
+
+
+
Index
+
The Index contains an alphabetic list of all classes, interfaces, constructors, methods, and fields.
+
+
+
Prev/Next
+
These links take you to the next or previous class, interface, package, or related page.
+
+
+
Frames/No Frames
+
These links show and hide the HTML frames. All pages are available with or without frames.
+
+
+
All Classes
+
The All Classes link shows all classes and interfaces except non-static nested types.
+
+
+
Serialized Form
+
Each serializable or externalizable class has a description of its serialization fields and methods. This information is of interest to re-implementors, not to developers using the API. While there is no link in the navigation bar, you can get to this information by going to any serialized class and clicking "Serialized Form" in the "See also" section of the class description.
+
+
+
diff --git a/docs/package-list b/docs/package-list
new file mode 100644
index 0000000..c60577f
--- /dev/null
+++ b/docs/package-list
@@ -0,0 +1,3 @@
+com.snowplowanalytics.redash
+com.snowplowanalytics.redash.model
+com.snowplowanalytics.redash.model.datasource
diff --git a/docs/script.js b/docs/script.js
new file mode 100644
index 0000000..b346356
--- /dev/null
+++ b/docs/script.js
@@ -0,0 +1,30 @@
+function show(type)
+{
+ count = 0;
+ for (var key in methods) {
+ var row = document.getElementById(key);
+ if ((methods[key] & type) != 0) {
+ row.style.display = '';
+ row.className = (count++ % 2) ? rowColor : altColor;
+ }
+ else
+ row.style.display = 'none';
+ }
+ updateTabs(type);
+}
+
+function updateTabs(type)
+{
+ for (var value in tabs) {
+ var sNode = document.getElementById(tabs[value][0]);
+ var spanNode = sNode.firstChild;
+ if (value == type) {
+ sNode.className = activeTableTab;
+ spanNode.innerHTML = tabs[value][1];
+ }
+ else {
+ sNode.className = tableTab;
+ spanNode.innerHTML = "" + tabs[value][1] + "";
+ }
+ }
+}
diff --git a/docs/stylesheet.css b/docs/stylesheet.css
new file mode 100644
index 0000000..98055b2
--- /dev/null
+++ b/docs/stylesheet.css
@@ -0,0 +1,574 @@
+/* Javadoc style sheet */
+/*
+Overall document style
+*/
+
+@import url('resources/fonts/dejavu.css');
+
+body {
+ background-color:#ffffff;
+ color:#353833;
+ font-family:'DejaVu Sans', Arial, Helvetica, sans-serif;
+ font-size:14px;
+ margin:0;
+}
+a:link, a:visited {
+ text-decoration:none;
+ color:#4A6782;
+}
+a:hover, a:focus {
+ text-decoration:none;
+ color:#bb7a2a;
+}
+a:active {
+ text-decoration:none;
+ color:#4A6782;
+}
+a[name] {
+ color:#353833;
+}
+a[name]:hover {
+ text-decoration:none;
+ color:#353833;
+}
+pre {
+ font-family:'DejaVu Sans Mono', monospace;
+ font-size:14px;
+}
+h1 {
+ font-size:20px;
+}
+h2 {
+ font-size:18px;
+}
+h3 {
+ font-size:16px;
+ font-style:italic;
+}
+h4 {
+ font-size:13px;
+}
+h5 {
+ font-size:12px;
+}
+h6 {
+ font-size:11px;
+}
+ul {
+ list-style-type:disc;
+}
+code, tt {
+ font-family:'DejaVu Sans Mono', monospace;
+ font-size:14px;
+ padding-top:4px;
+ margin-top:8px;
+ line-height:1.4em;
+}
+dt code {
+ font-family:'DejaVu Sans Mono', monospace;
+ font-size:14px;
+ padding-top:4px;
+}
+table tr td dt code {
+ font-family:'DejaVu Sans Mono', monospace;
+ font-size:14px;
+ vertical-align:top;
+ padding-top:4px;
+}
+sup {
+ font-size:8px;
+}
+/*
+Document title and Copyright styles
+*/
+.clear {
+ clear:both;
+ height:0px;
+ overflow:hidden;
+}
+.aboutLanguage {
+ float:right;
+ padding:0px 21px;
+ font-size:11px;
+ z-index:200;
+ margin-top:-9px;
+}
+.legalCopy {
+ margin-left:.5em;
+}
+.bar a, .bar a:link, .bar a:visited, .bar a:active {
+ color:#FFFFFF;
+ text-decoration:none;
+}
+.bar a:hover, .bar a:focus {
+ color:#bb7a2a;
+}
+.tab {
+ background-color:#0066FF;
+ color:#ffffff;
+ padding:8px;
+ width:5em;
+ font-weight:bold;
+}
+/*
+Navigation bar styles
+*/
+.bar {
+ background-color:#4D7A97;
+ color:#FFFFFF;
+ padding:.8em .5em .4em .8em;
+ height:auto;/*height:1.8em;*/
+ font-size:11px;
+ margin:0;
+}
+.topNav {
+ background-color:#4D7A97;
+ color:#FFFFFF;
+ float:left;
+ padding:0;
+ width:100%;
+ clear:right;
+ height:2.8em;
+ padding-top:10px;
+ overflow:hidden;
+ font-size:12px;
+}
+.bottomNav {
+ margin-top:10px;
+ background-color:#4D7A97;
+ color:#FFFFFF;
+ float:left;
+ padding:0;
+ width:100%;
+ clear:right;
+ height:2.8em;
+ padding-top:10px;
+ overflow:hidden;
+ font-size:12px;
+}
+.subNav {
+ background-color:#dee3e9;
+ float:left;
+ width:100%;
+ overflow:hidden;
+ font-size:12px;
+}
+.subNav div {
+ clear:left;
+ float:left;
+ padding:0 0 5px 6px;
+ text-transform:uppercase;
+}
+ul.navList, ul.subNavList {
+ float:left;
+ margin:0 25px 0 0;
+ padding:0;
+}
+ul.navList li{
+ list-style:none;
+ float:left;
+ padding: 5px 6px;
+ text-transform:uppercase;
+}
+ul.subNavList li{
+ list-style:none;
+ float:left;
+}
+.topNav a:link, .topNav a:active, .topNav a:visited, .bottomNav a:link, .bottomNav a:active, .bottomNav a:visited {
+ color:#FFFFFF;
+ text-decoration:none;
+ text-transform:uppercase;
+}
+.topNav a:hover, .bottomNav a:hover {
+ text-decoration:none;
+ color:#bb7a2a;
+ text-transform:uppercase;
+}
+.navBarCell1Rev {
+ background-color:#F8981D;
+ color:#253441;
+ margin: auto 5px;
+}
+.skipNav {
+ position:absolute;
+ top:auto;
+ left:-9999px;
+ overflow:hidden;
+}
+/*
+Page header and footer styles
+*/
+.header, .footer {
+ clear:both;
+ margin:0 20px;
+ padding:5px 0 0 0;
+}
+.indexHeader {
+ margin:10px;
+ position:relative;
+}
+.indexHeader span{
+ margin-right:15px;
+}
+.indexHeader h1 {
+ font-size:13px;
+}
+.title {
+ color:#2c4557;
+ margin:10px 0;
+}
+.subTitle {
+ margin:5px 0 0 0;
+}
+.header ul {
+ margin:0 0 15px 0;
+ padding:0;
+}
+.footer ul {
+ margin:20px 0 5px 0;
+}
+.header ul li, .footer ul li {
+ list-style:none;
+ font-size:13px;
+}
+/*
+Heading styles
+*/
+div.details ul.blockList ul.blockList ul.blockList li.blockList h4, div.details ul.blockList ul.blockList ul.blockListLast li.blockList h4 {
+ background-color:#dee3e9;
+ border:1px solid #d0d9e0;
+ margin:0 0 6px -8px;
+ padding:7px 5px;
+}
+ul.blockList ul.blockList ul.blockList li.blockList h3 {
+ background-color:#dee3e9;
+ border:1px solid #d0d9e0;
+ margin:0 0 6px -8px;
+ padding:7px 5px;
+}
+ul.blockList ul.blockList li.blockList h3 {
+ padding:0;
+ margin:15px 0;
+}
+ul.blockList li.blockList h2 {
+ padding:0px 0 20px 0;
+}
+/*
+Page layout container styles
+*/
+.contentContainer, .sourceContainer, .classUseContainer, .serializedFormContainer, .constantValuesContainer {
+ clear:both;
+ padding:10px 20px;
+ position:relative;
+}
+.indexContainer {
+ margin:10px;
+ position:relative;
+ font-size:12px;
+}
+.indexContainer h2 {
+ font-size:13px;
+ padding:0 0 3px 0;
+}
+.indexContainer ul {
+ margin:0;
+ padding:0;
+}
+.indexContainer ul li {
+ list-style:none;
+ padding-top:2px;
+}
+.contentContainer .description dl dt, .contentContainer .details dl dt, .serializedFormContainer dl dt {
+ font-size:12px;
+ font-weight:bold;
+ margin:10px 0 0 0;
+ color:#4E4E4E;
+}
+.contentContainer .description dl dd, .contentContainer .details dl dd, .serializedFormContainer dl dd {
+ margin:5px 0 10px 0px;
+ font-size:14px;
+ font-family:'DejaVu Sans Mono',monospace;
+}
+.serializedFormContainer dl.nameValue dt {
+ margin-left:1px;
+ font-size:1.1em;
+ display:inline;
+ font-weight:bold;
+}
+.serializedFormContainer dl.nameValue dd {
+ margin:0 0 0 1px;
+ font-size:1.1em;
+ display:inline;
+}
+/*
+List styles
+*/
+ul.horizontal li {
+ display:inline;
+ font-size:0.9em;
+}
+ul.inheritance {
+ margin:0;
+ padding:0;
+}
+ul.inheritance li {
+ display:inline;
+ list-style:none;
+}
+ul.inheritance li ul.inheritance {
+ margin-left:15px;
+ padding-left:15px;
+ padding-top:1px;
+}
+ul.blockList, ul.blockListLast {
+ margin:10px 0 10px 0;
+ padding:0;
+}
+ul.blockList li.blockList, ul.blockListLast li.blockList {
+ list-style:none;
+ margin-bottom:15px;
+ line-height:1.4;
+}
+ul.blockList ul.blockList li.blockList, ul.blockList ul.blockListLast li.blockList {
+ padding:0px 20px 5px 10px;
+ border:1px solid #ededed;
+ background-color:#f8f8f8;
+}
+ul.blockList ul.blockList ul.blockList li.blockList, ul.blockList ul.blockList ul.blockListLast li.blockList {
+ padding:0 0 5px 8px;
+ background-color:#ffffff;
+ border:none;
+}
+ul.blockList ul.blockList ul.blockList ul.blockList li.blockList {
+ margin-left:0;
+ padding-left:0;
+ padding-bottom:15px;
+ border:none;
+}
+ul.blockList ul.blockList ul.blockList ul.blockList li.blockListLast {
+ list-style:none;
+ border-bottom:none;
+ padding-bottom:0;
+}
+table tr td dl, table tr td dl dt, table tr td dl dd {
+ margin-top:0;
+ margin-bottom:1px;
+}
+/*
+Table styles
+*/
+.overviewSummary, .memberSummary, .typeSummary, .useSummary, .constantsSummary, .deprecatedSummary {
+ width:100%;
+ border-left:1px solid #EEE;
+ border-right:1px solid #EEE;
+ border-bottom:1px solid #EEE;
+}
+.overviewSummary, .memberSummary {
+ padding:0px;
+}
+.overviewSummary caption, .memberSummary caption, .typeSummary caption,
+.useSummary caption, .constantsSummary caption, .deprecatedSummary caption {
+ position:relative;
+ text-align:left;
+ background-repeat:no-repeat;
+ color:#253441;
+ font-weight:bold;
+ clear:none;
+ overflow:hidden;
+ padding:0px;
+ padding-top:10px;
+ padding-left:1px;
+ margin:0px;
+ white-space:pre;
+}
+.overviewSummary caption a:link, .memberSummary caption a:link, .typeSummary caption a:link,
+.useSummary caption a:link, .constantsSummary caption a:link, .deprecatedSummary caption a:link,
+.overviewSummary caption a:hover, .memberSummary caption a:hover, .typeSummary caption a:hover,
+.useSummary caption a:hover, .constantsSummary caption a:hover, .deprecatedSummary caption a:hover,
+.overviewSummary caption a:active, .memberSummary caption a:active, .typeSummary caption a:active,
+.useSummary caption a:active, .constantsSummary caption a:active, .deprecatedSummary caption a:active,
+.overviewSummary caption a:visited, .memberSummary caption a:visited, .typeSummary caption a:visited,
+.useSummary caption a:visited, .constantsSummary caption a:visited, .deprecatedSummary caption a:visited {
+ color:#FFFFFF;
+}
+.overviewSummary caption span, .memberSummary caption span, .typeSummary caption span,
+.useSummary caption span, .constantsSummary caption span, .deprecatedSummary caption span {
+ white-space:nowrap;
+ padding-top:5px;
+ padding-left:12px;
+ padding-right:12px;
+ padding-bottom:7px;
+ display:inline-block;
+ float:left;
+ background-color:#F8981D;
+ border: none;
+ height:16px;
+}
+.memberSummary caption span.activeTableTab span {
+ white-space:nowrap;
+ padding-top:5px;
+ padding-left:12px;
+ padding-right:12px;
+ margin-right:3px;
+ display:inline-block;
+ float:left;
+ background-color:#F8981D;
+ height:16px;
+}
+.memberSummary caption span.tableTab span {
+ white-space:nowrap;
+ padding-top:5px;
+ padding-left:12px;
+ padding-right:12px;
+ margin-right:3px;
+ display:inline-block;
+ float:left;
+ background-color:#4D7A97;
+ height:16px;
+}
+.memberSummary caption span.tableTab, .memberSummary caption span.activeTableTab {
+ padding-top:0px;
+ padding-left:0px;
+ padding-right:0px;
+ background-image:none;
+ float:none;
+ display:inline;
+}
+.overviewSummary .tabEnd, .memberSummary .tabEnd, .typeSummary .tabEnd,
+.useSummary .tabEnd, .constantsSummary .tabEnd, .deprecatedSummary .tabEnd {
+ display:none;
+ width:5px;
+ position:relative;
+ float:left;
+ background-color:#F8981D;
+}
+.memberSummary .activeTableTab .tabEnd {
+ display:none;
+ width:5px;
+ margin-right:3px;
+ position:relative;
+ float:left;
+ background-color:#F8981D;
+}
+.memberSummary .tableTab .tabEnd {
+ display:none;
+ width:5px;
+ margin-right:3px;
+ position:relative;
+ background-color:#4D7A97;
+ float:left;
+
+}
+.overviewSummary td, .memberSummary td, .typeSummary td,
+.useSummary td, .constantsSummary td, .deprecatedSummary td {
+ text-align:left;
+ padding:0px 0px 12px 10px;
+}
+th.colOne, th.colFirst, th.colLast, .useSummary th, .constantsSummary th,
+td.colOne, td.colFirst, td.colLast, .useSummary td, .constantsSummary td{
+ vertical-align:top;
+ padding-right:0px;
+ padding-top:8px;
+ padding-bottom:3px;
+}
+th.colFirst, th.colLast, th.colOne, .constantsSummary th {
+ background:#dee3e9;
+ text-align:left;
+ padding:8px 3px 3px 7px;
+}
+td.colFirst, th.colFirst {
+ white-space:nowrap;
+ font-size:13px;
+}
+td.colLast, th.colLast {
+ font-size:13px;
+}
+td.colOne, th.colOne {
+ font-size:13px;
+}
+.overviewSummary td.colFirst, .overviewSummary th.colFirst,
+.useSummary td.colFirst, .useSummary th.colFirst,
+.overviewSummary td.colOne, .overviewSummary th.colOne,
+.memberSummary td.colFirst, .memberSummary th.colFirst,
+.memberSummary td.colOne, .memberSummary th.colOne,
+.typeSummary td.colFirst{
+ width:25%;
+ vertical-align:top;
+}
+td.colOne a:link, td.colOne a:active, td.colOne a:visited, td.colOne a:hover, td.colFirst a:link, td.colFirst a:active, td.colFirst a:visited, td.colFirst a:hover, td.colLast a:link, td.colLast a:active, td.colLast a:visited, td.colLast a:hover, .constantValuesContainer td a:link, .constantValuesContainer td a:active, .constantValuesContainer td a:visited, .constantValuesContainer td a:hover {
+ font-weight:bold;
+}
+.tableSubHeadingColor {
+ background-color:#EEEEFF;
+}
+.altColor {
+ background-color:#FFFFFF;
+}
+.rowColor {
+ background-color:#EEEEEF;
+}
+/*
+Content styles
+*/
+.description pre {
+ margin-top:0;
+}
+.deprecatedContent {
+ margin:0;
+ padding:10px 0;
+}
+.docSummary {
+ padding:0;
+}
+
+ul.blockList ul.blockList ul.blockList li.blockList h3 {
+ font-style:normal;
+}
+
+div.block {
+ font-size:14px;
+ font-family:'DejaVu Serif', Georgia, "Times New Roman", Times, serif;
+}
+
+td.colLast div {
+ padding-top:0px;
+}
+
+
+td.colLast a {
+ padding-bottom:3px;
+}
+/*
+Formatting effect styles
+*/
+.sourceLineNo {
+ color:green;
+ padding:0 30px 0 0;
+}
+h1.hidden {
+ visibility:hidden;
+ overflow:hidden;
+ font-size:10px;
+}
+.block {
+ display:block;
+ margin:3px 10px 2px 0px;
+ color:#474747;
+}
+.deprecatedLabel, .descfrmTypeLabel, .memberNameLabel, .memberNameLink,
+.overrideSpecifyLabel, .packageHierarchyLabel, .paramLabel, .returnLabel,
+.seeLabel, .simpleTagLabel, .throwsLabel, .typeNameLabel, .typeNameLink {
+ font-weight:bold;
+}
+.deprecationComment, .emphasizedPhrase, .interfaceName {
+ font-style:italic;
+}
+
+div.block div.block span.deprecationComment, div.block div.block span.emphasizedPhrase,
+div.block div.block span.interfaceName {
+ font-style:normal;
+}
+
+div.contentContainer ul.blockList li.blockList h2{
+ padding-bottom:0px;
+}
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..8c0fb64
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..e4dbd54
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Mon Aug 22 09:08:51 CEST 2016
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..91a7e26
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched.
+if $cygwin ; then
+ [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+fi
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >&-
+APP_HOME="`pwd -P`"
+cd "$SAVED" >&-
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/integration/docker-compose.production.yml b/integration/docker-compose.production.yml
new file mode 100644
index 0000000..f0b9812
--- /dev/null
+++ b/integration/docker-compose.production.yml
@@ -0,0 +1,52 @@
+# This is an example configuration for Docker Compose. Make sure to atleast update
+# the cookie secret & postgres database password.
+#
+# Some other recommendations:
+# 1. To persist Postgres data, assign it a volume host location.
+# 2. Split the worker service to adhoc workers and scheduled queries workers.
+version: '2'
+services:
+ server:
+ image: redash/redash:latest
+ command: server
+ depends_on:
+ - postgres
+ - redis
+ ports:
+ - "5000:5000"
+ environment:
+ PYTHONUNBUFFERED: 0
+ REDASH_LOG_LEVEL: "INFO"
+ REDASH_REDIS_URL: "redis://redis:6379/0"
+ REDASH_DATABASE_URL: "postgresql://postgres@postgres/postgres"
+ REDASH_COOKIE_SECRET: veryverysecret
+ REDASH_WEB_WORKERS: 4
+ restart: always
+ worker:
+ image: redash/redash:latest
+ command: scheduler
+ environment:
+ PYTHONUNBUFFERED: 0
+ REDASH_LOG_LEVEL: "INFO"
+ REDASH_REDIS_URL: "redis://redis:6379/0"
+ REDASH_DATABASE_URL: "postgresql://postgres@postgres/postgres"
+ QUEUES: "queries,scheduled_queries,celery"
+ WORKERS_COUNT: 2
+ restart: always
+ redis:
+ image: redis:3.0-alpine
+ restart: always
+ postgres:
+ image: postgres:9.5.6-alpine
+ # volumes:
+ # - /opt/postgres-data:/var/lib/postgresql/data
+ restart: always
+ nginx:
+ image: redash/nginx:latest
+ ports:
+ - "80:80"
+ depends_on:
+ - server
+ links:
+ - server:redash
+ restart: always
diff --git a/integration/remove_redash.bash b/integration/remove_redash.bash
new file mode 100755
index 0000000..7e7b029
--- /dev/null
+++ b/integration/remove_redash.bash
@@ -0,0 +1,11 @@
+#!/bin/bash
+
+set -e
+
+local_dir="$(dirname "$0")"
+
+echo "==============="
+echo "REMOVING REDASH"
+echo "---------------"
+
+sudo docker-compose -f ${local_dir}/docker-compose.production.yml rm -f -s
diff --git a/integration/setup_redash.bash b/integration/setup_redash.bash
new file mode 100755
index 0000000..ce94eb6
--- /dev/null
+++ b/integration/setup_redash.bash
@@ -0,0 +1,50 @@
+#!/bin/bash
+
+set -e
+
+local_dir="$(dirname "$0")"
+
+echo "==============="
+echo "STARTING REDASH"
+echo "---------------"
+
+sudo docker-compose -f ${local_dir}/docker-compose.production.yml run --rm server create_db
+sleep 5
+sudo docker-compose -f ${local_dir}/docker-compose.production.yml up -d
+sleep 5
+sudo docker ps
+
+echo "================"
+echo "SETUP ADMIN USER"
+echo "----------------"
+
+curl -XPOST http://localhost:80/setup \
+ -F "name=Admin" \
+ -F "email=admin@snowplowanalytics.com" \
+ -F "password=password" \
+ -F "org_name=Snowplow"
+
+echo "=========================="
+echo "EXTRACT ADMIN USER API KEY"
+echo "--------------------------"
+
+curl -s -c ${local_dir}/cookies.txt -d "email=admin@snowplowanalytics.com&password=password" http://localhost:80/login
+
+admin_api_key=$(curl -b ${local_dir}/cookies.txt \
+ -c ${local_dir}/cookies.txt \
+ -d "email=admin@snowplowanalytics.com&password=password" \
+ -H "Content-Type: application/json" \
+ -XGET http://localhost:80/api/users/1 | \
+ python -c "import sys, json; print json.load(sys.stdin)['api_key']")
+
+echo "admin_api_key=${admin_api_key}" > ./src/test/resources/redash_dynamic.properties
+
+rm ${local_dir}/cookies.txt
+
+echo "=================="
+echo "SETUP DEFAULT USER"
+echo "------------------"
+
+set +e
+sudo docker exec -i integration_server_1 /app/manage.py users create default@snowplowanalytics.com Default --admin --password=password
+set -e
diff --git a/settings.gradle b/settings.gradle
new file mode 100755
index 0000000..32af9dc
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'redash-java-sdk'
diff --git a/src/main/java/com/snowplowanalytics/redash/CheckResponseStatus.java b/src/main/java/com/snowplowanalytics/redash/CheckResponseStatus.java
new file mode 100644
index 0000000..0ecb1c6
--- /dev/null
+++ b/src/main/java/com/snowplowanalytics/redash/CheckResponseStatus.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 2018 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.redash;
+
+public enum CheckResponseStatus {
+ YES,
+ NO
+}
diff --git a/src/main/java/com/snowplowanalytics/redash/RedashClient.java b/src/main/java/com/snowplowanalytics/redash/RedashClient.java
new file mode 100644
index 0000000..f4b9848
--- /dev/null
+++ b/src/main/java/com/snowplowanalytics/redash/RedashClient.java
@@ -0,0 +1,488 @@
+/*
+ * Copyright (c) 2018 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.redash;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.snowplowanalytics.redash.model.BaseEntity;
+import com.snowplowanalytics.redash.model.Group;
+import com.snowplowanalytics.redash.model.User;
+import com.snowplowanalytics.redash.model.datasource.DataSource;
+import okhttp3.*;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.util.*;
+
+public class RedashClient {
+
+ private static final String API_KEY_URL_PARAM = "?api_key=";
+ private static final String API_PREFIX = "/api";
+ private static final String DATA_SOURCES_URL_PREFIX = "/data_sources";
+ private static final String GROUPS_URL_PREFIX = "/groups";
+ private static final String USERS_URL_PREFIX = "/users";
+ private static final String MEMBERS_URL_PREFIX = "/members";
+
+ private static final String ID = "id";
+ private static final String USER_ID = "user_id";
+ private static final String DATA_SOURCE_ID = "data_source_id";
+ private static final String AUTH_TYPE = "auth_type";
+ private static final String CREATED_AT = "created_at";
+ private static final String MESSAGE = "message";
+ private static final String NULL = "null";
+ private static final String MESSAGE_URL_NOT_FOUND = "The requested URL was not found on the server. " +
+ "If you entered the URL manually please check your spelling and try again.";
+ private static final String MESSAGE_INTERNAL_SERVER_ERROR = "Internal Server Error";
+ private static final String JSON_CONTENT_TYPE = "application/json; charset=utf-8";
+
+ private static final MediaType JSON = MediaType.parse(JSON_CONTENT_TYPE);
+
+ public static final String DATA_SOURCE_ALREADY_EXISTS = "Data-source with this name already exists.";
+ public static final String USER_GROUP_ALREADY_EXISTS = "User group with this name already exists.";
+ public static final String USER_DOES_NOT_EXIST = "User with such name does not exist.";
+ public static final String DATA_SOURCE_DOES_NOT_EXIST = "Data-source with such name does not exist.";
+
+ private final OkHttpClient client;
+ private final String baseUrl;
+ private final String apiKey;
+ private final Headers headers;
+
+ public RedashClient(String schema, String host, int port, String apiKey) {
+ this.client = new OkHttpClient();
+ this.apiKey = apiKey;
+ this.baseUrl = schema + "://" + host + ":" + port + API_PREFIX;
+ this.headers = new Headers.Builder()
+ .add("Accept", "application/json, text/plain, */*")
+ .add("Content-Type", JSON_CONTENT_TYPE)
+ .build();
+ }
+
+ /**
+ * Creates a new data-source.
+ *
+ * @param dataSource A data-source object which should contain all necessary information.
+ * @return int Id of the successfully created data-source. In this case object that was transferred as argument
+ * receives that id.
+ * @throws IOException If the server is unavailable due to a connection error or if API key is invalid or
+ * if {@code url} is not a valid HTTP or HTTPS URL.
+ * @throws IllegalArgumentException If a data-source with the same name already exists.
+ */
+ public int createDataSource(DataSource dataSource) throws IOException, IllegalArgumentException {
+ if (isEntityAlreadyExists(getDataSources(), dataSource.getName())) {
+ throw new IllegalArgumentException(DATA_SOURCE_ALREADY_EXISTS);
+ }
+ String url = baseUrl + DATA_SOURCES_URL_PREFIX + API_KEY_URL_PARAM + apiKey;
+ String response = post(url, new Gson().toJson(dataSource), CheckResponseStatus.YES);
+ int id = getIdFromJson(response);
+ dataSource.setId(id);
+ return id;
+ }
+
+ /**
+ * Updates an already existing data-source. Please note that all the argument's fields must be present and have correct values.
+ * This function uses the data-source name to discover what needs to be updated.
+ *
+ * @param dataSource A data-source object which should contain all necessary information.
+ * @return boolean False if any of the argument's fields have null or empty value and True if entity successfully updated.
+ * @throws IOException If a data-source with this name could not be found or if the server is unavailable due to a connection error
+ * or if API key is invalid or if {@code url} is not a valid HTTP or HTTPS URL.
+ */
+ public boolean updateDataSource(DataSource dataSource) throws IOException {
+ DataSource fromDataBase;
+ try {
+ fromDataBase = getDataSource(dataSource.getName());
+ } catch (IllegalArgumentException e) {
+ throw new IOException(e.getMessage());
+ }
+ if (dataSourceIsInValid(dataSource)) {
+ return false;
+ }
+ String url = baseUrl + DATA_SOURCES_URL_PREFIX + "/" + fromDataBase.getId() + API_KEY_URL_PARAM + apiKey;
+ post(url, new Gson().toJson(dataSource), CheckResponseStatus.YES);
+ return true;
+ }
+
+ /**
+ * @return The list of all existing data-sources on the server. If there are no data-sources then the list will be empty.
+ * @throws IOException If the server is unavailable due to a connection error or if API key is invalid or
+ * if {@code url} is not a valid HTTP or HTTPS URL.
+ */
+ public List getDataSources() throws IOException {
+ String url = baseUrl + DATA_SOURCES_URL_PREFIX + API_KEY_URL_PARAM + apiKey;
+ Type listType = new TypeToken>() {}.getType();
+ return new Gson().fromJson(get(url, CheckResponseStatus.YES), listType);
+ }
+
+ /**
+ * Removes an existing data-source.
+ *
+ * @param dataSourceId Id of the data-source to delete.
+ * @return boolean False if a data-source with the provided id doesn't exist, if it does then the data-source will be
+ * deleted and True will be returned.
+ * @throws IOException If the server is unavailable due to a connection error or if API key is invalid or
+ * if {@code url} is not a valid HTTP or HTTPS URL.
+ */
+ public boolean deleteDataSource(int dataSourceId) throws IOException {
+ String url = baseUrl + DATA_SOURCES_URL_PREFIX + "/" + dataSourceId + API_KEY_URL_PARAM + apiKey;
+ try {
+ delete(url, CheckResponseStatus.YES);
+ } catch (IOException e) {
+ if ((MESSAGE_INTERNAL_SERVER_ERROR.toLowerCase()).equals(e.getMessage().toLowerCase())) {
+ return false;
+ }
+ throw new IOException(e.getMessage());
+ }
+ return true;
+ }
+
+ /**
+ * Creates a new user-group.
+ *
+ * @param group A group object which should contain all necessary information.
+ * @return int Id of the successfully created group. In this case object that was transferred as argument
+ * receives that id.
+ * @throws IllegalArgumentException If a user-group with the provided name already exists.
+ * @throws IOException If the server is unavailable due to a connection error or if API key is invalid or
+ * if {@code url} is not a valid HTTP or HTTPS URL.
+ */
+ public int createUserGroup(Group group) throws IOException {
+ if (isEntityAlreadyExists(getUserGroups(), group.getName())) {
+ throw new IllegalArgumentException(USER_GROUP_ALREADY_EXISTS);
+ }
+ String url = baseUrl + GROUPS_URL_PREFIX + API_KEY_URL_PARAM + apiKey;
+ String returnValue = post(url, new Gson().toJson(group), CheckResponseStatus.YES);
+ int id = getIdFromJson(returnValue);
+ group.setId(id);
+ return id;
+ }
+
+ /**
+ * Adds a user to a user-group; both specified by their ids.
+ *
+ * @param userId The id of the the user to add to the group.
+ * @param groupId The id of the group to add the user to.
+ * @return boolean False if specified user-group already contains specified user, True if not and user was added.
+ * @throws IllegalArgumentException If either the user or user-group does not exist.
+ * @throws IOException If the server is unavailable due to a connection error or if API key is invalid or
+ * if {@code url} is not a valid HTTP or HTTPS URL.
+ */
+ public boolean addUserToGroup(int userId, int groupId) throws IOException {
+ checkIfEntityExists(User.class, userId);
+ Group group = getWithUsersAndDataSources(groupId);
+ if (group.getUsers().stream().anyMatch(u -> u.getId() == userId)) {
+ return false;
+ }
+ String url = baseUrl + GROUPS_URL_PREFIX + "/" + groupId + MEMBERS_URL_PREFIX + API_KEY_URL_PARAM + apiKey;
+ post(url, new JSONObject().put(USER_ID, userId).toString(), CheckResponseStatus.YES);
+ return true;
+ }
+
+ /**
+ * Adds a data-source to a user-group; both specified by their ids.
+ *
+ * @param dataSourceId The id of the data-source to add to the group.
+ * @param groupId The id of the group to add the data-source to.
+ * @return boolean False if the data-source is already attached to the group, True if not and data-source was added.
+ * @throws IllegalArgumentException If either the data-source or user-group does not exist.
+ * @throws IOException If the server is unavailable due to a connection error or if API key is invalid or
+ * if {@code url} is not a valid HTTP or HTTPS URL.
+ */
+ public boolean addDataSourceToGroup(int dataSourceId, int groupId) throws IOException {
+ checkIfEntityExists(DataSource.class, dataSourceId);
+ Group group = getWithUsersAndDataSources(groupId);
+ if (group.getDataSources().stream().anyMatch(ds -> ds.getId() == dataSourceId)) {
+ return false;
+ }
+ String url = baseUrl + GROUPS_URL_PREFIX + "/" + groupId + DATA_SOURCES_URL_PREFIX + API_KEY_URL_PARAM + apiKey;
+ post(url, new JSONObject().put(DATA_SOURCE_ID, dataSourceId).toString(), CheckResponseStatus.YES);
+ return true;
+ }
+
+ /**
+ * Removes a user from a user-group; both specified by their ids.
+ *
+ * @param userId The id of the user to be removed.
+ * @param groupId The id of the group to remove the user from.
+ * @return boolean False if user is not a member of the group, True if it is and was removed.
+ * @throws IllegalArgumentException If either the user or user-group does not exist.
+ * @throws IOException If the server is unavailable due to a connection error or if API key is invalid or
+ * if {@code url} is not a valid HTTP or HTTPS URL.
+ */
+ public boolean removeUserFromGroup(int userId, int groupId) throws IOException {
+ checkIfEntityExists(User.class, userId);
+ Group group = getWithUsersAndDataSources(groupId);
+ if (group.getUsers().stream().noneMatch(u -> u.getId() == userId)) {
+ return false;
+ }
+ String url = baseUrl + GROUPS_URL_PREFIX + "/" + groupId + MEMBERS_URL_PREFIX + "/" + userId + API_KEY_URL_PARAM + apiKey;
+ delete(url, CheckResponseStatus.YES);
+ return true;
+ }
+
+ /**
+ * Removes a data-source from a user-group; both specified by their ids.
+ *
+ * @param dataSourceId The id of the data-source to be removed.
+ * @param groupId The id of the group to remove the data-source from.
+ * @return boolean False if data-source is not attached to the group, True if it is and was removed.
+ * @throws IllegalArgumentException If either the data-source or user-group does not exist.
+ * @throws IOException If the server is unavailable due to a connection error or if API key is invalid or
+ * if {@code url} is not a valid HTTP or HTTPS URL.
+ */
+ public boolean removeDataSourceFromGroup(int dataSourceId, int groupId) throws IOException {
+ checkIfEntityExists(DataSource.class, dataSourceId);
+ Group group = getWithUsersAndDataSources(groupId);
+ if (group.getDataSources().stream().noneMatch(ds -> ds.getId() == dataSourceId)) {
+ return false;
+ }
+ String url = baseUrl + GROUPS_URL_PREFIX + "/" + groupId + DATA_SOURCES_URL_PREFIX + "/" + dataSourceId + API_KEY_URL_PARAM + apiKey;
+ delete(url, CheckResponseStatus.YES);
+ return true;
+ }
+
+ /**
+ * Retrieves all available user-groups.
+ *
+ * @return List of groups which will contain at least two user groups with the names "admin" and "default".
+ * @throws IOException If the server is unavailable due to a connection error or if API key is invalid or
+ * if {@code url} is not a valid HTTP or HTTPS URL.
+ */
+ public List getUserGroups() throws IOException {
+ String url = baseUrl + GROUPS_URL_PREFIX + API_KEY_URL_PARAM + apiKey;
+ Type listType = new TypeToken>() {}.getType();
+ return new Gson().fromJson(get(url, CheckResponseStatus.YES), listType);
+ }
+
+ /**
+ * Removes a user-group with the specified id.
+ *
+ * @param userGroupId The id of the group to delete.
+ * @return boolean True if user group was found and deleted, False if the user-group did not exist.
+ * @throws IOException If the server is unavailable due to a connection error or if API key is invalid or
+ * if {@code url} is not a valid HTTP or HTTPS URL.
+ */
+ public boolean deleteUserGroup(int userGroupId) throws IOException {
+ String url = baseUrl + GROUPS_URL_PREFIX + "/" + userGroupId + API_KEY_URL_PARAM + apiKey;
+ String response = delete(url, CheckResponseStatus.NO);
+ if (!NULL.equals(response) && !MESSAGE_INTERNAL_SERVER_ERROR.equals(new JSONObject(response).getString(MESSAGE))) {
+ throw new IOException(response);
+ }
+ return NULL.equals(response);
+ }
+
+ /**
+ * Retrieves all available users.
+ *
+ * @return List of users.
+ * @throws IOException If the server is unavailable due to a connection error or if API key is invalid or
+ * if {@code url} is not a valid HTTP or HTTPS URL.
+ */
+ public List getUsers() throws IOException {
+ String url = baseUrl + USERS_URL_PREFIX + API_KEY_URL_PARAM + apiKey;
+ Type listType = new TypeToken>() {}.getType();
+ return new Gson().fromJson(get(url, CheckResponseStatus.YES), listType);
+ }
+
+ /**
+ * Attempts to retrieve a single user specified by their username.
+ *
+ * @param userName The name of the user to return.
+ * @return The user object that matches the name provided.
+ * @throws IllegalArgumentException If user with provided name does not exist.
+ * @throws IOException If the server is unavailable due to a connection error or if API key is invalid or
+ * if {@code url} is not a valid HTTP or HTTPS URL.
+ */
+ public User getUser(String userName) throws IOException {
+ Optional result = getUsers().stream().filter(e -> userName.equals(e.getName())).findFirst();
+ if (!result.isPresent()) {
+ throw new IllegalArgumentException(USER_DOES_NOT_EXIST);
+ }
+ return result.get();
+ }
+
+ /**
+ * Attempts to retrieve a single data-source specified by name.
+ *
+ * @param dataSourceName The name of the data-source to return.
+ * @return The data-source object that matches the name provided.
+ * @throws IllegalArgumentException If data-source with provided name does not exist.
+ * @throws IOException If the server is unavailable due to a connection error or if API key is invalid or
+ * if {@code url} is not a valid HTTP or HTTPS URL.
+ */
+ public DataSource getDataSource(String dataSourceName) throws IOException {
+ Optional result = getDataSources().stream().filter(e -> dataSourceName.equals(e.getName())).findFirst();
+ if (!result.isPresent()) {
+ throw new IllegalArgumentException(DATA_SOURCE_DOES_NOT_EXIST);
+ }
+ return getDataSourceById(result.get().getId());
+ }
+
+ /**
+ * Attempts to retrieve a single user-group specified by id.
+ *
+ * @param userGroupId The id of the user-group to return.
+ * @return The Group object that matches the id provided.
+ * @throws IllegalArgumentException If user-group with provided id does not exist.
+ * @throws IOException If the server is unavailable due to a connection error or if API key is invalid or
+ * if {@code url} is not a valid HTTP or HTTPS URL.
+ */
+ public Group getGroupById(int userGroupId) throws IOException {
+ String url = baseUrl + GROUPS_URL_PREFIX + "/" + userGroupId + API_KEY_URL_PARAM + apiKey;
+ String returnValue = get(url, CheckResponseStatus.NO);
+ return resultResolver(Group.class, returnValue);
+ }
+
+ /**
+ * Attempts to retrieve a single user specifed by id.
+ *
+ * @param userId The id of the user to return.
+ * @return The User object that matches the id provided.
+ * @throws IllegalArgumentException If user with provided id does not exist.
+ * @throws IOException If the server is unavailable due to a connection error or if API key is invalid or
+ * if {@code url} is not a valid HTTP or HTTPS URL.
+ */
+ public User getUserById(int userId) throws IOException {
+ String url = baseUrl + USERS_URL_PREFIX + "/" + userId + API_KEY_URL_PARAM + apiKey;
+ String returnValue = get(url, CheckResponseStatus.NO);
+ return resultResolver(User.class, returnValue);
+ }
+
+ /**
+ * Attempts to retrieve a single data-source specified by id.
+ *
+ * @param id The id of the data-source to return.
+ * @return The DataSource object that matches the id provided.
+ * @throws IllegalArgumentException If data-source with provided id does not exist.
+ * @throws IOException If the server is unavailable due to a connection error or if API key is invalid or
+ * if {@code url} is not a valid HTTP or HTTPS URL.
+ */
+ public DataSource getDataSourceById(int id) throws IOException {
+ String url = baseUrl + DATA_SOURCES_URL_PREFIX + "/" + id + API_KEY_URL_PARAM + apiKey;
+ String returnValue = get(url, CheckResponseStatus.NO);
+ JSONObject jsonObject = new JSONObject(returnValue);
+ if (jsonObject.has(MESSAGE) && MESSAGE_INTERNAL_SERVER_ERROR.equals(jsonObject.getString(MESSAGE))) {
+ throw new IllegalArgumentException(MESSAGE_INTERNAL_SERVER_ERROR);
+ }
+ return new Gson().fromJson(returnValue, DataSource.class);
+ }
+
+ /**
+ * Attempts to return a single user-group with all attached users and data-sources attached to the same object.
+ *
+ * @param userGroupId The id of the user-group to return.
+ * @return A Group object with all users and data-sources found and attached.
+ * @throws IllegalArgumentException If user-group with provided id does not exist.
+ * @throws IOException If the server is unavailable due to a connection error or if API key is invalid or
+ * if {@code url} is not a valid HTTP or HTTPS URL.
+ */
+ public Group getWithUsersAndDataSources(int userGroupId) throws IOException {
+ Group group = getGroupById(userGroupId);
+ String dataSourcesUrl = baseUrl + GROUPS_URL_PREFIX + "/" + userGroupId + DATA_SOURCES_URL_PREFIX + API_KEY_URL_PARAM + apiKey,
+ usersUrl = baseUrl + GROUPS_URL_PREFIX + "/" + userGroupId + MEMBERS_URL_PREFIX + API_KEY_URL_PARAM + apiKey;
+ Type userListType = new TypeToken>() {}.getType();
+ Type dataSourceListType = new TypeToken>() {}.getType();
+ String usersReturnValue = get(usersUrl, CheckResponseStatus.YES);
+ String dataSourcesReturnValue = get(dataSourcesUrl, CheckResponseStatus.YES);
+ group.setUsers(new Gson().fromJson(usersReturnValue, userListType));
+ group.setDataSources(new Gson().fromJson(dataSourcesReturnValue, dataSourceListType));
+ return group;
+ }
+
+ private boolean dataSourceIsInValid(DataSource dataSource) {
+ return dataSource.getName() == null || dataSource.getName().isEmpty()
+ || dataSource.getHost() == null || dataSource.getHost().isEmpty()
+ || dataSource.getPort() == 0
+ || dataSource.getUser() == null || dataSource.getUser().isEmpty()
+ || dataSource.getPassword() == null || dataSource.getPassword().isEmpty()
+ || dataSource.getDbName() == null || dataSource.getDbName().isEmpty();
+ }
+
+ private boolean isEntityAlreadyExists(List extends BaseEntity> list, String name) {
+ return list.stream().anyMatch(e -> name.equals(e.getName()));
+ }
+
+ private int getIdFromJson(String json) throws JSONException {
+ return new JSONObject(json).getInt(ID);
+ }
+
+ private T resultResolver(Class type, String returnValue) throws IOException {
+ JSONObject jsonObject = new JSONObject(returnValue);
+ if (jsonObject.has(AUTH_TYPE) || jsonObject.has(CREATED_AT) || jsonObject.has("name")) {
+ return new Gson().fromJson(returnValue, type);
+ }
+ if (jsonObject.getString(MESSAGE).startsWith(MESSAGE_URL_NOT_FOUND) ||
+ jsonObject.getString(MESSAGE).equals(MESSAGE_INTERNAL_SERVER_ERROR)) {
+ throw new IllegalArgumentException(returnValue);
+ }
+ throw new IOException(returnValue);
+ }
+
+ private String post(String url, String json, CheckResponseStatus checkResponseStatus) throws IOException {
+ validateURL(url);
+ RequestBody body = RequestBody.create(JSON, json);
+ Request request = new Request.Builder()
+ .url(url)
+ .post(body)
+ .headers(this.headers)
+ .build();
+ return this.performCall(request, checkResponseStatus);
+ }
+
+ private String get(String url, CheckResponseStatus checkResponseStatus) throws IOException {
+ validateURL(url);
+ Request request = new Request.Builder()
+ .url(url)
+ .get()
+ .headers(this.headers)
+ .build();
+ return this.performCall(request, checkResponseStatus);
+ }
+
+ private String delete(String url, CheckResponseStatus checkResponseStatus) throws IOException {
+ validateURL(url);
+ Request request = new Request.Builder()
+ .url(url)
+ .delete()
+ .headers(this.headers)
+ .build();
+ return this.performCall(request, checkResponseStatus);
+ }
+
+ private String performCall(Request request, CheckResponseStatus checkResponseStatus) throws IOException {
+ try (Response response = client.newCall(request).execute()) {
+ if (checkResponseStatus == CheckResponseStatus.YES && !response.isSuccessful()) {
+ throw new IOException(anonymisedUrl(response.message()));
+ }
+ return response.body().string();
+ }
+ }
+
+ private void validateURL(String url) throws IOException {
+ if (HttpUrl.parse(url) == null) throw new IOException("Incorrect URL. Please check it and try again");
+ }
+
+ private String anonymisedUrl(String message) {
+ return message.contains(apiKey) ? message.replace(apiKey, "XXX") : message;
+ }
+
+ private void checkIfEntityExists(Class extends BaseEntity> clazz, int id) throws IOException {
+ if (User.class.equals(clazz)) {
+ getUserById(id);
+ } else if (DataSource.class.equals(clazz)) {
+ getDataSourceById(id);
+ }
+ }
+}
diff --git a/src/main/java/com/snowplowanalytics/redash/model/BaseEntity.java b/src/main/java/com/snowplowanalytics/redash/model/BaseEntity.java
new file mode 100644
index 0000000..6f99978
--- /dev/null
+++ b/src/main/java/com/snowplowanalytics/redash/model/BaseEntity.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2018 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.redash.model;
+
+import java.util.Objects;
+
+public class BaseEntity {
+
+ private String name;
+ private int id;
+
+ public BaseEntity(String name) {
+ this.name = name;
+ }
+
+ public BaseEntity(String name, int id) {
+ this.name = name;
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(getId(), getName());
+ }
+
+ @Override
+ public String toString() {
+ return "BaseEntity{" +
+ "name='" + name + '\'' +
+ ", id=" + id +
+ '}';
+ }
+}
diff --git a/src/main/java/com/snowplowanalytics/redash/model/Group.java b/src/main/java/com/snowplowanalytics/redash/model/Group.java
new file mode 100644
index 0000000..5260fb1
--- /dev/null
+++ b/src/main/java/com/snowplowanalytics/redash/model/Group.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2018 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.redash.model;
+
+import com.snowplowanalytics.redash.model.datasource.DataSource;
+
+import java.util.List;
+import java.util.Objects;
+
+public class Group extends BaseEntity {
+
+ private List users;
+ private List dataSources;
+
+ public Group(String name) {
+ super(name);
+ }
+
+ public Group(String name, int id) {
+ super(name, id);
+ }
+
+ public List getUsers() {
+ return users;
+ }
+
+ public void setUsers(List users) {
+ this.users = users;
+ }
+
+ public List getDataSources() {
+ return dataSources;
+ }
+
+ public void setDataSources(List dataSources) {
+ this.dataSources = dataSources;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (!(o instanceof Group)) {
+ return false;
+ }
+ Group o1 = (Group) o;
+ return getId() == o1.getId() &&
+ Objects.equals(getName(), o1.getName());
+ }
+
+ @Override
+ public String toString() {
+ return "Group{" +
+ "name='" + getName() + '\'' +
+ ", id=" + getId() +
+ '}';
+ }
+}
diff --git a/src/main/java/com/snowplowanalytics/redash/model/User.java b/src/main/java/com/snowplowanalytics/redash/model/User.java
new file mode 100644
index 0000000..78250e7
--- /dev/null
+++ b/src/main/java/com/snowplowanalytics/redash/model/User.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2018 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.redash.model;
+
+import java.util.Objects;
+
+public class User extends BaseEntity {
+ public User(String name) {
+ super(name);
+ }
+
+ public User(String name, int id) {
+ super(name, id);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (!(o instanceof User)) {
+ return false;
+ }
+ User user = (User) o;
+ return getId() == user.getId() &&
+ Objects.equals(getName(), user.getName());
+ }
+
+ @Override
+ public String toString() {
+ return "User{" +
+ "name='" + getName() + '\'' +
+ ", id=" + getId() +
+ '}';
+ }
+}
diff --git a/src/main/java/com/snowplowanalytics/redash/model/datasource/DataSource.java b/src/main/java/com/snowplowanalytics/redash/model/datasource/DataSource.java
new file mode 100644
index 0000000..52f3af7
--- /dev/null
+++ b/src/main/java/com/snowplowanalytics/redash/model/datasource/DataSource.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2018 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.redash.model.datasource;
+
+import com.snowplowanalytics.redash.model.BaseEntity;
+
+public class DataSource extends BaseEntity{
+
+ private final String type;
+ private final Options options;
+
+ public DataSource(String name, String host, int port, String user, String password, String dbName, String type) {
+ super(name);
+ this.type = type;
+ this.options = new Options(host, port, user, password, dbName);
+ }
+
+ public String getHost() {
+ return this.options.getHost();
+ }
+
+ public int getPort() {
+ return this.options.getPort();
+ }
+
+ public String getUser() {
+ return this.options.getUser();
+ }
+
+ public String getPassword() {
+ return this.options.getPassword();
+ }
+
+ public String getDbName() {
+ return this.options.getDbName();
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public Options getOptions() {
+ return options;
+ }
+
+ @Override
+ public String toString() {
+ return "DataSource{" +
+ "type='" + type + '\'' +
+ ", options=" + options +
+ ", name='" + getName() + '\'' +
+ ", id=" + getId() +
+ "} ";
+ }
+}
diff --git a/src/main/java/com/snowplowanalytics/redash/model/datasource/Options.java b/src/main/java/com/snowplowanalytics/redash/model/datasource/Options.java
new file mode 100644
index 0000000..38b8d15
--- /dev/null
+++ b/src/main/java/com/snowplowanalytics/redash/model/datasource/Options.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2018 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.redash.model.datasource;
+
+import com.google.gson.annotations.SerializedName;
+
+public class Options {
+
+ private final String host;
+ private final int port;
+ private final String user;
+ private final String password;
+ @SerializedName("dbname")
+ private final String dbName;
+
+ public Options(String host, int port, String user, String password, String dbName) {
+ this.host = host;
+ this.port = port;
+ this.user = user;
+ this.password = password;
+ this.dbName = dbName;
+ }
+
+ public String getHost() {
+ return host;
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ public String getUser() {
+ return user;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public String getDbName() {
+ return dbName;
+ }
+
+ @Override
+ public String toString() {
+ return "Options{" +
+ "host='" + host + '\'' +
+ ", port=" + port +
+ ", user='" + user + '\'' +
+ ", password='" + password + '\'' +
+ ", dbName='" + dbName + '\'' +
+ '}';
+ }
+}
diff --git a/src/main/java/com/snowplowanalytics/redash/model/datasource/RedshiftDataSource.java b/src/main/java/com/snowplowanalytics/redash/model/datasource/RedshiftDataSource.java
new file mode 100644
index 0000000..e16669c
--- /dev/null
+++ b/src/main/java/com/snowplowanalytics/redash/model/datasource/RedshiftDataSource.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2018 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.redash.model.datasource;
+
+public class RedshiftDataSource extends DataSource {
+
+ private RedshiftDataSource(RedshiftDataSourceBuilder builder) {
+ super(builder.name, builder.host, builder.port, builder.user, builder.password, builder.dbName, "redshift");
+ }
+
+ public final static class RedshiftDataSourceBuilder {
+ private String name;
+ private String host;
+ private int port;
+ private String user;
+ private String password;
+ private String dbName;
+
+ public RedshiftDataSourceBuilder(String name) {
+ this.name = name;
+ }
+
+ public RedshiftDataSourceBuilder host(String host) {
+ this.host = host;
+ return this;
+ }
+
+ public RedshiftDataSourceBuilder port(int port) {
+ this.port = port;
+ return this;
+ }
+
+ public RedshiftDataSourceBuilder user(String user) {
+ this.user = user;
+ return this;
+ }
+
+ public RedshiftDataSourceBuilder password(String password) {
+ this.password = password;
+ return this;
+ }
+
+ public RedshiftDataSourceBuilder dbName(String dbName) {
+ this.dbName = dbName;
+ return this;
+ }
+
+ public RedshiftDataSource build() {
+ return new RedshiftDataSource(this);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "RedshiftDataSource{" +
+ "type='" + getType() + '\'' +
+ ", " + getOptions() +
+ ", name='" + getName() + '\'' +
+ ", id=" + getId() +
+ "} ";
+ }
+}
diff --git a/src/test/java/com/snowplowanalytics/redash/AbstractRedashClientTest.java b/src/test/java/com/snowplowanalytics/redash/AbstractRedashClientTest.java
new file mode 100644
index 0000000..e02bac7
--- /dev/null
+++ b/src/test/java/com/snowplowanalytics/redash/AbstractRedashClientTest.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2018 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.redash;
+
+import com.snowplowanalytics.redash.model.Group;
+import com.snowplowanalytics.redash.model.User;
+import com.snowplowanalytics.redash.model.datasource.RedshiftDataSource;
+import org.junit.BeforeClass;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
+
+public class AbstractRedashClientTest {
+
+ public static RedashClient redashClient, wrongClient;
+ public static RedshiftDataSource rds = new RedshiftDataSource.RedshiftDataSourceBuilder("name")
+ .host("host")
+ .port(5439)
+ .user("user")
+ .password("password")
+ .dbName("dbName")
+ .build();
+ public static User adminUser, defaultUser;
+ public static String invalidUserName;
+ public static Group adminGroup, defaultGroup;
+
+ @BeforeClass
+ public static void onlyOnce() throws IOException {
+ Properties prop_1 = new Properties();
+ Properties prop_2 = new Properties();
+
+ ClassLoader loader = Thread.currentThread().getContextClassLoader();
+
+ InputStream stream_1 = loader.getResourceAsStream("redash_client.properties");
+ prop_1.load(stream_1);
+
+ InputStream stream_2 = loader.getResourceAsStream("redash_dynamic.properties");
+ prop_2.load(stream_2);
+
+ redashClient = new RedashClient(prop_1.getProperty("redash_schema"), prop_1.getProperty("redash_host"),
+ Integer.parseInt(prop_1.getProperty("redash_port")),
+ prop_2.getProperty("admin_api_key"));
+
+ // Client which has wrong apikey for test case with IOException
+ wrongClient = new RedashClient(prop_1.getProperty("redash_schema"), prop_1.getProperty("redash_host"),
+ Integer.parseInt(prop_1.getProperty("redash_port")), "wrong");
+ }
+
+ @BeforeClass
+ public static void loadUser() throws IOException {
+ Properties prop = new Properties();
+ ClassLoader loader = Thread.currentThread().getContextClassLoader();
+ InputStream stream = loader.getResourceAsStream("user_group.properties");
+ prop.load(stream);
+ adminUser = new User(prop.getProperty("admin_username"), 1);
+ defaultUser = new User(prop.getProperty("default_username"), 2);
+ invalidUserName = prop.getProperty("non_existent_username");
+ adminGroup = new Group(prop.getProperty("admin_group"), 1);
+ defaultGroup = new Group(prop.getProperty("default_group"), 2);
+ }
+}
diff --git a/src/test/java/com/snowplowanalytics/redash/RedashClientDataSourceTest.java b/src/test/java/com/snowplowanalytics/redash/RedashClientDataSourceTest.java
new file mode 100644
index 0000000..a6a7bc6
--- /dev/null
+++ b/src/test/java/com/snowplowanalytics/redash/RedashClientDataSourceTest.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (c) 2018 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.redash;
+
+import com.snowplowanalytics.redash.model.Group;
+import com.snowplowanalytics.redash.model.datasource.DataSource;
+import com.snowplowanalytics.redash.model.datasource.RedshiftDataSource;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.List;
+
+import static com.snowplowanalytics.redash.RedashClient.*;
+import static org.hamcrest.core.Is.is;
+
+/**
+ * Tests should be performed on a clear database without any data sources.
+ *
+ * The included wipeDataSources() method will drop all user groups from Redash server
+ * except "admin" and "default".
+ */
+public class RedashClientDataSourceTest extends AbstractRedashClientTest {
+
+ @Before
+ public void isDataSourcesListEmpty() throws IOException {
+ wipeDataSources();
+ Assert.assertTrue(redashClient.getDataSources().isEmpty());
+ }
+
+ @Test
+ public void createRetrieveDeleteTest() throws IOException {
+ int id = redashClient.createDataSource(rds);
+ List dataSourceList = redashClient.getDataSources();
+ Assert.assertTrue(dataSourceList.size() == 1);
+ Assert.assertTrue(simpleDatasourceMatcher(dataSourceList.get(0)));
+ redashClient.deleteDataSource(id);
+ dataSourceList = redashClient.getDataSources();
+ Assert.assertTrue(dataSourceList.isEmpty());
+ }
+
+ @Test
+ public void createRetrieveDeleteWithExceptionTest() throws IOException {
+ int id = redashClient.createDataSource(rds);
+ try {
+ redashClient.createDataSource(rds);
+ } catch (Exception e) {
+ Assert.assertTrue(e.getClass().equals(IllegalArgumentException.class));
+ Assert.assertThat(e.getMessage(), is(DATA_SOURCE_ALREADY_EXISTS));
+ }
+ redashClient.deleteDataSource(id);
+ List dataSourceList = redashClient.getDataSources();
+ Assert.assertTrue(dataSourceList.isEmpty());
+ Assert.assertFalse(redashClient.deleteDataSource(id));
+ }
+
+ @Test
+ public void successfulCreateDataSourceTest() throws IOException {
+ int id = redashClient.createDataSource(rds);
+ DataSource fromDB = redashClient.getDataSource(rds.getName());
+ Assert.assertTrue(dataSourceMatcher(fromDB, rds));
+ redashClient.deleteDataSource(id);
+ }
+
+ @Test
+ public void unsuccessfulWithExistingNameCreateDataSourceTest() throws IOException {
+ int id = redashClient.createDataSource(rds);
+ try {
+ redashClient.createDataSource(rds);
+ } catch (Exception e) {
+ Assert.assertTrue(e.getClass().equals(IllegalArgumentException.class));
+ Assert.assertTrue(e.getMessage().equals(DATA_SOURCE_ALREADY_EXISTS));
+ } finally {
+ redashClient.deleteDataSource(id);
+ }
+ }
+
+ @Test
+ public void unsuccessfulWithIOExceptionCreateDataSourceTest() throws IOException {
+ int id = redashClient.createDataSource(rds);
+ try {
+ wrongClient.createDataSource(rds);
+ } catch (Exception e) {
+ Assert.assertTrue(e.getClass().equals(IOException.class));
+ Assert.assertTrue(e.getMessage().equals("NOT FOUND"));
+ } finally {
+ redashClient.deleteDataSource(id);
+ }
+ }
+
+ @Test
+ public void updateDataSourceTest() throws IOException {
+ int id = redashClient.createDataSource(rds);
+ RedshiftDataSource created = new RedshiftDataSource.RedshiftDataSourceBuilder("name")
+ .host("updatedHost")
+ .port(15439)
+ .user("updatedUser")
+ .password("updatedPassword")
+ .dbName("updatedDatabase")
+ .build();
+ Assert.assertTrue(redashClient.updateDataSource(created));
+ DataSource updatedFromDB = redashClient.getDataSource(rds.getName());
+ Assert.assertTrue(dataSourceMatcher(updatedFromDB, created));
+ redashClient.deleteDataSource(id);
+ }
+
+ @Test(expected = IOException.class)
+ public void updateDataSourceWithExceptionTest() throws IOException {
+ wrongClient.updateDataSource(rds);
+ }
+
+ @Test
+ public void getDataSourceTest() throws IOException {
+ int id = redashClient.createDataSource(rds);
+ System.out.println(rds.toString());
+ DataSource fromDB = redashClient.getDataSource(rds.getName());
+ Assert.assertTrue(dataSourceMatcher(fromDB, rds));
+ redashClient.deleteDataSource(id);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void getDataSourceWhichNotExistsTest() throws IOException {
+ redashClient.getDataSource("wrongName");
+ }
+
+ @Test
+ public void getDataSourceWithIOExceptionTest() throws IOException {
+ int id = redashClient.createDataSource(rds);
+ DataSource fromDB = redashClient.getDataSource(rds.getName());
+ Assert.assertTrue(dataSourceMatcher(fromDB, rds));
+ try {
+ wrongClient.getDataSource(rds.getName());
+ } catch (Exception e) {
+ Assert.assertTrue(e.getClass().equals(IOException.class));
+ } finally {
+ redashClient.deleteUserGroup(id);
+ }
+ }
+
+ @Test
+ public void addDataSourceToGroupTest() throws IOException {
+ int id = redashClient.createDataSource(rds);
+ Group groupFromDb = redashClient.getWithUsersAndDataSources(adminGroup.getId());
+ Assert.assertTrue(groupFromDb.getDataSources().isEmpty());
+ Assert.assertTrue(redashClient.addDataSourceToGroup(id, adminGroup.getId()));
+ groupFromDb = redashClient.getWithUsersAndDataSources(adminGroup.getId());
+ Assert.assertTrue(groupFromDb.getDataSources().size() == 1);
+ Assert.assertTrue(simpleDatasourceMatcher(groupFromDb.getDataSources().get(0)));
+ Assert.assertFalse(redashClient.addDataSourceToGroup(id, adminGroup.getId()));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void addNonExistingDataSourceToGroupTest() throws IOException {
+ redashClient.addDataSourceToGroup(1, adminGroup.getId());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void addDataSourceToNonExistingGroupTest() throws IOException {
+ int id = redashClient.createDataSource(rds);
+ redashClient.addDataSourceToGroup(id, defaultGroup.getId() + 1);
+ }
+
+ @Test(expected = IOException.class)
+ public void addDataSourceToGroupWithWrongClientTest() throws IOException {
+ int id = redashClient.createDataSource(rds);
+ wrongClient.addDataSourceToGroup(id, defaultGroup.getId());
+ }
+
+ @Test
+ public void removeDataSourceFromGroupTest() throws IOException {
+ int id = redashClient.createDataSource(rds);
+ Group groupFromDb = redashClient.getWithUsersAndDataSources(defaultGroup.getId());
+ Assert.assertTrue(groupFromDb.getDataSources().size() == 1);
+ Assert.assertTrue(redashClient.removeDataSourceFromGroup(id, defaultGroup.getId()));
+ groupFromDb = redashClient.getWithUsersAndDataSources(defaultGroup.getId());
+ Assert.assertTrue(groupFromDb.getDataSources().isEmpty());
+ Assert.assertFalse(redashClient.removeDataSourceFromGroup(id, defaultGroup.getId()));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void removeNonExistingDataSourceFromGroupTest() throws IOException {
+ int id = redashClient.createDataSource(rds);
+ redashClient.removeDataSourceFromGroup(id + 1, defaultGroup.getId());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void removeDataSourceFromNonExistingGroupTest() throws IOException {
+ int id = redashClient.createDataSource(rds);
+ redashClient.removeDataSourceFromGroup(id, defaultGroup.getId() + 1);
+ }
+
+ @Test(expected = IOException.class)
+ public void removeDataSourceFromGroupWithWrongClientTest() throws IOException {
+ int id = redashClient.createDataSource(rds);
+ wrongClient.removeDataSourceFromGroup(id, defaultGroup.getId());
+ }
+
+ public static boolean dataSourceMatcher(DataSource first, DataSource second) {
+ if (first.getName() == null || first.getType() == null) {
+ return false;
+ }
+ return first.getName().equals(second.getName()) &&
+ first.getType().equals(second.getType()) &&
+ first.getHost().equals(second.getHost()) &&
+ first.getPort() == second.getPort() &&
+ first.getDbName().equals(second.getDbName()) &&
+ first.getUser().equals(second.getUser());
+ }
+
+ public static boolean simpleDatasourceMatcher(DataSource dataSource) {
+ if (dataSource.getName() == null || dataSource.getType() == null) {
+ return false;
+ }
+ return dataSource.getName().equals(rds.getName()) && dataSource.getType().equals(rds.getType());
+ }
+
+ // Helpers
+
+ private void wipeDataSources() throws IOException {
+ List dataSources = redashClient.getDataSources();
+ if (dataSources.isEmpty()) {
+ return;
+ }
+ for (DataSource ds : dataSources) {
+ redashClient.deleteDataSource(ds.getId());
+ }
+ }
+}
diff --git a/src/test/java/com/snowplowanalytics/redash/RedashClientUserAndGroupTest.java b/src/test/java/com/snowplowanalytics/redash/RedashClientUserAndGroupTest.java
new file mode 100644
index 0000000..2900029
--- /dev/null
+++ b/src/test/java/com/snowplowanalytics/redash/RedashClientUserAndGroupTest.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (c) 2018 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.redash;
+
+import com.snowplowanalytics.redash.model.Group;
+import com.snowplowanalytics.redash.model.User;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Tests should be performed with only those user groups that are created by Redash server
+ * right after installation and registration process have been completed.
+ *
+ * The included wipeDataSources() method will drop all user groups from Redash server
+ * except "admin" and "default".
+ */
+public class RedashClientUserAndGroupTest extends AbstractRedashClientTest {
+
+ @Before
+ public void setup() throws IOException {
+ wipeAllCreatedUserGroups();
+ }
+
+ @Test
+ public void successfulCreateUserGroupTest() throws IOException {
+ List groups = redashClient.getUserGroups();
+ Group created = new Group("testGroup");
+ Assert.assertTrue(groups.size() == 2);
+ int id = redashClient.createUserGroup(created);
+ groups = redashClient.getUserGroups();
+ Assert.assertTrue(groups.size() == 3);
+ Assert.assertTrue(groups.contains(created));
+ redashClient.deleteUserGroup(id);
+ }
+
+ @Test(expected = IOException.class)
+ public void getUserGroupsTest() throws IOException {
+ wrongClient.getUserGroups();
+ }
+
+ @Test
+ public void deleteUserGroupTest() throws IOException {
+ Group created = new Group("name");
+ int id = redashClient.createUserGroup(created);
+ Assert.assertTrue(redashClient.getUserGroups().size() == 3);
+ Assert.assertFalse(redashClient.deleteUserGroup(id + 1));
+ Assert.assertTrue(redashClient.getUserGroups().size() == 3);
+ Assert.assertTrue(redashClient.deleteUserGroup(id));
+ Assert.assertTrue(redashClient.getUserGroups().size() == 2);
+ id = redashClient.createUserGroup(created);
+ Assert.assertTrue(redashClient.getUserGroups().size() == 3);
+ try {
+ wrongClient.deleteUserGroup(id);
+ } catch (Exception e) {
+ Assert.assertTrue(e.getClass().equals(IOException.class));
+ }
+ }
+
+ @Test
+ public void unsuccessfulWithExistingNameCreateUserGroupTest() throws IOException {
+ List groups = redashClient.getUserGroups();
+ Assert.assertTrue(groups.size() == 2);
+ try {
+ redashClient.createUserGroup(new Group(defaultGroup.getName()));
+ } catch (Exception e) {
+ Assert.assertTrue(e.getClass().equals(IllegalArgumentException.class));
+ }
+ groups = redashClient.getUserGroups();
+ Assert.assertTrue(groups.size() == 2);
+ }
+
+ @Test
+ public void unsuccessfulWithWrongClientCreateUserGroupTest() throws IOException {
+ List groups = redashClient.getUserGroups();
+ Assert.assertTrue(groups.size() == 2);
+ try {
+ wrongClient.createUserGroup(new Group(defaultUser.getName()));
+ } catch (Exception e) {
+ Assert.assertTrue(e.getClass().equals(IOException.class));
+ }
+ groups = redashClient.getUserGroups();
+ Assert.assertTrue(groups.size() == 2);
+ }
+
+ @Test
+ public void getUsersTest() throws IOException {
+ List users = redashClient.getUsers();
+ Assert.assertTrue(users.size() == 2);
+ Assert.assertTrue(users.get(0).equals(adminUser));
+ Assert.assertTrue(users.get(1).equals(defaultUser));
+ }
+
+ @Test(expected = IOException.class)
+ public void getUsersWithIOExceptionTest() throws IOException {
+ wrongClient.getUsers();
+ }
+
+ @Test
+ public void getUserTest() throws IOException {
+ User user = redashClient.getUser(adminUser.getName());
+ Assert.assertTrue(user.equals(adminUser));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void unsuccessfulGetUserTest() throws IOException {
+ redashClient.getUser(invalidUserName);
+ }
+
+ @Test(expected = IOException.class)
+ public void getUserWithIOExceptionTest() throws IOException {
+ wrongClient.getUser(defaultUser.getName());
+ }
+
+ @Test
+ public void addUserToUserGroupTest() throws IOException {
+ Group createdGroup = new Group("createdForTest");
+ int createdUserGroupId = redashClient.createUserGroup(createdGroup);
+ Group groupFromDb = redashClient.getWithUsersAndDataSources(createdUserGroupId);
+ User userFromDb = redashClient.getUser(defaultUser.getName());
+ Assert.assertFalse(groupFromDb.getUsers().contains(userFromDb));
+ Assert.assertTrue(redashClient.addUserToGroup(userFromDb.getId(), createdUserGroupId));
+ groupFromDb = redashClient.getWithUsersAndDataSources(createdUserGroupId);
+ Assert.assertTrue(groupFromDb.getUsers().size() == 1);
+ Assert.assertTrue(groupFromDb.getUsers().contains(userFromDb));
+ Assert.assertFalse(redashClient.addUserToGroup(userFromDb.getId(), createdUserGroupId));
+ groupFromDb = redashClient.getWithUsersAndDataSources(createdUserGroupId);
+ Assert.assertTrue(groupFromDb.getUsers().size() == 1);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void addNonExistingUserToUserGroup() throws IOException {
+ redashClient.addUserToGroup(4, defaultGroup.getId());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void addUserToNonExistingUserGroup() throws IOException {
+ redashClient.addUserToGroup(defaultUser.getId(), 3);
+ }
+
+ @Test(expected = IOException.class)
+ public void addUserToUserGroupTestWithWrongKey() throws IOException {
+ wrongClient.addUserToGroup(defaultUser.getId(), defaultGroup.getId());
+ }
+
+ @Test
+ public void removeUserFromGroupTest() throws IOException {
+ Group groupFromDb = redashClient.getWithUsersAndDataSources(defaultGroup.getId());
+ Assert.assertTrue(groupFromDb.getUsers().size()==2);
+ Assert.assertTrue(redashClient.removeUserFromGroup(defaultUser.getId(), defaultGroup.getId()));
+ groupFromDb = redashClient.getWithUsersAndDataSources(defaultGroup.getId());
+ Assert.assertTrue(groupFromDb.getUsers().size()==1);
+ Assert.assertTrue(!groupFromDb.getUsers().contains(defaultUser));
+ redashClient.addUserToGroup(defaultUser.getId(), defaultGroup.getId());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void removeNonExistingUserFromGroupTest() throws IOException {
+ redashClient.removeUserFromGroup(defaultUser.getId() + 1, defaultGroup.getId());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void removeUserFromNonExistingGroupTest() throws IOException {
+ redashClient.removeUserFromGroup(defaultUser.getId(), defaultGroup.getId() + 1);
+ }
+
+ @Test(expected = IOException.class)
+ public void removeUserGroupWithWrongClientTest() throws IOException {
+ wrongClient.removeUserFromGroup(defaultUser.getId(), defaultGroup.getId());
+ }
+
+ // Helpers
+
+ private void wipeAllCreatedUserGroups() throws IOException {
+ redashClient.getUserGroups().forEach(userGroup -> {
+ int id = userGroup.getId();
+ try {
+ if (id != 1 && id != 2) {
+ redashClient.deleteUserGroup(id);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ });
+ }
+}
diff --git a/src/test/java/com/snowplowanalytics/redash/UserScenariosTest.java b/src/test/java/com/snowplowanalytics/redash/UserScenariosTest.java
new file mode 100644
index 0000000..56b6298
--- /dev/null
+++ b/src/test/java/com/snowplowanalytics/redash/UserScenariosTest.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2018 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.redash;
+
+import com.snowplowanalytics.redash.model.Group;
+import com.snowplowanalytics.redash.model.User;
+import com.snowplowanalytics.redash.model.datasource.DataSource;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.List;
+
+import static com.snowplowanalytics.redash.RedashClientDataSourceTest.dataSourceMatcher;
+import static com.snowplowanalytics.redash.RedashClientDataSourceTest.simpleDatasourceMatcher;
+
+public class UserScenariosTest extends RedashClientUserAndGroupTest {
+
+ @Before
+ public void isDataSourcesListEmpty() throws IOException {
+ wipeDataSources();
+ Assert.assertTrue(redashClient.getDataSources().isEmpty());
+ }
+
+ /**
+ * UA-1
+ * I need to add a new user-group to redash. Once added I need to add a specific already created
+ * user to this group and then attach a data-source to this group.
+ * After sometime has passed I need to remove this data-source from this group.
+ */
+
+ @Test
+ public void firstScenarioTest() throws IOException {
+ Group group = new Group("test group");
+ int userGroupId = redashClient.createUserGroup(group);
+ int dataSourceId = redashClient.createDataSource(rds);
+ User user = redashClient.getUser(defaultUser.getName());
+ Assert.assertTrue(redashClient.addUserToGroup(user.getId(), userGroupId));
+ DataSource dataSource = redashClient.getDataSource(rds.getName());
+ Assert.assertTrue(dataSourceMatcher(dataSource, rds));
+ Assert.assertTrue(redashClient.addDataSourceToGroup(dataSourceId, userGroupId));
+ Group groupFromDb = redashClient.getWithUsersAndDataSources(userGroupId);
+ Assert.assertTrue(groupFromDb.getUsers().size() == 1);
+ Assert.assertTrue(groupFromDb.getDataSources().size() == 1);
+ Assert.assertTrue(groupFromDb.getUsers().contains(defaultUser));
+ Assert.assertTrue(simpleDatasourceMatcher(groupFromDb.getDataSources().get(0)));
+ Assert.assertTrue(redashClient.removeDataSourceFromGroup(dataSource.getId(), userGroupId));
+ groupFromDb = redashClient.getWithUsersAndDataSources(userGroupId);
+ Assert.assertTrue(groupFromDb.getDataSources().isEmpty());
+ }
+
+ /**
+ * UA - 2
+ * I need to add a new Redshift data-source to redash.
+ * Once added I need to attach this data-source to all created groups.
+ */
+
+ @Test
+ public void secondScenarioTest() throws IOException {
+ int dataSourceId = redashClient.createDataSource(rds);
+ for(int i = 0; i<5; i++){
+ Group group = new Group("test group" + i);
+ redashClient.createUserGroup(group);
+ }
+ List groups = redashClient.getUserGroups();
+ for (Group ug : groups) {
+ if (ug.getId() != defaultGroup.getId()) {
+ Assert.assertTrue(redashClient.addDataSourceToGroup(dataSourceId, ug.getId()));
+ }
+ }
+ groups = redashClient.getUserGroups();
+ for (Group ug : groups) {
+ if (ug.getId() != defaultGroup.getId()) {
+ Assert.assertTrue(simpleDatasourceMatcher(redashClient.getWithUsersAndDataSources(ug.getId()).getDataSources().get(0)));
+ }
+ }
+ }
+
+ // Helpers
+
+ private void wipeDataSources() throws IOException {
+ List dataSources = redashClient.getDataSources();
+ for (DataSource ds : dataSources) {
+ redashClient.deleteDataSource(ds.getId());
+ }
+ }
+}
diff --git a/src/test/resources/redash_client.properties b/src/test/resources/redash_client.properties
new file mode 100644
index 0000000..9a9f975
--- /dev/null
+++ b/src/test/resources/redash_client.properties
@@ -0,0 +1,3 @@
+redash_schema=http
+redash_host=localhost
+redash_port=80
diff --git a/src/test/resources/user_group.properties b/src/test/resources/user_group.properties
new file mode 100644
index 0000000..7531555
--- /dev/null
+++ b/src/test/resources/user_group.properties
@@ -0,0 +1,10 @@
+# Existing users
+admin_username=Admin
+default_username=Default
+
+# Non-existent user with wrong name
+non_existent_username=nonExistentUser
+
+# Existing groups
+admin_group=admin
+default_group=default
diff --git a/vagrant/.gitignore b/vagrant/.gitignore
new file mode 100644
index 0000000..1b4b29f
--- /dev/null
+++ b/vagrant/.gitignore
@@ -0,0 +1,3 @@
+.peru
+oss-playbooks
+ansible
diff --git a/vagrant/ansible.hosts b/vagrant/ansible.hosts
new file mode 100644
index 0000000..588fa08
--- /dev/null
+++ b/vagrant/ansible.hosts
@@ -0,0 +1,2 @@
+[vagrant]
+127.0.0.1:2222
diff --git a/vagrant/peru.yaml b/vagrant/peru.yaml
new file mode 100644
index 0000000..234ac18
--- /dev/null
+++ b/vagrant/peru.yaml
@@ -0,0 +1,7 @@
+imports:
+ ansible_playbooks: oss-playbooks
+
+git module ansible_playbooks:
+ url: https://github.com/snowplow/ansible-playbooks.git
+ # Comment out to fetch a specific rev instead of master:
+ # rev: xxx
diff --git a/vagrant/up.bash b/vagrant/up.bash
new file mode 100755
index 0000000..b4573de
--- /dev/null
+++ b/vagrant/up.bash
@@ -0,0 +1,62 @@
+#!/bin/bash
+set -e
+
+vagrant_dir=/vagrant/vagrant
+bashrc=/home/vagrant/.bashrc
+
+echo "========================================"
+echo "INSTALLING PERU AND ANSIBLE DEPENDENCIES"
+echo "----------------------------------------"
+apt-get update
+apt-get install -y language-pack-en git unzip libyaml-dev python3-pip python-pip python-yaml python-paramiko python-jinja2
+
+echo "==============="
+echo "INSTALLING PERU"
+echo "---------------"
+sudo pip3 install peru
+
+echo "=================="
+echo "INSTALLING ANSIBLE"
+echo "------------------"
+sudo pip install ansible=='2.2.1.0'
+
+echo "==================================="
+echo "CLONING ANSIBLE PLAYBOOKS WITH PERU"
+echo "-----------------------------------"
+cd ${vagrant_dir} && peru sync -v
+echo "... done"
+
+hosts=${vagrant_dir}/ansible.hosts
+ssh_checking=False
+
+echo "==================="
+echo "CONFIGURING ANSIBLE"
+echo "-------------------"
+touch ${bashrc}
+echo "export ANSIBLE_HOSTS=${hosts}" >> ${bashrc}
+echo "export ANSIBLE_HOST_KEY_CHECKING=${ssh_checking}" >> ${bashrc}
+echo "... done"
+
+echo "=========================================="
+echo "RUNNING PLAYBOOKS WITH ANSIBLE*"
+echo "* no output while each playbook is running"
+echo "------------------------------------------"
+while read pb; do
+ su - -c "ansible-playbook ${vagrant_dir}/${pb} --connection=local --inventory-file=${hosts}" vagrant
+done <${vagrant_dir}/up.playbooks
+
+guidance=${vagrant_dir}/up.guidance
+
+echo "==========================="
+echo "CONFIGURING DOCKER & REDASH"
+echo "---------------------------"
+sudo curl -L https://github.com/docker/compose/releases/download/1.20.0/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
+sudo chmod +x /usr/local/bin/docker-compose
+sudo docker-compose --version
+
+if [ -f ${guidance} ]; then
+ echo "==========="
+ echo "PLEASE READ"
+ echo "-----------"
+ cat $guidance
+fi
diff --git a/vagrant/up.guidance b/vagrant/up.guidance
new file mode 100644
index 0000000..0575dbc
--- /dev/null
+++ b/vagrant/up.guidance
@@ -0,0 +1,3 @@
+To get started:
+vagrant ssh
+cd /vagrant
diff --git a/vagrant/up.playbooks b/vagrant/up.playbooks
new file mode 100644
index 0000000..525b870
--- /dev/null
+++ b/vagrant/up.playbooks
@@ -0,0 +1,2 @@
+oss-playbooks/java8.yml
+oss-playbooks/docker.yml