Compare commits
27 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
a02abd3394 | ||
|
ea817b2520 | ||
|
d954557000 | ||
|
7d7f2eed87 | ||
|
a6a68a5320 | ||
|
200b69b960 | ||
|
e02b465ada | ||
|
cdaa29f07d | ||
|
5ce29aa86e | ||
|
fc5f29eaed | ||
|
4d89e45c7b | ||
|
a132f6540c | ||
|
409469c624 | ||
|
e7eb82ceb6 | ||
|
b3ad5016fb | ||
|
c89668031c | ||
|
62f68a25a5 | ||
|
3b77006381 | ||
|
5e81284220 | ||
|
0e16d42eaf | ||
|
58b00cbdb0 | ||
|
f551165a82 | ||
|
e89b2e0a02 | ||
|
c7bee5009a | ||
|
cad957394e | ||
|
8eb7918f7f | ||
|
b7970f6af8 |
14
.elasticbeanstalk/config.yml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
branch-defaults:
|
||||||
|
master:
|
||||||
|
environment: new-life-songs-prod
|
||||||
|
global:
|
||||||
|
application_name: new-life-songs
|
||||||
|
branch: null
|
||||||
|
default_ec2_keyname: id_jdb@jdb-maingear
|
||||||
|
default_platform: Tomcat 8 Java 8
|
||||||
|
default_region: us-west-2
|
||||||
|
profile: eb-cli
|
||||||
|
repository: null
|
||||||
|
sc: git
|
||||||
|
deploy:
|
||||||
|
artifact: service/build/ROOT.war
|
8
README.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# New Life Songs Database
|
||||||
|
|
||||||
|
This is Jonathan's database of worship songs performed at New Life Austin. The
|
||||||
|
service lives online at http://newlifesongs.jdbernard.com
|
||||||
|
|
||||||
|
API Documentation is [maintained online with the service](http://newlifesongs.jdbernard.com/doc/api/v1/).
|
||||||
|
|
||||||
|
You can also view the [annotated source code](https://doc.jdb-labs.com/new-life-songs/current/).
|
181
build.gradle
@ -1,169 +1,24 @@
|
|||||||
import org.apache.tools.ant.filters.ReplaceTokens
|
plugins { id 'com.palantir.git-version' version '0.5.2' }
|
||||||
|
|
||||||
apply plugin: "groovy"
|
allprojects {
|
||||||
apply plugin: "maven"
|
group = "com.jdbernard"
|
||||||
apply plugin: "war"
|
|
||||||
apply plugin: "jetty"
|
|
||||||
|
|
||||||
apply from: 'shell.gradle'
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
mavenLocal()
|
||||||
|
mavenCentral()
|
||||||
|
jcenter()
|
||||||
|
maven { url 'https://mvn.jdb-labs.com/repo' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
group = "com.jdbernard"
|
repositories {
|
||||||
|
|
||||||
version = new ProjectVersion()
|
|
||||||
|
|
||||||
// webAppDirName = "build/webapp/main"
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
mavenLocal()
|
mavenLocal()
|
||||||
mavenCentral() }
|
mavenCentral()
|
||||||
|
jcenter()
|
||||||
dependencies {
|
maven { url "https://mvn.jdb-labs.com/repo" }
|
||||||
compile 'ch.qos.logback:logback-classic:1.1.2'
|
}
|
||||||
compile 'ch.qos.logback:logback-core:1.1.2'
|
|
||||||
compile 'com.impossibl.pgjdbc-ng:pgjdbc-ng:0.3'
|
|
||||||
compile 'com.lambdaworks:scrypt:1.4.0'
|
|
||||||
compile 'com.zaxxer:HikariCP-java6:2.3.2'
|
|
||||||
compile 'javax:javaee-api:7.0'
|
|
||||||
compile 'javax.ws.rs:javax.ws.rs-api:2.0.1'
|
|
||||||
compile 'joda-time:joda-time:2.7'
|
|
||||||
compile 'org.codehaus.groovy:groovy-all:2.3.6'
|
|
||||||
compile 'org.slf4j:slf4j-api:1.7.10'
|
|
||||||
runtime 'com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider:2.3.2'
|
|
||||||
runtime 'org.glassfish.jersey.containers:jersey-container-servlet:2.16'
|
|
||||||
runtime 'org.glassfish.jersey.media:jersey-media-json-jackson:2.16'
|
|
||||||
providedCompile 'javax.servlet:javax.servlet-api:3.1.0'
|
|
||||||
|
|
||||||
testCompile 'com.jdbernard:jdb-util:3.4'
|
|
||||||
testCompile 'junit:junit:4.12'
|
|
||||||
testRuntime 'com.h2database:h2:1.4.186'
|
|
||||||
|
|
||||||
}
|
apply plugin: 'com.palantir.git-version'
|
||||||
|
version = gitVersion()
|
||||||
war {
|
|
||||||
from "resources/webapp"
|
|
||||||
from "build/webapp"
|
|
||||||
filter(ReplaceTokens, tokens: [version: version])
|
|
||||||
rename '(.+)(\\..*(css|js))', '$1-' + version + '$2'
|
|
||||||
version = project.version.releaseVersion
|
|
||||||
webInf { from 'resources/main/WEB-INF' }
|
|
||||||
exclude "**/.*.swp", "**/.sass-cache"
|
|
||||||
}
|
|
||||||
|
|
||||||
test { testLogging { events 'failed' } }
|
|
||||||
|
|
||||||
task testWar(type: War) {
|
|
||||||
from 'resources/webapp'
|
|
||||||
filter(ReplaceTokens, tokens: [version: version])
|
|
||||||
rename '(.+)(\\..*(css|js))', '$1-' + version + '$2'
|
|
||||||
version = project.version.releaseVersion
|
|
||||||
webInf { from 'resources/test/WEB-INF' }
|
|
||||||
classifier 'test' }
|
|
||||||
|
|
||||||
task compileScss(
|
|
||||||
group: 'build',
|
|
||||||
description: 'Compile SCSS files into CSS.',
|
|
||||||
type: Exec
|
|
||||||
) {
|
|
||||||
executable "scss"
|
|
||||||
args "--update", "src/main/webapp/css:build/webapp/css"
|
|
||||||
}
|
|
||||||
|
|
||||||
war.dependsOn compileScss
|
|
||||||
testWar.dependsOn compileScss
|
|
||||||
|
|
||||||
// ## Build Versioning task
|
|
||||||
task incrementBuildNumber(
|
|
||||||
group: 'versioning',
|
|
||||||
description: "Increment the project's build number."
|
|
||||||
) << { ++version.build }
|
|
||||||
|
|
||||||
task incrementMinorNumber(
|
|
||||||
group: 'versioning',
|
|
||||||
description: "Increment the project's minor version number."
|
|
||||||
) << { ++version.minor }
|
|
||||||
|
|
||||||
task incrementMajorNumber(
|
|
||||||
group: 'versioning',
|
|
||||||
description: "Increment the project's major version number."
|
|
||||||
) << { ++version.major }
|
|
||||||
|
|
||||||
task markReleaseBuild(
|
|
||||||
group: 'versioning',
|
|
||||||
description: "Mark this version of the project as a release version."
|
|
||||||
) << { version.release = true }
|
|
||||||
|
|
||||||
war.dependsOn << incrementBuildNumber
|
|
||||||
testWar.dependsOn << incrementBuildNumber
|
|
||||||
|
|
||||||
// ## Custom tasks for local deployment
|
|
||||||
|
|
||||||
task deployLocal(dependsOn: ['build']) << {
|
|
||||||
def warName = "${project.name}-${version.releaseVersion}.war"
|
|
||||||
def jettyHome = System.getenv("JETTY_HOME")
|
|
||||||
def deployedWar = new File("$jettyHome/webapps/$warName")
|
|
||||||
|
|
||||||
if (deployedWar.exists()) deployedWar.delete();
|
|
||||||
copy {
|
|
||||||
from "build/libs"
|
|
||||||
into "$jettyHome/webapps"
|
|
||||||
include warName } }
|
|
||||||
|
|
||||||
task killJettyLocal() << {
|
|
||||||
def pidFile = new File(System.properties['user.home'] + "/temp/jetty.pid")
|
|
||||||
|
|
||||||
println "Killing old Jetty instance."
|
|
||||||
shell_("sh", "-c", 'kill $(jps -l | grep start.jar | cut -f 1 -d " ")') }
|
|
||||||
|
|
||||||
task localJetty(dependsOn: ['killJettyLocal', 'deployLocal']) << {
|
|
||||||
spawn(["java", "-jar", "start.jar"], new File(jettyHome)) }
|
|
||||||
|
|
||||||
// ## Project Version
|
|
||||||
class ProjectVersion {
|
|
||||||
|
|
||||||
private File versionFile
|
|
||||||
|
|
||||||
int major
|
|
||||||
int minor
|
|
||||||
int build
|
|
||||||
boolean release
|
|
||||||
|
|
||||||
public ProjectVersion() { this(new File('version.properties')) }
|
|
||||||
|
|
||||||
public ProjectVersion(File versionFile) {
|
|
||||||
this.versionFile = versionFile
|
|
||||||
|
|
||||||
if (!versionFile.exists()) {
|
|
||||||
versionFile.createNewFile()
|
|
||||||
this.major = this.minor = this.build = 0
|
|
||||||
this.save() }
|
|
||||||
|
|
||||||
else this.load() }
|
|
||||||
|
|
||||||
@Override String toString() { "$major.$minor${release ? '' : '-build' + build}" }
|
|
||||||
|
|
||||||
public String getReleaseVersion() { "$major.$minor" }
|
|
||||||
|
|
||||||
public void setRelease(boolean release) { this.release = release; save() }
|
|
||||||
|
|
||||||
public void setMajor(int major) {
|
|
||||||
this.major = major; minor = build = 0; release = false; save() }
|
|
||||||
|
|
||||||
public void setMinor(int minor) {
|
|
||||||
this.minor = minor; build = 0; release = false; save() }
|
|
||||||
|
|
||||||
public void setBuild(int build) { this.build = build; save() }
|
|
||||||
|
|
||||||
private void save() {
|
|
||||||
def props = new Properties()
|
|
||||||
versionFile.withInputStream { props.load(it) }
|
|
||||||
["major", "minor", "build"].each { props[it] = this[it].toString() }
|
|
||||||
props["version.release"] = release.toString()
|
|
||||||
versionFile.withOutputStream { props.store(it, "") } }
|
|
||||||
|
|
||||||
private void load() {
|
|
||||||
def props = new Properties()
|
|
||||||
versionFile.withInputStream { props.load(it) }
|
|
||||||
["major", "minor", "build"].each {
|
|
||||||
this[it] = props[it] ? props[it] as int : 0 }
|
|
||||||
release = Boolean.parseBoolean(props["version.release"]) }
|
|
||||||
}
|
}
|
||||||
|
@ -1,34 +0,0 @@
|
|||||||
INSERT INTO SERVICES (date, service_type) values
|
|
||||||
('2015-02-01', 'SUN_AM'),
|
|
||||||
('2015-02-01', 'SUN_PM'),
|
|
||||||
('2015-02-04', 'WED'),
|
|
||||||
('2015-02-08', 'SUN_AM'),
|
|
||||||
('2015-02-08', 'SUN_PM'),
|
|
||||||
('2015-02-11', 'WED'),
|
|
||||||
('2015-02-15', 'SUN_AM'),
|
|
||||||
('2015-02-15', 'SUN_PM');
|
|
||||||
|
|
||||||
INSERT INTO songs (name, artists) VALUES
|
|
||||||
('Breathe On Us', 'Kari Jobe'),
|
|
||||||
('How Great Is Our God', 'Chris Tomlin'),
|
|
||||||
('Glorious', 'Martha Munizzi'),
|
|
||||||
('Rez Power', 'Israel Houghton');
|
|
||||||
|
|
||||||
INSERT INTO performances (service_id, song_id, pianist, organist, bassist, drummer, guitarist, leader) VALUES
|
|
||||||
(1, 1, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood'),
|
|
||||||
(1, 2, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood'),
|
|
||||||
(1, 3, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood'),
|
|
||||||
(2, 2, 'Trevor Delano', 'Connie Bernard', 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser', 'Rachel Wood'),
|
|
||||||
(2, 3, 'Trevor Delano', 'Connie Bernard', 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser', 'Rachel Wood'),
|
|
||||||
(2, 4, 'Trevor Delano', 'Connie Bernard', 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser', 'Rachel Wood'),
|
|
||||||
(3, 1, 'Rachel Wood', 'Krista Hatcher', 'Jonathan Bernard', 'Jared Wood', 'Tony Bagliore', 'Rachel Wood'),
|
|
||||||
(3, 2, 'Rachel Wood', 'Krista Hatcher', 'Jonathan Bernard', 'Jared Wood', 'Tony Bagliore', 'Rachel Wood'),
|
|
||||||
(4, 3, 'Trevor Delano', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser', 'Rachel Wood'),
|
|
||||||
(5, 4, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Tony Bagliore', 'Rachel Wood'),
|
|
||||||
(6, 1, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood'),
|
|
||||||
(7, 2, 'Trevor Delano', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood'),
|
|
||||||
(8, 3, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood');
|
|
||||||
|
|
||||||
INSERT INTO users (username, pwd, role) VALUES
|
|
||||||
('admin', '', 'admin'),
|
|
||||||
('test', '', '');
|
|
79
service/build.gradle
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import org.apache.tools.ant.filters.ReplaceTokens
|
||||||
|
|
||||||
|
apply plugin: "groovy"
|
||||||
|
apply plugin: "maven"
|
||||||
|
apply plugin: "war"
|
||||||
|
|
||||||
|
// webAppDirName = "build/webapp/main"
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
dependencies {
|
||||||
|
classpath 'com.jdbernard:gradle-exec-util:0.2.0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
import static com.jdbernard.gradle.ExecUtil.*
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compile localGroovy()
|
||||||
|
compile 'ch.qos.logback:logback-classic:1.1.8'
|
||||||
|
compile 'ch.qos.logback:logback-core:1.1.8'
|
||||||
|
compile 'org.slf4j:slf4j-api:1.7.22'
|
||||||
|
compile 'com.impossibl.pgjdbc-ng:pgjdbc-ng:0.6'
|
||||||
|
compile 'com.lambdaworks:scrypt:1.4.0'
|
||||||
|
compile 'com.zaxxer:HikariCP:2.5.1'
|
||||||
|
compile 'javax:javaee-api:7.0'
|
||||||
|
compile 'javax.ws.rs:javax.ws.rs-api:2.0.1'
|
||||||
|
compile 'joda-time:joda-time:2.7'
|
||||||
|
runtime 'com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider:2.3.2'
|
||||||
|
runtime 'org.glassfish.jersey.containers:jersey-container-servlet:2.16'
|
||||||
|
runtime 'org.glassfish.jersey.media:jersey-media-json-jackson:2.16'
|
||||||
|
providedCompile 'javax.servlet:javax.servlet-api:3.1.0'
|
||||||
|
|
||||||
|
testCompile 'junit:junit:4.12'
|
||||||
|
testCompile 'com.jdblabs:db-migrate.groovy:0.2.5'
|
||||||
|
testRuntime 'com.h2database:h2:1.4.186'
|
||||||
|
}
|
||||||
|
|
||||||
|
war {
|
||||||
|
from "resources/webapp"
|
||||||
|
from "build/webapp"
|
||||||
|
filter(ReplaceTokens, tokens: [version: version])
|
||||||
|
rename '(.+)(\\..*(css|js))', '$1-' + version + '$2'
|
||||||
|
webInf { from 'resources/main/WEB-INF' }
|
||||||
|
exclude "**/.*.swp", "**/.sass-cache"
|
||||||
|
}
|
||||||
|
|
||||||
|
test { testLogging { events 'failed' } }
|
||||||
|
|
||||||
|
task testWar(type: War) {
|
||||||
|
from 'resources/webapp'
|
||||||
|
filter(ReplaceTokens, tokens: [version: version])
|
||||||
|
rename '(.+)(\\..*(css|js))', '$1-' + version + '$2'
|
||||||
|
webInf { from 'resources/test/WEB-INF' }
|
||||||
|
classifier 'test' }
|
||||||
|
|
||||||
|
task compileScss(
|
||||||
|
group: 'build',
|
||||||
|
description: 'Compile SCSS files into CSS.',
|
||||||
|
type: Exec
|
||||||
|
) {
|
||||||
|
executable "scss"
|
||||||
|
args "--update", "src/main/webapp/css:build/webapp/css"
|
||||||
|
}
|
||||||
|
|
||||||
|
war.dependsOn compileScss
|
||||||
|
testWar.dependsOn compileScss
|
||||||
|
|
||||||
|
task deployProd(dependsOn: ['build']) { doLast {
|
||||||
|
def warName = "${project.name}-${version}.war"
|
||||||
|
def artifactName = "ROOT.war"
|
||||||
|
|
||||||
|
copy {
|
||||||
|
from "build/libs"
|
||||||
|
into "build"
|
||||||
|
include warName
|
||||||
|
rename warName, artifactName }
|
||||||
|
|
||||||
|
exec("eb", "deploy", "-l", "${parent.name}-${project.name}-${version}")
|
||||||
|
} }
|
18
service/resources/main/WEB-INF/classes/logback.groovy
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import ch.qos.logback.core.*;
|
||||||
|
import ch.qos.logback.core.encoder.*;
|
||||||
|
import ch.qos.logback.core.read.*;
|
||||||
|
import ch.qos.logback.core.rolling.*;
|
||||||
|
import ch.qos.logback.core.status.*;
|
||||||
|
import ch.qos.logback.classic.net.*;
|
||||||
|
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
|
||||||
|
|
||||||
|
|
||||||
|
appender("STDOUT", ConsoleAppender) {
|
||||||
|
encoder(PatternLayoutEncoder) {
|
||||||
|
pattern = "%level %logger - %msg%n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
root(INFO, ["STDOUT"])
|
||||||
|
logger('com.jdbernard', INFO)
|
||||||
|
|
34
service/resources/test/testdb.init.sql
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
INSERT INTO SERVICES (date, service_type) values
|
||||||
|
('2015-02-01', 'SUN_AM'),
|
||||||
|
('2015-02-01', 'SUN_PM'),
|
||||||
|
('2015-02-04', 'WED'),
|
||||||
|
('2015-02-08', 'SUN_AM'),
|
||||||
|
('2015-02-08', 'SUN_PM'),
|
||||||
|
('2015-02-11', 'WED'),
|
||||||
|
('2015-02-15', 'SUN_AM'),
|
||||||
|
('2015-02-15', 'SUN_PM');
|
||||||
|
|
||||||
|
INSERT INTO songs (name, artists) VALUES
|
||||||
|
('Breathe On Us', 'Kari Jobe'),
|
||||||
|
('How Great Is Our God', 'Chris Tomlin'),
|
||||||
|
('Glorious', 'Martha Munizzi'),
|
||||||
|
('Rez Power', 'Israel Houghton');
|
||||||
|
|
||||||
|
INSERT INTO performances (service_id, song_id, rank, pianist, organist, bassist, drummer, guitarist, leader) VALUES
|
||||||
|
(1, 1, 1, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood'),
|
||||||
|
(1, 2, 2, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood'),
|
||||||
|
(1, 3, 3, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood'),
|
||||||
|
(2, 2, 1, 'Trevor Delano', 'Connie Bernard', 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser', 'Rachel Wood'),
|
||||||
|
(2, 3, 2, 'Trevor Delano', 'Connie Bernard', 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser', 'Rachel Wood'),
|
||||||
|
(2, 4, 3, 'Trevor Delano', 'Connie Bernard', 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser', 'Rachel Wood'),
|
||||||
|
(3, 1, 0, 'Rachel Wood', 'Krista Hatcher', 'Jonathan Bernard', 'Jared Wood', 'Tony Bagliore', 'Rachel Wood'),
|
||||||
|
(3, 2, 0, 'Rachel Wood', 'Krista Hatcher', 'Jonathan Bernard', 'Jared Wood', 'Tony Bagliore', 'Rachel Wood'),
|
||||||
|
(4, 3, 0, 'Trevor Delano', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser', 'Rachel Wood'),
|
||||||
|
(5, 4, 1, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Tony Bagliore', 'Rachel Wood'),
|
||||||
|
(6, 1, 1, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood'),
|
||||||
|
(7, 2, 1, 'Trevor Delano', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood'),
|
||||||
|
(8, 3, 1, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood');
|
||||||
|
|
||||||
|
INSERT INTO users (username, pwd, role) VALUES
|
||||||
|
('admin', '', 'admin'),
|
||||||
|
('test', '', '');
|
BIN
service/resources/webapp/favicon.ico
Normal file
After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.3 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
@ -27,10 +27,7 @@ public class NLSongsDB {
|
|||||||
/// ### Common
|
/// ### Common
|
||||||
public def save(def model) {
|
public def save(def model) {
|
||||||
if (model.id > 0) return update(model)
|
if (model.id > 0) return update(model)
|
||||||
else {
|
else return create(model) }
|
||||||
if (create(model) > 0) return model
|
|
||||||
else return null } }
|
|
||||||
|
|
||||||
|
|
||||||
/// ### Services
|
/// ### Services
|
||||||
public Service findService(int id) {
|
public Service findService(int id) {
|
||||||
@ -155,10 +152,10 @@ public class NLSongsDB {
|
|||||||
public Performance create(Performance perf) {
|
public Performance create(Performance perf) {
|
||||||
// TODO: handle constraint violation (same service and song ids)
|
// TODO: handle constraint violation (same service and song ids)
|
||||||
sql.executeInsert(
|
sql.executeInsert(
|
||||||
"INSERT INTO performances (service_id, song_id, pianist, " +
|
"INSERT INTO performances (service_id, song_id, rank, pianist, " +
|
||||||
"organist, bassist, drummer, guitarist, leader) VALUES " +
|
"organist, bassist, drummer, guitarist, leader) VALUES " +
|
||||||
"(?, ?, ?, ?, ?, ?, ?, ?)", [perf.serviceId, perf.songId,
|
"(?, ?, ?, ?, ?, ?, ?, ?)", [perf.serviceId, perf.songId,
|
||||||
perf.pianist, perf.organist, perf.bassist, perf.drummer,
|
perf.rank, perf.pianist, perf.organist, perf.bassist, perf.drummer,
|
||||||
perf.guitarist, perf.leader])
|
perf.guitarist, perf.leader])
|
||||||
return perf }
|
return perf }
|
||||||
|
|
||||||
@ -166,10 +163,11 @@ public class NLSongsDB {
|
|||||||
// TODO: handle constraint violation (same service and song ids)
|
// TODO: handle constraint violation (same service and song ids)
|
||||||
return sql.executeUpdate(
|
return sql.executeUpdate(
|
||||||
"UPDATE performances SET pianist = ?, organist = ?, " +
|
"UPDATE performances SET pianist = ?, organist = ?, " +
|
||||||
"bassist = ?, drummer = ?, guitarist = ?, leader = ? " +
|
"bassist = ?, drummer = ?, guitarist = ?, leader = ?, " +
|
||||||
"WHERE service_id = ? AND song_id = ?",
|
"rank = ? WHERE service_id = ? AND song_id = ?",
|
||||||
[perf.pianist, perf.organist, perf.bassist, perf.drummer,
|
[perf.pianist, perf.organist, perf.bassist, perf.drummer,
|
||||||
perf.guitarist, perf.leader, perf.serviceId, perf.songId]) }
|
perf.guitarist, perf.leader, perf.rank, perf.serviceId,
|
||||||
|
perf.songId]) }
|
||||||
|
|
||||||
public int delete(Performance perf) {
|
public int delete(Performance perf) {
|
||||||
sql.execute(
|
sql.execute(
|
||||||
@ -315,7 +313,7 @@ public class NLSongsDB {
|
|||||||
return buildToken(row, user) }
|
return buildToken(row, user) }
|
||||||
|
|
||||||
public static List<String> unwrapArtists(String artists) {
|
public static List<String> unwrapArtists(String artists) {
|
||||||
return artists.split(';') as List<String> }
|
return artists.split(':') as List<String> }
|
||||||
|
|
||||||
public static String wrapArtists(List<String> artists) {
|
public static String wrapArtists(List<String> artists) {
|
||||||
return artists.join(':') }
|
return artists.join(':') }
|
@ -4,6 +4,7 @@ public class Performance implements Serializable {
|
|||||||
|
|
||||||
int serviceId
|
int serviceId
|
||||||
int songId
|
int songId
|
||||||
|
int rank
|
||||||
String pianist
|
String pianist
|
||||||
String organist
|
String organist
|
||||||
String bassist
|
String bassist
|
||||||
@ -19,6 +20,7 @@ public class Performance implements Serializable {
|
|||||||
|
|
||||||
return (this.serviceId == that.serviceId &&
|
return (this.serviceId == that.serviceId &&
|
||||||
this.songId == that.songId &&
|
this.songId == that.songId &&
|
||||||
|
this.rank == that.rank &&
|
||||||
this.pianist == that.pianist &&
|
this.pianist == that.pianist &&
|
||||||
this.organist == that.organist &&
|
this.organist == that.organist &&
|
||||||
this.bassist == that.bassist &&
|
this.bassist == that.bassist &&
|
||||||
@ -27,5 +29,5 @@ public class Performance implements Serializable {
|
|||||||
this.leader == that.leader) }
|
this.leader == that.leader) }
|
||||||
|
|
||||||
@Override String toString() {
|
@Override String toString() {
|
||||||
return "($serviceId, $songId): $leader - $pianist" }
|
return "($serviceId, $songId)-$rank: $leader - $pianist" }
|
||||||
}
|
}
|
@ -7,6 +7,7 @@ public class Service implements Serializable {
|
|||||||
int id
|
int id
|
||||||
private LocalDate date
|
private LocalDate date
|
||||||
ServiceType serviceType
|
ServiceType serviceType
|
||||||
|
String description
|
||||||
|
|
||||||
public boolean equals(Object thatObj) {
|
public boolean equals(Object thatObj) {
|
||||||
if (thatObj == null) return false
|
if (thatObj == null) return false
|
||||||
@ -15,7 +16,7 @@ public class Service implements Serializable {
|
|||||||
Service that = (Service) thatObj
|
Service that = (Service) thatObj
|
||||||
|
|
||||||
return (this.id == that.id &&
|
return (this.id == that.id &&
|
||||||
this.date == (that.@date) &&
|
this.date == (that.localDate) &&
|
||||||
this.serviceType == that.serviceType) }
|
this.serviceType == that.serviceType) }
|
||||||
|
|
||||||
public void setDate(Date date) { this.date = LocalDate.fromDateFields(date) }
|
public void setDate(Date date) { this.date = LocalDate.fromDateFields(date) }
|
||||||
@ -25,4 +26,8 @@ public class Service implements Serializable {
|
|||||||
public Date getDate() { return this.date.toDate() }
|
public Date getDate() { return this.date.toDate() }
|
||||||
|
|
||||||
public String toString() { return "$id: $date - $serviceType" }
|
public String toString() { return "$id: $date - $serviceType" }
|
||||||
|
|
||||||
|
// Needed only because the @directFieldAccesor syntax stopped working in
|
||||||
|
// Groovy 2.4.7
|
||||||
|
private LocalDate getLocalDate() { return this.date }
|
||||||
}
|
}
|
@ -7,11 +7,12 @@ import com.jdbernard.nlsongs.model.Song
|
|||||||
public class NLSongsContext {
|
public class NLSongsContext {
|
||||||
|
|
||||||
public static NLSongsDB songsDB
|
public static NLSongsDB songsDB
|
||||||
|
|
||||||
public static String mediaBaseUrl
|
public static String mediaBaseUrl
|
||||||
|
|
||||||
public static String makeUrl(Service service, Song song) {
|
public static String makeUrl(Service service, Song song) {
|
||||||
return mediaBaseUrl + '/' + service.@date.toString('yyyy-MM-dd') + '_' +
|
return mediaBaseUrl + '/' + service.localDate.toString('yyyy') + "/" +
|
||||||
|
service.localDate.toString('yyyy-MM-dd') + '_' +
|
||||||
service.serviceType.name().toLowerCase() + '_' +
|
service.serviceType.name().toLowerCase() + '_' +
|
||||||
song.name.replaceAll(/[\s'"\\\/\?!]/, '') + '.mp3' }
|
song.name.replaceAll(/[\s'"\\\/\?!]/, '') + '.mp3' }
|
||||||
}
|
}
|
@ -9,20 +9,40 @@ import com.jdbernard.nlsongs.db.NLSongsDB
|
|||||||
import com.zaxxer.hikari.HikariConfig
|
import com.zaxxer.hikari.HikariConfig
|
||||||
import com.zaxxer.hikari.HikariDataSource
|
import com.zaxxer.hikari.HikariDataSource
|
||||||
|
|
||||||
|
import org.slf4j.Logger
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
public final class NLSongsContextListener implements ServletContextListener {
|
public final class NLSongsContextListener implements ServletContextListener {
|
||||||
|
|
||||||
|
private static final log = LoggerFactory.getLogger(NLSongsContextListener)
|
||||||
|
|
||||||
public void contextInitialized(ServletContextEvent event) {
|
public void contextInitialized(ServletContextEvent event) {
|
||||||
def context = event.servletContext
|
def context = event.servletContext
|
||||||
|
|
||||||
// Load the context configuration.
|
|
||||||
Properties props = new Properties()
|
Properties props = new Properties()
|
||||||
|
|
||||||
|
// Load configuration details from the context configuration.
|
||||||
NLSongsContextListener.getResourceAsStream(
|
NLSongsContextListener.getResourceAsStream(
|
||||||
context.getInitParameter('context.config.file')).withStream { is ->
|
context.getInitParameter('context.config.file'))
|
||||||
props.load(is) }
|
.withStream { is -> props.load(is) }
|
||||||
|
|
||||||
|
// Load database configuration
|
||||||
|
Properties dataSourceProps = new Properties()
|
||||||
|
String dbConfigFile = context.getInitParameter('datasource.config.file')
|
||||||
|
|
||||||
|
if (dbConfigFile) {
|
||||||
|
NLSongsContextListener.getResourceAsStream(dbConfigFile)
|
||||||
|
.withStream { is -> dataSourceProps.load(is) } }
|
||||||
|
|
||||||
|
// Load database configuration from environment variables (may
|
||||||
|
// override settings in file).
|
||||||
|
System.properties.keySet().findAll { it.startsWith('DB_') }.each { key ->
|
||||||
|
dataSourceProps["dataSource.${key.substring(3)}"] = System.properties[key] }
|
||||||
|
|
||||||
|
log.debug("Database configuration: {}", dataSourceProps)
|
||||||
|
|
||||||
// Create the pooled data source
|
// Create the pooled data source
|
||||||
HikariConfig hcfg = new HikariConfig(
|
HikariConfig hcfg = new HikariConfig(dataSourceProps)
|
||||||
context.getInitParameter('datasource.config.file'))
|
|
||||||
|
|
||||||
HikariDataSource hds = new HikariDataSource(hcfg)
|
HikariDataSource hds = new HikariDataSource(hcfg)
|
||||||
|
|
||||||
@ -39,6 +59,6 @@ public final class NLSongsContextListener implements ServletContextListener {
|
|||||||
// Shutdown the Songs DB instance (it will shut down the data source).
|
// Shutdown the Songs DB instance (it will shut down the data source).
|
||||||
NLSongsDB songsDB = context.getAttribute('songsDB')
|
NLSongsDB songsDB = context.getAttribute('songsDB')
|
||||||
if (songsDB) songsDB.shutdown()
|
if (songsDB) songsDB.shutdown()
|
||||||
|
|
||||||
context.removeAttribute('songsDB') }
|
context.removeAttribute('songsDB') }
|
||||||
}
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
-- # New Life Songs DB
|
||||||
|
-- @author Jonathan Bernard <jdb@jdb-labs.com>
|
||||||
|
--
|
||||||
|
-- PostgreSQL database un-creation sript.
|
||||||
|
DROP TABLE performances;
|
||||||
|
DROP TABLE services;
|
||||||
|
DROP TABLE songs;
|
||||||
|
DROP TABLE tokens;
|
||||||
|
DROP TABLE users;
|
@ -4,18 +4,16 @@
|
|||||||
-- PostgreSQL database creation sript.
|
-- PostgreSQL database creation sript.
|
||||||
|
|
||||||
-- Services table
|
-- Services table
|
||||||
DROP TABLE IF EXISTS services;
|
CREATE TABLE services (
|
||||||
CREATE TABLE IF NOT EXISTS services (
|
|
||||||
id SERIAL,
|
id SERIAL,
|
||||||
date DATE NOT NULL,
|
date DATE NOT NULL,
|
||||||
service_type VARCHAR(16) DEFAULT NULL,
|
service_type VARCHAR(16) DEFAULT NULL,
|
||||||
|
description VARCHAR(255) DEFAULT NULL,
|
||||||
CONSTRAINT uc_serviceTypeAndDate UNIQUE (date, service_type),
|
CONSTRAINT uc_serviceTypeAndDate UNIQUE (date, service_type),
|
||||||
PRIMARY KEY (id));
|
PRIMARY KEY (id));
|
||||||
|
|
||||||
|
|
||||||
-- Songs table
|
-- Songs table
|
||||||
DROP TABLE IF EXISTS songs;
|
CREATE TABLE songs (
|
||||||
CREATE TABLE IF NOT EXISTS songs (
|
|
||||||
id SERIAL,
|
id SERIAL,
|
||||||
name VARCHAR(128) NOT NULL,
|
name VARCHAR(128) NOT NULL,
|
||||||
artists VARCHAR(256) DEFAULT NULL,
|
artists VARCHAR(256) DEFAULT NULL,
|
||||||
@ -24,8 +22,7 @@ CREATE TABLE IF NOT EXISTS songs (
|
|||||||
|
|
||||||
|
|
||||||
-- performances table
|
-- performances table
|
||||||
DROP TABLE IF EXISTS performances;
|
CREATE TABLE performances (
|
||||||
CREATE TABLE IF NOT EXISTS performances (
|
|
||||||
service_id INTEGER NOT NULL,
|
service_id INTEGER NOT NULL,
|
||||||
song_id INTEGER NOT NULL,
|
song_id INTEGER NOT NULL,
|
||||||
pianist VARCHAR(64) DEFAULT NULL,
|
pianist VARCHAR(64) DEFAULT NULL,
|
||||||
@ -39,16 +36,16 @@ CREATE TABLE IF NOT EXISTS performances (
|
|||||||
FOREIGN KEY (song_id) REFERENCES songs (id) ON DELETE CASCADE);
|
FOREIGN KEY (song_id) REFERENCES songs (id) ON DELETE CASCADE);
|
||||||
|
|
||||||
|
|
||||||
DROP TABLE IF EXISTS users;
|
-- Users table
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE users (
|
||||||
id SERIAL,
|
id SERIAL,
|
||||||
username VARCHAR(64) UNIQUE NOT NULL,
|
username VARCHAR(64) UNIQUE NOT NULL,
|
||||||
pwd VARCHAR(80),
|
pwd VARCHAR(80),
|
||||||
role VARCHAR(16) NOT NULL,
|
role VARCHAR(16) NOT NULL,
|
||||||
PRIMARY KEY (id));
|
PRIMARY KEY (id));
|
||||||
|
|
||||||
DROP TABLE IF EXISTS tokens;
|
-- Tokens table
|
||||||
CREATE TABLE IF NOT EXISTS tokens (
|
CREATE TABLE tokens (
|
||||||
token VARCHAR(64),
|
token VARCHAR(64),
|
||||||
user_id INTEGER NOT NULL,
|
user_id INTEGER NOT NULL,
|
||||||
expires TIMESTAMP NOT NULL,
|
expires TIMESTAMP NOT NULL,
|
@ -0,0 +1,5 @@
|
|||||||
|
-- # New Life Songs DB
|
||||||
|
-- @author Jonathan Bernard <jdb@jdb-labs.com>
|
||||||
|
--
|
||||||
|
-- Remove performances.rank
|
||||||
|
ALTER TABLE performances DROP COLUMN rank;
|
@ -0,0 +1,6 @@
|
|||||||
|
-- # New Life Songs DB
|
||||||
|
-- @author Jonathan Bernard <jdb@jdb-labs.com>
|
||||||
|
--
|
||||||
|
-- Add performances.rank: the rank of the performance in the service, aka. the
|
||||||
|
-- "track number" if the service were an album.
|
||||||
|
ALTER TABLE performances ADD COLUMN rank integer NOT NULL DEFAULT 0;
|
@ -65,7 +65,11 @@ table {
|
|||||||
|
|
||||||
pre { margin-left: 1rem; }
|
pre { margin-left: 1rem; }
|
||||||
|
|
||||||
h3 { margin: 1rem 0; }
|
h2 {
|
||||||
|
border-bottom: solid 2px $dark;
|
||||||
|
margin-top: 2em; }
|
||||||
|
|
||||||
|
h3 { margin: 2rem 0 1rem 0; }
|
||||||
|
|
||||||
dl {
|
dl {
|
||||||
margin: 1rem;
|
margin: 1rem;
|
||||||
@ -75,7 +79,20 @@ table {
|
|||||||
font-family: $monoFont;
|
font-family: $monoFont;
|
||||||
font-weight: bold; }
|
font-weight: bold; }
|
||||||
|
|
||||||
& > dd { padding: 0 0 0.5rem 1rem; } } }
|
& > dd { padding: 0 0 0.5rem 1rem; } }
|
||||||
|
|
||||||
|
table.method-summary {
|
||||||
|
padding: 0 2rem;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
th {
|
||||||
|
border-bottom: solid thin $dark;
|
||||||
|
text-align: left; }
|
||||||
|
|
||||||
|
th.action, td.action { width: 6em; }
|
||||||
|
th.path, td.path { width: 17em; }
|
||||||
|
th.public, td.public { width: 4em; }
|
||||||
|
} }
|
||||||
|
|
||||||
@include forSize(notSmall) {
|
@include forSize(notSmall) {
|
||||||
|
|
||||||
@ -109,7 +126,7 @@ table {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
& > h2 { display: none; }
|
& > h2 { display: none; }
|
||||||
& > h2.song-name, & > h2.service-date { display: block; }
|
& > h2.song-name, & > h2.service-desc { display: block; }
|
||||||
|
|
||||||
& > nav > ul > li {
|
& > nav > ul > li {
|
||||||
display: inline-block;
|
display: inline-block;
|
351
service/src/main/webapp/doc/api/v1/index.html
Normal file
@ -0,0 +1,351 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="referrer" content="origin">
|
||||||
|
<link rel="shortcut icon" href="../images/favicon.ico">
|
||||||
|
|
||||||
|
<title>API V1 - New Life Songs Database</title>
|
||||||
|
<link
|
||||||
|
href='http://fonts.googleapis.com/css?family=Roboto+Condensed|Cantarell|Anonymous+Pro' rel='stylesheet' type='text/css'>
|
||||||
|
<link href='../../../css/new-life-songs-@version@.css' rel='stylesheet' type='text/css'>
|
||||||
|
</head>
|
||||||
|
<body class=api-doc>
|
||||||
|
<header>
|
||||||
|
<h1><a href="../../../">New Life Songs API V1</a></h1>
|
||||||
|
<nav><ul>
|
||||||
|
<li><a href="../../../admin/">Admin</a></li>
|
||||||
|
<li><a href="../../../songs/">Songs</a></li>
|
||||||
|
<li><a href="../../../services/">Services</a></li>
|
||||||
|
</ul></nav>
|
||||||
|
</header>
|
||||||
|
<section id=api-overview>
|
||||||
|
The New Life Songs database exposes a REST API. This allows
|
||||||
|
programatic access to and modification of the data. Version 1 of
|
||||||
|
the API defines several endpoints, all of which are built off of
|
||||||
|
<code>http://newlifesongs.jdbernard.com/api/v1</code> as a base
|
||||||
|
URL.
|
||||||
|
|
||||||
|
<p>Some of the service's endpoints require the client to authenticate
|
||||||
|
itself to the server. See the <a href="#authentication">section on
|
||||||
|
authentication</a> for details concerning authentication.
|
||||||
|
|
||||||
|
<p>The endpoints that the API defines are:
|
||||||
|
<ul><li><a href="#songs"><code>/songs</code></a></li>
|
||||||
|
<li><a href="#services"><code>/services</code></a></li>
|
||||||
|
<li><a href="#users"><code>/users</code></a></li></ul>
|
||||||
|
|
||||||
|
<p>If you run across any problems or have questions, feel free to send me an email at
|
||||||
|
<a href='mailto:jdbernard@gmail.com'>jdbernard@gmail.com</a>
|
||||||
|
</section>
|
||||||
|
<section id=songs>
|
||||||
|
<h2><code>/songs</code></h2>
|
||||||
|
<h3 id=song-object>Song object</h3>
|
||||||
|
A song object is defined with the following fields:
|
||||||
|
<dl><dt>id</dt>
|
||||||
|
<dd>An identifier unique to this song record among all song
|
||||||
|
records. <em>Type: integer</em></dd>
|
||||||
|
|
||||||
|
<dt>name</dt>
|
||||||
|
<dd>The name of the song. <em>Type: string</em></dd>
|
||||||
|
|
||||||
|
<dt>artists</dt>
|
||||||
|
<dd>A list of the artists known to have written or performed
|
||||||
|
this song. <em>Type: list of strings</em></dl>
|
||||||
|
|
||||||
|
<h4>Example</h4>
|
||||||
|
<pre>
|
||||||
|
{
|
||||||
|
"id":8,
|
||||||
|
"name":"Here I Am To Worship",
|
||||||
|
"artists":[
|
||||||
|
"Tim Hughes",
|
||||||
|
"Chris Tomlin",
|
||||||
|
"Michael W. Smith"
|
||||||
|
]
|
||||||
|
}</pre>
|
||||||
|
<h3>Method Summary</h3>
|
||||||
|
<table class=method-summary>
|
||||||
|
<thead>
|
||||||
|
<tr><th class=action>HTTP Action</th>
|
||||||
|
<th class=path>Path</th>
|
||||||
|
<th class=desc>Description</th>
|
||||||
|
<th class=public>Public?</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td class=action>GET</td>
|
||||||
|
<td class=path><code>/songs</code></td>
|
||||||
|
<td class=desc>Retrieve all songs.</td>
|
||||||
|
<td class=public>yes</td></tr>
|
||||||
|
<tr><td class=acion>POST</td>
|
||||||
|
<td class=path><code>/songs</code></td>
|
||||||
|
<td class=desc>Create a new song record.</td>
|
||||||
|
<td class=public>no</td></tr>
|
||||||
|
<tr><td class=acion>GET</td>
|
||||||
|
<td class=path><code>/songs/<songId></code></td>
|
||||||
|
<td class=desc>Retrieve a single record.</td>
|
||||||
|
<td class=public>yes</td></tr>
|
||||||
|
<tr><td class=acion>PUT</td>
|
||||||
|
<td class=path><code>/songs/<songId></code></td>
|
||||||
|
<td class=desc>Update a song record.</td>
|
||||||
|
<td class=public>no</td></tr>
|
||||||
|
<tr><td class=acion>DELETE</td>
|
||||||
|
<td class=path><code>/songs/<songId></code></td>
|
||||||
|
<td class=desc>Delete a song record.</td>
|
||||||
|
<td class=public>no</td></tr>
|
||||||
|
<tr><td class=acion>GET</td>
|
||||||
|
<td class=path><code>/songs/forService/<serviceId></code></td>
|
||||||
|
<td class=desc>Retrieve all songs performed in a given service.</td>
|
||||||
|
<td class=public>yes</td></tr>
|
||||||
|
<tr><td class=acion>GET</td>
|
||||||
|
<td class=path><code>/songs/byArtist/<artist></code></td>
|
||||||
|
<td class=desc>Retrieve all songs performed by a given artist.</td>
|
||||||
|
<td class=public>yes</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<ul class=method-list>
|
||||||
|
<li><h3><code>GET /songs</code></h3>
|
||||||
|
|
||||||
|
<p>Retrieve all songs.
|
||||||
|
<p><h4>Response</h4>
|
||||||
|
A list of <a href="song-object">song objects</a>
|
||||||
|
|
||||||
|
<p><h4>Example</h4>
|
||||||
|
<pre>
|
||||||
|
GET http://newlifesongs.jdbernard.com/api/v1/songs</pre>
|
||||||
|
<p><pre>
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Content-Length: 433
|
||||||
|
Content-Type: application/json
|
||||||
|
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
|
||||||
|
|
||||||
|
[{"id":1,"name":"Welcome Holy Spirit","artists":["Mark Condon"]},
|
||||||
|
{"id":3,"name":"Let's Sing Praises to our God","artists":["Traditional"]},
|
||||||
|
{"id":5,"name":"Blessed Assurance","artists":["Frances J. Crosby"]},
|
||||||
|
{"id":8,"name":"Here I Am To Worship","artists":["Tim Hughes", "Chris Tomlin", "Michael W. Smith"]},
|
||||||
|
{"id":12,"name":"Healer","artists":["Kari Jobe", "Hillsong"]},
|
||||||
|
{"id":15,"name":"I Am Free","artists":["Newsboys"]}]
|
||||||
|
</pre></li>
|
||||||
|
|
||||||
|
<li><h3><code>POST /songs</code></h3>
|
||||||
|
|
||||||
|
<p>Create a new song record. In order to be allowed access to
|
||||||
|
this method, the request must be made with a valid
|
||||||
|
authentication token which belongs to a user with
|
||||||
|
administrative priviliges. See <a href="#authentication">Authentication</a>
|
||||||
|
for details.
|
||||||
|
|
||||||
|
<p><h4>Request Body</h4>
|
||||||
|
Must be a <a href="#song-object">song object</a>. The
|
||||||
|
<code>name</code> field is required. Any <code>id</code> passed
|
||||||
|
in with the request will be ignored.
|
||||||
|
|
||||||
|
<p><h4>Reponse</h4>
|
||||||
|
The newly-created song record. If a value is given in the
|
||||||
|
request for the <tt>id</tt> attribute it is ignored. The
|
||||||
|
attribute for new records is determined by the service and
|
||||||
|
returned as part of the response.
|
||||||
|
|
||||||
|
<p><h4>Example</h4>
|
||||||
|
<pre>
|
||||||
|
POST http://newlifesongs.jdbernard.com/api/v1/songs
|
||||||
|
Content-Length: 60
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{"id":22,"name":"This is How We Praise Him","artists":[""]}
|
||||||
|
</pre>
|
||||||
|
<p><pre>
|
||||||
|
HTTP/1.1 201 Created
|
||||||
|
Content-Length:
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
|
||||||
|
</pre></li>
|
||||||
|
|
||||||
|
<li><h3><code>GET /songs/<songId></code></h3>
|
||||||
|
|
||||||
|
<p>Retrieve song data for the given song id.
|
||||||
|
|
||||||
|
<p><h4>Response</h4>
|
||||||
|
A <a href="song-object">song object</a>.
|
||||||
|
|
||||||
|
<p><h4>Example</h4>
|
||||||
|
<pre>
|
||||||
|
GET http://newlifesongs.jdbernard.com/api/v1/songs/1</pre>
|
||||||
|
<p><pre>
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Content-Length: 63
|
||||||
|
Content-Type: application/json
|
||||||
|
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
|
||||||
|
|
||||||
|
{"id":1,"name":"Welcome Holy Spirit","artists":["Mark Condon"]}
|
||||||
|
</pre></li>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li><h3><code>PUT /songs/<songId></code></h3>
|
||||||
|
|
||||||
|
<p>Method description
|
||||||
|
<p><h4>Request Body</h4>
|
||||||
|
Request body description
|
||||||
|
|
||||||
|
<p><h4>Response</h4>
|
||||||
|
Return value description
|
||||||
|
|
||||||
|
<p><h4>Example</h4>
|
||||||
|
<pre>
|
||||||
|
</pre></li>
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li><h3><code>DELETE /songs/<songId></code></h3>
|
||||||
|
|
||||||
|
<p>Method description
|
||||||
|
<p><h4>Request Body</h4>
|
||||||
|
Request body description
|
||||||
|
|
||||||
|
<p><h4>Response</h4>
|
||||||
|
Return value description
|
||||||
|
|
||||||
|
<p><h4>Example</h4>
|
||||||
|
<pre>
|
||||||
|
</pre></li>
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li><h3><code>GET /songs/forService/<serviceId></code></h3>
|
||||||
|
|
||||||
|
<p>Method description
|
||||||
|
<p><h4>Request Body</h4>
|
||||||
|
Request body description
|
||||||
|
|
||||||
|
<p><h4>Response</h4>
|
||||||
|
Return value description
|
||||||
|
|
||||||
|
<p><h4>Example</h4>
|
||||||
|
<pre>
|
||||||
|
GET /api/v1/songs/forService/1 HTTP/1.1</pre>
|
||||||
|
<p><pre>
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Content-Length: 256
|
||||||
|
Content-Type: application/json
|
||||||
|
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
|
||||||
|
|
||||||
|
[{"id":7,"name":"Mighty God","artists":[""]},
|
||||||
|
{"id":8,"name":"Here I Am To Worship","artists":["Tim Hughes: Chris Tomlin, Michael W. Smith"]},
|
||||||
|
{"id":9,"name":"Worthy","artists":[""]},
|
||||||
|
{"id":4,"name":"I Am A Friend Of God","artists":["Israel Houghton"]}]j
|
||||||
|
</pre></li>
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li><h3><code>GET /songs/byArtist/<artist></code></h3>
|
||||||
|
|
||||||
|
<p>Method description
|
||||||
|
<p><h4>Request Body</h4>
|
||||||
|
Request body description
|
||||||
|
|
||||||
|
<p><h4>Response</h4>
|
||||||
|
Return value description
|
||||||
|
|
||||||
|
<p><h4>Example</h4>
|
||||||
|
<pre>
|
||||||
|
</pre></li>
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
<section id=services>
|
||||||
|
<h2><code>/services</code></h2>
|
||||||
|
|
||||||
|
<h3 id=service-object>Service object</h3>
|
||||||
|
A Service object is defined with the following fields:
|
||||||
|
<dl><dt>id</dt>
|
||||||
|
<dd>An identifier unique to this service record among all
|
||||||
|
service records. <em>Type: integer</em></dd>
|
||||||
|
|
||||||
|
<dt>date</dt>
|
||||||
|
<dd>The date of the service. <em>Type: Date</em></dd>
|
||||||
|
|
||||||
|
<dt>serviceType</dt>
|
||||||
|
<dd>Service type. <em>Type: string</em> Valid values:
|
||||||
|
<table><thead><tr><th>Value</th><th>Description</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td><code>SUN_AM</code></td>
|
||||||
|
<td>Sunday morning service.</td></tr>
|
||||||
|
<tr><td><code>SUN_PM</code></td>
|
||||||
|
<td>Sunday evening service</td></tr>
|
||||||
|
<tr><td><code>WED</code></td>
|
||||||
|
<td>Wednesday, midweek Bible study.</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<h4>Example</h4>
|
||||||
|
<pre>
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"date": 1235887200000,
|
||||||
|
"serviceType": "SUN_PM"
|
||||||
|
}</pre>
|
||||||
|
|
||||||
|
<h3>Method Summary</h3>
|
||||||
|
<table class=method-summary>
|
||||||
|
<thead>
|
||||||
|
<tr><th class=action>HTTP Action</th>
|
||||||
|
<th class=path>Path</th>
|
||||||
|
<th class=desc>Description</th>
|
||||||
|
<th class=public>Public?</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td class=action>GET</td>
|
||||||
|
<td class=path><code>/services</code></td>
|
||||||
|
<td class=desc>Retrieve all services.</td>
|
||||||
|
<td class=public>yes</td></tr>
|
||||||
|
<tr><td class=acion>POST</td>
|
||||||
|
<td class=path><code>/services</code></td>
|
||||||
|
<td class=desc>Create a new service record.</td>
|
||||||
|
<td class=public>no</td></tr>
|
||||||
|
<tr><td class=acion>GET</td>
|
||||||
|
<td class=path><code>/services/<serviceId></code></td>
|
||||||
|
<td class=desc>Retrieve a single service record.</td>
|
||||||
|
<td class=public>yes</td></tr>
|
||||||
|
<tr><td class=acion>PUT</td>
|
||||||
|
<td class=path><code>/services/<serviceId></code></td>
|
||||||
|
<td class=desc>Update a service record.</td>
|
||||||
|
<td class=public>no</td></tr>
|
||||||
|
<tr><td class=acion>DELETE</td>
|
||||||
|
<td class=path><code>/services/<serviceId></code></td>
|
||||||
|
<td class=desc>Delete a service record.</td>
|
||||||
|
<td class=public>no</td></tr>
|
||||||
|
<tr><td class=acion>GET</td>
|
||||||
|
<td class=path><code>/services/withSong/<serviceId></code></td>
|
||||||
|
<td class=desc>Retrieve all services in which the given song was performed.</td>
|
||||||
|
<td class=public>yes</td></tr>
|
||||||
|
<tr><td class=acion>GET</td>
|
||||||
|
<td class=path><code>/services/byDate/after/<date></code></td>
|
||||||
|
<td class=desc>Retrieve all services after the given date.</td>
|
||||||
|
<td class=public>yes</td></tr>
|
||||||
|
<tr><td class=acion>GET</td>
|
||||||
|
<td class=path><code>/services/byDate/before/<date></code></td>
|
||||||
|
<td class=desc>Retrieve all services before the given date.</td>
|
||||||
|
<td class=public>yes</td></tr>
|
||||||
|
<tr><td class=acion>GET</td>
|
||||||
|
<td class=path><code>/services/byDate/between/<date1>/<date2></code></td>
|
||||||
|
<td class=desc>Retrieve all services between the two given dates.</td>
|
||||||
|
<td class=public>yes</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
|
||||||
|
</section>
|
||||||
|
<section id=users>
|
||||||
|
<h2><code>/users</code></h2>
|
||||||
|
</section>
|
||||||
|
<section id=authentication>
|
||||||
|
<h2>Authentication</h2>
|
||||||
|
</section>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
@ -22,7 +22,7 @@ if (!service) { response.sendError(response.SC_NOT_FOUND); return }
|
|||||||
<meta name="referrer" content="origin">
|
<meta name="referrer" content="origin">
|
||||||
<link rel="shortcut icon" href="../images/favicon.ico">
|
<link rel="shortcut icon" href="../images/favicon.ico">
|
||||||
|
|
||||||
<title><%= service.@date.toString("yyyy-MM-dd")
|
<title><%= service.localDate.toString("yyyy-MM-dd")
|
||||||
%> (<%= service.serviceType.displayName %>) - New Life Songs Database</title>
|
%> (<%= service.serviceType.displayName %>) - New Life Songs Database</title>
|
||||||
<script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
|
<script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
|
||||||
<!--<script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.2/underscore-min.js"></script>-->
|
<!--<script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.2/underscore-min.js"></script>-->
|
||||||
@ -38,8 +38,8 @@ if (!service) { response.sendError(response.SC_NOT_FOUND); return }
|
|||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<h1><a href="../">New Life Songs</a></h1>
|
<h1><a href="../">New Life Songs</a></h1>
|
||||||
<h2 class=service-date><%= service.@date.toString("yyyy-MM-dd") %> (<%=
|
<h2 class=service-desc><%= service.localDate.toString("yyyy-MM-dd") %>: (<%=
|
||||||
service.serviceType.displayName %>)</h2>
|
service.description ?: service.serviceType.displayName %>)</h2>
|
||||||
|
|
||||||
<nav><ul>
|
<nav><ul>
|
||||||
<li><a href="../admin/">Admin</a></li>
|
<li><a href="../admin/">Admin</a></li>
|
||||||
@ -64,7 +64,7 @@ if (!service) { response.sendError(response.SC_NOT_FOUND); return }
|
|||||||
<tbody>
|
<tbody>
|
||||||
<% songsDB.findPerformancesForServiceId(service.id).
|
<% songsDB.findPerformancesForServiceId(service.id).
|
||||||
collect { [perf: it, song: songsDB.findSong(it.songId)] }.
|
collect { [perf: it, song: songsDB.findSong(it.songId)] }.
|
||||||
sort { it.song.name }.each { row -> %>
|
sort { it.song.name }.sort { it.perf.rank }.each { row -> %>
|
||||||
<tr><td class=actions><a href="<%= NLSongsContext.makeUrl(service, row.song) %>"><i class="fa fa-download"></i></a></td>
|
<tr><td class=actions><a href="<%= NLSongsContext.makeUrl(service, row.song) %>"><i class="fa fa-download"></i></a></td>
|
||||||
<td class=song-name><a href='../song/<%= row.song.id %>'><%=
|
<td class=song-name><a href='../song/<%= row.song.id %>'><%=
|
||||||
row.song.name %></a></td>
|
row.song.name %></a></td>
|
@ -44,8 +44,9 @@ songsDB = NLSongsContext.songsDB
|
|||||||
<tbody>
|
<tbody>
|
||||||
<% songsDB.findAllServices().sort { it.date }.reverse().each { service -> %>
|
<% songsDB.findAllServices().sort { it.date }.reverse().each { service -> %>
|
||||||
<tr><td class=date><a href="../service/<%= service.id %>"><%=
|
<tr><td class=date><a href="../service/<%= service.id %>"><%=
|
||||||
service.@date.toString("yyyy-MM-dd") %></a></td>
|
service.localDate.toString("yyyy-MM-dd") %></a></td>
|
||||||
<td class=service-type><%= service.serviceType.displayName %></td></tr><% } %>
|
<td class=service-type><%= service.description ?:
|
||||||
|
service.serviceType.displayName %></td></tr><% } %>
|
||||||
</tbody>
|
</tbody>
|
||||||
<!--<tfoot><tr>
|
<!--<tfoot><tr>
|
||||||
<th class="dt-left">Date</th>
|
<th class="dt-left">Date</th>
|
||||||
@ -56,7 +57,8 @@ songsDB = NLSongsContext.songsDB
|
|||||||
|
|
||||||
<script type="application/javascript">
|
<script type="application/javascript">
|
||||||
window.onload = function() { \$("#services-table").
|
window.onload = function() { \$("#services-table").
|
||||||
dataTable({ "paging": false }); };
|
dataTable({ "paging": false,
|
||||||
|
"order": [[0, "desc"]]}); };
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
@ -67,7 +67,7 @@ if (!song) { response.sendError(response.SC_NOT_FOUND); return }
|
|||||||
sort { it.svc.date }.each { row -> %>
|
sort { it.svc.date }.each { row -> %>
|
||||||
<tr><td class=actions><a href='<%= NLSongsContext.makeUrl(row.svc, song) %>'><i class="fa fa-download"></i></a></td>
|
<tr><td class=actions><a href='<%= NLSongsContext.makeUrl(row.svc, song) %>'><i class="fa fa-download"></i></a></td>
|
||||||
<td class=performance-date><a href='../service/<%= row.svc.id %>'><%=
|
<td class=performance-date><a href='../service/<%= row.svc.id %>'><%=
|
||||||
row.svc.@date.toString("yyyy-MM-dd") %></a></td>
|
row.svc.localDate.toString("yyyy-MM-dd") %></a></td>
|
||||||
<td class=service-type><%= row.svc.serviceType.displayName %></td>
|
<td class=service-type><%= row.svc.serviceType.displayName %></td>
|
||||||
<td class=not-small><%= row.perf.leader ?: "" %></td>
|
<td class=not-small><%= row.perf.leader ?: "" %></td>
|
||||||
<td class=not-small><%= row.perf.pianist ?: "" %></td>
|
<td class=not-small><%= row.perf.pianist ?: "" %></td>
|
@ -5,8 +5,7 @@ import com.jdbernard.nlsongs.model.*
|
|||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
|
||||||
sdf = new SimpleDateFormat('yyyy-MM-dd')
|
sdf = new SimpleDateFormat('yyyy-MM-dd')
|
||||||
hcfg = new
|
hcfg = new HikariConfig("/home/jdbernard/projects/new-life-songs/src/main/webapp/WEB-INF/classes/datasource.properties")
|
||||||
HikariConfig("/home/jdbernard/projects/new-life-songs/src/main/webapp/WEB-INF/classes/datasource.properties")
|
|
||||||
|
|
||||||
makeService = { svcRow ->
|
makeService = { svcRow ->
|
||||||
Service svc = new Service()
|
Service svc = new Service()
|
@ -1,6 +1,5 @@
|
|||||||
package com.jdbernard.nlsongs.rest
|
package com.jdbernard.nlsongs.rest
|
||||||
|
|
||||||
import com.jdbernard.net.HttpContext
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.AfterClass
|
import org.junit.AfterClass
|
||||||
import org.junit.BeforeClass
|
import org.junit.BeforeClass
|
@ -3,6 +3,7 @@ package com.jdbernard.nlsongs.service
|
|||||||
import com.jdbernard.nlsongs.db.NLSongsDB
|
import com.jdbernard.nlsongs.db.NLSongsDB
|
||||||
import com.jdbernard.nlsongs.model.*
|
import com.jdbernard.nlsongs.model.*
|
||||||
import com.jdbernard.nlsongs.servlet.NLSongsContext
|
import com.jdbernard.nlsongs.servlet.NLSongsContext
|
||||||
|
import com.jdblabs.dbmigrate.DbMigrate
|
||||||
import com.zaxxer.hikari.HikariConfig
|
import com.zaxxer.hikari.HikariConfig
|
||||||
import com.zaxxer.hikari.HikariDataSource
|
import com.zaxxer.hikari.HikariDataSource
|
||||||
|
|
||||||
@ -24,8 +25,9 @@ import org.slf4j.LoggerFactory
|
|||||||
|
|
||||||
public class NLSongsDBTest {
|
public class NLSongsDBTest {
|
||||||
|
|
||||||
static NLSongsDB songsDB;
|
static NLSongsDB songsDB
|
||||||
static Sql sql
|
static Sql sql
|
||||||
|
static DbMigrate dbmigrate
|
||||||
static Logger log = LoggerFactory.getLogger(NLSongsDBTest)
|
static Logger log = LoggerFactory.getLogger(NLSongsDBTest)
|
||||||
|
|
||||||
def dateFormat
|
def dateFormat
|
||||||
@ -61,23 +63,23 @@ public class NLSongsDBTest {
|
|||||||
new Song(id: it[0], name: it[1], artists: it[2]) }
|
new Song(id: it[0], name: it[1], artists: it[2]) }
|
||||||
|
|
||||||
this.performances = [
|
this.performances = [
|
||||||
[1, 1, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood'],
|
[1, 1, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood', 1],
|
||||||
[1, 2, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood'],
|
[1, 2, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood', 2],
|
||||||
[1, 3, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood'],
|
[1, 3, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood', 3],
|
||||||
[2, 2, 'Trevor Delano', 'Connie Bernard', 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser', 'Rachel Wood'],
|
[2, 2, 'Trevor Delano', 'Connie Bernard', 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser', 'Rachel Wood', 1],
|
||||||
[2, 3, 'Trevor Delano', 'Connie Bernard', 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser', 'Rachel Wood'],
|
[2, 3, 'Trevor Delano', 'Connie Bernard', 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser', 'Rachel Wood', 2],
|
||||||
[2, 4, 'Trevor Delano', 'Connie Bernard', 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser', 'Rachel Wood'],
|
[2, 4, 'Trevor Delano', 'Connie Bernard', 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser', 'Rachel Wood', 3],
|
||||||
[3, 1, 'Rachel Wood', 'Krista Hatcher', 'Jonathan Bernard', 'Jared Wood', 'Tony Bagliore', 'Rachel Wood'],
|
[3, 1, 'Rachel Wood', 'Krista Hatcher', 'Jonathan Bernard', 'Jared Wood', 'Tony Bagliore', 'Rachel Wood', 0],
|
||||||
[3, 2, 'Rachel Wood', 'Krista Hatcher', 'Jonathan Bernard', 'Jared Wood', 'Tony Bagliore', 'Rachel Wood'],
|
[3, 2, 'Rachel Wood', 'Krista Hatcher', 'Jonathan Bernard', 'Jared Wood', 'Tony Bagliore', 'Rachel Wood', 0],
|
||||||
[4, 3, 'Trevor Delano', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser', 'Rachel Wood'],
|
[4, 3, 'Trevor Delano', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser', 'Rachel Wood', 0],
|
||||||
[5, 4, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Tony Bagliore', 'Rachel Wood'],
|
[5, 4, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Tony Bagliore', 'Rachel Wood', 1],
|
||||||
[6, 1, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood'],
|
[6, 1, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood', 1],
|
||||||
[7, 2, 'Trevor Delano', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood'],
|
[7, 2, 'Trevor Delano', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood', 1],
|
||||||
[8, 3, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood'] ].collect {
|
[8, 3, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood', 1] ].collect {
|
||||||
|
|
||||||
new Performance(serviceId: it[0], songId: it[1], pianist: it[2],
|
new Performance(serviceId: it[0], songId: it[1], pianist: it[2],
|
||||||
organist: it[3], bassist: it[4], drummer: it[5],
|
organist: it[3], bassist: it[4], drummer: it[5],
|
||||||
guitarist: it[6], leader: it[7]) }
|
guitarist: it[6], leader: it[7], rank: it[8]) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@BeforeClass
|
@BeforeClass
|
||||||
@ -90,8 +92,13 @@ public class NLSongsDBTest {
|
|||||||
HikariDataSource dataSource = new HikariDataSource(hcfg)
|
HikariDataSource dataSource = new HikariDataSource(hcfg)
|
||||||
|
|
||||||
// Create NLSongsDB
|
// Create NLSongsDB
|
||||||
this.songsDB = new NLSongsDB(dataSource)
|
NLSongsDBTest.songsDB = new NLSongsDB(dataSource)
|
||||||
this.sql = new Sql(dataSource)
|
NLSongsDBTest.sql = new Sql(dataSource)
|
||||||
|
|
||||||
|
// Setup our DB migration tool
|
||||||
|
NLSongsDBTest.dbmigrate = new DbMigrate(
|
||||||
|
migrationsDir: new File('src/main/sql'),
|
||||||
|
sql: NLSongsDBTest.sql)
|
||||||
|
|
||||||
// Set NLSongsContext
|
// Set NLSongsContext
|
||||||
NLSongsContext.songsDB = songsDB }
|
NLSongsContext.songsDB = songsDB }
|
||||||
@ -103,18 +110,18 @@ public class NLSongsDBTest {
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void initData() {
|
public void initData() {
|
||||||
// Get the DB Schema and test data.
|
// Create the DB schema
|
||||||
File createSchemaSql = new File("src/main/sql/create-tables.sql")
|
dbmigrate.up()
|
||||||
File testDataSql = new File("resources/test/testdb.init.sql")
|
|
||||||
|
|
||||||
// Create the DB Schema
|
|
||||||
sql.execute(createSchemaSql.text)
|
|
||||||
|
|
||||||
// Populate the DB with test data.
|
// Populate the DB with test data.
|
||||||
|
File testDataSql = new File("resources/test/testdb.init.sql")
|
||||||
sql.execute(testDataSql.text) }
|
sql.execute(testDataSql.text) }
|
||||||
|
|
||||||
/// ### Services
|
@After
|
||||||
|
public void destroyData() {
|
||||||
|
dbmigrate.down(Integer.MAX_VALUE) }
|
||||||
|
|
||||||
|
/// ### Services
|
||||||
@Test public void shouldCreateService() {
|
@Test public void shouldCreateService() {
|
||||||
def service = new Service(
|
def service = new Service(
|
||||||
date: new Date(), serviceType: ServiceType.SUN_AM)
|
date: new Date(), serviceType: ServiceType.SUN_AM)
|
||||||
@ -178,8 +185,8 @@ public class NLSongsDBTest {
|
|||||||
assertCollectionsEqual(
|
assertCollectionsEqual(
|
||||||
performances.findAll { it.serviceId != 1 },
|
performances.findAll { it.serviceId != 1 },
|
||||||
songsDB.findAllPerformances()) }
|
songsDB.findAllPerformances()) }
|
||||||
/// ### Songs
|
|
||||||
|
|
||||||
|
/// ### Songs
|
||||||
@Test public void shoudCreateSong() {
|
@Test public void shoudCreateSong() {
|
||||||
def song = new Song(name: "Test Song", artists: ["Bob Sam"])
|
def song = new Song(name: "Test Song", artists: ["Bob Sam"])
|
||||||
def newSong = songsDB.create(song)
|
def newSong = songsDB.create(song)
|
||||||
@ -247,5 +254,10 @@ public class NLSongsDBTest {
|
|||||||
log.info("C2: $c2")
|
log.info("C2: $c2")
|
||||||
assertEquals(c1.size(), c2.size())
|
assertEquals(c1.size(), c2.size())
|
||||||
|
|
||||||
|
c1.each {
|
||||||
|
def isPresent = c2.contains(it)
|
||||||
|
if (!isPresent) log.info("$it is not within $c2.")
|
||||||
|
assertTrue(isPresent) }
|
||||||
|
|
||||||
assertTrue(c1.every { c2.contains(it) }) }
|
assertTrue(c1.every { c2.contains(it) }) }
|
||||||
}
|
}
|
6
service/version.properties
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#
|
||||||
|
#Sat Dec 17 21:52:48 CST 2016
|
||||||
|
major=2
|
||||||
|
version.release=true
|
||||||
|
minor=5
|
||||||
|
build=2
|
1
settings.gradle
Normal file
@ -0,0 +1 @@
|
|||||||
|
include 'service', 'uploader'
|
33
shell.gradle
@ -1,33 +0,0 @@
|
|||||||
// ## Utility methods for working with processes.
|
|
||||||
|
|
||||||
def shell_(List<String> cmd) { shell(cmd, null, false) }
|
|
||||||
def shell_(String... cmd) { shell(cmd, null, false) }
|
|
||||||
def shell(String... cmd) { shell(cmd, null, true) }
|
|
||||||
|
|
||||||
def shell(List<String> cmd, File workingDir, boolean checkExit) {
|
|
||||||
shell(cmd as String[], workingDir, checkExit) }
|
|
||||||
|
|
||||||
def shell(String[] cmd, File workingDir, boolean checkExit) {
|
|
||||||
def pb = new ProcessBuilder(cmd)
|
|
||||||
if (workingDir) pb.directory(workingDir)
|
|
||||||
def process = pb.start()
|
|
||||||
process.waitForProcessOutput(System.out, System.err)
|
|
||||||
|
|
||||||
if (process.exitValue() != 0)
|
|
||||||
println "Command $cmd exited with non-zero result code."
|
|
||||||
if (checkExit) assert process.exitValue() == 0 : "Not ignoring failed command." }
|
|
||||||
|
|
||||||
def shell(List<List<String>> cmds, File workingDir) {
|
|
||||||
cmds.each {
|
|
||||||
ProcessBuilder pb = new ProcessBuilder(it)
|
|
||||||
pb.directory(workingDir)
|
|
||||||
pb.start().waitForProcessOutput(System.out, System.err) } }
|
|
||||||
|
|
||||||
def spawn(String... cmd) { spawn(cmd, null) }
|
|
||||||
def spawn(List<String> cmd, File workingDir) { spawn(cmd as String[], workingDir) }
|
|
||||||
def spawn(String[] cmd, File workingDir) {
|
|
||||||
def pb = new ProcessBuilder(cmd)
|
|
||||||
if (workingDir) pb.directory(workingDir)
|
|
||||||
def process = pb.start() }
|
|
||||||
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
|||||||
-- DROP DATABASE IF EXISTS nlsongs;
|
|
||||||
CREATE DATABASE nlsongs
|
|
||||||
ENCODING = 'UTF8'
|
|
||||||
LC_COLLATE = 'en_US.UTF-8'
|
|
||||||
LC_CTYPE = 'en_US.UTF-8'
|
|
||||||
CONNECTION LIMIT = 1;
|
|
||||||
|
|
||||||
\c nlsongs
|
|
@ -1,5 +0,0 @@
|
|||||||
DROP TABLE tokens;
|
|
||||||
DROP TABLE users;
|
|
||||||
DROP TABLE performances;
|
|
||||||
DROP TABLE songs;
|
|
||||||
DROP TABLE services;
|
|
@ -1,121 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="referrer" content="origin">
|
|
||||||
<link rel="shortcut icon" href="../images/favicon.ico">
|
|
||||||
|
|
||||||
<title>API V1 - New Life Songs Database</title>
|
|
||||||
<link
|
|
||||||
href='http://fonts.googleapis.com/css?family=Roboto+Condensed|Cantarell|Anonymous+Pro' rel='stylesheet' type='text/css'>
|
|
||||||
<link href='../../../css/new-life-songs-@version@.css' rel='stylesheet' type='text/css'>
|
|
||||||
</head>
|
|
||||||
<body class=api-doc>
|
|
||||||
<header>
|
|
||||||
<h1><a href="../../../">New Life Songs API V1</a></h1>
|
|
||||||
<nav><ul>
|
|
||||||
<li><a href="../../../admin/">Admin</a></li>
|
|
||||||
<li><a href="../../../songs/">Songs</a></li>
|
|
||||||
<li><a href="../../../services/">Services</a></li>
|
|
||||||
</ul></nav>
|
|
||||||
</header>
|
|
||||||
<section id=api-overview>
|
|
||||||
The New Life Songs database exposes a REST API. This allows
|
|
||||||
programatic access and modification to the data. Version 1 of the
|
|
||||||
API defines several endpoints, all of which are built off of
|
|
||||||
<code>http://newlifesongs.jdbernard.com/api/v1</code> as a base
|
|
||||||
URL.
|
|
||||||
|
|
||||||
<p>Some of the service's endpoints require the client to authenticate
|
|
||||||
itself to the server. See the <a href="#authentication">section on
|
|
||||||
authentication</a> for details concerning authentication.
|
|
||||||
|
|
||||||
<p>The endpoints that the API defines are:
|
|
||||||
<ul><li><a href="#songs"><code>/songs</code></a></li>
|
|
||||||
<li><a href="#services"><code>/services</code></a></li>
|
|
||||||
<li><a href="#users"><code>/users</code></a></li></ul>
|
|
||||||
|
|
||||||
<p>If you run across any problems or have questions, feel free to send me an email at
|
|
||||||
<a href='mailto:jdbernard@gmail.com'>jdbernard@gmail.com</a>
|
|
||||||
</section>
|
|
||||||
<section id=songs>
|
|
||||||
<h2><code>/songs</code></h2>
|
|
||||||
<h3 id=song-object>Song object</h3>
|
|
||||||
A song object is defined with the following fields:
|
|
||||||
<dl><dt>id</dt>
|
|
||||||
<dd>An identifier unique to this song record among all song
|
|
||||||
records. <em>Type: integer</em></dd>
|
|
||||||
|
|
||||||
<dt>name</dt>
|
|
||||||
<dd>The name of the song. <em>Type: string</em></dd>
|
|
||||||
|
|
||||||
<dt>artists</dt>
|
|
||||||
<dd>A list of the artists known to have written or performed
|
|
||||||
this song. <em>Type: list of strings</em></dl>
|
|
||||||
|
|
||||||
<h4>Example</h4>
|
|
||||||
<pre>
|
|
||||||
{
|
|
||||||
"id":8,
|
|
||||||
"name":"Here I Am To Worship",
|
|
||||||
"artists":[
|
|
||||||
"Tim Hughes",
|
|
||||||
"Chris Tomlin",
|
|
||||||
"Michael W. Smith"
|
|
||||||
]
|
|
||||||
}</pre>
|
|
||||||
<ul class=method-list>
|
|
||||||
<li><h3><code>GET /songs</code></h3>
|
|
||||||
|
|
||||||
<p>Retrieve all songs.
|
|
||||||
<p><h4>Response</h4>
|
|
||||||
A list of <a href="song-object">song objects</a>
|
|
||||||
|
|
||||||
<h4>Example</h4>
|
|
||||||
<pre>
|
|
||||||
GET http://newlifesongs.jdbernard.com/api/v1/songs</pre>
|
|
||||||
<p><pre>
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Content-Length: 5146
|
|
||||||
Content-Type: application/json
|
|
||||||
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
|
|
||||||
Server: Jetty(6.1.25)
|
|
||||||
|
|
||||||
[{"id":1,"name":"Welcome Holy Spirit","artists":["Mark Condon"]},
|
|
||||||
{"id":3,"name":"Let's Sing Praises to our God","artists":["Traditional"]},
|
|
||||||
{"id":5,"name":"Blessed Assurance","artists":["Frances J. Crosby"]},
|
|
||||||
{"id":8,"name":"Here I Am To Worship","artists":["Tim Hughes", "Chris Tomlin", "Michael W. Smith"]},
|
|
||||||
{"id":12,"name":"Healer","artists":["Kari Jobe", "Hillsong"]},
|
|
||||||
{"id":15,"name":"I Am Free","artists":["Newsboys"]}]</pre></li>
|
|
||||||
|
|
||||||
<li><h3><code>POST /songs</code></h3>
|
|
||||||
|
|
||||||
<p>Create a new song record. In order to be allowed access to
|
|
||||||
this method, the request must be made with a valid
|
|
||||||
authentication token which belongs to a user with
|
|
||||||
administrative priviliges. See <a href="#authentication">Authentication</a>
|
|
||||||
for details.
|
|
||||||
|
|
||||||
<p><h4>Request Body</h4>
|
|
||||||
Must be a <a href="#song-object">song object</a>. The
|
|
||||||
<code>name</code> field is required. Any <code>id</code> passed
|
|
||||||
in with the request will be ignored.
|
|
||||||
|
|
||||||
<p><h4>Reponse</h4>
|
|
||||||
The newly-created song record.
|
|
||||||
|
|
||||||
<p><h4>Example</h4>
|
|
||||||
|
|
||||||
</section>
|
|
||||||
<section id=services>
|
|
||||||
<h2><code>/services</code></h2>
|
|
||||||
</section>
|
|
||||||
<section id=users>
|
|
||||||
<h2><code>/users</code></h2>
|
|
||||||
</section>
|
|
||||||
<section id=authentication>
|
|
||||||
<h2>Authentication</h2>
|
|
||||||
</section>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
@ -1,32 +0,0 @@
|
|||||||
package com.jdbernard.nlsongs.db
|
|
||||||
|
|
||||||
public class GenerateQueries {
|
|
||||||
|
|
||||||
public static void main(String[] args) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Map<String, Map<String, String> > generateQueries(String ddl) {
|
|
||||||
|
|
||||||
def tables = [:]
|
|
||||||
|
|
||||||
// Find the table definitions
|
|
||||||
String tableRegex = /(?ms)(?:CREATE TABLE (?:IF NOT EXISTS )?([^\s]+) \(([^\s]+);.+?)+/
|
|
||||||
|
|
||||||
ddl.eachMatch(tableRegex) { matchGroups ->
|
|
||||||
String tableName = matchGroups[1]
|
|
||||||
|
|
||||||
// Parse the column definitions.
|
|
||||||
|
|
||||||
// Create new record insert statements.
|
|
||||||
|
|
||||||
// Create insert queries.
|
|
||||||
|
|
||||||
// Create update queries.
|
|
||||||
|
|
||||||
// Create delete queries.
|
|
||||||
|
|
||||||
// Create ID lookup queries.
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
24
uploader/build.gradle
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
apply plugin: 'groovy'
|
||||||
|
apply plugin: 'application'
|
||||||
|
|
||||||
|
mainClassName = 'com.jdbernard.nlsongs.NLSongsUploader'
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compile localGroovy()
|
||||||
|
compile 'ch.qos.logback:logback-classic:1.1.8'
|
||||||
|
compile 'ch.qos.logback:logback-core:1.1.8'
|
||||||
|
compile 'org.slf4j:slf4j-api:1.7.22'
|
||||||
|
compile 'com.impossibl.pgjdbc-ng:pgjdbc-ng:0.6'
|
||||||
|
compile 'com.zaxxer:HikariCP:2.5.1'
|
||||||
|
compile 'com.miglayout:miglayout-swing:5.0'
|
||||||
|
compile project(':service')
|
||||||
|
}
|
||||||
|
|
||||||
|
task writeVersionFile(
|
||||||
|
group: 'build',
|
||||||
|
description: 'Write the version to VERSION.txt') { doLast {
|
||||||
|
|
||||||
|
(new File("${buildDir}/classes/main/VERSION.txt")).text = version
|
||||||
|
} }
|
||||||
|
|
||||||
|
build.dependsOn writeVersionFile
|
@ -0,0 +1,37 @@
|
|||||||
|
package com.jdbernard.nlsongs
|
||||||
|
|
||||||
|
import groovy.beans.Bindable
|
||||||
|
import groovy.swing.SwingBuilder
|
||||||
|
import javax.swing.JFrame
|
||||||
|
import net.miginfocom.swing.MigLayout
|
||||||
|
|
||||||
|
public class NLSongsUploader {
|
||||||
|
|
||||||
|
public static final String VERSION =
|
||||||
|
NLSongsUploader.getResourceAsStream('/VERSION.txt').text
|
||||||
|
|
||||||
|
// GUI Elements (View)
|
||||||
|
SwingBuilder swing = new SwingBuilder()
|
||||||
|
JFrame rootFrame
|
||||||
|
|
||||||
|
public static void main(String[] args) { def inst = new NLSongsUploader() }
|
||||||
|
|
||||||
|
public NLSongsUploader() {
|
||||||
|
|
||||||
|
initGui()
|
||||||
|
rootFrame.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initGui() {
|
||||||
|
|
||||||
|
swing.edtBuilder {
|
||||||
|
this.rootFrame = frame(title: "New Life Songs Uploader ${VERSION}",
|
||||||
|
iconImages: [imageIcon('/icon.png').image],
|
||||||
|
preferredSize: [1024, 768], pack: true,
|
||||||
|
layout: new MigLayout("ins 0, fill"),
|
||||||
|
defaultCloseOperation: JFrame.EXIT_ON_CLOSE) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
uploader/src/main/resources/icon.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
@ -1,6 +0,0 @@
|
|||||||
#
|
|
||||||
#Mon Mar 30 02:11:59 CDT 2015
|
|
||||||
major=2
|
|
||||||
version.release=true
|
|
||||||
minor=1
|
|
||||||
build=1
|
|