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:
@ -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) }) }
|
||||
}
|
Reference in New Issue
Block a user