Compare commits

..

No commits in common. "main" and "v2.1" have entirely different histories.
main ... v2.1

68 changed files with 472 additions and 732 deletions

View File

@ -1,14 +0,0 @@
branch-defaults:
master:
environment: new-life-songs-prod
global:
application_name: new-life-songs
branch: null
default_ec2_keyname: id_jdb@jdb-maingear
default_platform: Tomcat 8 Java 8
default_region: us-west-2
profile: eb-cli
repository: null
sc: git
deploy:
artifact: service/build/ROOT.war

View File

@ -1,8 +0,0 @@
# New Life Songs Database
This is Jonathan's database of worship songs performed at New Life Austin. The
service lives online at http://newlifesongs.jdbernard.com
API Documentation is [maintained online with the service](http://newlifesongs.jdbernard.com/doc/api/v1/).
You can also view the [annotated source code](https://doc.jdb-labs.com/new-life-songs/current/).

View File

@ -1,24 +1,169 @@
plugins { id 'com.palantir.git-version' version '0.5.2' } import org.apache.tools.ant.filters.ReplaceTokens
apply plugin: "groovy"
apply plugin: "maven"
apply plugin: "war"
apply plugin: "jetty"
apply from: 'shell.gradle'
allprojects {
group = "com.jdbernard" group = "com.jdbernard"
buildscript { version = new ProjectVersion()
repositories {
mavenLocal() // webAppDirName = "build/webapp/main"
mavenCentral()
jcenter()
maven { url 'https://mvn.jdb-labs.com/repo' }
}
}
repositories { repositories {
mavenLocal() mavenLocal()
mavenCentral() mavenCentral() }
jcenter()
maven { url "https://mvn.jdb-labs.com/repo" } dependencies {
compile 'ch.qos.logback:logback-classic:1.1.2'
compile 'ch.qos.logback:logback-core:1.1.2'
compile 'com.impossibl.pgjdbc-ng:pgjdbc-ng:0.3'
compile 'com.lambdaworks:scrypt:1.4.0'
compile 'com.zaxxer:HikariCP-java6:2.3.2'
compile 'javax:javaee-api:7.0'
compile 'javax.ws.rs:javax.ws.rs-api:2.0.1'
compile 'joda-time:joda-time:2.7'
compile 'org.codehaus.groovy:groovy-all:2.3.6'
compile 'org.slf4j:slf4j-api:1.7.10'
runtime 'com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider:2.3.2'
runtime 'org.glassfish.jersey.containers:jersey-container-servlet:2.16'
runtime 'org.glassfish.jersey.media:jersey-media-json-jackson:2.16'
providedCompile 'javax.servlet:javax.servlet-api:3.1.0'
testCompile 'com.jdbernard:jdb-util:3.4'
testCompile 'junit:junit:4.12'
testRuntime 'com.h2database:h2:1.4.186'
} }
apply plugin: 'com.palantir.git-version' war {
version = gitVersion() from "resources/webapp"
from "build/webapp"
filter(ReplaceTokens, tokens: [version: version])
rename '(.+)(\\..*(css|js))', '$1-' + version + '$2'
version = project.version.releaseVersion
webInf { from 'resources/main/WEB-INF' }
exclude "**/.*.swp", "**/.sass-cache"
}
test { testLogging { events 'failed' } }
task testWar(type: War) {
from 'resources/webapp'
filter(ReplaceTokens, tokens: [version: version])
rename '(.+)(\\..*(css|js))', '$1-' + version + '$2'
version = project.version.releaseVersion
webInf { from 'resources/test/WEB-INF' }
classifier 'test' }
task compileScss(
group: 'build',
description: 'Compile SCSS files into CSS.',
type: Exec
) {
executable "scss"
args "--update", "src/main/webapp/css:build/webapp/css"
}
war.dependsOn compileScss
testWar.dependsOn compileScss
// ## Build Versioning task
task incrementBuildNumber(
group: 'versioning',
description: "Increment the project's build number."
) << { ++version.build }
task incrementMinorNumber(
group: 'versioning',
description: "Increment the project's minor version number."
) << { ++version.minor }
task incrementMajorNumber(
group: 'versioning',
description: "Increment the project's major version number."
) << { ++version.major }
task markReleaseBuild(
group: 'versioning',
description: "Mark this version of the project as a release version."
) << { version.release = true }
war.dependsOn << incrementBuildNumber
testWar.dependsOn << incrementBuildNumber
// ## Custom tasks for local deployment
task deployLocal(dependsOn: ['build']) << {
def warName = "${project.name}-${version.releaseVersion}.war"
def jettyHome = System.getenv("JETTY_HOME")
def deployedWar = new File("$jettyHome/webapps/$warName")
if (deployedWar.exists()) deployedWar.delete();
copy {
from "build/libs"
into "$jettyHome/webapps"
include warName } }
task killJettyLocal() << {
def pidFile = new File(System.properties['user.home'] + "/temp/jetty.pid")
println "Killing old Jetty instance."
shell_("sh", "-c", 'kill $(jps -l | grep start.jar | cut -f 1 -d " ")') }
task localJetty(dependsOn: ['killJettyLocal', 'deployLocal']) << {
spawn(["java", "-jar", "start.jar"], new File(jettyHome)) }
// ## Project Version
class ProjectVersion {
private File versionFile
int major
int minor
int build
boolean release
public ProjectVersion() { this(new File('version.properties')) }
public ProjectVersion(File versionFile) {
this.versionFile = versionFile
if (!versionFile.exists()) {
versionFile.createNewFile()
this.major = this.minor = this.build = 0
this.save() }
else this.load() }
@Override String toString() { "$major.$minor${release ? '' : '-build' + build}" }
public String getReleaseVersion() { "$major.$minor" }
public void setRelease(boolean release) { this.release = release; save() }
public void setMajor(int major) {
this.major = major; minor = build = 0; release = false; save() }
public void setMinor(int minor) {
this.minor = minor; build = 0; release = false; save() }
public void setBuild(int build) { this.build = build; save() }
private void save() {
def props = new Properties()
versionFile.withInputStream { props.load(it) }
["major", "minor", "build"].each { props[it] = this[it].toString() }
props["version.release"] = release.toString()
versionFile.withOutputStream { props.store(it, "") } }
private void load() {
def props = new Properties()
versionFile.withInputStream { props.load(it) }
["major", "minor", "build"].each {
this[it] = props[it] ? props[it] as int : 0 }
release = Boolean.parseBoolean(props["version.release"]) }
} }

View File

@ -0,0 +1,34 @@
INSERT INTO SERVICES (date, service_type) values
('2015-02-01', 'SUN_AM'),
('2015-02-01', 'SUN_PM'),
('2015-02-04', 'WED'),
('2015-02-08', 'SUN_AM'),
('2015-02-08', 'SUN_PM'),
('2015-02-11', 'WED'),
('2015-02-15', 'SUN_AM'),
('2015-02-15', 'SUN_PM');
INSERT INTO songs (name, artists) VALUES
('Breathe On Us', 'Kari Jobe'),
('How Great Is Our God', 'Chris Tomlin'),
('Glorious', 'Martha Munizzi'),
('Rez Power', 'Israel Houghton');
INSERT INTO performances (service_id, song_id, pianist, organist, bassist, drummer, guitarist, leader) VALUES
(1, 1, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood'),
(1, 2, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood'),
(1, 3, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood'),
(2, 2, 'Trevor Delano', 'Connie Bernard', 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser', 'Rachel Wood'),
(2, 3, 'Trevor Delano', 'Connie Bernard', 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser', 'Rachel Wood'),
(2, 4, 'Trevor Delano', 'Connie Bernard', 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser', 'Rachel Wood'),
(3, 1, 'Rachel Wood', 'Krista Hatcher', 'Jonathan Bernard', 'Jared Wood', 'Tony Bagliore', 'Rachel Wood'),
(3, 2, 'Rachel Wood', 'Krista Hatcher', 'Jonathan Bernard', 'Jared Wood', 'Tony Bagliore', 'Rachel Wood'),
(4, 3, 'Trevor Delano', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser', 'Rachel Wood'),
(5, 4, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Tony Bagliore', 'Rachel Wood'),
(6, 1, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood'),
(7, 2, 'Trevor Delano', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood'),
(8, 3, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood');
INSERT INTO users (username, pwd, role) VALUES
('admin', '', 'admin'),
('test', '', '');

View File

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -1,79 +0,0 @@
import org.apache.tools.ant.filters.ReplaceTokens
apply plugin: "groovy"
apply plugin: "maven"
apply plugin: "war"
// webAppDirName = "build/webapp/main"
buildscript {
dependencies {
classpath 'com.jdbernard:gradle-exec-util:0.2.0'
}
}
import static com.jdbernard.gradle.ExecUtil.*
dependencies {
compile localGroovy()
compile 'ch.qos.logback:logback-classic:1.1.8'
compile 'ch.qos.logback:logback-core:1.1.8'
compile 'org.slf4j:slf4j-api:1.7.22'
compile 'com.impossibl.pgjdbc-ng:pgjdbc-ng:0.6'
compile 'com.lambdaworks:scrypt:1.4.0'
compile 'com.zaxxer:HikariCP:2.5.1'
compile 'javax:javaee-api:7.0'
compile 'javax.ws.rs:javax.ws.rs-api:2.0.1'
compile 'joda-time:joda-time:2.7'
runtime 'com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider:2.3.2'
runtime 'org.glassfish.jersey.containers:jersey-container-servlet:2.16'
runtime 'org.glassfish.jersey.media:jersey-media-json-jackson:2.16'
providedCompile 'javax.servlet:javax.servlet-api:3.1.0'
testCompile 'junit:junit:4.12'
testCompile 'com.jdblabs:db-migrate.groovy:0.2.5'
testRuntime 'com.h2database:h2:1.4.186'
}
war {
from "resources/webapp"
from "build/webapp"
filter(ReplaceTokens, tokens: [version: version])
rename '(.+)(\\..*(css|js))', '$1-' + version + '$2'
webInf { from 'resources/main/WEB-INF' }
exclude "**/.*.swp", "**/.sass-cache"
}
test { testLogging { events 'failed' } }
task testWar(type: War) {
from 'resources/webapp'
filter(ReplaceTokens, tokens: [version: version])
rename '(.+)(\\..*(css|js))', '$1-' + version + '$2'
webInf { from 'resources/test/WEB-INF' }
classifier 'test' }
task compileScss(
group: 'build',
description: 'Compile SCSS files into CSS.',
type: Exec
) {
executable "scss"
args "--update", "src/main/webapp/css:build/webapp/css"
}
war.dependsOn compileScss
testWar.dependsOn compileScss
task deployProd(dependsOn: ['build']) { doLast {
def warName = "${project.name}-${version}.war"
def artifactName = "ROOT.war"
copy {
from "build/libs"
into "build"
include warName
rename warName, artifactName }
exec("eb", "deploy", "-l", "${parent.name}-${project.name}-${version}")
} }

View File

@ -1,18 +0,0 @@
import ch.qos.logback.core.*;
import ch.qos.logback.core.encoder.*;
import ch.qos.logback.core.read.*;
import ch.qos.logback.core.rolling.*;
import ch.qos.logback.core.status.*;
import ch.qos.logback.classic.net.*;
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
appender("STDOUT", ConsoleAppender) {
encoder(PatternLayoutEncoder) {
pattern = "%level %logger - %msg%n"
}
}
root(INFO, ["STDOUT"])
logger('com.jdbernard', INFO)

View File

@ -1,34 +0,0 @@
INSERT INTO SERVICES (date, service_type) values
('2015-02-01', 'SUN_AM'),
('2015-02-01', 'SUN_PM'),
('2015-02-04', 'WED'),
('2015-02-08', 'SUN_AM'),
('2015-02-08', 'SUN_PM'),
('2015-02-11', 'WED'),
('2015-02-15', 'SUN_AM'),
('2015-02-15', 'SUN_PM');
INSERT INTO songs (name, artists) VALUES
('Breathe On Us', 'Kari Jobe'),
('How Great Is Our God', 'Chris Tomlin'),
('Glorious', 'Martha Munizzi'),
('Rez Power', 'Israel Houghton');
INSERT INTO performances (service_id, song_id, rank, pianist, organist, bassist, drummer, guitarist, leader) VALUES
(1, 1, 1, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood'),
(1, 2, 2, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood'),
(1, 3, 3, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood'),
(2, 2, 1, 'Trevor Delano', 'Connie Bernard', 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser', 'Rachel Wood'),
(2, 3, 2, 'Trevor Delano', 'Connie Bernard', 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser', 'Rachel Wood'),
(2, 4, 3, 'Trevor Delano', 'Connie Bernard', 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser', 'Rachel Wood'),
(3, 1, 0, 'Rachel Wood', 'Krista Hatcher', 'Jonathan Bernard', 'Jared Wood', 'Tony Bagliore', 'Rachel Wood'),
(3, 2, 0, 'Rachel Wood', 'Krista Hatcher', 'Jonathan Bernard', 'Jared Wood', 'Tony Bagliore', 'Rachel Wood'),
(4, 3, 0, 'Trevor Delano', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser', 'Rachel Wood'),
(5, 4, 1, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Tony Bagliore', 'Rachel Wood'),
(6, 1, 1, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood'),
(7, 2, 1, 'Trevor Delano', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood'),
(8, 3, 1, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood');
INSERT INTO users (username, pwd, role) VALUES
('admin', '', 'admin'),
('test', '', '');

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1,9 +0,0 @@
-- # New Life Songs DB
-- @author Jonathan Bernard <jdb@jdb-labs.com>
--
-- PostgreSQL database un-creation sript.
DROP TABLE performances;
DROP TABLE services;
DROP TABLE songs;
DROP TABLE tokens;
DROP TABLE users;

View File

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

View File

@ -1,6 +0,0 @@
-- # New Life Songs DB
-- @author Jonathan Bernard <jdb@jdb-labs.com>
--
-- Add performances.rank: the rank of the performance in the service, aka. the
-- "track number" if the service were an album.
ALTER TABLE performances ADD COLUMN rank integer NOT NULL DEFAULT 0;

View File

@ -1,351 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="referrer" content="origin">
<link rel="shortcut icon" href="../images/favicon.ico">
<title>API V1 - New Life Songs Database</title>
<link
href='http://fonts.googleapis.com/css?family=Roboto+Condensed|Cantarell|Anonymous+Pro' rel='stylesheet' type='text/css'>
<link href='../../../css/new-life-songs-@version@.css' rel='stylesheet' type='text/css'>
</head>
<body class=api-doc>
<header>
<h1><a href="../../../">New Life Songs API V1</a></h1>
<nav><ul>
<li><a href="../../../admin/">Admin</a></li>
<li><a href="../../../songs/">Songs</a></li>
<li><a href="../../../services/">Services</a></li>
</ul></nav>
</header>
<section id=api-overview>
The New Life Songs database exposes a REST API. This allows
programatic access to and modification of the data. Version 1 of
the API defines several endpoints, all of which are built off of
<code>http://newlifesongs.jdbernard.com/api/v1</code> as a base
URL.
<p>Some of the service's endpoints require the client to authenticate
itself to the server. See the <a href="#authentication">section on
authentication</a> for details concerning authentication.
<p>The endpoints that the API defines are:
<ul><li><a href="#songs"><code>/songs</code></a></li>
<li><a href="#services"><code>/services</code></a></li>
<li><a href="#users"><code>/users</code></a></li></ul>
<p>If you run across any problems or have questions, feel free to send me an email at
<a href='&#109;&#97;&#105;&#108;&#116;&#111;&#58;&#106;&#100;&#98;&#101;&#114;&#110;&#97;&#114;&#100;&#64;&#103;&#109;&#97;&#105;&#108;&#46;&#99;&#111;&#109;'>&#106;&#100;&#98;&#101;&#114;&#110;&#97;&#114;&#100;&#64;&#103;&#109;&#97;&#105;&#108;&#46;&#99;&#111;&#109;</a>
</section>
<section id=songs>
<h2><code>/songs</code></h2>
<h3 id=song-object>Song object</h3>
A song object is defined with the following fields:
<dl><dt>id</dt>
<dd>An identifier unique to this song record among all song
records. <em>Type: integer</em></dd>
<dt>name</dt>
<dd>The name of the song. <em>Type: string</em></dd>
<dt>artists</dt>
<dd>A list of the artists known to have written or performed
this song. <em>Type: list of strings</em></dl>
<h4>Example</h4>
<pre>
{
"id":8,
"name":"Here I Am To Worship",
"artists":[
"Tim Hughes",
"Chris Tomlin",
"Michael W. Smith"
]
}</pre>
<h3>Method Summary</h3>
<table class=method-summary>
<thead>
<tr><th class=action>HTTP Action</th>
<th class=path>Path</th>
<th class=desc>Description</th>
<th class=public>Public?</th></tr>
</thead>
<tbody>
<tr><td class=action>GET</td>
<td class=path><code>/songs</code></td>
<td class=desc>Retrieve all songs.</td>
<td class=public>yes</td></tr>
<tr><td class=acion>POST</td>
<td class=path><code>/songs</code></td>
<td class=desc>Create a new song record.</td>
<td class=public>no</td></tr>
<tr><td class=acion>GET</td>
<td class=path><code>/songs/&lt;songId&gt;</code></td>
<td class=desc>Retrieve a single record.</td>
<td class=public>yes</td></tr>
<tr><td class=acion>PUT</td>
<td class=path><code>/songs/&lt;songId&gt;</code></td>
<td class=desc>Update a song record.</td>
<td class=public>no</td></tr>
<tr><td class=acion>DELETE</td>
<td class=path><code>/songs/&lt;songId&gt;</code></td>
<td class=desc>Delete a song record.</td>
<td class=public>no</td></tr>
<tr><td class=acion>GET</td>
<td class=path><code>/songs/forService/&lt;serviceId&gt;</code></td>
<td class=desc>Retrieve all songs performed in a given service.</td>
<td class=public>yes</td></tr>
<tr><td class=acion>GET</td>
<td class=path><code>/songs/byArtist/&lt;artist&gt;</code></td>
<td class=desc>Retrieve all songs performed by a given artist.</td>
<td class=public>yes</td></tr>
</tbody>
</table>
<ul class=method-list>
<li><h3><code>GET /songs</code></h3>
<p>Retrieve all songs.
<p><h4>Response</h4>
A list of <a href="song-object">song objects</a>
<p><h4>Example</h4>
<pre>
GET http://newlifesongs.jdbernard.com/api/v1/songs</pre>
<p><pre>
HTTP/1.1 200 OK
Content-Length: 433
Content-Type: application/json
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
[{"id":1,"name":"Welcome Holy Spirit","artists":["Mark Condon"]},
{"id":3,"name":"Let's Sing Praises to our God","artists":["Traditional"]},
{"id":5,"name":"Blessed Assurance","artists":["Frances J. Crosby"]},
{"id":8,"name":"Here I Am To Worship","artists":["Tim Hughes", "Chris Tomlin", "Michael W. Smith"]},
{"id":12,"name":"Healer","artists":["Kari Jobe", "Hillsong"]},
{"id":15,"name":"I Am Free","artists":["Newsboys"]}]
</pre></li>
<li><h3><code>POST /songs</code></h3>
<p>Create a new song record. In order to be allowed access to
this method, the request must be made with a valid
authentication token which belongs to a user with
administrative priviliges. See <a href="#authentication">Authentication</a>
for details.
<p><h4>Request Body</h4>
Must be a <a href="#song-object">song object</a>. The
<code>name</code> field is required. Any <code>id</code> passed
in with the request will be ignored.
<p><h4>Reponse</h4>
The newly-created song record. If a value is given in the
request for the <tt>id</tt> attribute it is ignored. The
attribute for new records is determined by the service and
returned as part of the response.
<p><h4>Example</h4>
<pre>
POST http://newlifesongs.jdbernard.com/api/v1/songs
Content-Length: 60
Content-Type: application/json
{"id":22,"name":"This is How We Praise Him","artists":[""]}
</pre>
<p><pre>
HTTP/1.1 201 Created
Content-Length:
Content-Type: application/json
</pre></li>
<li><h3><code>GET /songs/&lt;songId&gt;</code></h3>
<p>Retrieve song data for the given song id.
<p><h4>Response</h4>
A <a href="song-object">song object</a>.
<p><h4>Example</h4>
<pre>
GET http://newlifesongs.jdbernard.com/api/v1/songs/1</pre>
<p><pre>
HTTP/1.1 200 OK
Content-Length: 63
Content-Type: application/json
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
{"id":1,"name":"Welcome Holy Spirit","artists":["Mark Condon"]}
</pre></li>
</li>
<li><h3><code>PUT /songs/&lt;songId&gt;</code></h3>
<p>Method description
<p><h4>Request Body</h4>
Request body description
<p><h4>Response</h4>
Return value description
<p><h4>Example</h4>
<pre>
</pre></li>
</li>
<li><h3><code>DELETE /songs/&lt;songId&gt;</code></h3>
<p>Method description
<p><h4>Request Body</h4>
Request body description
<p><h4>Response</h4>
Return value description
<p><h4>Example</h4>
<pre>
</pre></li>
</li>
<li><h3><code>GET /songs/forService/&lt;serviceId&gt;</code></h3>
<p>Method description
<p><h4>Request Body</h4>
Request body description
<p><h4>Response</h4>
Return value description
<p><h4>Example</h4>
<pre>
GET /api/v1/songs/forService/1 HTTP/1.1</pre>
<p><pre>
HTTP/1.1 200 OK
Content-Length: 256
Content-Type: application/json
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
[{"id":7,"name":"Mighty God","artists":[""]},
{"id":8,"name":"Here I Am To Worship","artists":["Tim Hughes: Chris Tomlin, Michael W. Smith"]},
{"id":9,"name":"Worthy","artists":[""]},
{"id":4,"name":"I Am A Friend Of God","artists":["Israel Houghton"]}]j
</pre></li>
</li>
<li><h3><code>GET /songs/byArtist/&lt;artist&gt;</code></h3>
<p>Method description
<p><h4>Request Body</h4>
Request body description
<p><h4>Response</h4>
Return value description
<p><h4>Example</h4>
<pre>
</pre></li>
</li>
</section>
<section id=services>
<h2><code>/services</code></h2>
<h3 id=service-object>Service object</h3>
A Service object is defined with the following fields:
<dl><dt>id</dt>
<dd>An identifier unique to this service record among all
service records. <em>Type: integer</em></dd>
<dt>date</dt>
<dd>The date of the service. <em>Type: Date</em></dd>
<dt>serviceType</dt>
<dd>Service type. <em>Type: string</em> Valid values:
<table><thead><tr><th>Value</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>SUN_AM</code></td>
<td>Sunday morning service.</td></tr>
<tr><td><code>SUN_PM</code></td>
<td>Sunday evening service</td></tr>
<tr><td><code>WED</code></td>
<td>Wednesday, midweek Bible study.</td></tr>
</tbody>
</table>
</dd>
</dl>
<h4>Example</h4>
<pre>
{
"id": 1,
"date": 1235887200000,
"serviceType": "SUN_PM"
}</pre>
<h3>Method Summary</h3>
<table class=method-summary>
<thead>
<tr><th class=action>HTTP Action</th>
<th class=path>Path</th>
<th class=desc>Description</th>
<th class=public>Public?</th></tr>
</thead>
<tbody>
<tr><td class=action>GET</td>
<td class=path><code>/services</code></td>
<td class=desc>Retrieve all services.</td>
<td class=public>yes</td></tr>
<tr><td class=acion>POST</td>
<td class=path><code>/services</code></td>
<td class=desc>Create a new service record.</td>
<td class=public>no</td></tr>
<tr><td class=acion>GET</td>
<td class=path><code>/services/&lt;serviceId&gt;</code></td>
<td class=desc>Retrieve a single service record.</td>
<td class=public>yes</td></tr>
<tr><td class=acion>PUT</td>
<td class=path><code>/services/&lt;serviceId&gt;</code></td>
<td class=desc>Update a service record.</td>
<td class=public>no</td></tr>
<tr><td class=acion>DELETE</td>
<td class=path><code>/services/&lt;serviceId&gt;</code></td>
<td class=desc>Delete a service record.</td>
<td class=public>no</td></tr>
<tr><td class=acion>GET</td>
<td class=path><code>/services/withSong/&lt;serviceId&gt;</code></td>
<td class=desc>Retrieve all services in which the given song was performed.</td>
<td class=public>yes</td></tr>
<tr><td class=acion>GET</td>
<td class=path><code>/services/byDate/after/&lt;date&gt;</code></td>
<td class=desc>Retrieve all services after the given date.</td>
<td class=public>yes</td></tr>
<tr><td class=acion>GET</td>
<td class=path><code>/services/byDate/before/&lt;date&gt;</code></td>
<td class=desc>Retrieve all services before the given date.</td>
<td class=public>yes</td></tr>
<tr><td class=acion>GET</td>
<td class=path><code>/services/byDate/between/&lt;date1&gt;/&lt;date2&gt;</code></td>
<td class=desc>Retrieve all services between the two given dates.</td>
<td class=public>yes</td></tr>
</tbody>
</table>
</section>
<section id=users>
<h2><code>/users</code></h2>
</section>
<section id=authentication>
<h2>Authentication</h2>
</section>
</body>
</html>

View File

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

View File

@ -1 +0,0 @@
include 'service', 'uploader'

33
shell.gradle Normal file
View File

@ -0,0 +1,33 @@
// ## Utility methods for working with processes.
def shell_(List<String> cmd) { shell(cmd, null, false) }
def shell_(String... cmd) { shell(cmd, null, false) }
def shell(String... cmd) { shell(cmd, null, true) }
def shell(List<String> cmd, File workingDir, boolean checkExit) {
shell(cmd as String[], workingDir, checkExit) }
def shell(String[] cmd, File workingDir, boolean checkExit) {
def pb = new ProcessBuilder(cmd)
if (workingDir) pb.directory(workingDir)
def process = pb.start()
process.waitForProcessOutput(System.out, System.err)
if (process.exitValue() != 0)
println "Command $cmd exited with non-zero result code."
if (checkExit) assert process.exitValue() == 0 : "Not ignoring failed command." }
def shell(List<List<String>> cmds, File workingDir) {
cmds.each {
ProcessBuilder pb = new ProcessBuilder(it)
pb.directory(workingDir)
pb.start().waitForProcessOutput(System.out, System.err) } }
def spawn(String... cmd) { spawn(cmd, null) }
def spawn(List<String> cmd, File workingDir) { spawn(cmd as String[], workingDir) }
def spawn(String[] cmd, File workingDir) {
def pb = new ProcessBuilder(cmd)
if (workingDir) pb.directory(workingDir)
def process = pb.start() }

View File

@ -27,7 +27,10 @@ public class NLSongsDB {
/// ### Common /// ### Common
public def save(def model) { public def save(def model) {
if (model.id > 0) return update(model) if (model.id > 0) return update(model)
else return create(model) } else {
if (create(model) > 0) return model
else return null } }
/// ### Services /// ### Services
public Service findService(int id) { public Service findService(int id) {
@ -152,10 +155,10 @@ public class NLSongsDB {
public Performance create(Performance perf) { public Performance create(Performance perf) {
// TODO: handle constraint violation (same service and song ids) // TODO: handle constraint violation (same service and song ids)
sql.executeInsert( sql.executeInsert(
"INSERT INTO performances (service_id, song_id, rank, pianist, " + "INSERT INTO performances (service_id, song_id, pianist, " +
"organist, bassist, drummer, guitarist, leader) VALUES " + "organist, bassist, drummer, guitarist, leader) VALUES " +
"(?, ?, ?, ?, ?, ?, ?, ?)", [perf.serviceId, perf.songId, "(?, ?, ?, ?, ?, ?, ?, ?)", [perf.serviceId, perf.songId,
perf.rank, perf.pianist, perf.organist, perf.bassist, perf.drummer, perf.pianist, perf.organist, perf.bassist, perf.drummer,
perf.guitarist, perf.leader]) perf.guitarist, perf.leader])
return perf } return perf }
@ -163,11 +166,10 @@ public class NLSongsDB {
// TODO: handle constraint violation (same service and song ids) // TODO: handle constraint violation (same service and song ids)
return sql.executeUpdate( return sql.executeUpdate(
"UPDATE performances SET pianist = ?, organist = ?, " + "UPDATE performances SET pianist = ?, organist = ?, " +
"bassist = ?, drummer = ?, guitarist = ?, leader = ?, " + "bassist = ?, drummer = ?, guitarist = ?, leader = ? " +
"rank = ? WHERE service_id = ? AND song_id = ?", "WHERE service_id = ? AND song_id = ?",
[perf.pianist, perf.organist, perf.bassist, perf.drummer, [perf.pianist, perf.organist, perf.bassist, perf.drummer,
perf.guitarist, perf.leader, perf.rank, perf.serviceId, perf.guitarist, perf.leader, perf.serviceId, perf.songId]) }
perf.songId]) }
public int delete(Performance perf) { public int delete(Performance perf) {
sql.execute( sql.execute(
@ -313,7 +315,7 @@ public class NLSongsDB {
return buildToken(row, user) } return buildToken(row, user) }
public static List<String> unwrapArtists(String artists) { public static List<String> unwrapArtists(String artists) {
return artists.split(':') as List<String> } return artists.split(';') as List<String> }
public static String wrapArtists(List<String> artists) { public static String wrapArtists(List<String> artists) {
return artists.join(':') } return artists.join(':') }

View File

@ -4,7 +4,6 @@ public class Performance implements Serializable {
int serviceId int serviceId
int songId int songId
int rank
String pianist String pianist
String organist String organist
String bassist String bassist
@ -20,7 +19,6 @@ public class Performance implements Serializable {
return (this.serviceId == that.serviceId && return (this.serviceId == that.serviceId &&
this.songId == that.songId && this.songId == that.songId &&
this.rank == that.rank &&
this.pianist == that.pianist && this.pianist == that.pianist &&
this.organist == that.organist && this.organist == that.organist &&
this.bassist == that.bassist && this.bassist == that.bassist &&
@ -29,5 +27,5 @@ public class Performance implements Serializable {
this.leader == that.leader) } this.leader == that.leader) }
@Override String toString() { @Override String toString() {
return "($serviceId, $songId)-$rank: $leader - $pianist" } return "($serviceId, $songId): $leader - $pianist" }
} }

View File

@ -7,7 +7,6 @@ public class Service implements Serializable {
int id int id
private LocalDate date private LocalDate date
ServiceType serviceType ServiceType serviceType
String description
public boolean equals(Object thatObj) { public boolean equals(Object thatObj) {
if (thatObj == null) return false if (thatObj == null) return false
@ -16,7 +15,7 @@ public class Service implements Serializable {
Service that = (Service) thatObj Service that = (Service) thatObj
return (this.id == that.id && return (this.id == that.id &&
this.date == (that.localDate) && this.date == (that.@date) &&
this.serviceType == that.serviceType) } this.serviceType == that.serviceType) }
public void setDate(Date date) { this.date = LocalDate.fromDateFields(date) } public void setDate(Date date) { this.date = LocalDate.fromDateFields(date) }
@ -26,8 +25,4 @@ public class Service implements Serializable {
public Date getDate() { return this.date.toDate() } public Date getDate() { return this.date.toDate() }
public String toString() { return "$id: $date - $serviceType" } public String toString() { return "$id: $date - $serviceType" }
// Needed only because the @directFieldAccesor syntax stopped working in
// Groovy 2.4.7
private LocalDate getLocalDate() { return this.date }
} }

View File

@ -11,8 +11,7 @@ public class NLSongsContext {
public static String mediaBaseUrl public static String mediaBaseUrl
public static String makeUrl(Service service, Song song) { public static String makeUrl(Service service, Song song) {
return mediaBaseUrl + '/' + service.localDate.toString('yyyy') + "/" + return mediaBaseUrl + '/' + service.@date.toString('yyyy-MM-dd') + '_' +
service.localDate.toString('yyyy-MM-dd') + '_' +
service.serviceType.name().toLowerCase() + '_' + service.serviceType.name().toLowerCase() + '_' +
song.name.replaceAll(/[\s'"\\\/\?!]/, '') + '.mp3' } song.name.replaceAll(/[\s'"\\\/\?!]/, '') + '.mp3' }
} }

View File

@ -9,40 +9,20 @@ import com.jdbernard.nlsongs.db.NLSongsDB
import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource import com.zaxxer.hikari.HikariDataSource
import org.slf4j.Logger
import org.slf4j.LoggerFactory
public final class NLSongsContextListener implements ServletContextListener { public final class NLSongsContextListener implements ServletContextListener {
private static final log = LoggerFactory.getLogger(NLSongsContextListener)
public void contextInitialized(ServletContextEvent event) { public void contextInitialized(ServletContextEvent event) {
def context = event.servletContext def context = event.servletContext
// Load the context configuration.
Properties props = new Properties() Properties props = new Properties()
// Load configuration details from the context configuration.
NLSongsContextListener.getResourceAsStream( NLSongsContextListener.getResourceAsStream(
context.getInitParameter('context.config.file')) context.getInitParameter('context.config.file')).withStream { is ->
.withStream { is -> props.load(is) } props.load(is) }
// Load database configuration
Properties dataSourceProps = new Properties()
String dbConfigFile = context.getInitParameter('datasource.config.file')
if (dbConfigFile) {
NLSongsContextListener.getResourceAsStream(dbConfigFile)
.withStream { is -> dataSourceProps.load(is) } }
// Load database configuration from environment variables (may
// override settings in file).
System.properties.keySet().findAll { it.startsWith('DB_') }.each { key ->
dataSourceProps["dataSource.${key.substring(3)}"] = System.properties[key] }
log.debug("Database configuration: {}", dataSourceProps)
// Create the pooled data source // Create the pooled data source
HikariConfig hcfg = new HikariConfig(dataSourceProps) HikariConfig hcfg = new HikariConfig(
context.getInitParameter('datasource.config.file'))
HikariDataSource hds = new HikariDataSource(hcfg) HikariDataSource hds = new HikariDataSource(hcfg)

View File

@ -0,0 +1,8 @@
-- DROP DATABASE IF EXISTS nlsongs;
CREATE DATABASE nlsongs
ENCODING = 'UTF8'
LC_COLLATE = 'en_US.UTF-8'
LC_CTYPE = 'en_US.UTF-8'
CONNECTION LIMIT = 1;
\c nlsongs

View File

@ -4,16 +4,18 @@
-- PostgreSQL database creation sript. -- PostgreSQL database creation sript.
-- Services table -- Services table
CREATE TABLE services ( DROP TABLE IF EXISTS services;
CREATE TABLE IF NOT EXISTS services (
id SERIAL, id SERIAL,
date DATE NOT NULL, date DATE NOT NULL,
service_type VARCHAR(16) DEFAULT NULL, service_type VARCHAR(16) DEFAULT NULL,
description VARCHAR(255) DEFAULT NULL,
CONSTRAINT uc_serviceTypeAndDate UNIQUE (date, service_type), CONSTRAINT uc_serviceTypeAndDate UNIQUE (date, service_type),
PRIMARY KEY (id)); PRIMARY KEY (id));
-- Songs table -- Songs table
CREATE TABLE songs ( DROP TABLE IF EXISTS songs;
CREATE TABLE IF NOT EXISTS songs (
id SERIAL, id SERIAL,
name VARCHAR(128) NOT NULL, name VARCHAR(128) NOT NULL,
artists VARCHAR(256) DEFAULT NULL, artists VARCHAR(256) DEFAULT NULL,
@ -22,7 +24,8 @@ CREATE TABLE songs (
-- performances table -- performances table
CREATE TABLE performances ( DROP TABLE IF EXISTS performances;
CREATE TABLE IF NOT EXISTS performances (
service_id INTEGER NOT NULL, service_id INTEGER NOT NULL,
song_id INTEGER NOT NULL, song_id INTEGER NOT NULL,
pianist VARCHAR(64) DEFAULT NULL, pianist VARCHAR(64) DEFAULT NULL,
@ -36,16 +39,16 @@ CREATE TABLE performances (
FOREIGN KEY (song_id) REFERENCES songs (id) ON DELETE CASCADE); FOREIGN KEY (song_id) REFERENCES songs (id) ON DELETE CASCADE);
-- Users table DROP TABLE IF EXISTS users;
CREATE TABLE users ( CREATE TABLE IF NOT EXISTS users (
id SERIAL, id SERIAL,
username VARCHAR(64) UNIQUE NOT NULL, username VARCHAR(64) UNIQUE NOT NULL,
pwd VARCHAR(80), pwd VARCHAR(80),
role VARCHAR(16) NOT NULL, role VARCHAR(16) NOT NULL,
PRIMARY KEY (id)); PRIMARY KEY (id));
-- Tokens table DROP TABLE IF EXISTS tokens;
CREATE TABLE tokens ( CREATE TABLE IF NOT EXISTS tokens (
token VARCHAR(64), token VARCHAR(64),
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
expires TIMESTAMP NOT NULL, expires TIMESTAMP NOT NULL,

View File

@ -0,0 +1,5 @@
DROP TABLE tokens;
DROP TABLE users;
DROP TABLE performances;
DROP TABLE songs;
DROP TABLE services;

View File

@ -65,11 +65,7 @@ table {
pre { margin-left: 1rem; } pre { margin-left: 1rem; }
h2 { h3 { margin: 1rem 0; }
border-bottom: solid 2px $dark;
margin-top: 2em; }
h3 { margin: 2rem 0 1rem 0; }
dl { dl {
margin: 1rem; margin: 1rem;
@ -79,20 +75,7 @@ table {
font-family: $monoFont; font-family: $monoFont;
font-weight: bold; } font-weight: bold; }
& > dd { padding: 0 0 0.5rem 1rem; } } & > dd { padding: 0 0 0.5rem 1rem; } } }
table.method-summary {
padding: 0 2rem;
width: 100%;
th {
border-bottom: solid thin $dark;
text-align: left; }
th.action, td.action { width: 6em; }
th.path, td.path { width: 17em; }
th.public, td.public { width: 4em; }
} }
@include forSize(notSmall) { @include forSize(notSmall) {
@ -126,7 +109,7 @@ table {
text-align: center; text-align: center;
& > h2 { display: none; } & > h2 { display: none; }
& > h2.song-name, & > h2.service-desc { display: block; } & > h2.song-name, & > h2.service-date { display: block; }
& > nav > ul > li { & > nav > ul > li {
display: inline-block; display: inline-block;

View File

@ -0,0 +1,121 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="referrer" content="origin">
<link rel="shortcut icon" href="../images/favicon.ico">
<title>API V1 - New Life Songs Database</title>
<link
href='http://fonts.googleapis.com/css?family=Roboto+Condensed|Cantarell|Anonymous+Pro' rel='stylesheet' type='text/css'>
<link href='../../../css/new-life-songs-@version@.css' rel='stylesheet' type='text/css'>
</head>
<body class=api-doc>
<header>
<h1><a href="../../../">New Life Songs API V1</a></h1>
<nav><ul>
<li><a href="../../../admin/">Admin</a></li>
<li><a href="../../../songs/">Songs</a></li>
<li><a href="../../../services/">Services</a></li>
</ul></nav>
</header>
<section id=api-overview>
The New Life Songs database exposes a REST API. This allows
programatic access and modification to the data. Version 1 of the
API defines several endpoints, all of which are built off of
<code>http://newlifesongs.jdbernard.com/api/v1</code> as a base
URL.
<p>Some of the service's endpoints require the client to authenticate
itself to the server. See the <a href="#authentication">section on
authentication</a> for details concerning authentication.
<p>The endpoints that the API defines are:
<ul><li><a href="#songs"><code>/songs</code></a></li>
<li><a href="#services"><code>/services</code></a></li>
<li><a href="#users"><code>/users</code></a></li></ul>
<p>If you run across any problems or have questions, feel free to send me an email at
<a href='&#109;&#97;&#105;&#108;&#116;&#111;&#58;&#106;&#100;&#98;&#101;&#114;&#110;&#97;&#114;&#100;&#64;&#103;&#109;&#97;&#105;&#108;&#46;&#99;&#111;&#109;'>&#106;&#100;&#98;&#101;&#114;&#110;&#97;&#114;&#100;&#64;&#103;&#109;&#97;&#105;&#108;&#46;&#99;&#111;&#109;</a>
</section>
<section id=songs>
<h2><code>/songs</code></h2>
<h3 id=song-object>Song object</h3>
A song object is defined with the following fields:
<dl><dt>id</dt>
<dd>An identifier unique to this song record among all song
records. <em>Type: integer</em></dd>
<dt>name</dt>
<dd>The name of the song. <em>Type: string</em></dd>
<dt>artists</dt>
<dd>A list of the artists known to have written or performed
this song. <em>Type: list of strings</em></dl>
<h4>Example</h4>
<pre>
{
"id":8,
"name":"Here I Am To Worship",
"artists":[
"Tim Hughes",
"Chris Tomlin",
"Michael W. Smith"
]
}</pre>
<ul class=method-list>
<li><h3><code>GET /songs</code></h3>
<p>Retrieve all songs.
<p><h4>Response</h4>
A list of <a href="song-object">song objects</a>
<h4>Example</h4>
<pre>
GET http://newlifesongs.jdbernard.com/api/v1/songs</pre>
<p><pre>
HTTP/1.1 200 OK
Content-Length: 5146
Content-Type: application/json
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Server: Jetty(6.1.25)
[{"id":1,"name":"Welcome Holy Spirit","artists":["Mark Condon"]},
{"id":3,"name":"Let's Sing Praises to our God","artists":["Traditional"]},
{"id":5,"name":"Blessed Assurance","artists":["Frances J. Crosby"]},
{"id":8,"name":"Here I Am To Worship","artists":["Tim Hughes", "Chris Tomlin", "Michael W. Smith"]},
{"id":12,"name":"Healer","artists":["Kari Jobe", "Hillsong"]},
{"id":15,"name":"I Am Free","artists":["Newsboys"]}]</pre></li>
<li><h3><code>POST /songs</code></h3>
<p>Create a new song record. In order to be allowed access to
this method, the request must be made with a valid
authentication token which belongs to a user with
administrative priviliges. See <a href="#authentication">Authentication</a>
for details.
<p><h4>Request Body</h4>
Must be a <a href="#song-object">song object</a>. The
<code>name</code> field is required. Any <code>id</code> passed
in with the request will be ignored.
<p><h4>Reponse</h4>
The newly-created song record.
<p><h4>Example</h4>
</section>
<section id=services>
<h2><code>/services</code></h2>
</section>
<section id=users>
<h2><code>/users</code></h2>
</section>
<section id=authentication>
<h2>Authentication</h2>
</section>
</body>
</html>

View File

@ -22,7 +22,7 @@ if (!service) { response.sendError(response.SC_NOT_FOUND); return }
<meta name="referrer" content="origin"> <meta name="referrer" content="origin">
<link rel="shortcut icon" href="../images/favicon.ico"> <link rel="shortcut icon" href="../images/favicon.ico">
<title><%= service.localDate.toString("yyyy-MM-dd") <title><%= service.@date.toString("yyyy-MM-dd")
%> (<%= service.serviceType.displayName %>) - New Life Songs Database</title> %> (<%= service.serviceType.displayName %>) - New Life Songs Database</title>
<script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script> <script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<!--<script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.2/underscore-min.js"></script>--> <!--<script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.2/underscore-min.js"></script>-->
@ -38,8 +38,8 @@ if (!service) { response.sendError(response.SC_NOT_FOUND); return }
<body> <body>
<header> <header>
<h1><a href="../">New Life Songs</a></h1> <h1><a href="../">New Life Songs</a></h1>
<h2 class=service-desc><%= service.localDate.toString("yyyy-MM-dd") %>: (<%= <h2 class=service-date><%= service.@date.toString("yyyy-MM-dd") %> (<%=
service.description ?: service.serviceType.displayName %>)</h2> service.serviceType.displayName %>)</h2>
<nav><ul> <nav><ul>
<li><a href="../admin/">Admin</a></li> <li><a href="../admin/">Admin</a></li>
@ -64,7 +64,7 @@ if (!service) { response.sendError(response.SC_NOT_FOUND); return }
<tbody> <tbody>
<% songsDB.findPerformancesForServiceId(service.id). <% songsDB.findPerformancesForServiceId(service.id).
collect { [perf: it, song: songsDB.findSong(it.songId)] }. collect { [perf: it, song: songsDB.findSong(it.songId)] }.
sort { it.song.name }.sort { it.perf.rank }.each { row -> %> sort { it.song.name }.each { row -> %>
<tr><td class=actions><a href="<%= NLSongsContext.makeUrl(service, row.song) %>"><i class="fa fa-download"></i></a></td> <tr><td class=actions><a href="<%= NLSongsContext.makeUrl(service, row.song) %>"><i class="fa fa-download"></i></a></td>
<td class=song-name><a href='../song/<%= row.song.id %>'><%= <td class=song-name><a href='../song/<%= row.song.id %>'><%=
row.song.name %></a></td> row.song.name %></a></td>

View File

@ -44,9 +44,8 @@ songsDB = NLSongsContext.songsDB
<tbody> <tbody>
<% songsDB.findAllServices().sort { it.date }.reverse().each { service -> %> <% songsDB.findAllServices().sort { it.date }.reverse().each { service -> %>
<tr><td class=date><a href="../service/<%= service.id %>"><%= <tr><td class=date><a href="../service/<%= service.id %>"><%=
service.localDate.toString("yyyy-MM-dd") %></a></td> service.@date.toString("yyyy-MM-dd") %></a></td>
<td class=service-type><%= service.description ?: <td class=service-type><%= service.serviceType.displayName %></td></tr><% } %>
service.serviceType.displayName %></td></tr><% } %>
</tbody> </tbody>
<!--<tfoot><tr> <!--<tfoot><tr>
<th class="dt-left">Date</th> <th class="dt-left">Date</th>
@ -57,8 +56,7 @@ songsDB = NLSongsContext.songsDB
<script type="application/javascript"> <script type="application/javascript">
window.onload = function() { \$("#services-table"). window.onload = function() { \$("#services-table").
dataTable({ "paging": false, dataTable({ "paging": false }); };
"order": [[0, "desc"]]}); };
</script> </script>
</body> </body>
</html> </html>

View File

@ -67,7 +67,7 @@ if (!song) { response.sendError(response.SC_NOT_FOUND); return }
sort { it.svc.date }.each { row -> %> sort { it.svc.date }.each { row -> %>
<tr><td class=actions><a href='<%= NLSongsContext.makeUrl(row.svc, song) %>'><i class="fa fa-download"></i></a></td> <tr><td class=actions><a href='<%= NLSongsContext.makeUrl(row.svc, song) %>'><i class="fa fa-download"></i></a></td>
<td class=performance-date><a href='../service/<%= row.svc.id %>'><%= <td class=performance-date><a href='../service/<%= row.svc.id %>'><%=
row.svc.localDate.toString("yyyy-MM-dd") %></a></td> row.svc.@date.toString("yyyy-MM-dd") %></a></td>
<td class=service-type><%= row.svc.serviceType.displayName %></td> <td class=service-type><%= row.svc.serviceType.displayName %></td>
<td class=not-small><%= row.perf.leader ?: "" %></td> <td class=not-small><%= row.perf.leader ?: "" %></td>
<td class=not-small><%= row.perf.pianist ?: "" %></td> <td class=not-small><%= row.perf.pianist ?: "" %></td>

View File

@ -5,7 +5,8 @@ import com.jdbernard.nlsongs.model.*
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
sdf = new SimpleDateFormat('yyyy-MM-dd') sdf = new SimpleDateFormat('yyyy-MM-dd')
hcfg = new HikariConfig("/home/jdbernard/projects/new-life-songs/src/main/webapp/WEB-INF/classes/datasource.properties") hcfg = new
HikariConfig("/home/jdbernard/projects/new-life-songs/src/main/webapp/WEB-INF/classes/datasource.properties")
makeService = { svcRow -> makeService = { svcRow ->
Service svc = new Service() Service svc = new Service()

View File

@ -1,5 +1,6 @@
package com.jdbernard.nlsongs.rest package com.jdbernard.nlsongs.rest
import com.jdbernard.net.HttpContext
import org.junit.Test import org.junit.Test
import org.junit.AfterClass import org.junit.AfterClass
import org.junit.BeforeClass import org.junit.BeforeClass

View File

@ -3,7 +3,6 @@ package com.jdbernard.nlsongs.service
import com.jdbernard.nlsongs.db.NLSongsDB import com.jdbernard.nlsongs.db.NLSongsDB
import com.jdbernard.nlsongs.model.* import com.jdbernard.nlsongs.model.*
import com.jdbernard.nlsongs.servlet.NLSongsContext import com.jdbernard.nlsongs.servlet.NLSongsContext
import com.jdblabs.dbmigrate.DbMigrate
import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource import com.zaxxer.hikari.HikariDataSource
@ -25,9 +24,8 @@ import org.slf4j.LoggerFactory
public class NLSongsDBTest { public class NLSongsDBTest {
static NLSongsDB songsDB static NLSongsDB songsDB;
static Sql sql static Sql sql
static DbMigrate dbmigrate
static Logger log = LoggerFactory.getLogger(NLSongsDBTest) static Logger log = LoggerFactory.getLogger(NLSongsDBTest)
def dateFormat def dateFormat
@ -63,23 +61,23 @@ public class NLSongsDBTest {
new Song(id: it[0], name: it[1], artists: it[2]) } new Song(id: it[0], name: it[1], artists: it[2]) }
this.performances = [ this.performances = [
[1, 1, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood', 1], [1, 1, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood'],
[1, 2, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood', 2], [1, 2, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood'],
[1, 3, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood', 3], [1, 3, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood'],
[2, 2, 'Trevor Delano', 'Connie Bernard', 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser', 'Rachel Wood', 1], [2, 2, 'Trevor Delano', 'Connie Bernard', 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser', 'Rachel Wood'],
[2, 3, 'Trevor Delano', 'Connie Bernard', 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser', 'Rachel Wood', 2], [2, 3, 'Trevor Delano', 'Connie Bernard', 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser', 'Rachel Wood'],
[2, 4, 'Trevor Delano', 'Connie Bernard', 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser', 'Rachel Wood', 3], [2, 4, 'Trevor Delano', 'Connie Bernard', 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser', 'Rachel Wood'],
[3, 1, 'Rachel Wood', 'Krista Hatcher', 'Jonathan Bernard', 'Jared Wood', 'Tony Bagliore', 'Rachel Wood', 0], [3, 1, 'Rachel Wood', 'Krista Hatcher', 'Jonathan Bernard', 'Jared Wood', 'Tony Bagliore', 'Rachel Wood'],
[3, 2, 'Rachel Wood', 'Krista Hatcher', 'Jonathan Bernard', 'Jared Wood', 'Tony Bagliore', 'Rachel Wood', 0], [3, 2, 'Rachel Wood', 'Krista Hatcher', 'Jonathan Bernard', 'Jared Wood', 'Tony Bagliore', 'Rachel Wood'],
[4, 3, 'Trevor Delano', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser', 'Rachel Wood', 0], [4, 3, 'Trevor Delano', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser', 'Rachel Wood'],
[5, 4, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Tony Bagliore', 'Rachel Wood', 1], [5, 4, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Tony Bagliore', 'Rachel Wood'],
[6, 1, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood', 1], [6, 1, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood'],
[7, 2, 'Trevor Delano', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood', 1], [7, 2, 'Trevor Delano', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood'],
[8, 3, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood', 1] ].collect { [8, 3, 'Jared Wood', null, 'Jonathan Bernard', 'Christian Thompson', 'Andrew Fraiser; Tony Bagliore', 'Rachel Wood'] ].collect {
new Performance(serviceId: it[0], songId: it[1], pianist: it[2], new Performance(serviceId: it[0], songId: it[1], pianist: it[2],
organist: it[3], bassist: it[4], drummer: it[5], organist: it[3], bassist: it[4], drummer: it[5],
guitarist: it[6], leader: it[7], rank: it[8]) } guitarist: it[6], leader: it[7]) }
} }
@BeforeClass @BeforeClass
@ -92,13 +90,8 @@ public class NLSongsDBTest {
HikariDataSource dataSource = new HikariDataSource(hcfg) HikariDataSource dataSource = new HikariDataSource(hcfg)
// Create NLSongsDB // Create NLSongsDB
NLSongsDBTest.songsDB = new NLSongsDB(dataSource) this.songsDB = new NLSongsDB(dataSource)
NLSongsDBTest.sql = new Sql(dataSource) this.sql = new Sql(dataSource)
// Setup our DB migration tool
NLSongsDBTest.dbmigrate = new DbMigrate(
migrationsDir: new File('src/main/sql'),
sql: NLSongsDBTest.sql)
// Set NLSongsContext // Set NLSongsContext
NLSongsContext.songsDB = songsDB } NLSongsContext.songsDB = songsDB }
@ -110,18 +103,18 @@ public class NLSongsDBTest {
@Before @Before
public void initData() { public void initData() {
// Create the DB schema // Get the DB Schema and test data.
dbmigrate.up() File createSchemaSql = new File("src/main/sql/create-tables.sql")
File testDataSql = new File("resources/test/testdb.init.sql")
// Create the DB Schema
sql.execute(createSchemaSql.text)
// Populate the DB with test data. // Populate the DB with test data.
File testDataSql = new File("resources/test/testdb.init.sql")
sql.execute(testDataSql.text) } sql.execute(testDataSql.text) }
@After
public void destroyData() {
dbmigrate.down(Integer.MAX_VALUE) }
/// ### Services /// ### Services
@Test public void shouldCreateService() { @Test public void shouldCreateService() {
def service = new Service( def service = new Service(
date: new Date(), serviceType: ServiceType.SUN_AM) date: new Date(), serviceType: ServiceType.SUN_AM)
@ -185,8 +178,8 @@ public class NLSongsDBTest {
assertCollectionsEqual( assertCollectionsEqual(
performances.findAll { it.serviceId != 1 }, performances.findAll { it.serviceId != 1 },
songsDB.findAllPerformances()) } songsDB.findAllPerformances()) }
/// ### Songs /// ### Songs
@Test public void shoudCreateSong() { @Test public void shoudCreateSong() {
def song = new Song(name: "Test Song", artists: ["Bob Sam"]) def song = new Song(name: "Test Song", artists: ["Bob Sam"])
def newSong = songsDB.create(song) def newSong = songsDB.create(song)
@ -254,10 +247,5 @@ public class NLSongsDBTest {
log.info("C2: $c2") log.info("C2: $c2")
assertEquals(c1.size(), c2.size()) assertEquals(c1.size(), c2.size())
c1.each {
def isPresent = c2.contains(it)
if (!isPresent) log.info("$it is not within $c2.")
assertTrue(isPresent) }
assertTrue(c1.every { c2.contains(it) }) } assertTrue(c1.every { c2.contains(it) }) }
} }

View File

@ -0,0 +1,32 @@
package com.jdbernard.nlsongs.db
public class GenerateQueries {
public static void main(String[] args) {
}
public static Map<String, Map<String, String> > generateQueries(String ddl) {
def tables = [:]
// Find the table definitions
String tableRegex = /(?ms)(?:CREATE TABLE (?:IF NOT EXISTS )?([^\s]+) \(([^\s]+);.+?)+/
ddl.eachMatch(tableRegex) { matchGroups ->
String tableName = matchGroups[1]
// Parse the column definitions.
// Create new record insert statements.
// Create insert queries.
// Create update queries.
// Create delete queries.
// Create ID lookup queries.
}
}

View File

@ -1,24 +0,0 @@
apply plugin: 'groovy'
apply plugin: 'application'
mainClassName = 'com.jdbernard.nlsongs.NLSongsUploader'
dependencies {
compile localGroovy()
compile 'ch.qos.logback:logback-classic:1.1.8'
compile 'ch.qos.logback:logback-core:1.1.8'
compile 'org.slf4j:slf4j-api:1.7.22'
compile 'com.impossibl.pgjdbc-ng:pgjdbc-ng:0.6'
compile 'com.zaxxer:HikariCP:2.5.1'
compile 'com.miglayout:miglayout-swing:5.0'
compile project(':service')
}
task writeVersionFile(
group: 'build',
description: 'Write the version to VERSION.txt') { doLast {
(new File("${buildDir}/classes/main/VERSION.txt")).text = version
} }
build.dependsOn writeVersionFile

View File

@ -1,37 +0,0 @@
package com.jdbernard.nlsongs
import groovy.beans.Bindable
import groovy.swing.SwingBuilder
import javax.swing.JFrame
import net.miginfocom.swing.MigLayout
public class NLSongsUploader {
public static final String VERSION =
NLSongsUploader.getResourceAsStream('/VERSION.txt').text
// GUI Elements (View)
SwingBuilder swing = new SwingBuilder()
JFrame rootFrame
public static void main(String[] args) { def inst = new NLSongsUploader() }
public NLSongsUploader() {
initGui()
rootFrame.show()
}
private void initGui() {
swing.edtBuilder {
this.rootFrame = frame(title: "New Life Songs Uploader ${VERSION}",
iconImages: [imageIcon('/icon.png').image],
preferredSize: [1024, 768], pack: true,
layout: new MigLayout("ins 0, fill"),
defaultCloseOperation: JFrame.EXIT_ON_CLOSE) {
}
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

6
version.properties Normal file
View File

@ -0,0 +1,6 @@
#
#Mon Mar 30 02:11:59 CDT 2015
major=2
version.release=true
minor=1
build=1