Skip to content

Commit

Permalink
Back end endpoints for MetaData (#397)
Browse files Browse the repository at this point in the history
* Add paths for jobcodes

* Add paths for named references

* Enable EnableGlobalMethodSecurity
  • Loading branch information
manuel-delvillar authored May 31, 2023
1 parent fedf594 commit 9f4aea8
Show file tree
Hide file tree
Showing 11 changed files with 437 additions and 0 deletions.
15 changes: 15 additions & 0 deletions api/src/main/kotlin/edu/wgu/osmt/RoutePaths.kt
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,21 @@ object RoutePaths {
const val ES_ADMIN_DELETE_INDICES = "$ES_ADMIN/delete-indices"
const val ES_ADMIN_REINDEX = "$ES_ADMIN/reindex"

const val METADATA_PATH = "$API/metadata"
const val JOB_CODE_PATH = "$METADATA_PATH/jobcodes"
const val JOB_CODE_CREATE = JOB_CODE_PATH
const val JOB_CODE_LIST = JOB_CODE_PATH
const val JOB_CODE_DETAIL = "$JOB_CODE_PATH/{id}"
const val JOB_CODE_UPDATE = "$JOB_CODE_DETAIL/update"
const val JOB_CODE_REMOVE = "$JOB_CODE_DETAIL/remove"

const val NAMED_REFERENCES_PATH = "$METADATA_PATH/named-references"
const val NAMED_REFERENCES_CREATE = NAMED_REFERENCES_PATH
const val NAMED_REFERENCES_LIST = NAMED_REFERENCES_PATH
const val NAMED_REFERENCES_DETAIL = "$NAMED_REFERENCES_PATH/{id}"
const val NAMED_REFERENCES_UPDATE = "$NAMED_REFERENCES_DETAIL/update"
const val NAMED_REFERENCES_REMOVE = "$NAMED_REFERENCES_DETAIL/remove"

object QueryParams {
const val FROM = "from"
const val SIZE = "size"
Expand Down
20 changes: 20 additions & 0 deletions api/src/main/kotlin/edu/wgu/osmt/api/model/JobCodeUpdate.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package edu.wgu.osmt.api.model

import com.fasterxml.jackson.annotation.JsonProperty
import edu.wgu.osmt.db.JobCodeLevel

data class JobCodeUpdate(
@JsonProperty("code")
val code: String,
@JsonProperty("targetNode")
val targetNode: String? = null,
@JsonProperty("targetNodeName")
val targetNodeName: String? = null,
@JsonProperty("frameworkName")
val framework: String? = null,
@JsonProperty("level")
val level: JobCodeLevel? = null,
@JsonProperty("parents")
val parents: List<JobCodeUpdate>? = null
) {
}
82 changes: 82 additions & 0 deletions api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package edu.wgu.osmt.jobcode;

import edu.wgu.osmt.RoutePaths
import edu.wgu.osmt.api.model.ApiJobCode
import edu.wgu.osmt.api.model.JobCodeUpdate
import edu.wgu.osmt.elasticsearch.OffsetPageable
import edu.wgu.osmt.task.TaskResult
import edu.wgu.osmt.task.TaskStatus
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.HttpEntity
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.stereotype.Controller
import org.springframework.transaction.annotation.Transactional
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.RequestBody
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.server.ResponseStatusException

@Controller
@Transactional
class JobCodeController @Autowired constructor(
val jobCodeEsRepo: JobCodeEsRepo,
val jobCodeRepository: JobCodeRepository
) {

@GetMapping(RoutePaths.JOB_CODE_LIST, produces = [MediaType.APPLICATION_JSON_VALUE])
@PreAuthorize("isAuthenticated()")
fun allPaginated(
@RequestParam(required = true) size: Int,
@RequestParam(required = true) from: Int,
@RequestParam(required = false) sort: String?
): HttpEntity<List<ApiJobCode>> {
val searchResults = jobCodeEsRepo.typeAheadSearch("", OffsetPageable(from, size, null))
return ResponseEntity.status(200).body(searchResults.map { ApiJobCode.fromJobCode(it.content) }.toList())
}

@GetMapping(RoutePaths.JOB_CODE_DETAIL, produces = [MediaType.APPLICATION_JSON_VALUE])
@PreAuthorize("isAuthenticated()")
fun byId(
@PathVariable id: Long,
): HttpEntity<ApiJobCode> {
val jobCode = jobCodeRepository.findById(id)
if (jobCode != null) {
return ResponseEntity.status(200).body(ApiJobCode.fromJobCode(jobCode.toModel()))
} else {
throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
}

@PostMapping(RoutePaths.JOB_CODE_CREATE, produces = [MediaType.APPLICATION_JSON_VALUE])
@PreAuthorize("hasAuthority(@appConfig.roleAdmin)")
fun createJobCode(
@RequestBody jobCodes: List<JobCodeUpdate>
): HttpEntity<List<ApiJobCode>> {
val newJobCodes = jobCodeRepository.createFromApi(jobCodes)
return ResponseEntity.status(200).body(newJobCodes.map { ApiJobCode.fromJobCode(it.toModel()) }.toList())
}

@PostMapping(RoutePaths.JOB_CODE_UPDATE, produces = [MediaType.APPLICATION_JSON_VALUE])
@PreAuthorize("hasAuthority(@appConfig.roleAdmin)")
fun updateJobCode(
@PathVariable id: Int,
@RequestBody jobCodeUpdate: JobCodeUpdate
): HttpEntity<ApiJobCode> {
return ResponseEntity.status(200).body(ApiJobCode(code = "1", targetNode = "target", targetNodeName = "targetNodeName", frameworkName = "frameworkName", parents = listOf()))
}

@DeleteMapping(RoutePaths.JOB_CODE_REMOVE)
@PreAuthorize("hasAuthority(@appConfig.roleAdmin)")
fun deleteJobCode(
@PathVariable id: Int,
): HttpEntity<TaskResult> {
return ResponseEntity.status(200).body(TaskResult(uuid = "uuid", contentType = "application/json", status = TaskStatus.Processing, apiResultPath = "path"))
}

}
11 changes: 11 additions & 0 deletions api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeEsRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ interface CustomJobCodeRepository {
val elasticSearchTemplate: ElasticsearchRestTemplate
fun typeAheadSearch(query: String): SearchHits<JobCode>

fun typeAheadSearch(query: String, pageable: OffsetPageable): SearchHits<JobCode>

fun deleteIndex() {
elasticSearchTemplate.indexOps(IndexCoordinates.of(INDEX_JOBCODE_DOC)).delete()
}
Expand All @@ -28,6 +30,15 @@ interface CustomJobCodeRepository {
class CustomJobCodeRepositoryImpl @Autowired constructor(override val elasticSearchTemplate: ElasticsearchRestTemplate) :
CustomJobCodeRepository {

override fun typeAheadSearch(query: String, pageable: OffsetPageable): SearchHits<JobCode> {
val nsq: NativeSearchQueryBuilder
val disjunctionQuery = JobCodeQueries.multiPropertySearch(query)
nsq =
NativeSearchQueryBuilder().withPageable(pageable).withQuery(disjunctionQuery)
.withSort(SortBuilders.fieldSort("${JobCode::code.name}.keyword").order(SortOrder.ASC))
return elasticSearchTemplate.search(nsq.build(), JobCode::class.java)
}

override fun typeAheadSearch(query: String): SearchHits<JobCode> {
val nsq: NativeSearchQueryBuilder

Expand Down
15 changes: 15 additions & 0 deletions api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeRepository.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package edu.wgu.osmt.jobcode

import edu.wgu.osmt.api.model.JobCodeUpdate
import org.jetbrains.exposed.sql.SizedIterable
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.and
Expand All @@ -19,6 +20,7 @@ interface JobCodeRepository {
fun findByCodeOrCreate(code: String, framework: String? = null): JobCodeDao
fun findBlsCode(code: String): JobCodeDao?
fun create(code: String, framework: String? = null): JobCodeDao
fun createFromApi(jobCodes: List<JobCodeUpdate>): List<JobCodeDao>
fun onetsByDetailCode(detailedCode: String): SizedIterable<JobCodeDao>

companion object {
Expand Down Expand Up @@ -52,6 +54,19 @@ class JobCodeRepositoryImpl: JobCodeRepository {
.firstOrNull()?.let { dao.wrapRow(it) }
}

override fun createFromApi(jobCodes: List<JobCodeUpdate>): List<JobCodeDao> {
return jobCodes.map { jobCodeUpdate ->
dao.new {
this.code = jobCodeUpdate.code
this.framework = jobCodeUpdate.framework
this.name = jobCodeUpdate.targetNodeName
this.creationDate = LocalDateTime.now(ZoneOffset.UTC)
this.name = "my name"
this.major = "my major"
}.also { jobCodeEsRepo.save(it.toModel()) }
}
}

override fun findByCodeOrCreate(code: String, framework: String?): JobCodeDao {
val existing = findByCode(code)
return existing ?: create(code, framework)
Expand Down
31 changes: 31 additions & 0 deletions api/src/main/kotlin/edu/wgu/osmt/keyword/ApiKeyword.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package edu.wgu.osmt.keyword

import com.fasterxml.jackson.annotation.JsonProperty

data class ApiKeywordUpdate(
@JsonProperty("name")
val name: String?,
@JsonProperty("value")
val value: String?,
@JsonProperty("type")
val type: KeywordTypeEnum,
@JsonProperty("framework")
val framework: String?
) {
}

data class NamedReference(
val id: Long?,
val name: String?,
val value: String?,
val type: KeywordTypeEnum,
val framework: String?
) {

companion object factory {
fun fromKeyword(keyword: Keyword): NamedReference {
return NamedReference(keyword.id, keyword.value, keyword.value, keyword.type, keyword.framework)
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package edu.wgu.osmt.keyword

import edu.wgu.osmt.PaginationDefaults
import edu.wgu.osmt.RoutePaths
import edu.wgu.osmt.task.TaskResult
import edu.wgu.osmt.task.TaskStatus
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.HttpEntity
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.stereotype.Controller
import org.springframework.transaction.annotation.Transactional
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.RequestBody
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.server.ResponseStatusException

@Controller
@Transactional
class NamedReferencesController @Autowired constructor(
val keywordEsRepo: KeywordEsRepo,
val keywordRepository: KeywordRepository
) {

@GetMapping(RoutePaths.NAMED_REFERENCES_LIST, produces = [MediaType.APPLICATION_JSON_VALUE])
@PreAuthorize("isAuthenticated()")
fun allPaginated(
@RequestParam(required = true) type: String,
@RequestParam(required = false, defaultValue = PaginationDefaults.size.toString()) size: Int,
@RequestParam(required = false, defaultValue = "0") from: Int,
@RequestParam(required = false) sort: String?
): HttpEntity<List<NamedReference>> {
val keywordType = KeywordTypeEnum.forApiValue(type) ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST)
val searchResults = keywordEsRepo.typeAheadSearch("", keywordType)
return ResponseEntity.status(200).body(searchResults.map { NamedReference.fromKeyword(it.content) }.toList())
}

@GetMapping(RoutePaths.NAMED_REFERENCES_DETAIL, produces = [MediaType.APPLICATION_JSON_VALUE])
@PreAuthorize("isAuthenticated()")
fun byId(
@PathVariable id: Long,
): HttpEntity<NamedReference> {
val keyword = keywordRepository.findById(id)
if (keyword != null) {
return ResponseEntity.status(200).body(NamedReference.fromKeyword(keyword.toModel()))
} else {
throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
}

@PostMapping(RoutePaths.NAMED_REFERENCES_CREATE, produces = [MediaType.APPLICATION_JSON_VALUE])
@PreAuthorize("hasAuthority(@appConfig.roleAdmin)")
fun createNamedReference(
@RequestBody keywords: List<ApiKeywordUpdate>
): HttpEntity<List<NamedReference>> {
return ResponseEntity.status(200).body(
keywords.map {
NamedReference(id = 1, name = it.name, value = it.value, type = it.type, framework = it.framework)
}
)
}

@PostMapping(RoutePaths.NAMED_REFERENCES_UPDATE, produces = [MediaType.APPLICATION_JSON_VALUE])
@PreAuthorize("hasAuthority(@appConfig.roleAdmin)")
fun updateNamedReference(
@PathVariable id: Int,
@RequestBody apiKeyword: ApiKeywordUpdate
): HttpEntity<NamedReference> {
return ResponseEntity.status(200).body(NamedReference(134, "my name", "my value", KeywordTypeEnum.Keyword, "my framework"))
}

@DeleteMapping(RoutePaths.NAMED_REFERENCES_REMOVE)
@PreAuthorize("hasAuthority(@appConfig.roleAdmin)")
fun deleteNamedReference(
@PathVariable id: Int,
): HttpEntity<TaskResult> {
return ResponseEntity.status(200).body(TaskResult(uuid = "uuid", contentType = "application/json", status = TaskStatus.Processing, apiResultPath = "path"))
}

}
2 changes: 2 additions & 0 deletions api/src/main/kotlin/edu/wgu/osmt/security/SecurityConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Profile
import org.springframework.http.HttpMethod.*
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
Expand All @@ -58,6 +59,7 @@ import javax.servlet.http.HttpServletResponse
@Configuration
@EnableWebSecurity
@Profile("oauth2-okta | OTHER-OAUTH-PROFILE")
@EnableGlobalMethodSecurity(prePostEnabled = true)
class SecurityConfig : WebSecurityConfigurerAdapter() {

@Autowired
Expand Down
Loading

0 comments on commit 9f4aea8

Please sign in to comment.