24 Commits
v2.0 ... v2.6

Author SHA1 Message Date
200b69b960 Updated build to use git versioning and extract shell utilities. 2016-12-17 23:24:38 -06:00
e02b465ada Update elastic beanstalk config for new environment. 2016-12-17 21:55:24 -06:00
cdaa29f07d Release v2.5 2016-12-17 21:50:33 -06:00
5ce29aa86e apply from not working in Gradle 3. Moved helper script into main build script. 2016-12-17 21:48:42 -06:00
fc5f29eaed fixup 2016-12-17 21:48:42 -06:00
4d89e45c7b service.@date -> service.getLocalDate because direct field accessor isn't working properly anymore. 2016-12-17 21:48:42 -06:00
a132f6540c Update code and build for deployment to ElasticBeanstalk. 2016-12-17 21:48:30 -06:00
409469c624 Remove ElasticBeanstalk from .gitignore. 2016-12-17 13:34:30 -06:00
e7eb82ceb6 Update dependencies and code to play nice. 2016-12-17 13:31:24 -06:00
b3ad5016fb Add ElasticBeanstalk configuration files. 2016-12-17 13:24:32 -06:00
c89668031c Updated .gitignore for elastic beanstalkfiles. 2016-12-16 22:33:04 -06:00
62f68a25a5 Build missing task declaration 2016-07-15 17:08:33 -05:00
3b77006381 Fixed bug in NLSongsDB implementation. 2015-12-11 13:59:04 -06:00
5e81284220 Fixed artists split in DB layer. Services sorted most recent first. 2015-07-22 09:26:53 -05:00
0e16d42eaf Increment minor version number. New work belongs on v2.4 2015-05-12 20:52:49 -05:00
58b00cbdb0 Migrated source documentation to doc.jdb-labs.com. 2015-05-12 20:51:58 -05:00
f551165a82 Increment minor version number. New work belongs on v2.3 2015-04-14 15:48:49 -05:00
e89b2e0a02 Added service descriptions.
Pages now use service descriptions to describe services, falling back on the
service type's displayable value if no description is given.
2015-04-14 15:13:21 -05:00
c7bee5009a Continued work on API documentation. 2015-04-10 19:14:30 -05:00
cad957394e Fleahed out documentation. 2015-04-09 21:47:48 -05:00
8eb7918f7f Added favicon in ICO format. 2015-03-30 21:07:17 -05:00
b7970f6af8 Increment minor version so new work is on v2.2 2015-03-30 02:17:49 -05:00
4580709d29 Version 2.1: Default to MP3s instead of OGGs for song files. 2015-03-30 02:15:06 -05:00
2bf0412629 Increment minor version so new work is on v2.1. 2015-03-23 04:16:09 -05:00
20 changed files with 381 additions and 213 deletions

View File

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

8
README.md Normal file
View File

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

View File

@ -1,42 +1,51 @@
import org.apache.tools.ant.filters.ReplaceTokens import org.apache.tools.ant.filters.ReplaceTokens
buildscript {
repositories {
maven { url 'https://mvn.jdb-labs.com/repo' }
}
dependencies {
classpath 'com.jdbernard:gradle-exec-util:0.2.0'
}
}
plugins { id 'com.palantir.git-version' version '0.5.2' }
apply plugin: "groovy" apply plugin: "groovy"
apply plugin: "maven" apply plugin: "maven"
apply plugin: "war" apply plugin: "war"
apply plugin: "jetty"
apply from: 'shell.gradle'
group = "com.jdbernard" group = "com.jdbernard"
version = new ProjectVersion() import static com.jdbernard.gradle.ExecUtil.*
version = gitVersion()
// webAppDirName = "build/webapp/main" // webAppDirName = "build/webapp/main"
repositories { repositories {
mavenLocal() mavenLocal()
mavenCentral() } mavenCentral()
}
dependencies { dependencies {
compile 'ch.qos.logback:logback-classic:1.1.2' compile 'ch.qos.logback:logback-classic:1.1.8'
compile 'ch.qos.logback:logback-core:1.1.2' compile 'ch.qos.logback:logback-core:1.1.8'
compile 'com.impossibl.pgjdbc-ng:pgjdbc-ng:0.3' compile 'com.impossibl.pgjdbc-ng:pgjdbc-ng:0.6'
compile 'com.lambdaworks:scrypt:1.4.0' compile 'com.lambdaworks:scrypt:1.4.0'
compile 'com.zaxxer:HikariCP-java6:2.3.2' compile 'com.zaxxer:HikariCP:2.5.1'
compile 'javax:javaee-api:7.0' compile 'javax:javaee-api:7.0'
compile 'javax.ws.rs:javax.ws.rs-api:2.0.1' compile 'javax.ws.rs:javax.ws.rs-api:2.0.1'
compile 'joda-time:joda-time:2.7' compile 'joda-time:joda-time:2.7'
compile 'org.codehaus.groovy:groovy-all:2.3.6' compile 'org.codehaus.groovy:groovy-all:2.4.7'
compile 'org.slf4j:slf4j-api:1.7.10' compile 'org.slf4j:slf4j-api:1.7.22'
runtime 'com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider:2.3.2' 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.containers:jersey-container-servlet:2.16'
runtime 'org.glassfish.jersey.media:jersey-media-json-jackson:2.16' runtime 'org.glassfish.jersey.media:jersey-media-json-jackson:2.16'
providedCompile 'javax.servlet:javax.servlet-api:3.1.0' providedCompile 'javax.servlet:javax.servlet-api:3.1.0'
testCompile 'com.jdbernard:jdb-util:3.4'
testCompile 'junit:junit:4.12' testCompile 'junit:junit:4.12'
testRuntime 'com.h2database:h2:1.4.186' testRuntime 'com.h2database:h2:1.4.186'
} }
war { war {
@ -44,7 +53,6 @@ war {
from "build/webapp" from "build/webapp"
filter(ReplaceTokens, tokens: [version: version]) filter(ReplaceTokens, tokens: [version: version])
rename '(.+)(\\..*(css|js))', '$1-' + version + '$2' rename '(.+)(\\..*(css|js))', '$1-' + version + '$2'
version = project.version.releaseVersion
webInf { from 'resources/main/WEB-INF' } webInf { from 'resources/main/WEB-INF' }
exclude "**/.*.swp", "**/.sass-cache" exclude "**/.*.swp", "**/.sass-cache"
} }
@ -55,7 +63,6 @@ task testWar(type: War) {
from 'resources/webapp' from 'resources/webapp'
filter(ReplaceTokens, tokens: [version: version]) filter(ReplaceTokens, tokens: [version: version])
rename '(.+)(\\..*(css|js))', '$1-' + version + '$2' rename '(.+)(\\..*(css|js))', '$1-' + version + '$2'
version = project.version.releaseVersion
webInf { from 'resources/test/WEB-INF' } webInf { from 'resources/test/WEB-INF' }
classifier 'test' } classifier 'test' }
@ -71,99 +78,15 @@ task compileScss(
war.dependsOn compileScss war.dependsOn compileScss
testWar.dependsOn compileScss testWar.dependsOn compileScss
// ## Build Versioning task task deployProd(dependsOn: ['build']) { doLast {
task incrementBuildNumber( def warName = "${project.name}-${version}.war"
group: 'versioning', def artifactName = "ROOT.war"
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 { copy {
from "build/libs" from "build/libs"
into "$jettyHome/webapps" into "build"
include warName } } include warName
rename warName, artifactName }
task killJettyLocal() << { exec("eb", "deploy", "-l", "${project.name}-${version}")
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,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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

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

View File

@ -27,10 +27,7 @@ public class NLSongsDB {
/// ### Common /// ### Common
public def save(def model) { public def save(def model) {
if (model.id > 0) return update(model) if (model.id > 0) return update(model)
else { else return create(model) }
if (create(model) > 0) return model
else return null } }
/// ### Services /// ### Services
public Service findService(int id) { public Service findService(int id) {
@ -315,7 +312,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

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

View File

@ -11,7 +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.@date.toString('yyyy-MM-dd') + '_' + return mediaBaseUrl + '/' + service.localDate.toString('yyyy-MM-dd') + '_' +
service.serviceType.name().toLowerCase() + '_' + service.serviceType.name().toLowerCase() + '_' +
song.name.replaceAll(/[\s'"\\\/\?!]/, '') + '.ogg' } song.name.replaceAll(/[\s'"\\\/\?!]/, '') + '.mp3' }
} }

View File

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

View File

@ -9,6 +9,7 @@ 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));

View File

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

View File

@ -21,8 +21,8 @@
</header> </header>
<section id=api-overview> <section id=api-overview>
The New Life Songs database exposes a REST API. This allows The New Life Songs database exposes a REST API. This allows
programatic access and modification to the data. Version 1 of the programatic access to and modification of the data. Version 1 of
API defines several endpoints, all of which are built off of the API defines several endpoints, all of which are built off of
<code>http://newlifesongs.jdbernard.com/api/v1</code> as a base <code>http://newlifesongs.jdbernard.com/api/v1</code> as a base
URL. URL.
@ -64,6 +64,46 @@
"Michael W. Smith" "Michael W. Smith"
] ]
}</pre> }</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> <ul class=method-list>
<li><h3><code>GET /songs</code></h3> <li><h3><code>GET /songs</code></h3>
@ -71,22 +111,22 @@
<p><h4>Response</h4> <p><h4>Response</h4>
A list of <a href="song-object">song objects</a> A list of <a href="song-object">song objects</a>
<h4>Example</h4> <p><h4>Example</h4>
<pre> <pre>
GET http://newlifesongs.jdbernard.com/api/v1/songs</pre> GET http://newlifesongs.jdbernard.com/api/v1/songs</pre>
<p><pre> <p><pre>
HTTP/1.1 200 OK HTTP/1.1 200 OK
Content-Length: 5146 Content-Length: 433
Content-Type: application/json Content-Type: application/json
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Server: Jetty(6.1.25)
[{"id":1,"name":"Welcome Holy Spirit","artists":["Mark Condon"]}, [{"id":1,"name":"Welcome Holy Spirit","artists":["Mark Condon"]},
{"id":3,"name":"Let's Sing Praises to our God","artists":["Traditional"]}, {"id":3,"name":"Let's Sing Praises to our God","artists":["Traditional"]},
{"id":5,"name":"Blessed Assurance","artists":["Frances J. Crosby"]}, {"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":8,"name":"Here I Am To Worship","artists":["Tim Hughes", "Chris Tomlin", "Michael W. Smith"]},
{"id":12,"name":"Healer","artists":["Kari Jobe", "Hillsong"]}, {"id":12,"name":"Healer","artists":["Kari Jobe", "Hillsong"]},
{"id":15,"name":"I Am Free","artists":["Newsboys"]}]</pre></li> {"id":15,"name":"I Am Free","artists":["Newsboys"]}]
</pre></li>
<li><h3><code>POST /songs</code></h3> <li><h3><code>POST /songs</code></h3>
@ -102,13 +142,203 @@ Server: Jetty(6.1.25)
in with the request will be ignored. in with the request will be ignored.
<p><h4>Reponse</h4> <p><h4>Reponse</h4>
The newly-created song record. 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> <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>
<section id=services> <section id=services>
<h2><code>/services</code></h2> <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>
<section id=users> <section id=users>
<h2><code>/users</code></h2> <h2><code>/users</code></h2>

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.@date.toString("yyyy-MM-dd") <title><%= service.localDate.toString("yyyy-MM-dd")
%> (<%= service.serviceType.displayName %>) - New Life Songs Database</title> %> (<%= service.serviceType.displayName %>) - New Life Songs Database</title>
<script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script> <script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<!--<script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.2/underscore-min.js"></script>--> <!--<script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.2/underscore-min.js"></script>-->
@ -38,8 +38,8 @@ if (!service) { response.sendError(response.SC_NOT_FOUND); return }
<body> <body>
<header> <header>
<h1><a href="../">New Life Songs</a></h1> <h1><a href="../">New Life Songs</a></h1>
<h2 class=service-date><%= service.@date.toString("yyyy-MM-dd") %> (<%= <h2 class=service-desc><%= service.localDate.toString("yyyy-MM-dd") %>: (<%=
service.serviceType.displayName %>)</h2> service.description ?: service.serviceType.displayName %>)</h2>
<nav><ul> <nav><ul>
<li><a href="../admin/">Admin</a></li> <li><a href="../admin/">Admin</a></li>

View File

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

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.@date.toString("yyyy-MM-dd") %></a></td> row.svc.localDate.toString("yyyy-MM-dd") %></a></td>
<td class=service-type><%= row.svc.serviceType.displayName %></td> <td class=service-type><%= row.svc.serviceType.displayName %></td>
<td class=not-small><%= row.perf.leader ?: "" %></td> <td class=not-small><%= row.perf.leader ?: "" %></td>
<td class=not-small><%= row.perf.pianist ?: "" %></td> <td class=not-small><%= row.perf.pianist ?: "" %></td>

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
# #
#Mon Mar 23 03:03:08 CDT 2015 #Sat Dec 17 21:52:48 CST 2016
major=2 major=2
version.release=true version.release=true
minor=0 minor=5
build=101 build=2