Re-organized into two submodules: service and uploader.

Moved all the existing service code into the `service` submodule.

Stubbed out project and GUI frame for the uploader. Idea is to have a GUI that
infers all the correct meta-data from media tag values and creates service,
songs, and performance records appropriately based on the tagged mp3/ogg files
of the performances.
This commit is contained in:
Jonathan Bernard
2017-02-11 23:53:04 -06:00
parent a6a68a5320
commit 7d7f2eed87
59 changed files with 156 additions and 86 deletions

77
service/build.gradle Normal file
View File

@ -0,0 +1,77 @@
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'
}
}
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}")
} }

View File

@ -0,0 +1 @@
nlsongs.media.baseUrl=https://s3.amazonaws.com/new-life-austin-songs/public

View File

@ -0,0 +1,9 @@
dataSourceClassName=com.impossibl.postgres.jdbc.PGDataSource
dataSource.user=jdbernard
dataSource.password=wh!73bl@k
dataSource.database=nlsongs
dataSource.host=localhost
#dataSource.cachePrepStmts=true
#dataSource.prepStmtCacheSize=250
#dataSource.prepStmtCacheSqlLimit=2048
#dataSource.useServerPrepStmts=true

View 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)

View File

@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- This web.xml file is not required when using Servlet 3.0 container,
see implementation details http://jersey.java.net/nonav/documentation/latest/jax-rs.html -->
<!-- PRODUCTION -->
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">
<context-param>
<param-name>datasource.config.file</param-name>
<param-value>/datasource.properties</param-value>
</context-param>
<context-param>
<param-name>context.config.file</param-name>
<param-value>/context.properties</param-value>
</context-param>
<listener>
<listener-class>com.jdbernard.nlsongs.servlet.NLSongsContextListener</listener-class>
</listener>
<servlet>
<servlet-name>New Life Songs REST API</servlet-name>
<servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
<init-param>
<param-name>jersey.config.server.provider.packages</param-name>
<param-value>com.jdbernard.nlsongs.rest</param-value>
</init-param>
<init-param>
<param-name>jersey.config.server.provider.classnames</param-name>
<param-value>org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet>
<servlet-name>GroovyTemplate</servlet-name>
<servlet-class>groovy.servlet.TemplateServlet</servlet-class>
</servlet>
<servlet>
<servlet-name>SongViewServlet</servlet-name>
<servlet-class>groovy.servlet.TemplateServlet</servlet-class>
<init-param>
<param-name>resource.name.regex</param-name>
<param-value>/song/?.*</param-value>
</init-param>
<init-param>
<param-name>resource.name.replacement</param-name>
<param-value>/song/index.gsp</param-value>
</init-param>
</servlet>
<servlet>
<servlet-name>ServiceViewServlet</servlet-name>
<servlet-class>groovy.servlet.TemplateServlet</servlet-class>
<init-param>
<param-name>resource.name.regex</param-name>
<param-value>/service/?.*</param-value>
</init-param>
<init-param>
<param-name>resource.name.replacement</param-name>
<param-value>/service/index.gsp</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>New Life Songs REST API</servlet-name>
<url-pattern>/api/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>SongViewServlet</servlet-name>
<url-pattern>/song/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>ServiceViewServlet</servlet-name>
<url-pattern>/service/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>GroovyTemplate</servlet-name>
<url-pattern>*.gsp</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.gsp</welcome-file>
<welcome-file>index.jsp</welcome-file>
<welcome-file>index.html</welcome-file>
</welcome-file-list>
</web-app>

View File

@ -0,0 +1,222 @@
services = [
[id: 2, date: '2009-03-01', serviceType: 'SUN_PM'],
[id: 3, date: '2009-03-08', serviceType: 'SUN_AM'],
[id: 4, date: '2009-03-08', serviceType: 'SUN_PM'],
[id: 5, date: '2009-03-22', serviceType: 'SUN_AM'],
[id: 6, date: '2009-03-22', serviceType: 'SUN_PM'],
[id: 7, date: '2009-04-08', serviceType: 'WED'],
[id: 8, date: '2009-04-12', serviceType: 'SUN_AM'],
[id: 9, date: '2009-04-12', serviceType: 'SUN_PM'],
[id: 10, date: '2009-02-25', serviceType: 'WED'],
[id: 11, date: '2009-04-22', serviceType: 'WED'],
[id: 12, date: '2009-05-03', serviceType: 'SUN_AM'],
[id: 13, date: '2009-05-10', serviceType: 'SUN_PM'],
[id: 14, date: '2009-05-06', serviceType: 'WED'],
[id: 17, date: '2009-04-26', serviceType: 'SUN_PM'],
[id: 18, date: '2010-05-05', serviceType: 'WED'],
[id: 19, date: '2010-05-09', serviceType: 'SUN_PM'],
[id: 20, date: '2010-05-16', serviceType: 'SUN_PM'],
[id: 21, date: '2010-08-15', serviceType: 'SUN_PM'],
[id: 22, date: '2011-03-13', serviceType: 'SUN_AM'],
[id: 23, date: '2011-03-13', serviceType: 'SUN_PM'],
[id: 24, date: '2011-03-16', serviceType: 'WED'],
[id: 25, date: '2011-03-20', serviceType: 'SUN_AM'],
[id: 26, date: '2015-02-08', serviceType: 'SUN_PM'],
[id: 27, date: '2015-01-07', serviceType: 'WED'],
[id: 28, date: '2015-02-11', serviceType: 'WED'],
[id: 29, date: '2014-02-02', serviceType: 'WED']]
songs = [
[id: 1, name: 'Welcome Holy Spirit', artists: ['Mark Condon']],
[id: 2, name: 'We Worship You', artists: ['']],
[id: 3, name: "Let's Sing Praises to our God", artists: ['Traditional']],
[id: 4, name: 'I am a Friend of God', artists: ['Israel Houghton']],
[id: 5, name: 'Blessed Assurance', artists: ['Frances J. Crosby']],
[id: 6, name: 'Sing Unto the Lord a New Song', artists: ['Becky Fender']],
[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: 10, name: 'Come and Let Us Sing', artists: ['Israel Houghton']],
[id: 11, name: 'I Feel the Joy', artists: ['']],
[id: 12, name: 'Healer', artists: ['Kari Jobe', ' Hillsong']],
[id: 13, name: 'This World is Not My Home', artists: ['Ricky Skaggs']],
[id: 14, name: 'Praise the Lord With Me', artists: ['Carlton Pearson', ' T.D. Jakes']],
[id: 15, name: 'I Am Free', artists: ['Newsboys']],
[id: 16, name: 'You Are Great', artists: ['Juanita Bynum']],
[id: 17, name: 'Lion of Judah', artists: ['Eddie James', ' Beverly Crawford']],
[id: 18, name: 'We Are Standing on Holy Ground', artists: ['Bill Gaither']],
[id: 19, name: 'Draw Me Nearer', artists: ['Meredith Andrews']],
[id: 20, name: 'Nothing but the Blood', artists: ['Robert Lowry']],
[id: 21, name: 'I Will Search For You', artists: ['Israel Houghton']],
[id: 22, name: 'This is How We Praise Him', artists: ['']],
[id: 23, name: 'We Have Overcome', artists: ['Israel Houghton']],
[id: 24, name: 'Breakthrough', artists: ['']],
[id: 25, name: 'He is Here', artists: ['Martha Munizzi']],
[id: 26, name: 'Lead Me Lord', artists: ['Brooklyn Tabernacle Choir']],
[id: 28, name: 'Power in the Name', artists: ['Gateway College']],
[id: 29, name: 'Praise the Lord', artists: ['']],
[id: 30, name: 'Ready Now', artists: ['Desperation Band']],
[id: 31, name: 'Come Into This House', artists: ['Carlton Pearson']],
[id: 32, name: "You're the One", artists: ['']],
[id: 33, name: 'How Great is Our God', artists: ['Chris Tomlin']],
[id: 34, name: 'We Will Worship the Lamb of Glory', artists: ['Dennis Jernigan']],
[id: 35, name: 'Let It Rise', artists: ['Big Daddy Weave']],
[id: 36, name: 'God Is My Refuge And Strength', artists: ['']],
[id: 37, name: 'Shout to the Lord', artists: ['Darlene Zschech', ' Chris Tomlin', ' Hillsong']],
[id: 38, name: 'In the Presence of Jehovah', artists: ['Damaris Carbaugh']],
[id: 39, name: 'In the Sanctuary', artists: ['']],
[id: 40, name: 'Mighty To Save', artists: ['Hillsong', ' Michael W. Smith']],
[id: 41, name: 'Rejoice', artists: ['']],
[id: 42, name: 'He Lives', artists: ['']],
[id: 43, name: 'Breathe', artists: ['']],
[id: 44, name: 'Healing Rain', artists: ['Michael W. Smith']],
[id: 51, name: 'Lord I Praise Your Name', artists: ['']],
[id: 46, name: "It's a New Season", artists: ['']],
[id: 47, name: 'Let Us Have a Little Talk With Jesus', artists: ['Jimmy Dean']],
[id: 48, name: 'Lord You Are Good', artists: ['']],
[id: 49, name: "Enemy's Camp", artists: ['']],
[id: 50, name: 'Whose Report Will You Believe?', artists: ['']],
[id: 52, name: 'Lord We Give You Glory', artists: ['']],
[id: 53, name: 'Revelation Song', artists: ['Jennie Lee Riddle', ' Phillips, Craig and Dean', ' Kari Jobe']],
[id: 54, name: 'Moving Forward', artists: ['']],
[id: 55, name: 'Let It Rain', artists: ['']],
[id: 56, name: 'Say So', artists: ['']],
[id: 57, name: 'We Praise Your Name', artists: ['']],
[id: 58, name: 'You Never Let Go', artists: ['Matt Redman']],
[id: 59, name: "I'd Rather Have Jesus", artists: ['']],
[id: 60, name: 'Everybody Will Be Happy Over There', artists: ['E.M. Bartlett']],
[id: 61, name: 'Awesome God', artists: ['']],
[id: 62, name: 'Whisper His Name', artists: ['Deluge']],
[id: 63, name: 'How He Loves Us', artists: ['John Mark McMillan', ' David Crowder Band']],
[id: 64, name: 'Lord I Lift Up My Hands', artists: ['Trent Corey']],
[id: 65, name: 'Give Him the Glory', artists: ['']],
[id: 66, name: 'Blessed Be The Name Of The Lord', artists: ['']],
[id: 67, name: 'Heart of Worship', artists: ['Matt Redman']],
[id: 68, name: 'Freedom Is', artists: ['']],
[id: 69, name: 'I Give My All', artists: ['']],
[id: 70, name: 'Come Unto Me', artists: ['']],
[id: 71, name: 'Shout With A Voice Of Triumph', artists: ['']],
[id: 72, name: 'Oh, How I Love Jesus', artists: ['']],
[id: 73, name: "It Ain't Over", artists: ['Maurette Brown Clark']],
[id: 74, name: 'Our God', artists: ['']],
[id: 75, name: "Can't Stop Praising His Name", artists: ['']],
[id: 76, name: 'Lord I Lift Your Name On High', artists: ['Petra', ' Mercy Me']],
[id: 77, name: 'We Are Here To Worship You', artists: ['']],
[id: 78, name: 'I Feel Jesus', artists: ['']],
[id: 79, name: 'It Is You', artists: ['Newsboys']],
[id: 80, name: 'I Surrender', artists: ['Hillsong']],
[id: 81, name: 'Our Father', artists: ['Israel Houghton']],
[id: 82, name: 'Kingdom Come', artists: ['']],
[id: 83, name: 'Father Along', artists: ['']],
[id: 84, name: "We'll Understand It Better By and By", artists: ['Charles Albert Tindley; arr. by F.A. Clark']],
[id: 85, name: 'Because Of You', artists: ['Eddie James']]]
performances = [
[serviceId: 7, songId: 34, pianist: 'Rachel Wood', organist: 'Connie Bernard', bassist: 'Jonathan Bernard', drummer: 'Jared Wood', guitarist: null, leader: null],
[serviceId: 7, songId: 33, pianist: 'Rachel Wood', organist: 'Connie Bernard', bassist: 'Jonathan Bernard', drummer: 'Jared Wood', guitarist: null, leader: null],
[serviceId: 7, songId: 32, pianist: 'Rachel Wood', organist: 'Connie Bernard', bassist: 'Jonathan Bernard', drummer: 'Jared Wood', guitarist: null, leader: null],
[serviceId: 10, songId: 3, pianist: 'Rachel Wood', organist: null, bassist: 'Jonathan Bernard', drummer: 'Jared Wood', guitarist: null, leader: null],
[serviceId: 10, songId: 2, pianist: 'Rachel Wood', organist: null, bassist: 'Jonathan Bernard', drummer: 'Jared Wood', guitarist: null, leader: null],
[serviceId: 2, songId: 8, pianist: 'Nicole Brantley', organist: 'Jared Wood', bassist: 'Jonathan Bernard', drummer: 'Daniel Bernard', guitarist: null, leader: null],
[serviceId: 2, songId: 9, pianist: 'Nicole Brantley', organist: 'Jared Wood', bassist: 'Jonathan Bernard', drummer: 'Daniel Bernard', guitarist: null, leader: null],
[serviceId: 3, songId: 10, pianist: 'Jared Wood', organist: 'Connie Bernard', bassist: 'Jonathan Bernard', drummer: 'Daniel Bernard', guitarist: null, leader: null],
[serviceId: 3, songId: 11, pianist: 'Jared Wood', organist: 'Connie Bernard', bassist: 'Jonathan Bernard', drummer: 'Daniel Bernard', guitarist: null, leader: null],
[serviceId: 3, songId: 12, pianist: 'Jared Wood', organist: 'Connie Bernard', bassist: 'Jonathan Bernard', drummer: 'Daniel Bernard', guitarist: null, leader: null],
[serviceId: 3, songId: 13, pianist: 'Jared Wood', organist: 'Connie Bernard', bassist: 'Jonathan Bernard', drummer: 'Daniel Bernard', guitarist: null, leader: null],
[serviceId: 4, songId: 12, pianist: 'Jared Wood', organist: null, bassist: 'Jonathan Bernard', drummer: 'Chris Green', guitarist: null, leader: null],
[serviceId: 4, songId: 14, pianist: 'Jared Wood', organist: null, bassist: 'Jonathan Bernard', drummer: 'Chris Green', guitarist: null, leader: null],
[serviceId: 4, songId: 15, pianist: 'Jared Wood', organist: null, bassist: 'Jonathan Bernard', drummer: 'Chris Green', guitarist: null, leader: null],
[serviceId: 4, songId: 16, pianist: 'Jared Wood', organist: null, bassist: 'Jonathan Bernard', drummer: 'Chris Green', guitarist: null, leader: null],
[serviceId: 4, songId: 17, pianist: 'Jared Wood', organist: null, bassist: 'Jonathan Bernard', drummer: 'Chris Green', guitarist: null, leader: null],
[serviceId: 4, songId: 18, pianist: 'Jared Wood', organist: null, bassist: 'Jonathan Bernard', drummer: 'Chris Green', guitarist: null, leader: null],
[serviceId: 5, songId: 19, pianist: 'Jared Wood', organist: 'Connie Bernard', bassist: 'Jonathan Bernard', drummer: 'Chris Green', guitarist: null, leader: null],
[serviceId: 5, songId: 20, pianist: 'Jared Wood', organist: 'Connie Bernard', bassist: 'Jonathan Bernard', drummer: 'Chris Green', guitarist: null, leader: null],
[serviceId: 5, songId: 21, pianist: 'Jared Wood', organist: 'Connie Bernard', bassist: 'Jonathan Bernard', drummer: 'Chris Green', guitarist: null, leader: null],
[serviceId: 5, songId: 22, pianist: 'Jared Wood', organist: 'Connie Bernard', bassist: 'Jonathan Bernard', drummer: 'Chris Green', guitarist: null, leader: null],
[serviceId: 5, songId: 23, pianist: 'Jared Wood', organist: 'Connie Bernard', bassist: 'Jonathan Bernard', drummer: 'Chris Green', guitarist: null, leader: null],
[serviceId: 6, songId: 30, pianist: 'Jared Wood', organist: 'Connie Bernard', bassist: 'Edgar Zarate', drummer: 'Daniel Bernard', guitarist: null, leader: null],
[serviceId: 6, songId: 24, pianist: 'Jared Wood', organist: 'Connie Bernard', bassist: 'Edgar Zarate', drummer: 'Daniel Bernard', guitarist: null, leader: null],
[serviceId: 10, songId: 1, pianist: 'Rachel Wood', organist: null, bassist: 'Jonathan Bernard', drummer: 'Jared Wood', guitarist: null, leader: null],
[serviceId: 7, songId: 31, pianist: 'Rachel Wood', organist: 'Connie Bernard', bassist: 'Jonathan Bernard', drummer: 'Jared Wood', guitarist: null, leader: null],
[serviceId: 6, songId: 25, pianist: 'Jared Wood', organist: 'Connie Bernard', bassist: 'Edgar Zarate', drummer: 'Daniel Bernard', guitarist: null, leader: null],
[serviceId: 2, songId: 7, pianist: 'Nicole Brantley', organist: 'Jared Wood', bassist: 'Jonathan Bernard', drummer: 'Daniel Bernard', guitarist: null, leader: null],
[serviceId: 2, songId: 4, pianist: 'Nicole Brantley', organist: 'Jared Wood', bassist: 'Jonathan Bernard', drummer: 'Daniel Bernard', guitarist: null, leader: null],
[serviceId: 6, songId: 26, pianist: 'Jared Wood', organist: 'Connie Bernard', bassist: 'Edgar Zarate', drummer: 'Daniel Bernard', guitarist: null, leader: null],
[serviceId: 6, songId: 81, pianist: 'Jared Wood', organist: 'Connie Bernard', bassist: 'Edgar Zarate', drummer: 'Daniel Bernard', guitarist: null, leader: null],
[serviceId: 6, songId: 28, pianist: 'Jared Wood', organist: 'Connie Bernard', bassist: 'Edgar Zarate', drummer: 'Daniel Bernard', guitarist: null, leader: null],
[serviceId: 6, songId: 29, pianist: 'Jared Wood', organist: 'Connie Bernard', bassist: 'Edgar Zarate', drummer: 'Daniel Bernard', guitarist: null, leader: null],
[serviceId: 11, songId: 36, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 11, songId: 35, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 11, songId: 37, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 11, songId: 38, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 12, songId: 11, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 12, songId: 39, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 8, songId: 40, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 8, songId: 41, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 8, songId: 42, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 13, songId: 7, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 13, songId: 81, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 17, songId: 43, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 17, songId: 44, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 17, songId: 46, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 17, songId: 47, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 17, songId: 48, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 18, songId: 49, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 18, songId: 50, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 18, songId: 16, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 20, songId: 51, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 20, songId: 52, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 20, songId: 28, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 20, songId: 14, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 20, songId: 53, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 21, songId: 56, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 21, songId: 44, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 21, songId: 54, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 21, songId: 55, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 21, songId: 57, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 21, songId: 58, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 13, songId: 68, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 14, songId: 67, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 14, songId: 65, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 14, songId: 66, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 12, songId: 64, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 12, songId: 63, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 19, songId: 61, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 19, songId: 59, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 19, songId: 46, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 19, songId: 54, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 19, songId: 23, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 19, songId: 62, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 13, songId: 12, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 13, songId: 58, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 22, songId: 70, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 22, songId: 69, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 22, songId: 13, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 23, songId: 75, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 23, songId: 44, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 23, songId: 73, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 23, songId: 72, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 23, songId: 74, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 23, songId: 71, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 24, songId: 61, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 24, songId: 78, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 24, songId: 76, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 24, songId: 77, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 25, songId: 10, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 25, songId: 73, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 25, songId: 79, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 25, songId: 6, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 8, songId: 53, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 27, songId: 80, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 27, songId: 14, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 27, songId: 23, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 28, songId: 83, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 28, songId: 82, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 28, songId: 81, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 28, songId: 84, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 29, songId: 85, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 29, songId: 57, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null],
[serviceId: 29, songId: 62, pianist: null, organist: null, bassist: null, drummer: null, guitarist: null, leader: null]]

View File

@ -0,0 +1 @@
nlsongs.media.baseUrl=https://s3.amazonaws.com/new-life-austin-songs/public

View File

@ -0,0 +1,4 @@
dataSourceClassName=org.h2.jdbcx.JdbcDataSource
dataSource.url=jdbc:h2:mem:
dataSource.user=test
dataSource.password=test

View File

@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- This web.xml file is not required when using Servlet 3.0 container,
see implementation details http://jersey.java.net/nonav/documentation/latest/jax-rs.html -->
<!-- PRODUCTION -->
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">
<context-param>
<param-name>datasource.config.file</param-name>
<param-value>/datasource.properties</param-value>
</context-param>
<context-param>
<param-name>context.config.file</param-name>
<param-value>/context.properties</param-value>
</context-param>
<listener>
<listener-class>com.jdbernard.nlsongs.servlet.NLSongsContextListener</listener-class>
</listener>
<servlet>
<servlet-name>New Life Songs REST API</servlet-name>
<servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
<init-param>
<param-name>jersey.config.server.provider.packages</param-name>
<param-value>com.jdbernard.nlsongs.rest</param-value>
</init-param>
<init-param>
<param-name>jersey.config.server.provider.classnames</param-name>
<param-value>org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet>
<servlet-name>GroovyTemplate</servlet-name>
<servlet-class>groovy.servlet.TemplateServlet</servlet-class>
</servlet>
<servlet>
<servlet-name>SongViewServlet</servlet-name>
<servlet-class>groovy.servlet.TemplateServlet</servlet-class>
<init-param>
<param-name>resource.name.regex</param-name>
<param-value>/song/?.*</param-value>
</init-param>
<init-param>
<param-name>resource.name.replacement</param-name>
<param-value>/song/index.gsp</param-value>
</init-param>
</servlet>
<servlet>
<servlet-name>ServiceViewServlet</servlet-name>
<servlet-class>groovy.servlet.TemplateServlet</servlet-class>
<init-param>
<param-name>resource.name.regex</param-name>
<param-value>/service/?.*</param-value>
</init-param>
<init-param>
<param-name>resource.name.replacement</param-name>
<param-value>/service/index.gsp</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>New Life Songs REST API</servlet-name>
<url-pattern>/api/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>SongViewServlet</servlet-name>
<url-pattern>/song/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>ServiceViewServlet</servlet-name>
<url-pattern>/service/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>GroovyTemplate</servlet-name>
<url-pattern>*.gsp</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.gsp</welcome-file>
<welcome-file>index.jsp</welcome-file>
<welcome-file>index.html</welcome-file>
</welcome-file-list>
</web-app>

View 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', '', '');

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@ -0,0 +1,146 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="64"
height="64"
id="svg2"
version="1.1"
inkscape:version="0.48.4 r9939"
sodipodi:docname="favicon.svg"
inkscape:export-filename="/home/jdbernard/projects/new-life-songs/src/main/webapp/images/favicon.png"
inkscape:export-xdpi="180"
inkscape:export-ydpi="180">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="5.6"
inkscape:cx="36.631424"
inkscape:cy="36.08398"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="1920"
inkscape:window-height="1028"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-196,-232.71932)">
<g
id="g3972"
transform="matrix(1.1988541,0,0,1.1988541,-34.689684,-59.360988)">
<path
sodipodi:nodetypes="ccccc"
inkscape:connector-curvature="0"
id="path3066"
d="m 197.85715,270.75504 c 18.44039,-9.68553 31.88947,-7.2133 43.21428,0.17857 l 0.17857,23.39286 c -15.06612,-7.66829 -29.63623,-7.15253 -43.75,0.89285 z"
style="fill:none;stroke:#000000;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
sodipodi:nodetypes="ccc"
inkscape:connector-curvature="0"
id="path3836"
d="m 218.57143,288.34432 c 5.66926,-15.13182 -22.94395,-29.42428 4.28571,-44.10714 -18.57938,17.81207 6.32776,26.01559 -4.28571,44.10714 z"
style="fill:#f47321;fill-opacity:1;stroke:#f47321;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
sodipodi:nodetypes="ccc"
inkscape:connector-curvature="0"
id="path3838"
d="m 220.89286,250.84432 c -7.46377,9.88399 15.90915,18.84168 0.89285,36.96429 5.71398,-18.35279 -10.64059,-29.80509 -0.89285,-36.96429 z"
style="fill:#f8981d;fill-opacity:1;stroke:#f8981d;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<g
style="stroke:#666666"
transform="translate(30.241442,19.824244)"
id="g3957">
<g
style="stroke:#666666"
id="g3954">
<path
sodipodi:nodetypes="cccc"
inkscape:connector-curvature="0"
id="path3928"
d="m 181.13839,263.45593 -0.40178,-9.53125 -6.07143,-0.26785 0.37948,9.66517"
style="fill:none;stroke:#666666;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
</g>
<path
style="fill:none;stroke:#666666;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 174.64286,256.73718 6.11607,0.29018"
id="path3930"
inkscape:connector-curvature="0" />
<path
sodipodi:type="arc"
style="fill:none;stroke:#666666;stroke-width:3;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="path3932"
sodipodi:cx="172.16518"
sodipodi:cy="261.9046"
sodipodi:rx="1.1607143"
sodipodi:ry="0.390625"
d="m 173.32589,261.9046 c 0,0.21574 -0.51967,0.39063 -1.16071,0.39063 -0.64105,0 -1.16072,-0.17489 -1.16072,-0.39063 0,-0.21573 0.51967,-0.39062 1.16072,-0.39062 0.64104,0 1.16071,0.17489 1.16071,0.39062 z"
transform="matrix(0.80146427,0,0,0.86720805,35.41978,35.950686)" />
<path
sodipodi:type="arc"
style="fill:none;stroke:#666666;stroke-width:3;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="path3932-8"
sodipodi:cx="172.16518"
sodipodi:cy="261.9046"
sodipodi:rx="1.1607143"
sodipodi:ry="0.390625"
d="m 173.32589,261.9046 c 0,0.21574 -0.51967,0.39063 -1.16071,0.39063 -0.64105,0 -1.16072,-0.17489 -1.16072,-0.39063 0,-0.21573 0.51967,-0.39062 1.16072,-0.39062 0.64104,0 1.16071,0.17489 1.16071,0.39062 z"
transform="matrix(0.80146427,0,0,0.86720805,41.50237,36.374793)" />
</g>
<g
style="fill:#666666;fill-opacity:1;stroke:#666666;stroke-opacity:1"
transform="translate(45.892857,26.25)"
id="g3968">
<path
style="fill:#666666;fill-opacity:1;stroke:#666666;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 187.76786,245.59879 0.13393,10.87054"
id="path3964"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
<path
sodipodi:type="arc"
style="fill:#666666;fill-opacity:1;stroke:#666666;stroke-width:3;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="path3966"
sodipodi:cx="184.96652"
sodipodi:cy="255.05191"
sodipodi:rx="0.8370536"
sodipodi:ry="0.41294643"
d="m 185.80358,255.05191 c 0,0.22806 -0.37477,0.41295 -0.83706,0.41295 -0.46229,0 -0.83705,-0.18489 -0.83705,-0.41295 0,-0.22806 0.37476,-0.41295 0.83705,-0.41295 0.46229,0 0.83706,0.18489 0.83706,0.41295 z"
transform="matrix(1.103543,0,0,0.80447905,-18.013595,51.017545)" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="319.93701"
height="41.225643"
id="svg2"
version="1.1"
inkscape:version="0.48.4 r9939"
sodipodi:docname="new-life-songs.svg"
inkscape:export-filename="/home/jdbernard/projects/new-life-songs/src/main/webapp/images/new-life-songs.png"
inkscape:export-xdpi="112.52"
inkscape:export-ydpi="112.52">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="3.959798"
inkscape:cx="84.054843"
inkscape:cy="-31.95016"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="1920"
inkscape:window-height="1028"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-102.563,-63.265607)">
<text
xml:space="preserve"
style="font-size:40px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Aldrich;-inkscape-font-specification:Aldrich"
x="110"
y="95.933609"
id="text2985"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan2987"
x="110"
y="95.933609">New Life Songs</tspan></text>
<path
style="fill:none;stroke:#000000;stroke-width:3.84500003;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 389.73551,102.40638 c -45.27776,0 -285.24751,0.0799 -285.24751,0.0799 0.18336,-10.359756 0,-26.719287 0,-37.295643"
id="path2989"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1,331 @@
package com.jdbernard.nlsongs.db
import java.sql.Connection
import java.sql.PreparedStatement
import java.sql.ResultSet
import java.text.SimpleDateFormat
import com.zaxxer.hikari.HikariDataSource
import groovy.sql.Sql
import groovy.transform.CompileStatic
import com.jdbernard.nlsongs.model.*
//@CompileStatic
public class NLSongsDB {
private HikariDataSource dataSource
private Sql sql
public NLSongsDB(HikariDataSource dataSource) {
this.dataSource = dataSource
this.sql = new Sql(dataSource) }
public void shutdown() { dataSource.shutdown() }
/// ### Common
public def save(def model) {
if (model.id > 0) return update(model)
else return create(model) }
/// ### Services
public Service findService(int id) {
def row = sql.firstRow("SELECT * FROM services WHERE id = ?", [id])
recordToModel(row, Service) }
public List<Service> findAllServices() {
return sql.rows("SELECT * FROM services").
collect { recordToModel(it, Service) } }
public List<Service> findServicesForSongId(int songId) {
return sql.rows("SELECT svc.* " +
"FROM services svc JOIN " +
"performances prf ON " +
"svc.id = prf.service_id " +
"WHERE prf.song_id = ?", [songId]).
collect { recordToModel(it, Service) } }
public List<Service> findServicesAfter(Date d) {
def sdf = new SimpleDateFormat("YYYY-MM-dd")
return sql.rows('SELECT * FROM services WHERE date > ?',
[sdf.format(d)]). collect { recordToModel(it, Service) } }
public List<Service> findServicesBefore(Date d) {
def sdf = new SimpleDateFormat("YYYY-MM-dd")
return sql.rows('SELECT * FROM services WHERE date < ?',
[sdf.format(d)]).collect { recordToModel(it, Service) } }
public List<Service> findServicesBetween(Date b, Date e) {
def sdf = new SimpleDateFormat("YYYY-MM-dd")
return sql.rows('SELECT * FROM services WHERE date BETWEEN ? AND ?',
[sdf.format(b),sdf.format(e)]).
collect { recordToModel(it, Service) } }
public Service create(Service service) {
def sdf = new SimpleDateFormat("YYYY-MM-dd")
int newId = sql.executeInsert(
'INSERT INTO services (date, service_type) VALUES (?, ?)',
[sdf.format(service.date), service.serviceType.toString()])[0][0]
service.id = newId
return service }
public int update(Service service) {
def sdf = new SimpleDateFormat("YYYY-MM-dd")
return sql.executeUpdate(
'UPDATE services SET date = ?, service_type = ? WHERE id = ?',
[sdf.format(service.date), service.serviceType.toString(), service.id] ) }
public int delete(Service service) {
sql.execute("DELETE FROM services WHERE id = ?", [service.id])
return sql.updateCount }
/// ### Songs
public Song findSong(int id) {
def row = sql.firstRow("SELECT * FROM songs WHERE id = ?", [id])
return recordToModel(row, Song) }
public List<Song> findAllSongs() {
return sql.rows("SELECT * FROM songs").
collect { recordToModel(it, Song) } }
public List<Song> findSongsForServiceId(int serviceId) {
return sql.rows("SELECT sng.* " +
"FROM songs sng JOIN " +
"performances prf ON " +
"sng.id = prf.song_id " +
"WHERE prf.service_id = ?", [serviceId]).
collect { recordToModel(it, Song) } }
public List<Song> findSongsByName(String name) {
return sql.rows("SELECT * FROM songs WHERE name = ?", [name]).
collect { recordToModel(it, Song) } }
public List<Song> findSongsLikeName(String name) {
return sql.rows("SELECT * FROM songs WHERE name LIKE '%${name}%'".toString()).
collect { recordToModel(it, Song) } }
public List<Song> findSongsByArtist(String artist) {
return sql.rows("SELECT * FROM songs WHERE artists LIKE '%${artist}%'".toString()).
collect { recordToModel(it, Song) } }
public List<Song> findSongsByNameAndArtist(String name, String artist) {
return sql.rows("SELECT * FROM songs WHERE name = '${name}' AND artists LIKE '%${artist}%'".toString()).collect { recordToModel(it, Song) } }
public Song create(Song song) {
int newId = sql.executeInsert(
"INSERT INTO songs (name, artists) VALUES (?, ?)",
[song.name, wrapArtists(song.artists)])[0][0]
song.id = newId
return song }
public int update(Song song) {
return sql.executeUpdate(
"UPDATE songs SET name = ?, artists = ? WHERE id = ?",
[song.name, wrapArtists(song.artists), song.id] ) }
public int delete(Song song) {
sql.execute("DELETE FROM songs WHERE id = ?", [song.id])
return sql.updateCount }
/// ### Performances
public Performance findPerformance(int serviceId, int songId) {
def perf = sql.firstRow(
"SELECT * FROM performances WHERE service_id = ? AND song_id = ?",
[serviceId, songId])
return recordToModel(perf, Performance) }
public List<Performance> findAllPerformances() {
return sql.rows("SELECT * FROM performances").collect {
recordToModel(it, Performance) } }
public List<Performance> findPerformancesForServiceId(int serviceId) {
return sql.rows("SELECT * FROM performances WHERE service_id = ?",
[serviceId]).collect { recordToModel(it, Performance) } }
public List<Performance> findPerformancesForSongId(int songId) {
return sql.rows("SELECT * FROM performances WHERE song_id = ?",
[songId]).collect { recordToModel(it, Performance) } }
public Performance create(Performance perf) {
// TODO: handle constraint violation (same service and song ids)
sql.executeInsert(
"INSERT INTO performances (service_id, song_id, rank, pianist, " +
"organist, bassist, drummer, guitarist, leader) VALUES " +
"(?, ?, ?, ?, ?, ?, ?, ?)", [perf.serviceId, perf.songId,
perf.rank, perf.pianist, perf.organist, perf.bassist, perf.drummer,
perf.guitarist, perf.leader])
return perf }
public int update(Performance perf) {
// TODO: handle constraint violation (same service and song ids)
return sql.executeUpdate(
"UPDATE performances SET pianist = ?, organist = ?, " +
"bassist = ?, drummer = ?, guitarist = ?, leader = ?, " +
"rank = ? WHERE service_id = ? AND song_id = ?",
[perf.pianist, perf.organist, perf.bassist, perf.drummer,
perf.guitarist, perf.leader, perf.rank, perf.serviceId,
perf.songId]) }
public int delete(Performance perf) {
sql.execute(
"DELETE FROM performances WHERE service_id = ? AND song_id = ?",
[perf.service_id, perf.song_id] )
return sql.updateCount }
/// ### Users
public List<User> findAllUsers() {
return sql.rows("SELECT * FROM users").
collect { buildUser(it); } }
public User findUser(String username) {
def row = sql.firstRow("SELECT * FROM users WHERE username = ?",
[username])
return buildUser(row) }
public User save(User user) {
if (findUser(user.username)) {
update(user); return user }
else return create(user) }
public User create(User user) {
int newId = sql.executeInsert(
"INSERT INTO users (username, pwd, role) VALUES (?, ?, ?)",
[user.username, user.pwd, user.role])[0][0]
user.id = newId
return user }
public int update(User user) {
return sql.executeUpdate(
"UPDATE user SET username = ?, pwd = ?, role = ? WHERE id = ?",
[user.username, user.pwd, user.role, user.id]) }
public int delete(User user) {
sql.execute("DELETE FROM users WHERE username = ?")
return sql.updateCount }
private static User buildUser(def row) {
if (!row) return null
User user = new User(username: row["username"], role: row["role"])
user.@pwd = row["pwd"]
return user; }
/// ### Tokens
public Token findToken(String token) {
def row = sql.firstRow("""\
SELECT t.*, u.*
FROM
tokens t JOIN
users u ON
t.user_id = u.id
WHERE t.token = ?""", [token])
return buildToken(row) }
public Token findTokenForUser(User user) {
def row = sql.firstRow("SELECT * FROM tokens WHERE user_id = ?",
[user.id])
return buildToken(row, user) }
public Token renewToken(Token token) {
def foundToken = findToken(token.token);
// If the token has expired we will not renew it.
if (new Date() > token.expires) return null;
// Otherwise, renew and return the new values.
assert sql.executeUpdate("UPDATE tokens SET " +
"expires = current_timestamp + interval '1 day' WHEREtoken = ?",
[token.token]) == 1
def updatedToken = findToken(token.token);
token.expires = updatedToken.expires;
return token; }
public Token save(Token token) {
if (findToken(token.token)) {
update(token); return token }
else return create(token) }
public Token create(Token token) {
sql.executeInsert("INSERT INTO tokens VALUES (?, ?, ?)",
[token.token, token.user.id, token.expires])
return Token }
public int update(Token token) {
return sql.executeUpdate(
"UPDATE tokens SET expires = ? WHERE token = ?",
[token.expires, token.token]) }
public int delete(Token token) {
sql.execute("DELETE FROM tokens WHERE token = ?", [token.token])
return sql.updateCount }
/// ### Utility functions
static def recordToModel(def record, Class clazz) {
if (record == null) return null
def model = clazz.newInstance()
record.each { recordKey, v ->
def pts = recordKey.toLowerCase().split('_')
def modelKey = pts.length == 1 ? pts[0] :
pts[0] + pts[1..-1].collect { it.capitalize() }.join()
// Hacky, there should be a better way
if (modelKey == "artists") v = unwrapArtists(v);
model[modelKey] = v }
return model }
static def modelToRecord(def model) {
if (model == null) return null
def record = [:]
model.properties.each { modelKey, v ->
if (modelKey == "class") return
def recordKey = modelKey.
replaceAll(/(\p{javaUpperCase})/, /_$1/).toLowerCase()
// Hack
if (modelKey == "artists") v = wrapArtists(v)
record[recordKey] = v }
return record }
private static Token buildToken(def row, User user) {
if (!row?.token) return null
return new Token(
token: row["token"], user: user, expires: row["expires"]) }
private static Token buildToken(def row) {
if (!row?.token) return null
User user = buildUser(row)
assert user != null
return buildToken(row, user) }
public static List<String> unwrapArtists(String artists) {
return artists.split(':') as List<String> }
public static String wrapArtists(List<String> artists) {
return artists.join(':') }
/*
static Object recordToModel(GroovyRowResult row, Class clazz) {
Object model = clazz.newInstance()
row.each { recordKey, v ->
String[] pts = ((String) recordKey).split('_')
String modelKey = pts[0] +
pts[1..-1].collect { it.capitalize() }.join()
model[modelKey] = v }
}
*/
}

View File

@ -0,0 +1,33 @@
package com.jdbernard.nlsongs.model
public class Performance implements Serializable {
int serviceId
int songId
int rank
String pianist
String organist
String bassist
String drummer
String guitarist
String leader
@Override public boolean equals(Object thatObj) {
if (thatObj == null) return false
if (!(thatObj instanceof Performance)) return false
Performance that = (Performance) thatObj
return (this.serviceId == that.serviceId &&
this.songId == that.songId &&
this.rank == that.rank &&
this.pianist == that.pianist &&
this.organist == that.organist &&
this.bassist == that.bassist &&
this.drummer == that.drummer &&
this.guitarist == that.guitarist &&
this.leader == that.leader) }
@Override String toString() {
return "($serviceId, $songId)-$rank: $leader - $pianist" }
}

View File

@ -0,0 +1,3 @@
package com.jdbernard.nlsongs.model;
public enum Role { admin, user }

View File

@ -0,0 +1,33 @@
package com.jdbernard.nlsongs.model
import org.joda.time.LocalDate
public class Service implements Serializable {
int id
private LocalDate date
ServiceType serviceType
String description
public boolean equals(Object thatObj) {
if (thatObj == null) return false
if (!(thatObj instanceof Service)) return false
Service that = (Service) thatObj
return (this.id == that.id &&
this.date == (that.localDate) &&
this.serviceType == that.serviceType) }
public void setDate(Date date) { this.date = LocalDate.fromDateFields(date) }
public void setDate(LocalDate date) { this.date = date }
public Date getDate() { return this.date.toDate() }
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 }
}

View File

@ -0,0 +1,11 @@
package com.jdbernard.nlsongs.model;
public enum ServiceType {
SUN_AM("Sunday AM"), SUN_PM("Sunday PM"), WED("Wednesday");
private final String displayName;
ServiceType(String displayName) { this.displayName = displayName; }
public String getDisplayName() { return this.displayName; }
}

View File

@ -0,0 +1,19 @@
package com.jdbernard.nlsongs.model
public class Song implements Serializable {
int id
String name
List<String> artists
@Override public boolean equals(Object thatObj) {
if (thatObj == null) return false
if (!(thatObj instanceof Song)) return false
Song that = (Song) thatObj
return (this.id == that.id &&
this.name == that.name &&
this.artists == that.artists) }
@Override public String toString() { return "$id: $name ($artists)" }
}

View File

@ -0,0 +1,33 @@
package com.jdbernard.nlsongs.model
public class Token implements Serializable {
public static final long EXPIRY_WINDOW = 1000 * 60 * 60 * 24;
String token
User user
Date expires
public Token(Map namedArgs) {
if (!namedArgs.user) throw new IllegalArgumentException(
"Cannot create Token object: missing user property.")
if (namedArgs.expire) this.expires = namedArgs.expires
else this.expires = new Date((new Date()).time + EXPIRY_WINDOW)
if (namedArgs.token) this.token = namedArgs.token
else this.token = UUID.randomUUID().toString() }
public Token(User user) { this([user: user]) }
public void refresh() { this.expires = new Date((new Date()).time + EXPIRY_WINDOW) }
@Override
public boolean equals(Object thatObj) {
if (thatObj == null) return false
if (!(thatObj instanceof Token)) return false
Token that = (Token) thatObj
return (this.token == that?.token) }
}

View File

@ -0,0 +1,17 @@
package com.jdbernard.nlsongs.model
import com.lambdaworks.crypto.SCryptUtil
public class User {
int id
String username
String pwd
Role role
public void setPwd(String pwd) {
this.pwd = SCryptUtil.scrypt(pwd, 16384, 16, 1) }
public boolean checkPwd(String givenPwd) {
return SCryptUtil.check(this.pwd, givenPwd) }
}

View File

@ -0,0 +1,6 @@
package com.jdbernard.nlsongs.model
public class UserCredentials {
String username
String password
}

View File

@ -0,0 +1,12 @@
package com.jdbernard.nlsongs.rest;
import javax.ws.rs.NameBinding;
import java.lang.annotation.ElementType;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
@NameBinding
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(value = RetentionPolicy.RUNTIME)
public @interface AllowCors {}

View File

@ -0,0 +1,30 @@
package com.jdbernard.nlsongs.rest;
import javax.annotation.Priority;
import javax.ws.rs.Priorities;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.ext.Provider;
@Provider @AllowCors @Priority(Priorities.HEADER_DECORATOR)
public class CorsResponseFilter implements ContainerResponseFilter {
@Override
public void filter(ContainerRequestContext reqCtx,
ContainerResponseContext respCtx) {
MultivaluedMap<String, Object> headers = respCtx.getHeaders();
headers.add("Access-Control-Allow-Origin",
reqCtx.getHeaderString("Origin"));
headers.add("Access-Control-Allow-Methods",
"GET, POST, PUT, DELETE, OPTIONS");
headers.add("Access-Control-Allow-Headers",
reqCtx.getHeaderString("Access-Control-Request-Headers"));
}
}

View File

@ -0,0 +1,12 @@
package com.jdbernard.nlsongs.rest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
@Path("v1/ping") @AllowCors
public class PingResource {
@GET
@Produces("text/plain")
public String ping() { return "pong"; } }

View File

@ -0,0 +1,66 @@
package com.jdbernard.nlsongs.rest;
import java.util.Date;
import java.util.List;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Consumes;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import com.jdbernard.nlsongs.servlet.NLSongsContext;
import com.jdbernard.nlsongs.model.Service;
@Path("v1/services") @AllowCors
@Produces({MediaType.APPLICATION_JSON})
@Consumes({MediaType.APPLICATION_JSON})
public class ServicesResource {
@GET
public List<Service> getServices() {
return NLSongsContext.songsDB.findAllServices(); }
@POST
public Service postService(Service service) {
return NLSongsContext.songsDB.create(service); }
@GET @Path("/{serviceId}")
public Service getService(@PathParam("serviceId") int serviceId) {
return NLSongsContext.songsDB.findService(serviceId); }
@PUT @Path("/{serviceId}")
public Service putService(@PathParam("serviceId") int serviceId,
Service service) {
service.setId(serviceId);
NLSongsContext.songsDB.update(service);
return service; }
@DELETE @Path("/{serviceId}")
public Service deleteService(@PathParam("serviceId") int serviceId) {
Service service = NLSongsContext.songsDB.findService(serviceId);
if (service != null) { NLSongsContext.songsDB.delete(service); }
return service; }
@GET @Path("/withSong/{songId}")
public List<Service> getServicesForSong(@PathParam("songId") int songId) {
return NLSongsContext.songsDB.findServicesForSongId(songId); }
@GET @Path("/byDate/after/{date}")
public List<Service> getServicesAfter(@PathParam("date") Date date) {
return NLSongsContext.songsDB.findServicesAfter(date); }
@GET @Path("/byDate/before/{date}")
public List<Service> getServicesBefore(@PathParam("date") Date date) {
return NLSongsContext.songsDB.findServicesBefore(date); }
@GET @Path("/byDate/between/{b}/{e}")
public List<Service> getServicesBetween
(@PathParam("b") Date b, @PathParam("e") Date e) {
return NLSongsContext.songsDB.findServicesBetween(b, e); }
}

View File

@ -0,0 +1,58 @@
package com.jdbernard.nlsongs.rest;
import java.util.List;
import javax.annotation.security.RolesAllowed;
import javax.annotation.security.PermitAll;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Consumes;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import com.jdbernard.nlsongs.servlet.NLSongsContext;
import com.jdbernard.nlsongs.model.Song;
@Path("v1/songs") @AllowCors
@Produces({MediaType.APPLICATION_JSON})
@Consumes({MediaType.APPLICATION_JSON})
public class SongsResource {
@GET
public List<Song> getSongs() {
return NLSongsContext.songsDB.findAllSongs(); }
@POST @RolesAllowed("admin")
public Song postSong(Song song) {
return NLSongsContext.songsDB.create(song); }
@GET @Path("/{songId}")
public Song getSong(@PathParam("songId") int songId) {
return NLSongsContext.songsDB.findSong(songId); }
@PUT @Path("/{songId}") @RolesAllowed("admin")
public Song putSong(@PathParam("songId") int songId, Song song) {
song.setId(songId);
NLSongsContext.songsDB.update(song);
return song; }
@DELETE @Path("/{songId}") @RolesAllowed("admin")
public Song deleteSong(@PathParam("songId") int songId) {
Song song = NLSongsContext.songsDB.findSong(songId);
if (song != null) { NLSongsContext.songsDB.delete(song); }
return song; }
@GET @Path("/forService/{serviceId}")
public List<Song> getSongsForService(@PathParam("serviceId") int serviceId) {
return NLSongsContext.songsDB.findSongsForServiceId(serviceId); }
@GET @Path("/byArtist/{artist}")
public List<Song> getSongsForArtist(@PathParam("artist") String artist) {
return NLSongsContext.songsDB.findSongsByArtist(artist); }
}

View File

@ -0,0 +1,111 @@
package com.jdbernard.nlsongs.rest;
import java.util.Date;
import java.util.List;
import javax.annotation.security.RolesAllowed;
import javax.annotation.security.PermitAll;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Consumes;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;
import com.jdbernard.nlsongs.servlet.NLSongsContext;
import com.jdbernard.nlsongs.model.User;
import com.jdbernard.nlsongs.model.UserCredentials;
import com.jdbernard.nlsongs.model.Token;
import static javax.ws.rs.core.Response.Status.*;
@Path("v1/users") @AllowCors @PermitAll
@Produces({MediaType.APPLICATION_JSON})
@Consumes({MediaType.APPLICATION_JSON})
public class UsersResource {
@Context SecurityContext secCtx;
@GET @RolesAllowed("admin")
public List<User> getUsers() {
return NLSongsContext.songsDB.findAllUsers(); }
@POST @RolesAllowed("admin")
public User postUser(User user) {
return NLSongsContext.songsDB.create(user); }
@GET @Path("/{username}")
public Response getUser(@PathParam("username") String username) {
// If they are looking up their own information, OK.
if (username == secCtx.getUserPrincipal().getName() ||
// Or if they are an admin, OK.
secCtx.isUserInRole("admin")) {
return Response.ok(
NLSongsContext.songsDB.findUser(username)).build(); }
else return Response.status(FORBIDDEN).build(); }
@PUT @Path("/{username}")
public Response putUser(@PathParam("username") String username, User user) {
// If they are looking up their own information, OK.
if (username == secCtx.getUserPrincipal().getName() ||
// Or if they are an admin, OK.
secCtx.isUserInRole("admin")) {
NLSongsContext.songsDB.update(user);
return Response.ok(user).build(); }
else return Response.status(FORBIDDEN).build(); }
@DELETE @Path("/{username}")
public Response deleteUser(@PathParam("username") String username) {
// If they are looking up their own information, OK.
if (username == secCtx.getUserPrincipal().getName() ||
// Or if they are an admin, OK.
secCtx.isUserInRole("admin")) {
User user = NLSongsContext.songsDB.findUser(username);
if (user != null) NLSongsContext.songsDB.delete(user);
return Response.ok(user).build(); }
else return Response.status(FORBIDDEN).build(); }
@POST @Path("/login")
public Response postLogin(UserCredentials cred) {
User user = NLSongsContext.songsDB.findUser(cred.getUsername());
if (!user.checkPwd(cred.getPassword())) {
return Response.status(UNAUTHORIZED).build(); }
else {
// Look for a token already belonging to this user.
Token token = NLSongsContext.songsDB.findTokenForUser(user);
// If there is no token, create a new one.
if (token == null) token = new Token(user);
// If the token has expired, delete it and create a new one.
else if (token.getExpires().compareTo(new Date()) < 0) {
NLSongsContext.songsDB.delete(token);
token = new Token(user); }
// If the token exists and is still good refresh it and keep using
// it.
else token.refresh();
// Save our updated token and return it.
NLSongsContext.songsDB.save(token);
return Response.ok(token).build(); } }
}

View File

@ -0,0 +1,43 @@
package com.jdbernard.nlsongs.rest.security
import java.security.Principal
import javax.ws.rs.container.ContainerRequestContext
import javax.ws.rs.core.SecurityContext
import com.jdbernard.nlsongs.model.Role
import com.jdbernard.nlsongs.model.Token
import com.jdbernard.nlsongs.servlet.NLSongsContext
public class NLSongsSecurityContext implements SecurityContext {
public final TokenPrincipal principal
public NLSongsSecurityContext(ContainerRequestContext ctx) {
// Extract the authentication token (if present)
String tokenVal = ctx.getHeaderString("Authorization-Token")
// Look up the token in the database.
Token token = NLSongsContext.songsDB.findToken(tokenVal)
// Create our principal based on this token.
this.principal = new TokenPrincipal(token)
}
@Override
public String getAuthenticationScheme() { return "Authorization-Token" }
@Override
public Principal getUserPrincipal() { return principal }
@Override
public boolean isSecure() { /* TODO */ return false }
@Override
public boolean isUserInRole(String role) {
println "Required Role: $role"
println "User's Role: ${principal?.token?.user?.role}"
println "Required Role == User's Role? ${principal?.token?.user?.role == ((Role)role)} "
return principal?.token?.user?.role == ((Role) role) }
}

View File

@ -0,0 +1,17 @@
package com.jdbernard.nlsongs.rest.security;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.PreMatching;
import javax.ws.rs.ext.Provider;
import com.jdbernard.nlsongs.servlet.NLSongsContext;
@Provider
@PreMatching
public class SecurityRequestFilter implements ContainerRequestFilter {
@Override
public void filter(ContainerRequestContext reqCtx) {
reqCtx.setSecurityContext(new NLSongsSecurityContext(reqCtx)); }
}

View File

@ -0,0 +1,35 @@
package com.jdbernard.nlsongs.rest.security;
import java.security.Principal;
import com.jdbernard.nlsongs.model.Token;
public class TokenPrincipal implements Principal {
public final Token token;
public TokenPrincipal(Token token) { this.token = token; }
@Override
public boolean equals(Object thatObj) {
if (thatObj == null) return false;
if (!(thatObj instanceof TokenPrincipal)) return false;
TokenPrincipal that = (TokenPrincipal) thatObj;
if (this.token == null) { return that.token == null; }
else { return this.token.equals(that.token); } }
@Override
public String getName() {
if (this.token == null) return null;
else return this.token.getUser().getUsername(); }
@Override
public int hashCode() {
if (this.token == null) return 0;
return this.token.getUser().getUsername().hashCode(); }
@Override
public String toString() { return getName(); }
}

View File

@ -0,0 +1,17 @@
package com.jdbernard.nlsongs.servlet
import com.jdbernard.nlsongs.db.NLSongsDB
import com.jdbernard.nlsongs.model.Service
import com.jdbernard.nlsongs.model.Song
public class NLSongsContext {
public static NLSongsDB songsDB
public static String mediaBaseUrl
public static String makeUrl(Service service, Song song) {
return mediaBaseUrl + '/' + service.localDate.toString('yyyy-MM-dd') + '_' +
service.serviceType.name().toLowerCase() + '_' +
song.name.replaceAll(/[\s'"\\\/\?!]/, '') + '.mp3' }
}

View File

@ -0,0 +1,64 @@
package com.jdbernard.nlsongs.servlet
import javax.servlet.ServletContext
import javax.servlet.ServletContextEvent
import javax.servlet.ServletContextListener
import com.jdbernard.nlsongs.db.NLSongsDB
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import org.slf4j.Logger
import org.slf4j.LoggerFactory
public final class NLSongsContextListener implements ServletContextListener {
private static final log = LoggerFactory.getLogger(NLSongsContextListener)
public void contextInitialized(ServletContextEvent event) {
def context = event.servletContext
Properties props = new Properties()
// Load configuration details from the context configuration.
NLSongsContextListener.getResourceAsStream(
context.getInitParameter('context.config.file'))
.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
HikariConfig hcfg = new HikariConfig(dataSourceProps)
HikariDataSource hds = new HikariDataSource(hcfg)
// Create the NLSonsDB instance.
NLSongsDB songsDB = new NLSongsDB(hds)
context.setAttribute('songsDB', songsDB)
NLSongsContext.songsDB = songsDB
NLSongsContext.mediaBaseUrl = props["nlsongs.media.baseUrl"] }
public void contextDestroyed(ServletContextEvent event) {
def context = event.servletContext
// Shutdown the Songs DB instance (it will shut down the data source).
NLSongsDB songsDB = context.getAttribute('songsDB')
if (songsDB) songsDB.shutdown()
context.removeAttribute('songsDB') }
}

View File

@ -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;

View File

@ -0,0 +1,53 @@
-- # New Life Songs DB
-- @author Jonathan Bernard <jdb@jdb-labs.com>
--
-- PostgreSQL database creation sript.
-- Services table
CREATE TABLE services (
id SERIAL,
date DATE NOT NULL,
service_type VARCHAR(16) DEFAULT NULL,
description VARCHAR(255) DEFAULT NULL,
CONSTRAINT uc_serviceTypeAndDate UNIQUE (date, service_type),
PRIMARY KEY (id));
-- Songs table
CREATE TABLE songs (
id SERIAL,
name VARCHAR(128) NOT NULL,
artists VARCHAR(256) DEFAULT NULL,
CONSTRAINT uc_songNameAndArtist UNIQUE (name, artists),
PRIMARY KEY (id));
-- performances table
CREATE TABLE performances (
service_id INTEGER NOT NULL,
song_id INTEGER NOT NULL,
pianist VARCHAR(64) DEFAULT NULL,
organist VARCHAR(64) DEFAULT NULL,
bassist VARCHAR(64) DEFAULT NULL,
drummer VARCHAR(64) DEFAULT NULL,
guitarist VARCHAR(64) DEFAULT NULL,
leader VARCHAR(64) DEFAULT NULL,
PRIMARY KEY (service_id, song_id),
FOREIGN KEY (service_id) REFERENCES services (id) ON DELETE CASCADE,
FOREIGN KEY (song_id) REFERENCES songs (id) ON DELETE CASCADE);
-- Users table
CREATE TABLE users (
id SERIAL,
username VARCHAR(64) UNIQUE NOT NULL,
pwd VARCHAR(80),
role VARCHAR(16) NOT NULL,
PRIMARY KEY (id));
-- Tokens table
CREATE TABLE tokens (
token VARCHAR(64),
user_id INTEGER NOT NULL,
expires TIMESTAMP NOT NULL,
PRIMARY KEY (token),
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE);

View File

@ -0,0 +1,5 @@
-- # New Life Songs DB
-- @author Jonathan Bernard <jdb@jdb-labs.com>
--
-- Remove performances.rank
ALTER TABLE performances DROP COLUMN rank;

View File

@ -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;

View File

@ -0,0 +1,33 @@
$xSmallScreen: 320px;
$smallScreen: 640px;
$wideScreen: 1200px;
$ultraWideScreen: 1600px;
/** ### forSize
* This mixin allows us to apply some rules selectively based on the screen
* size. There are three primary sizes: `small`, `medium`, and `large`, which
* are mutually exclusive. Additionally there are two additional sizes:
* `notSmall` and `ultraLarge`. `notSmall`, as the name implies matches any
* value which is not the small screen size, so it overlaps with medium,
* large, and ultraLarge. `ultraLarge` defines a wider minimum screen size
* than large, but neither large nor ultraLarge specify maximum widths,
* so ultraLarge is a strict subset of large. A screen large enough to match
* ultraLarge will also match large (compare with medium and large: matching
* medium means it will not match large, and vice versa). */
@mixin forSize($size) {
@if $size == xsmall {
@media screen and (max-width: $xSmallScreen) { @content; } }
@else if $size == small {
@media screen and (max-width: $smallScreen) { @content; } }
@else if $size == notSmall {
@media screen and (min-width: $smallScreen + 1) { @content; } }
@else if $size == medium {
@media screen and (min-width: $smallScreen + 1) and (max-width: $wideScreen - 1) { @content; } }
@else if $size == large {
@media screen and (min-width: $wideScreen) { @content; } }
@else if $size == ultraLarge {
@media screen and (min-width: $ultraWideScreen) { @content; } }
}

View File

@ -0,0 +1,149 @@
/**
* # New Life Songs DB
* @author Jonathan Bernard <jdb@jdb-labs.com>
*/
$dark: #333;
$monoFont: 'Anonymous Pro';
$headFont: 'Roboto Condensed';
$bodyFont: 'Cantarell';
@import "forSize.mixin.scss";
@import "reset.scss";
body {
color: $dark;
font-family: $bodyFont; }
header {
& > h1 > a {
color: $dark;
text-decoration: none; }
&> h1, & > h2 { font-family: $headFont; }
nav > ul > li > a {
color: $dark;
display: block;
padding: 0.1rem 0.4rem;
text-decoration: none;
&:hover, &.current {
background-color: $dark;
border-radius: 3px;
color: white; } }
}
p { margin-top: 1rem; }
section {
margin-bottom: 2rem;
& > ul {
padding: 1rem 2rem;
a { color: $dark; }
a:visited { color: $dark; } } }
section#welcome { padding: 1rem; }
table {
th { font-family: $headFont; }
td a {
color: $dark;
display: block;
text-decoration: none; } }
.api-doc {
pre, code {
background-color: #EEE;
font-family: $monoFont; }
pre { margin-left: 1rem; }
h2 {
border-bottom: solid 2px $dark;
margin-top: 2em; }
h3 { margin: 2rem 0 1rem 0; }
dl {
margin: 1rem;
& > dt {
background-color: #EEE;
font-family: $monoFont;
font-weight: bold; }
& > 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) {
body { margin: 2rem auto; }
header {
position: relative;
& > h1, & > h2 { margin-bottom: 1.5em; }
nav {
position: absolute;
top: 0;
right: 0;
ul {
list-style: none;
li {
display: block;
float: right;
padding: 0.4rem 0.6rem;
} } } }
}
@include forSize(small) {
header {
margin-bottom: 1rem;
text-align: center;
& > h2 { display: none; }
& > h2.song-name, & > h2.service-desc { display: block; }
& > nav > ul > li {
display: inline-block;
font-size: 125%;
width: 32%;
} }
section { font-size: 125%; }
.dataTables_length { display: none; }
table#songs-table {
td.artists, th.artists { display: none; } }
.not-small { display: none; }
}
@include forSize(medium) { body { width: 40rem; } }
@include forSize(large) { body { width: 60rem; } }

View File

@ -0,0 +1,14 @@
/// Global Rules
* {
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
margin: 0;
padding: 0; }
/* HTML5 elements */
article,aside,details,figcaption,figure,
footer,header,hgroup,menu,nav,section {
display:block; }

View 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='&#109;&#97;&#105;&#108;&#116;&#111;&#58;&#106;&#100;&#98;&#101;&#114;&#110;&#97;&#114;&#100;&#64;&#103;&#109;&#97;&#105;&#108;&#46;&#99;&#111;&#109;'>&#106;&#100;&#98;&#101;&#114;&#110;&#97;&#114;&#100;&#64;&#103;&#109;&#97;&#105;&#108;&#46;&#99;&#111;&#109;</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/&lt;songId&gt;</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/&lt;songId&gt;</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/&lt;songId&gt;</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/&lt;serviceId&gt;</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/&lt;artist&gt;</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/&lt;songId&gt;</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/&lt;songId&gt;</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/&lt;songId&gt;</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/&lt;serviceId&gt;</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/&lt;artist&gt;</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/&lt;serviceId&gt;</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/&lt;serviceId&gt;</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/&lt;serviceId&gt;</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/&lt;serviceId&gt;</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/&lt;date&gt;</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/&lt;date&gt;</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/&lt;date1&gt;/&lt;date2&gt;</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>

View File

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no">
<meta name="referrer" content="origin">
<link rel="shortcut icon" href="../images/favicon.ico">
<title>New Life Songs Database</title>
<link href='http://fonts.googleapis.com/css?family=Roboto+Condensed|Cantarell' rel='stylesheet' type='text/css'>
<link href='css/new-life-songs-@version@.css' rel='stylesheet' type='text/css'>
</head>
<body>
<header>
<h1>New Life Songs</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=welcome>
This is Jonathan's database of worship songs performed at New Life
Austin. Please feel free to take a look around:
<ul><li><a href="songs/">Songs</a></li>
<li><a href="services/">Services</a></li>
<li><a href="doc/api/v1/">API Documentation</a>: Yes, you can
build apps around this database. <em>Under
construction.</em></li></ul>
<p>If you run across any problems, feel free to send me an email at
<a href='&#109;&#97;&#105;&#108;&#116;&#111;&#58;&#106;&#100;&#98;&#101;&#114;&#110;&#97;&#114;&#100;&#64;&#103;&#109;&#97;&#105;&#108;&#46;&#99;&#111;&#109;'>&#106;&#100;&#98;&#101;&#114;&#110;&#97;&#114;&#100;&#64;&#103;&#109;&#97;&#105;&#108;&#46;&#99;&#111;&#109;</a>
</section>
</body>
</html>

View File

@ -0,0 +1,36 @@
(function() {
var NLS = window.NewLifeSongs = {};
// #######################################################################
/// ## Models
// #######################################################################
/// ### SongModel
NLS.SongModel = Backbone.Model.extend({ });
/// ### ServiceModel
NLS.ServiceModel = Backbone.Model.extend({ });
/// ### PerformanceModel
NLS.PerformanceModel = Backbone.Model.extend({ });
// #######################################################################
/// ## Views
// #######################################################################
/// ### SongsView
NLS.SongsView = Backbone.View.extend({
el: $("#songs-table")[0],
initialize: function(options) { this.$el.dataTables(); }
});
/// ### ServicesView
NLS.ServicesView = Backbone.View.extend({
el: $("#services-table")[0],
initialize: function(options) { this.$el.dataTables(); }
});
})();

View File

@ -0,0 +1,88 @@
<!DOCTYPE html>
<%
import com.jdbernard.nlsongs.servlet.NLSongsContext
import static com.jdbernard.nlsongs.model.ServiceType.*
songsDB = NLSongsContext.songsDB
pathInfo = ((request.pathInfo?:"").split('/') as List).findAll()
if (pathInfo.size() != 1 || !pathInfo[0].isInteger()) {
response.sendError(response.SC_NOT_FOUND); return }
service = songsDB.findService(pathInfo[0] as int)
if (!service) { response.sendError(response.SC_NOT_FOUND); return }
%>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no">
<meta name="referrer" content="origin">
<link rel="shortcut icon" href="../images/favicon.ico">
<title><%= service.localDate.toString("yyyy-MM-dd")
%> (<%= 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/underscore.js/1.8.2/underscore-min.js"></script>-->
<!--<script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.1.2/backbone-min.js"></script>-->
<script type="application/javascript" src="https://cdn.datatables.net/1.10.5/js/jquery.dataTables.js"></script>
<!--<script type="application/javascript" src="https://cdn.datatables.net/1.10.5/js/jquery.dataTables.min.js"></script>-->
<!--<script type="application/javascript" src="../js/new-life-songs-@version@.js"></script>-->
<link href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.3.0/css/font-awesome.css' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Roboto+Condensed|Cantarell' rel='stylesheet' type='text/css'>
<link href='http://cdn.datatables.net/1.10.5/css/jquery.dataTables.css' rel='stylesheet' type='text/css'>
<link href='../css/new-life-songs-@version@.css' rel='stylesheet' type='text/css'>
</head>
<body>
<header>
<h1><a href="../">New Life Songs</a></h1>
<h2 class=service-desc><%= service.localDate.toString("yyyy-MM-dd") %>: (<%=
service.description ?: service.serviceType.displayName %>)</h2>
<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 class=service>
<h2>Performances</h2>
<table id=performances-table class="row-border dataTable hover compact" cellspacing=0>
<thead><tr>
<th class=actions />
<th class="dt-left song-name">Song</th>
<th class="dt-left artists">Artists</th>
<th class="dt-left not-small">Worship Leader</th>
<th class="dt-left not-small">Piano</th>
<th class="dt-left not-small">Organ</th>
<th class="dt-left not-small">Bass</th>
<th class="dt-left not-small">Drums</th>
<th class="dt-left not-small">Guitar</th>
</tr></thead>
<tbody>
<% songsDB.findPerformancesForServiceId(service.id).
collect { [perf: it, song: songsDB.findSong(it.songId)] }.
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>
<td class=song-name><a href='../song/<%= row.song.id %>'><%=
row.song.name %></a></td>
<td class=artists><%= row.song.artists.join(", ") %></td>
<td class=not-small><%= row.perf.leader ?: "" %></td>
<td class=not-small><%= row.perf.pianist ?: "" %></td>
<td class=not-small><%= row.perf.organist ?: "" %></td>
<td class=not-small><%= row.perf.bassist ?: "" %></td>
<td class=not-small><%= row.perf.drummer ?: "" %></td>
<td class=not-small><%= row.perf.guitarist ?: "" %></td></tr><% } %>
</tbody>
</table>
</section>
<script type="application/javascript">
window.onload = function() { \$("#performances-table").
dataTable({ "paging": false }); };
</script>
</body>
</html>

View File

@ -0,0 +1,64 @@
<!DOCTYPE html>
<%
import com.jdbernard.nlsongs.servlet.NLSongsContext
import static com.jdbernard.nlsongs.model.ServiceType.*
songsDB = NLSongsContext.songsDB
%>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no">
<meta name="referrer" content="origin">
<link rel="shortcut icon" href="../images/favicon.ico">
<title>Services - 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/underscore.js/1.8.2/underscore-min.js"></script>-->
<!--<script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.1.2/backbone-min.js"></script>-->
<script type="application/javascript" src="https://cdn.datatables.net/1.10.5/js/jquery.dataTables.js"></script>
<!--<script type="application/javascript" src="https://cdn.datatables.net/1.10.5/js/jquery.dataTables.min.js"></script>-->
<!--<script type="application/javascript" src="../js/new-life-songs-@version@.js"></script>-->
<link href='http://fonts.googleapis.com/css?family=Roboto+Condensed|Cantarell' rel='stylesheet' type='text/css'>
<link href='http://cdn.datatables.net/1.10.5/css/jquery.dataTables.css' rel='stylesheet' type='text/css'>
<link href='../css/new-life-songs-@version@.css' rel='stylesheet' type='text/css'>
</head>
<body>
<header>
<h1><a href="../">New Life Songs</a></h1>
<h2>Services</h2>
<nav><ul>
<li><a href="../admin/">Admin</a></li>
<li><a href="../songs/">Songs</a></li>
<li><a href="../services/" class=current>Services</a></li>
</ul></nav>
</header>
<section class=services>
<table id=services-table class="row-border dataTable hover compact" cellspacing=0>
<thead><tr>
<th class="dt-left" class=date>Date</th>
<th class="dt-left service-type">Service Type</th>
</tr></thead>
<tbody>
<% songsDB.findAllServices().sort { it.date }.reverse().each { service -> %>
<tr><td class=date><a href="../service/<%= service.id %>"><%=
service.localDate.toString("yyyy-MM-dd") %></a></td>
<td class=service-type><%= service.description ?:
service.serviceType.displayName %></td></tr><% } %>
</tbody>
<!--<tfoot><tr>
<th class="dt-left">Date</th>
<th class="dt-left">Service Type</th>
</tr></tfoot>-->
</table>
</section>
<script type="application/javascript">
window.onload = function() { \$("#services-table").
dataTable({ "paging": false,
"order": [[0, "desc"]]}); };
</script>
</body>
</html>

View File

@ -0,0 +1,88 @@
<!DOCTYPE html>
<%
import com.jdbernard.nlsongs.servlet.NLSongsContext
import static com.jdbernard.nlsongs.model.ServiceType.*
songsDB = NLSongsContext.songsDB
pathInfo = ((request.pathInfo?:"").split('/') as List).findAll()
if (pathInfo.size() != 1 || !pathInfo[0].isInteger()) {
response.sendError(response.SC_NOT_FOUND); return }
song = songsDB.findSong(pathInfo[0] as int)
if (!song) { response.sendError(response.SC_NOT_FOUND); return }
%>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no">
<meta name="referrer" content="origin">
<link rel="shortcut icon" href="../images/favicon.ico">
<title><%= song.name %> - 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/underscore.js/1.8.2/underscore-min.js"></script>-->
<!--<script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.1.2/backbone-min.js"></script>-->
<script type="application/javascript" src="https://cdn.datatables.net/1.10.5/js/jquery.dataTables.js"></script>
<!--<script type="application/javascript" src="https://cdn.datatables.net/1.10.5/js/jquery.dataTables.min.js"></script>-->
<!--<script type="application/javascript" src="../js/new-life-songs-@version@.js"></script>-->
<link href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.3.0/css/font-awesome.css' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Roboto+Condensed|Cantarell' rel='stylesheet' type='text/css'>
<link href='http://cdn.datatables.net/1.10.5/css/jquery.dataTables.css' rel='stylesheet' type='text/css'>
<link href='../css/new-life-songs-@version@.css' rel='stylesheet' type='text/css'>
</head>
<body>
<header>
<h1><a href="../">New Life Songs</a></h1>
<h2 class=song-name><%= song.name %></h2><%
if (song.artists.findAll().size() > 0) {
%><h3>by <%= song.artists.join(", ") %></h3> <% } %>
<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 class=song>
<h2>Performances</h2>
<table id=performances-table class="row-border dataTable hover compact" cellspacing=0>
<thead><tr>
<th class=actions />
<th class="dt-left performance-date">Date</th>
<th class="dt-left service-type">Service Type</th>
<th class="dt-left not-small">Worship Leader</th>
<th class="dt-left not-small">Piano</th>
<th class="dt-left not-small">Organ</th>
<th class="dt-left not-small">Bass</th>
<th class="dt-left not-small">Drums</th>
<th class="dt-left not-small">Guitar</th>
</tr></thead>
<tbody>
<% songsDB.findPerformancesForSongId(song.id).
collect { [perf: it, svc: songsDB.findService(it.serviceId)] }.
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>
<td class=performance-date><a href='../service/<%= row.svc.id %>'><%=
row.svc.localDate.toString("yyyy-MM-dd") %></a></td>
<td class=service-type><%= row.svc.serviceType.displayName %></td>
<td class=not-small><%= row.perf.leader ?: "" %></td>
<td class=not-small><%= row.perf.pianist ?: "" %></td>
<td class=not-small><%= row.perf.organist ?: "" %></td>
<td class=not-small><%= row.perf.bassist ?: "" %></td>
<td class=not-small><%= row.perf.drummer ?: "" %></td>
<td class=not-small><%= row.perf.guitarist ?: "" %></td></tr><% } %>
</tbody>
</table>
</section>
<script type="application/javascript">
window.onload = function() { \$("#performances-table").
dataTable({ "paging": false }); };
</script>
</body>
</html>

View File

@ -0,0 +1,61 @@
<!DOCTYPE html>
<%
import com.jdbernard.nlsongs.servlet.NLSongsContext
songsDB = NLSongsContext.songsDB
%>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no">
<meta name="referrer" content="origin">
<link rel="shortcut icon" href="../images/favicon.ico">
<title>Songs - 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/underscore.js/1.8.2/underscore-min.js"></script>-->
<!--<script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.1.2/backbone-min.js"></script>-->
<script type="application/javascript" src="https://cdn.datatables.net/1.10.5/js/jquery.dataTables.js"></script>
<!--<script type="application/javascript" src="https://cdn.datatables.net/1.10.5/js/jquery.dataTables.min.js"></script>-->
<!--<script type="application/javascript" src="../js/new-life-songs-@version@.js"></script>-->
<link href='http://fonts.googleapis.com/css?family=Roboto+Condensed|Cantarell' rel='stylesheet' type='text/css'>
<link href='http://cdn.datatables.net/1.10.5/css/jquery.dataTables.css' rel='stylesheet' type='text/css'>
<link href='../css/new-life-songs-@version@.css' rel='stylesheet' type='text/css'>
</head>
<body>
<header>
<h1><a href="../">New Life Songs</a></h1>
<h2>Songs</h2>
<nav><ul>
<li><a href="../admin/">Admin</a></li>
<li><a href="../songs/" class=current>Songs</a></li>
<li><a href="../services/">Services</a></li>
</ul></nav>
</header>
<section class=songs>
<table id=songs-table class="row-border dataTable hover compact" cellspacing=0>
<thead><tr>
<th class="dt-left" class=song-name>Name</th>
<th class="dt-left artists">Artists</th>
</tr></thead>
<tbody>
<% songsDB.findAllSongs().sort { it.name }.each { song -> %>
<tr><td class=song-name><a href='../song/<%= song.id %>'><%= song.name %></a></td>
<td class=artists><%= song.artists.join(", ") %></td></tr> <% } %>
</tbody>
<!--<tfoot><tr>
<th class="dt-left">Name</th>
<th class="dt-left">Artists</th>
</tr></tfoot>-->
</table>
</section>
<script type="application/javascript">
window.onload = function() { \$("#songs-table").
dataTable({ "paging": false }); };
</script>
</body>
</html>

View File

@ -0,0 +1,50 @@
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import com.jdbernard.nlsongs.db.NLSongsDB
import com.jdbernard.nlsongs.model.*
import java.text.SimpleDateFormat
sdf = new SimpleDateFormat('yyyy-MM-dd')
hcfg = new HikariConfig("/home/jdbernard/projects/new-life-songs/src/main/webapp/WEB-INF/classes/datasource.properties")
makeService = { svcRow ->
Service svc = new Service()
svc.date = sdf.parse(svcRow.date)
svc.serviceType = svcRow.serviceType
return svc }
pushService = { svcRow ->
Service svc = makeService(svcRow)
svc = songsDB.create(svc)
svcRow.newId = svc.id
return svc.id }
makeSong = { songRow ->
Song song = new Song()
song.name = songRow.name
song.artists = songRow.artists
return song }
pushSong = { songRow ->
Song song = makeSong(songRow)
song = songsDB.create(song)
songRow.newId = song.id
return song.id }
makePerformance = { perfRow ->
Performance perf = new Performance()
perfRow.each { k, v -> perf[k] = v }
// Replace with new DB ids
perf.serviceId = services.find { it.id == perf.serviceId }.newId
perf.songId = songs.find { it.id == perf.songId }.newId
return perf }
pushPerformance = { perfRow ->
Performance perf = makePerformance(perfRow)
return songsDB.create(perf) }
makeSongsDB = {
hds = new HikariDataSource(hcfg)
songsDB = new NLSongsDB(hds)
return songsDB }

View File

@ -0,0 +1,11 @@
package com.jdbernard.nlsongs.rest
import org.junit.Test
import org.junit.AfterClass
import org.junit.BeforeClass
import static org.junit.Assert.*
public class SongsResourceTest {
}

View File

@ -0,0 +1,263 @@
package com.jdbernard.nlsongs.service
import com.jdbernard.nlsongs.db.NLSongsDB
import com.jdbernard.nlsongs.model.*
import com.jdbernard.nlsongs.servlet.NLSongsContext
import com.jdblabs.dbmigrate.DbMigrate
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import groovy.sql.Sql
import java.text.SimpleDateFormat
import org.junit.After
import org.junit.AfterClass
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import static org.junit.Assert.*
import static com.jdbernard.nlsongs.model.ServiceType.*
import org.slf4j.Logger
import org.slf4j.LoggerFactory
public class NLSongsDBTest {
static NLSongsDB songsDB
static Sql sql
static DbMigrate dbmigrate
static Logger log = LoggerFactory.getLogger(NLSongsDBTest)
def dateFormat
def services
def songs
def performances
/// ### Setup
public NLSongsDBTest() {
this.dateFormat = new SimpleDateFormat("yyyy-MM-dd")
this.services = [
[1, '2015-02-01', SUN_AM],
[2, '2015-02-01', SUN_PM],
[3, '2015-02-04', WED],
[4, '2015-02-08', SUN_AM],
[5, '2015-02-08', SUN_PM],
[6, '2015-02-11', WED],
[7, '2015-02-15', SUN_AM],
[8, '2015-02-15', SUN_PM]].collect {
new Service(id: it[0],
date: dateFormat.parse(it[1]),
serviceType: it[2]) }
this.songs = [
[1, 'Breathe On Us', ['Kari Jobe']],
[2, 'How Great Is Our God', ['Chris Tomlin']],
[3, 'Glorious', ['Martha Munizzi']],
[4, 'Rez Power', ['Israel Houghton']]].collect {
new Song(id: it[0], name: it[1], artists: it[2]) }
this.performances = [
[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', 2],
[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', 1],
[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', 3],
[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', 0],
[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', 1],
[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', 1],
[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],
organist: it[3], bassist: it[4], drummer: it[5],
guitarist: it[6], leader: it[7], rank: it[8]) }
}
@BeforeClass
public static void setupDB() {
// Create Hikari datasource
HikariConfig hcfg = new HikariConfig(
"resources/test/WEB-INF/classes/datasource.properties")
HikariDataSource dataSource = new HikariDataSource(hcfg)
// Create NLSongsDB
NLSongsDBTest.songsDB = new NLSongsDB(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
NLSongsContext.songsDB = songsDB }
@AfterClass
public static void tearDownDB() {
if (NLSongsContext.songsDB)
NLSongsContext.songsDB.shutdown() }
@Before
public void initData() {
// Create the DB schema
dbmigrate.up()
// Populate the DB with test data.
File testDataSql = new File("resources/test/testdb.init.sql")
sql.execute(testDataSql.text) }
@After
public void destroyData() {
dbmigrate.down(Integer.MAX_VALUE) }
/// ### Services
@Test public void shouldCreateService() {
def service = new Service(
date: new Date(), serviceType: ServiceType.SUN_AM)
def newService = songsDB.create(service)
assertTrue(service == songsDB.findService(newService.id)) }
@Test public void shouldFindServiceById() {
assertTrue(songsDB.findService(1) == services[0]) }
@Test public void shouldListAllServices() {
assertCollectionsEqual(services, songsDB.findAllServices()) }
@Test public void shouldFindServicesForSongId() {
def foundPerfs = performances.findAll { it.songId == 1}
def foundServices = services.findAll { svc ->
foundPerfs.any { p -> p.serviceId == svc.id } }
assertCollectionsEqual(
songsDB.findServicesForSongId(1),
foundServices) }
@Test public void shouldFindServicesAfter() {
Date d = dateFormat.parse('2015-02-08')
def foundServices = songsDB.findServicesAfter(d)
assertCollectionsEqual(foundServices, services.findAll { it.date > d })
assertEquals(foundServices.size(), 3) }
@Test public void shouldFindServicesBefore() {
Date d = dateFormat.parse('2015-02-08')
def foundServices = songsDB.findServicesBefore(d)
assertCollectionsEqual(foundServices, services.findAll { it.date < d })
assertEquals(foundServices.size(), 3) }
@Test public void shouldFindServicesBetween() {
Date b = dateFormat.parse('2015-02-05')
Date e = dateFormat.parse('2015-02-09')
def foundServices = songsDB.findServicesBetween(b, e)
assertCollectionsEqual(foundServices, services.findAll {
it.date > b && it.date < e })
assertEquals(foundServices.size(), 2) }
@Test public void shouldUpdateService() {
// Find the service
def service = songsDB.findService(1)
// Update it
service.date = dateFormat.parse('2015-01-01')
songsDB.update(service)
// Check it
assertTrue(service == songsDB.findService(1)) }
@Test public void shouldDeleteService() {
songsDB.delete(services[0])
assertCollectionsEqual(
services - services[0], songsDB.findAllServices())
assertCollectionsEqual(
performances.findAll { it.serviceId != 1 },
songsDB.findAllPerformances()) }
/// ### Songs
@Test public void shoudCreateSong() {
def song = new Song(name: "Test Song", artists: ["Bob Sam"])
def newSong = songsDB.create(song)
assertTrue(song == songsDB.findSong(newSong.id)) }
@Test public void shoudUpdateSong() {
def song = songsDB.findSong(1)
song.name += " - Test"
songsDB.update(song)
assertTrue(song == songsDB.findSong(1)) }
@Test public void shouldFindSongById() {
assertTrue(songsDB.findSong(1) == songs[0]) }
@Test public void shouldListAllSongs() {
assertCollectionsEqual(songs, songsDB.findAllSongs()) }
@Test public void shouldFindSongsForService() {
def foundPerfs = performances.findAll { it.serviceId == 1}
def foundSongs = songs.findAll { song ->
foundPerfs.any { p -> p.songId == song.id } }
assertCollectionsEqual(
foundSongs,
songsDB.findSongsForServiceId(1)) }
@Test public void shouldFindSongsByName() {
assertCollectionsEqual(
songsDB.findSongsByName('Glorious'),
songs.findAll { it.name == 'Glorious' }) }
@Test public void shouldFindSongsLikeName() {
assertCollectionsEqual(
songsDB.findSongsLikeName('G'),
songs.findAll { it.name =~ 'G' }) }
@Test public void shouldFindSongsByArtist() {
assertCollectionsEqual(
songs.findAll { s ->
s.artists.any { a -> a =~ 'Chris' } },
songsDB.findSongsByArtist('Chris')) }
@Test public void shouldFindSongsByNameAndArtist() {
assertCollectionsEqual(
songs.findAll { s ->
s.artists.any { a -> a =~ 'Chris'} &&
s.name == 'How Great Is Our God' },
songsDB.findSongsByNameAndArtist('How Great Is Our God', 'Chris')) }
@Test public void shouldDeleteSong() {
songsDB.delete(songs[0])
assertCollectionsEqual(
songs - songs[0], songsDB.findAllSongs())
assertCollectionsEqual(
performances.findAll { it.songId != 1 },
songsDB.findAllPerformances()) }
private void assertCollectionsEqual(Collection c1, Collection c2) {
log.info("C1: $c1")
log.info("C2: $c2")
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) }) }
}

View File

@ -0,0 +1,6 @@
#
#Sat Dec 17 21:52:48 CST 2016
major=2
version.release=true
minor=5
build=2