diff --git a/server/grails-app/controllers/org/gokb/AdminController.groovy b/server/grails-app/controllers/org/gokb/AdminController.groovy index 78acdb55f..c9e7cf52f 100644 --- a/server/grails-app/controllers/org/gokb/AdminController.groovy +++ b/server/grails-app/controllers/org/gokb/AdminController.groovy @@ -9,7 +9,7 @@ import org.hibernate.criterion.CriteriaSpecification import org.springframework.security.access.annotation.Secured import org.springframework.security.acls.domain.BasePermission -import java.time.LocalDateTime +import java.time.* import java.util.concurrent.CancellationException import grails.gorm.transactions.* @@ -477,9 +477,9 @@ class AdminController { def cacheSinglePackage() { log.debug("Manual package caching for ID ${params.id}") - def result = [params: params, result: null] + Map result = [params: params, result: null] - def pkg = Package.findByUuid(params.id) + Package pkg = Package.findByUuid(params.id) if (!pkg && params.long('id')) { pkg = Package.get(params.long('id')) @@ -503,8 +503,8 @@ class AdminController { def deduplicatePackageTipps() { log.debug("Manual TIPP deduplication for ID ${params.id}") - def result = [params: params, result: null] - def pkgId = params.int('id') ?: null + Map result = [params: params, result: null] + Long pkgId = params.long('id') ?: null if (pkgId) { Job j = concurrencyManagerService.createJob { job -> @@ -519,6 +519,40 @@ class AdminController { render(view: "logViewer", model: logViewer()) } + def revertLinkedTiIds() { + log.debug("Manual TIPP deduplication for ID ${params.id}") + Map result = [params: params, result: 'OK'] + Long pkgId = params.long('id') ?: null + LocalDate linkDate + + try { + linkDate = LocalDate.from(params.date) + } + catch (Exception e) { + result.result = 'ERROR' + result.message = 'Unable to parse date parameter!' + } + + if (pkgId && linkDate) { + Job j = concurrencyManagerService.createJob { job -> + packageCleanupService.revertTitleIds(pkgId, linkDate, job) + }.startOrQueue() + + j.description = "Deduplicating package TIPPs for package ${pkgId}" + j.type = RefdataCategory.lookupOrCreate('Job.Type', 'Package TI Id Cleanup') + j.startTime = new Date() + + render(view: "logViewer", model: logViewer()) + } + else { + result.result = 'ERROR' + response.status = 400 + + render result as JSON + } + } + + def fetchEzbCollections() { log.debug("Triggering EZB open collections sync") diff --git a/server/grails-app/services/org/gokb/PackageCleanupService.groovy b/server/grails-app/services/org/gokb/PackageCleanupService.groovy index c0fb16830..37024c46b 100644 --- a/server/grails-app/services/org/gokb/PackageCleanupService.groovy +++ b/server/grails-app/services/org/gokb/PackageCleanupService.groovy @@ -6,12 +6,17 @@ import grails.gorm.transactions.Transactional import groovy.util.logging.Slf4j +import java.time.* + import org.gokb.cred.* +import org.hibernate.HibernateException @Slf4j class PackageCleanupService { def tippService + def titleAugmentService + def sessionFactory def reactivateReplacedTipps(pid, Job j = null) { def result = [result: 'OK', cases: 0, additionalDeletes: 0, total: 0] @@ -101,4 +106,75 @@ class PackageCleanupService { result } + + public Map revertTitleIds(Long pid, LocalDate date, Job j = null) { + Map result = [:] + + try { + def session = sessionFactory.currentSession + result = processTitleIdCleanup(session, pid, date, j) + } + catch (HibernateException e) { + log.debug("Failed session lookup..") + + Package.withNewSession { session -> + log.debug("revertTitleIds :: creating new session ..") + result = processTitleIdCleanup(session, pid, date, j) + } + } + + result + } + + private Map processTitleIdCleanup(session, Long pid, LocalDate date, Job job = null) { + Map result = [result: 'OK', cleanups: 0] + RefdataValue type_ids = RefdataCategory.lookup('Combo.Type', 'KBComponent.Ids') + RefdataValue type_ti_tipps = RefdataCategory.lookup('Combo.Type', 'TitleInstance.Tipps') + RefdataValue type_pkg_tipps = RefdataCategory.lookup('Combo.Type', 'Package.Tipps') + log.debug("revertTitleIds :: Processing id links for Package ${pid} between ${java.sql.Date.valueOf(date)} and ${java.sql.Date.valueOf(date.plusDays(1))} ..") + + def deletion_candidates_qry = '''select id, fromComponent.id from Combo as cid + where type = :cti + and dateCreated between :dateStart and :dateEnd + and exists ( + select 1 from Combo as ct + where type = :ctt + and fromComponent = cid.fromComponent + and exists ( + select 1 from Combo as cp + where type = :ctp + and toComponent = ct.toComponent + and fromComponent.id = :pid + ) + ) + order by fromComponent.id''' + + Map pars = [ + pid: pid, + cti: type_ids, + ctt: type_ti_tipps, + ctp: type_pkg_tipps, + dateStart: java.sql.Date.valueOf(date), + dateEnd: java.sql.Date.valueOf(date.plusDays(1)) + ] + + List delete_candidates = Combo.executeQuery(deletion_candidates_qry, pars) + + for (c in delete_candidates) { + Combo ctd = Combo.get(c[0]).delete(flush: true) + result.cleanups++ + TitleInstance ti = TitleInstance.get(c[1]) + + titleAugmentService.touchTitleTipps(ti, false) + + if (result.cleanups % 50 == 0) { + session.flush() + session.clear() + } + } + + job?.endTime = new Date() + + result + } } \ No newline at end of file diff --git a/server/src/integration-test/groovy/org/gokb/PackageCleanupServiceSpec.groovy b/server/src/integration-test/groovy/org/gokb/PackageCleanupServiceSpec.groovy new file mode 100644 index 000000000..a6bfc978b --- /dev/null +++ b/server/src/integration-test/groovy/org/gokb/PackageCleanupServiceSpec.groovy @@ -0,0 +1,102 @@ +package org.gokb + +import grails.testing.mixin.integration.Integration + +import java.time.* + +import org.gokb.PackageCleanupService +import org.gokb.cred.* +import org.hibernate.SessionFactory +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.test.annotation.Rollback +import org.springframework.transaction.annotation.Transactional + +import spock.lang.Specification + +@Integration +@Transactional +@Rollback +class PackageCleanupServiceSpec extends Specification { + + @Autowired + PackageCleanupService packageCleanupService + + def autoTimestampEventListener + LocalDate old_id_ld + + def setup() { + RefdataValue status_deleted = RefdataCategory.lookup('KBComponent.Status', 'Deleted') + RefdataValue combo_type_id = RefdataCategory.lookup('Combo.Type', 'KBComponent.Ids') + IdentifierNamespace ns_doi = IdentifierNamespace.findByValue('doi') + old_id_ld = LocalDate.now().minusMonths(1) + Date old_id_date = Date.from(LocalDateTime.now().minusMonths(1).atZone(ZoneId.systemDefault()).toInstant()) + + BookInstance test_ti = BookInstance.findByName('PackageCleanupTitle') + Identifier new_id = Identifier.findByValue('10.2333/663633444') ?: new Identifier(value: '10.2333/663633444', namespace: ns_doi).save(flush: true, failOnError: true) + Identifier old_id = Identifier.findByValue('10.2333/345435535') ?: new Identifier(value: '10.2333/345435535', namespace: ns_doi).save(flush: true, failOnError: true) + + if (!test_ti) { + test_ti = new BookInstance(name: 'PackageCleanupTitle').save(flush: true, failOnError: true) + test_ti.ids << new_id + test_ti.save(flush: true) + + autoTimestampEventListener.withoutTimestamps { + Combo nc = new Combo(fromComponent: test_ti, toComponent: old_id, type: combo_type_id).save(flush: true, failOnError: true) + nc.dateCreated = old_id_date + nc.save(flush: true) + } + } + + Package package_cleanup_pkg = Package.findByName('PackageCleanupPackage') + Org package_cleanup_provider = Org.findByName('PackageCleanupProvider') ?: new Org(name: 'PackageCleanupProvider').save(flush: true) + Platform package_cleanup_platform = Platform.findByName('PackageCleanupPlatform') ?: new Platform(name: 'PackageCleanupProvider', provider: package_cleanup_provider).save(flush:true) + + if (!package_cleanup_pkg) { + package_cleanup_pkg = new Package(name: 'PackageCleanupPackage', provider: package_cleanup_provider, nominalPlatform: package_cleanup_platform).save(flush: true) + } + + TitleInstancePackagePlatform package_cleanup_tipp = TitleInstancePackagePlatform.findByName('PackageCleanupTipp') + + if (!package_cleanup_tipp) { + package_cleanup_tipp = new TitleInstancePackagePlatform(name: 'PackageCleanupTipp', url: 'https://test.com/gokbcleanuptest', hostPlatform: package_cleanup_platform, pkg: package_cleanup_pkg, title: test_ti).save(flush: true, failOnError: true) + } + } + + def cleanup() { + TitleInstancePackagePlatform.findByName('PackageCleanupTipp')?.expunge() + BookInstance.findByName('PackageCleanupTitle')?.expunge() + } + + void "test selective ID removal by date"() { + given: + Package package_cleanup_pkg = Package.findByName('PackageCleanupPackage') + BookInstance test_ti = BookInstance.findByName('PackageCleanupTitle') + + Map total_pars = [ + pid: package_cleanup_pkg.id, + cti: RefdataCategory.lookup('Combo.Type', 'KBComponent.Ids'), + ctt: RefdataCategory.lookup('Combo.Type', 'TitleInstance.Tipps'), + ctp: RefdataCategory.lookup('Combo.Type', 'Package.Tipps') + ] + + when: + Map result = packageCleanupService.revertTitleIds(package_cleanup_pkg.id, old_id_ld) + then: + def combos = Combo.executeQuery('''from Combo as cid + where type = :cti + and exists ( + select 1 from Combo as ct + where type = :ctt + and fromComponent = cid.fromComponent + and exists ( + select 1 from Combo as cp + where type = :ctp + and toComponent = ct.toComponent + and fromComponent.id = :pid + ) + ) + order by fromComponent.id''', total_pars) + combos.size() == 1 + result.cleanups == 1 + } +}