applications-tracker/api/src/main/kotlin/com/andr3h3nriqu3s/applications/ApplicationsController.kt

654 lines
22 KiB
Kotlin

package com.andr3h3nriqu3s.applications
import java.sql.ResultSet
import java.text.SimpleDateFormat
import java.util.Date
import java.util.UUID
import kotlin.collections.emptyList
import kotlin.collections.setOf
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.jdbc.core.RowMapper
import org.springframework.stereotype.Service
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestHeader
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.server.ResponseStatusException
data class Application(
var id: String,
var url: String,
var original_url: String?,
var unique_url: String?,
var title: String,
var user_id: String,
var extra_data: String,
var payrange: String,
var status: Int,
var company: String,
var recruiter: String,
var message: String,
var linked_application: String,
var status_history: String,
var application_time: String,
var create_time: String,
var flairs: List<Flair>,
var views: List<View>,
var events: List<Event>,
) {
companion object : RowMapper<Application> {
override public fun mapRow(rs: ResultSet, rowNum: Int): Application {
return Application(
rs.getString("id"),
rs.getString("url"),
rs.getString("original_url"),
rs.getString("unique_url"),
rs.getString("title"),
rs.getString("user_id"),
rs.getString("extra_data"),
rs.getString("payrange"),
rs.getInt("status"),
rs.getString("company"),
rs.getString("recruiter"),
rs.getString("message"),
rs.getString("linked_application"),
rs.getString("status_history"),
rs.getString("application_time"),
rs.getString("create_time"),
emptyList(),
emptyList(),
emptyList(),
)
}
}
}
data class SubmitRequest(val text: String)
data class ListRequest(val status: Int? = null, val views: Boolean? = null)
data class StatusRequest(val id: String, val status: Int)
data class FlairRequest(val id: String, val text: String)
data class UpdateUrl(val id: String, val url: String)
data class CVData(
val company: String,
val recruiter: String,
val message: String,
val flairs: List<SimpleFlair>
)
@RestController
@ControllerAdvice
@RequestMapping("/api/application")
class ApplicationsController(
val sessionService: SessionService,
val applicationService: ApplicationService,
val flairService: FlairService,
val viewService: ViewService,
val eventService: EventService,
) {
@GetMapping(path = ["/cv/{id}"], produces = [MediaType.APPLICATION_JSON_VALUE])
public fun getCV(@PathVariable id: String, @RequestHeader("token") token: String?): CVData? {
val user = sessionService.verifyToken(token)
val application = applicationService.findApplicationByIdNoUser(id)
if (application == null) return null
if (user == null) {
eventService.create(application.id, EventType.View)
}
val flairs = application.flairs.map { it.toFlairSimple() }
return CVData(application.company, application.recruiter, application.message, flairs)
}
/** Create a new application from the link */
@PostMapping(path = ["/link"], produces = [MediaType.APPLICATION_JSON_VALUE])
public fun submitLink(
@RequestBody submit: SubmitRequest,
@RequestHeader("token") token: String
): Application {
val user = sessionService.verifyTokenThrow(token)
var application =
Application(
UUID.randomUUID().toString(),
submit.text,
submit.text,
submit.text,
"New Application",
user.id,
"",
"",
0,
"",
"",
"",
"",
"",
"",
"",
emptyList(),
emptyList(),
emptyList(),
)
if (!applicationService.createApplication(user, application)) {
throw ResponseStatusException(HttpStatus.CONFLICT, "Application already exists", null)
}
print("Created: ")
println(application)
return application
}
@PostMapping(path = ["/text/flair"], produces = [MediaType.APPLICATION_JSON_VALUE])
public fun textFlair(
@RequestBody info: FlairRequest,
@RequestHeader("token") token: String
): Int {
val user = sessionService.verifyTokenThrow(token)
val application = applicationService.findApplicationById(user, info.id)
if (application == null) {
throw ResponseStatusException(HttpStatus.NOT_FOUND, "Application not found", null)
}
val flairs = flairService.listUser(user)
var count = 0
for (flair: Flair in flairs) {
val regex =
Regex(
".*" + flair.expr + ".*",
setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL)
)
if (regex.matches(info.text)) {
count += 1
flairService.linkFlair(application, flair)
}
}
return count
}
@PostMapping(path = ["/text"], produces = [MediaType.APPLICATION_JSON_VALUE])
public fun submitText(
@RequestBody submit: SubmitRequest,
@RequestHeader("token") token: String
): Int {
val user = sessionService.verifyTokenThrow(token)
var text = submit.text.replace("=\n", "")
var urls: List<String> = emptyList()
while (true) {
var index = text.indexOf("href")
if (index == -1) {
break
}
var new_url = StringBuilder()
var found_start = false
while (true) {
if (found_start) {
if (text[index] == '"') {
break
}
new_url.append(text[index])
} else if (text[index] == '"') {
found_start = true
}
index++
}
text = text.substring(index)
urls = urls.plus(new_url.toString().replace("&amp;", "&").replace("=3D", "="))
}
print("found: ")
print(urls.size)
print(" links\n")
// jobListing is for glassdoor urls
// jobs/view is for linkeding urls
urls =
urls.filter { predicate ->
print("url: ")
print(predicate)
print("\n")
predicate.contains("jobListing") || predicate.contains("jobs/view")
}
print("found fileted: ")
print(urls.size)
print(" links\n")
urls = urls.toSet().toList()
print("removed duplicates: ")
print(urls.size)
print(" links\n")
var applications =
urls.map { elm ->
Application(
UUID.randomUUID().toString(),
if (elm.contains("linkedin")) elm.split("?")[0] else elm,
if (elm.contains("linkedin")) elm.split("?")[0] else null,
if (elm.contains("linkedin")) elm.split("?")[0] else null,
"New Application",
user.id,
"",
"",
0,
"",
"",
"",
"",
"",
"",
"",
emptyList(),
emptyList(),
emptyList(),
)
}
applications =
applications.filter { elm -> applicationService.createApplication(user, elm) }
print("created new: ")
print(applications.size)
print(" links\n")
return applications.size
}
@PostMapping(path = ["/list"], produces = [MediaType.APPLICATION_JSON_VALUE])
public fun list(
@RequestBody info: ListRequest,
@RequestHeader("token") token: String
): List<Application> {
val user = sessionService.verifyTokenThrow(token)
return applicationService.findAll(user, info)
}
@GetMapping(path = ["/active"], produces = [MediaType.APPLICATION_JSON_VALUE])
public fun active(@RequestHeader("token") token: String): Application? {
val user = sessionService.verifyTokenThrow(token)
val possibleApplications = applicationService.findAll(user, ListRequest(1))
if (possibleApplications.size == 0) {
return null
}
return applicationService.findApplicationById(user, possibleApplications[0].id)
}
@PutMapping(path = ["/status"], produces = [MediaType.APPLICATION_JSON_VALUE])
public fun status(
@RequestBody info: StatusRequest,
@RequestHeader("token") token: String,
): Application {
val user = sessionService.verifyTokenThrow(token)
var application = applicationService.findApplicationById(user, info.id)
if (application == null) {
throw NotFound()
}
if (application.status == info.status) {
return application;
}
application.status = info.status
applicationService.updateStatus(application)
return application
}
@PutMapping(path = ["/update"], produces = [MediaType.APPLICATION_JSON_VALUE])
public fun update(
@RequestBody info: Application,
@RequestHeader("token") token: String
): Application {
val user = sessionService.verifyTokenThrow(token)
var application = applicationService.findApplicationById(user, info.id)
if (application == null) {
throw NotFound()
}
applicationService.update(info)
return info
}
@PostMapping(path = ["/update/url"], produces = [MediaType.APPLICATION_JSON_VALUE])
public fun updateUrl(
@RequestBody info: UpdateUrl,
@RequestHeader("token") token: String
): Application {
val user = sessionService.verifyTokenThrow(token)
var application = applicationService.findApplicationById(user, info.id)
if (application == null) {
throw ResponseStatusException(HttpStatus.NOT_FOUND, "Application not found", null)
}
if (application.unique_url != null) {
throw ResponseStatusException(
HttpStatus.BAD_REQUEST,
"Application already has unique_url",
null
)
}
application.original_url = application.url
application.url = info.url
application.unique_url = info.url.split("?")[0]
var maybe_exists =
applicationService.findApplicationByUrl(
user,
application.url,
application.unique_url
)
if (maybe_exists != null) {
applicationService.delete(application)
if (maybe_exists.status == 0 && application.status == 1) {
maybe_exists.status = 1
applicationService.update(maybe_exists)
}
maybe_exists.flairs = flairService.listFromLinkApplicationId(maybe_exists.id)
return maybe_exists
}
applicationService.update(application)
application.flairs = flairService.listFromLinkApplicationId(application.id)
application.views = viewService.listFromApplicationId(application.id)
return application
}
@PostMapping(path = ["/reset/url/{id}"], produces = [MediaType.APPLICATION_JSON_VALUE])
public fun updateUrl(
@PathVariable id: String,
@RequestHeader("token") token: String
): Application {
val user = sessionService.verifyTokenThrow(token)
var application = applicationService.findApplicationById(user, id)
if (application == null) {
throw NotFound()
}
if (application.unique_url == null) {
throw BadRequest()
}
application.url = application.original_url!!
application.original_url = null
application.unique_url = null
applicationService.update(application)
application.flairs = flairService.listFromLinkApplicationId(application.id)
return application
}
@DeleteMapping(path = ["/flair/{id}/{flairid}"], produces = [MediaType.APPLICATION_JSON_VALUE])
public fun delete(
@PathVariable id: String,
@PathVariable flairid: String,
@RequestHeader("token") token: String
): Application {
val user = sessionService.verifyTokenThrow(token)
val application = applicationService.findApplicationById(user, id)
if (application == null) {
throw NotFound()
}
flairService.unlinkFlair(id, flairid)
return applicationService.findApplicationById(user, id)!!
}
@DeleteMapping(path = ["/{id}"], produces = [MediaType.APPLICATION_JSON_VALUE])
public fun delete(
@PathVariable id: String,
@RequestHeader("token") token: String
): Application {
val user = sessionService.verifyTokenThrow(token)
val application = applicationService.findApplicationById(user, id)
if (application == null) {
throw NotFound()
}
applicationService.delete(application)
return application
}
}
@Service
class ApplicationService(
val db: JdbcTemplate,
val flairService: FlairService,
val viewService: ViewService,
val eventService: EventService,
) {
public fun findApplicationByUrl(user: UserDb, url: String, unique_url: String?): Application? {
if (unique_url != null) {
val unique: List<Application> =
db.query(
"select * from applications where unique_url=? and user_id=?",
arrayOf(unique_url, user.id),
Application
)
.toList()
if (unique.size != 0) {
return unique[0]
}
}
val applications: List<Application> =
db.query(
"select * from applications where url=? and user_id=?",
arrayOf(url, user.id),
Application
)
.toList()
if (applications.size == 0) {
return null
}
return applications[0]
}
public fun findApplicationById(user: UserDb, id: String): Application? {
var applications =
db.query(
"select * from applications where id=? and user_id=?",
arrayOf(id, user.id),
Application
)
.toList()
if (applications.size == 0) {
return null
}
var application = applications[0]
application.flairs = flairService.listFromLinkApplicationId(application.id)
application.views = viewService.listFromApplicationId(application.id)
application.events = eventService.listFromApplicationId(application.id).toList()
return application
}
public fun findApplicationByIdNoUser(id: String): Application? {
var applications =
db.query("select * from applications where id=?", arrayOf(id), Application).toList()
if (applications.size == 0) {
return null
}
var application = applications[0]
// Views / Events are not needed for this request
application.flairs = flairService.listFromLinkApplicationId(application.id)
return application
}
public fun createApplication(user: UserDb, application: Application): Boolean {
if (this.findApplicationByUrl(user, application.url, application.unique_url) != null) {
return false
}
// Create time is auto created by the database
db.update(
"insert into applications (id, url, original_url, unique_url, title, user_id, extra_data, payrange, status, company, recruiter, message, linked_application, status_history, application_time) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
application.id,
application.url,
application.original_url,
application.unique_url,
application.title,
application.user_id,
application.extra_data,
application.payrange,
application.status,
application.company,
application.recruiter,
application.message,
application.linked_application,
application.status_history,
application.application_time,
)
eventService.create(application.id, EventType.Creation)
return true
}
private fun internalFindAll(user: UserDb, info: ListRequest): Iterable<Application> {
if (info.status == null) {
return db.query(
"select * from applications where user_id=? order by title asc;",
arrayOf(user.id),
Application
)
}
// If it's to apply also remove the linked_application to only show the main
if (info.status == 0) {
return db.query(
"select * from applications where user_id=? and linked_application='' and status=0 order by title asc;",
arrayOf(user.id),
Application
)
}
return db.query(
"select * from applications where user_id=? and status=? order by title asc;",
arrayOf(user.id, info.status),
Application,
)
}
public fun findAll(user: UserDb, info: ListRequest): List<Application> {
var iter = internalFindAll(user, info);
if (info.views == true) {
iter = iter.map {
it.views = viewService.listFromApplicationId(it.id)
it
}
}
return iter.toList()
}
// Update the stauts on the application object before giving it to this function
public fun updateStatus(application: Application): Application {
val status_string = "${application.status}"
var status_history = application.status_history.split(",").filter { it.length >= 1 }
if (status_history.indexOf(status_string) == -1) {
status_history = status_history.plus(status_string)
}
application.status_history = status_history.joinToString(",") { it }
if (application.status == 4) {
val sdf = SimpleDateFormat("dd/MM/yyyy hh:mm:ss")
application.application_time = sdf.format(Date())
}
eventService.create(application.id, EventType.StatusUpdate, application.status)
db.update(
"update applications set status=?, status_history=?, application_time=? where id=?",
application.status,
application.status_history,
application.application_time,
application.id,
)
return application
}
// Note this does not update status
public fun update(application: Application): Application {
// I don't want ot update create_time
db.update(
"update applications set url=?, original_url=?, unique_url=?, title=?, user_id=?, extra_data=?, payrange=?, company=?, recruiter=?, message=?, linked_application=? where id=?",
application.url,
application.original_url,
application.unique_url,
application.title,
application.user_id,
application.extra_data,
application.payrange,
application.company,
application.recruiter,
application.message,
application.linked_application,
application.id,
)
return application
}
public fun delete(application: Application) {
db.update(
"delete from applications where id=?",
application.id,
)
}
}