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:
77
service/build.gradle
Normal file
77
service/build.gradle
Normal 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}")
|
||||
} }
|
@ -0,0 +1 @@
|
||||
nlsongs.media.baseUrl=https://s3.amazonaws.com/new-life-austin-songs/public
|
@ -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
|
18
service/resources/main/WEB-INF/classes/logback.groovy
Normal file
18
service/resources/main/WEB-INF/classes/logback.groovy
Normal file
@ -0,0 +1,18 @@
|
||||
import ch.qos.logback.core.*;
|
||||
import ch.qos.logback.core.encoder.*;
|
||||
import ch.qos.logback.core.read.*;
|
||||
import ch.qos.logback.core.rolling.*;
|
||||
import ch.qos.logback.core.status.*;
|
||||
import ch.qos.logback.classic.net.*;
|
||||
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
|
||||
|
||||
|
||||
appender("STDOUT", ConsoleAppender) {
|
||||
encoder(PatternLayoutEncoder) {
|
||||
pattern = "%level %logger - %msg%n"
|
||||
}
|
||||
}
|
||||
|
||||
root(INFO, ["STDOUT"])
|
||||
logger('com.jdbernard', INFO)
|
||||
|
100
service/resources/main/WEB-INF/web.xml
Normal file
100
service/resources/main/WEB-INF/web.xml
Normal 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>
|
222
service/resources/migration/old-songs-db.groovy
Normal file
222
service/resources/migration/old-songs-db.groovy
Normal 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]]
|
||||
|
||||
|
@ -0,0 +1 @@
|
||||
nlsongs.media.baseUrl=https://s3.amazonaws.com/new-life-austin-songs/public
|
@ -0,0 +1,4 @@
|
||||
dataSourceClassName=org.h2.jdbcx.JdbcDataSource
|
||||
dataSource.url=jdbc:h2:mem:
|
||||
dataSource.user=test
|
||||
dataSource.password=test
|
100
service/resources/test/WEB-INF/web.xml
Normal file
100
service/resources/test/WEB-INF/web.xml
Normal 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>
|
34
service/resources/test/testdb.init.sql
Normal file
34
service/resources/test/testdb.init.sql
Normal file
@ -0,0 +1,34 @@
|
||||
INSERT INTO SERVICES (date, service_type) values
|
||||
('2015-02-01', 'SUN_AM'),
|
||||
('2015-02-01', 'SUN_PM'),
|
||||
('2015-02-04', 'WED'),
|
||||
('2015-02-08', 'SUN_AM'),
|
||||
('2015-02-08', 'SUN_PM'),
|
||||
('2015-02-11', 'WED'),
|
||||
('2015-02-15', 'SUN_AM'),
|
||||
('2015-02-15', 'SUN_PM');
|
||||
|
||||
INSERT INTO songs (name, artists) VALUES
|
||||
('Breathe On Us', 'Kari Jobe'),
|
||||
('How Great Is Our God', 'Chris Tomlin'),
|
||||
('Glorious', 'Martha Munizzi'),
|
||||
('Rez Power', 'Israel Houghton');
|
||||
|
||||
INSERT INTO performances (service_id, song_id, rank, pianist, organist, bassist, drummer, guitarist, leader) VALUES
|
||||
(1, 1, 1, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood'),
|
||||
(1, 2, 2, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood'),
|
||||
(1, 3, 3, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood'),
|
||||
(2, 2, 1, 'Trevor Delano', 'Connie Bernard', 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser', 'Rachel Wood'),
|
||||
(2, 3, 2, 'Trevor Delano', 'Connie Bernard', 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser', 'Rachel Wood'),
|
||||
(2, 4, 3, 'Trevor Delano', 'Connie Bernard', 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser', 'Rachel Wood'),
|
||||
(3, 1, 0, 'Rachel Wood', 'Krista Hatcher', 'Jonathan Bernard', 'Jared Wood', 'Tony Bagliore', 'Rachel Wood'),
|
||||
(3, 2, 0, 'Rachel Wood', 'Krista Hatcher', 'Jonathan Bernard', 'Jared Wood', 'Tony Bagliore', 'Rachel Wood'),
|
||||
(4, 3, 0, 'Trevor Delano', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser', 'Rachel Wood'),
|
||||
(5, 4, 1, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Tony Bagliore', 'Rachel Wood'),
|
||||
(6, 1, 1, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood'),
|
||||
(7, 2, 1, 'Trevor Delano', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood'),
|
||||
(8, 3, 1, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood');
|
||||
|
||||
INSERT INTO users (username, pwd, role) VALUES
|
||||
('admin', '', 'admin'),
|
||||
('test', '', '');
|
BIN
service/resources/webapp/favicon.ico
Normal file
BIN
service/resources/webapp/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
BIN
service/resources/webapp/favicon.png
Normal file
BIN
service/resources/webapp/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.1 KiB |
BIN
service/resources/webapp/images/favicon.png
Normal file
BIN
service/resources/webapp/images/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.1 KiB |
146
service/resources/webapp/images/favicon.svg
Normal file
146
service/resources/webapp/images/favicon.svg
Normal 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 |
BIN
service/resources/webapp/images/new-life-songs.png
Normal file
BIN
service/resources/webapp/images/new-life-songs.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.7 KiB |
80
service/resources/webapp/images/new-life-songs.svg
Normal file
80
service/resources/webapp/images/new-life-songs.svg
Normal 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 |
@ -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 }
|
||||
}
|
||||
*/
|
||||
}
|
@ -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" }
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
package com.jdbernard.nlsongs.model;
|
||||
|
||||
public enum Role { admin, user }
|
@ -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 }
|
||||
}
|
@ -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; }
|
||||
}
|
@ -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)" }
|
||||
}
|
@ -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) }
|
||||
}
|
@ -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) }
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
package com.jdbernard.nlsongs.model
|
||||
|
||||
public class UserCredentials {
|
||||
String username
|
||||
String password
|
||||
}
|
@ -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 {}
|
@ -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"));
|
||||
|
||||
}
|
||||
}
|
@ -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"; } }
|
@ -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); }
|
||||
}
|
@ -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); }
|
||||
|
||||
}
|
@ -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(); } }
|
||||
}
|
@ -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) }
|
||||
}
|
||||
|
@ -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)); }
|
||||
}
|
@ -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(); }
|
||||
}
|
||||
|
@ -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' }
|
||||
}
|
@ -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') }
|
||||
}
|
@ -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;
|
53
service/src/main/sql/20170209113022-create-schema-up.sql
Normal file
53
service/src/main/sql/20170209113022-create-schema-up.sql
Normal 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);
|
@ -0,0 +1,5 @@
|
||||
-- # New Life Songs DB
|
||||
-- @author Jonathan Bernard <jdb@jdb-labs.com>
|
||||
--
|
||||
-- Remove performances.rank
|
||||
ALTER TABLE performances DROP COLUMN rank;
|
@ -0,0 +1,6 @@
|
||||
-- # New Life Songs DB
|
||||
-- @author Jonathan Bernard <jdb@jdb-labs.com>
|
||||
--
|
||||
-- Add performances.rank: the rank of the performance in the service, aka. the
|
||||
-- "track number" if the service were an album.
|
||||
ALTER TABLE performances ADD COLUMN rank integer NOT NULL DEFAULT 0;
|
33
service/src/main/webapp/css/forSize.mixin.scss
Normal file
33
service/src/main/webapp/css/forSize.mixin.scss
Normal 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; } }
|
||||
}
|
||||
|
||||
|
149
service/src/main/webapp/css/new-life-songs.scss
Normal file
149
service/src/main/webapp/css/new-life-songs.scss
Normal 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; } }
|
14
service/src/main/webapp/css/reset.scss
Normal file
14
service/src/main/webapp/css/reset.scss
Normal 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; }
|
||||
|
||||
|
351
service/src/main/webapp/doc/api/v1/index.html
Normal file
351
service/src/main/webapp/doc/api/v1/index.html
Normal file
@ -0,0 +1,351 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="referrer" content="origin">
|
||||
<link rel="shortcut icon" href="../images/favicon.ico">
|
||||
|
||||
<title>API V1 - New Life Songs Database</title>
|
||||
<link
|
||||
href='http://fonts.googleapis.com/css?family=Roboto+Condensed|Cantarell|Anonymous+Pro' rel='stylesheet' type='text/css'>
|
||||
<link href='../../../css/new-life-songs-@version@.css' rel='stylesheet' type='text/css'>
|
||||
</head>
|
||||
<body class=api-doc>
|
||||
<header>
|
||||
<h1><a href="../../../">New Life Songs API V1</a></h1>
|
||||
<nav><ul>
|
||||
<li><a href="../../../admin/">Admin</a></li>
|
||||
<li><a href="../../../songs/">Songs</a></li>
|
||||
<li><a href="../../../services/">Services</a></li>
|
||||
</ul></nav>
|
||||
</header>
|
||||
<section id=api-overview>
|
||||
The New Life Songs database exposes a REST API. This allows
|
||||
programatic access to and modification of the data. Version 1 of
|
||||
the API defines several endpoints, all of which are built off of
|
||||
<code>http://newlifesongs.jdbernard.com/api/v1</code> as a base
|
||||
URL.
|
||||
|
||||
<p>Some of the service's endpoints require the client to authenticate
|
||||
itself to the server. See the <a href="#authentication">section on
|
||||
authentication</a> for details concerning authentication.
|
||||
|
||||
<p>The endpoints that the API defines are:
|
||||
<ul><li><a href="#songs"><code>/songs</code></a></li>
|
||||
<li><a href="#services"><code>/services</code></a></li>
|
||||
<li><a href="#users"><code>/users</code></a></li></ul>
|
||||
|
||||
<p>If you run across any problems or have questions, feel free to send me an email at
|
||||
<a href='mailto:jdbernard@gmail.com'>jdbernard@gmail.com</a>
|
||||
</section>
|
||||
<section id=songs>
|
||||
<h2><code>/songs</code></h2>
|
||||
<h3 id=song-object>Song object</h3>
|
||||
A song object is defined with the following fields:
|
||||
<dl><dt>id</dt>
|
||||
<dd>An identifier unique to this song record among all song
|
||||
records. <em>Type: integer</em></dd>
|
||||
|
||||
<dt>name</dt>
|
||||
<dd>The name of the song. <em>Type: string</em></dd>
|
||||
|
||||
<dt>artists</dt>
|
||||
<dd>A list of the artists known to have written or performed
|
||||
this song. <em>Type: list of strings</em></dl>
|
||||
|
||||
<h4>Example</h4>
|
||||
<pre>
|
||||
{
|
||||
"id":8,
|
||||
"name":"Here I Am To Worship",
|
||||
"artists":[
|
||||
"Tim Hughes",
|
||||
"Chris Tomlin",
|
||||
"Michael W. Smith"
|
||||
]
|
||||
}</pre>
|
||||
<h3>Method Summary</h3>
|
||||
<table class=method-summary>
|
||||
<thead>
|
||||
<tr><th class=action>HTTP Action</th>
|
||||
<th class=path>Path</th>
|
||||
<th class=desc>Description</th>
|
||||
<th class=public>Public?</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td class=action>GET</td>
|
||||
<td class=path><code>/songs</code></td>
|
||||
<td class=desc>Retrieve all songs.</td>
|
||||
<td class=public>yes</td></tr>
|
||||
<tr><td class=acion>POST</td>
|
||||
<td class=path><code>/songs</code></td>
|
||||
<td class=desc>Create a new song record.</td>
|
||||
<td class=public>no</td></tr>
|
||||
<tr><td class=acion>GET</td>
|
||||
<td class=path><code>/songs/<songId></code></td>
|
||||
<td class=desc>Retrieve a single record.</td>
|
||||
<td class=public>yes</td></tr>
|
||||
<tr><td class=acion>PUT</td>
|
||||
<td class=path><code>/songs/<songId></code></td>
|
||||
<td class=desc>Update a song record.</td>
|
||||
<td class=public>no</td></tr>
|
||||
<tr><td class=acion>DELETE</td>
|
||||
<td class=path><code>/songs/<songId></code></td>
|
||||
<td class=desc>Delete a song record.</td>
|
||||
<td class=public>no</td></tr>
|
||||
<tr><td class=acion>GET</td>
|
||||
<td class=path><code>/songs/forService/<serviceId></code></td>
|
||||
<td class=desc>Retrieve all songs performed in a given service.</td>
|
||||
<td class=public>yes</td></tr>
|
||||
<tr><td class=acion>GET</td>
|
||||
<td class=path><code>/songs/byArtist/<artist></code></td>
|
||||
<td class=desc>Retrieve all songs performed by a given artist.</td>
|
||||
<td class=public>yes</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<ul class=method-list>
|
||||
<li><h3><code>GET /songs</code></h3>
|
||||
|
||||
<p>Retrieve all songs.
|
||||
<p><h4>Response</h4>
|
||||
A list of <a href="song-object">song objects</a>
|
||||
|
||||
<p><h4>Example</h4>
|
||||
<pre>
|
||||
GET http://newlifesongs.jdbernard.com/api/v1/songs</pre>
|
||||
<p><pre>
|
||||
HTTP/1.1 200 OK
|
||||
Content-Length: 433
|
||||
Content-Type: application/json
|
||||
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
|
||||
|
||||
[{"id":1,"name":"Welcome Holy Spirit","artists":["Mark Condon"]},
|
||||
{"id":3,"name":"Let's Sing Praises to our God","artists":["Traditional"]},
|
||||
{"id":5,"name":"Blessed Assurance","artists":["Frances J. Crosby"]},
|
||||
{"id":8,"name":"Here I Am To Worship","artists":["Tim Hughes", "Chris Tomlin", "Michael W. Smith"]},
|
||||
{"id":12,"name":"Healer","artists":["Kari Jobe", "Hillsong"]},
|
||||
{"id":15,"name":"I Am Free","artists":["Newsboys"]}]
|
||||
</pre></li>
|
||||
|
||||
<li><h3><code>POST /songs</code></h3>
|
||||
|
||||
<p>Create a new song record. In order to be allowed access to
|
||||
this method, the request must be made with a valid
|
||||
authentication token which belongs to a user with
|
||||
administrative priviliges. See <a href="#authentication">Authentication</a>
|
||||
for details.
|
||||
|
||||
<p><h4>Request Body</h4>
|
||||
Must be a <a href="#song-object">song object</a>. The
|
||||
<code>name</code> field is required. Any <code>id</code> passed
|
||||
in with the request will be ignored.
|
||||
|
||||
<p><h4>Reponse</h4>
|
||||
The newly-created song record. If a value is given in the
|
||||
request for the <tt>id</tt> attribute it is ignored. The
|
||||
attribute for new records is determined by the service and
|
||||
returned as part of the response.
|
||||
|
||||
<p><h4>Example</h4>
|
||||
<pre>
|
||||
POST http://newlifesongs.jdbernard.com/api/v1/songs
|
||||
Content-Length: 60
|
||||
Content-Type: application/json
|
||||
|
||||
{"id":22,"name":"This is How We Praise Him","artists":[""]}
|
||||
</pre>
|
||||
<p><pre>
|
||||
HTTP/1.1 201 Created
|
||||
Content-Length:
|
||||
Content-Type: application/json
|
||||
|
||||
|
||||
</pre></li>
|
||||
|
||||
<li><h3><code>GET /songs/<songId></code></h3>
|
||||
|
||||
<p>Retrieve song data for the given song id.
|
||||
|
||||
<p><h4>Response</h4>
|
||||
A <a href="song-object">song object</a>.
|
||||
|
||||
<p><h4>Example</h4>
|
||||
<pre>
|
||||
GET http://newlifesongs.jdbernard.com/api/v1/songs/1</pre>
|
||||
<p><pre>
|
||||
HTTP/1.1 200 OK
|
||||
Content-Length: 63
|
||||
Content-Type: application/json
|
||||
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
|
||||
|
||||
{"id":1,"name":"Welcome Holy Spirit","artists":["Mark Condon"]}
|
||||
</pre></li>
|
||||
</li>
|
||||
|
||||
<li><h3><code>PUT /songs/<songId></code></h3>
|
||||
|
||||
<p>Method description
|
||||
<p><h4>Request Body</h4>
|
||||
Request body description
|
||||
|
||||
<p><h4>Response</h4>
|
||||
Return value description
|
||||
|
||||
<p><h4>Example</h4>
|
||||
<pre>
|
||||
</pre></li>
|
||||
|
||||
</li>
|
||||
|
||||
<li><h3><code>DELETE /songs/<songId></code></h3>
|
||||
|
||||
<p>Method description
|
||||
<p><h4>Request Body</h4>
|
||||
Request body description
|
||||
|
||||
<p><h4>Response</h4>
|
||||
Return value description
|
||||
|
||||
<p><h4>Example</h4>
|
||||
<pre>
|
||||
</pre></li>
|
||||
|
||||
</li>
|
||||
|
||||
<li><h3><code>GET /songs/forService/<serviceId></code></h3>
|
||||
|
||||
<p>Method description
|
||||
<p><h4>Request Body</h4>
|
||||
Request body description
|
||||
|
||||
<p><h4>Response</h4>
|
||||
Return value description
|
||||
|
||||
<p><h4>Example</h4>
|
||||
<pre>
|
||||
GET /api/v1/songs/forService/1 HTTP/1.1</pre>
|
||||
<p><pre>
|
||||
HTTP/1.1 200 OK
|
||||
Content-Length: 256
|
||||
Content-Type: application/json
|
||||
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
|
||||
|
||||
[{"id":7,"name":"Mighty God","artists":[""]},
|
||||
{"id":8,"name":"Here I Am To Worship","artists":["Tim Hughes: Chris Tomlin, Michael W. Smith"]},
|
||||
{"id":9,"name":"Worthy","artists":[""]},
|
||||
{"id":4,"name":"I Am A Friend Of God","artists":["Israel Houghton"]}]j
|
||||
</pre></li>
|
||||
|
||||
</li>
|
||||
|
||||
<li><h3><code>GET /songs/byArtist/<artist></code></h3>
|
||||
|
||||
<p>Method description
|
||||
<p><h4>Request Body</h4>
|
||||
Request body description
|
||||
|
||||
<p><h4>Response</h4>
|
||||
Return value description
|
||||
|
||||
<p><h4>Example</h4>
|
||||
<pre>
|
||||
</pre></li>
|
||||
|
||||
</li>
|
||||
|
||||
</section>
|
||||
<section id=services>
|
||||
<h2><code>/services</code></h2>
|
||||
|
||||
<h3 id=service-object>Service object</h3>
|
||||
A Service object is defined with the following fields:
|
||||
<dl><dt>id</dt>
|
||||
<dd>An identifier unique to this service record among all
|
||||
service records. <em>Type: integer</em></dd>
|
||||
|
||||
<dt>date</dt>
|
||||
<dd>The date of the service. <em>Type: Date</em></dd>
|
||||
|
||||
<dt>serviceType</dt>
|
||||
<dd>Service type. <em>Type: string</em> Valid values:
|
||||
<table><thead><tr><th>Value</th><th>Description</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>SUN_AM</code></td>
|
||||
<td>Sunday morning service.</td></tr>
|
||||
<tr><td><code>SUN_PM</code></td>
|
||||
<td>Sunday evening service</td></tr>
|
||||
<tr><td><code>WED</code></td>
|
||||
<td>Wednesday, midweek Bible study.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<h4>Example</h4>
|
||||
<pre>
|
||||
{
|
||||
"id": 1,
|
||||
"date": 1235887200000,
|
||||
"serviceType": "SUN_PM"
|
||||
}</pre>
|
||||
|
||||
<h3>Method Summary</h3>
|
||||
<table class=method-summary>
|
||||
<thead>
|
||||
<tr><th class=action>HTTP Action</th>
|
||||
<th class=path>Path</th>
|
||||
<th class=desc>Description</th>
|
||||
<th class=public>Public?</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td class=action>GET</td>
|
||||
<td class=path><code>/services</code></td>
|
||||
<td class=desc>Retrieve all services.</td>
|
||||
<td class=public>yes</td></tr>
|
||||
<tr><td class=acion>POST</td>
|
||||
<td class=path><code>/services</code></td>
|
||||
<td class=desc>Create a new service record.</td>
|
||||
<td class=public>no</td></tr>
|
||||
<tr><td class=acion>GET</td>
|
||||
<td class=path><code>/services/<serviceId></code></td>
|
||||
<td class=desc>Retrieve a single service record.</td>
|
||||
<td class=public>yes</td></tr>
|
||||
<tr><td class=acion>PUT</td>
|
||||
<td class=path><code>/services/<serviceId></code></td>
|
||||
<td class=desc>Update a service record.</td>
|
||||
<td class=public>no</td></tr>
|
||||
<tr><td class=acion>DELETE</td>
|
||||
<td class=path><code>/services/<serviceId></code></td>
|
||||
<td class=desc>Delete a service record.</td>
|
||||
<td class=public>no</td></tr>
|
||||
<tr><td class=acion>GET</td>
|
||||
<td class=path><code>/services/withSong/<serviceId></code></td>
|
||||
<td class=desc>Retrieve all services in which the given song was performed.</td>
|
||||
<td class=public>yes</td></tr>
|
||||
<tr><td class=acion>GET</td>
|
||||
<td class=path><code>/services/byDate/after/<date></code></td>
|
||||
<td class=desc>Retrieve all services after the given date.</td>
|
||||
<td class=public>yes</td></tr>
|
||||
<tr><td class=acion>GET</td>
|
||||
<td class=path><code>/services/byDate/before/<date></code></td>
|
||||
<td class=desc>Retrieve all services before the given date.</td>
|
||||
<td class=public>yes</td></tr>
|
||||
<tr><td class=acion>GET</td>
|
||||
<td class=path><code>/services/byDate/between/<date1>/<date2></code></td>
|
||||
<td class=desc>Retrieve all services between the two given dates.</td>
|
||||
<td class=public>yes</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
</section>
|
||||
<section id=users>
|
||||
<h2><code>/users</code></h2>
|
||||
</section>
|
||||
<section id=authentication>
|
||||
<h2>Authentication</h2>
|
||||
</section>
|
||||
</body>
|
||||
|
||||
</html>
|
36
service/src/main/webapp/index.html
Normal file
36
service/src/main/webapp/index.html
Normal 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='mailto:jdbernard@gmail.com'>jdbernard@gmail.com</a>
|
||||
</section>
|
||||
</body>
|
||||
|
||||
</html>
|
36
service/src/main/webapp/js/new-life-songs.js
Normal file
36
service/src/main/webapp/js/new-life-songs.js
Normal 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(); }
|
||||
});
|
||||
|
||||
})();
|
88
service/src/main/webapp/service/index.gsp
Normal file
88
service/src/main/webapp/service/index.gsp
Normal 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>
|
64
service/src/main/webapp/services/index.gsp
Normal file
64
service/src/main/webapp/services/index.gsp
Normal 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>
|
88
service/src/main/webapp/song/index.gsp
Normal file
88
service/src/main/webapp/song/index.gsp
Normal 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>
|
61
service/src/main/webapp/songs/index.gsp
Normal file
61
service/src/main/webapp/songs/index.gsp
Normal 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>
|
50
service/src/migration/migrate.groovy
Normal file
50
service/src/migration/migrate.groovy
Normal 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 }
|
@ -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 {
|
||||
|
||||
}
|
@ -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) }) }
|
||||
}
|
6
service/version.properties
Normal file
6
service/version.properties
Normal file
@ -0,0 +1,6 @@
|
||||
#
|
||||
#Sat Dec 17 21:52:48 CST 2016
|
||||
major=2
|
||||
version.release=true
|
||||
minor=5
|
||||
build=2
|
Reference in New Issue
Block a user