From 3de0e18546abb8f4e1fe91838a37eb2c27dfcc05 Mon Sep 17 00:00:00 2001 From: Sagar Sane Date: Thu, 25 Aug 2016 14:11:03 -0600 Subject: [PATCH 1/2] Git ignore and git attributes --- .gitattributes | 1 + .gitignore | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..94bec0a --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +gradlew -crlf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dea9297 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +*.vlt +*.tmp +.DS_Store +*.iml +*.ipr +*.iws +*.jar +.gradle +.gradletasknamecache +.project +.settings +.idea +.classpath +.metadata/ +dist/ +atlassian-ide-plugin.xml +install/ +out/ +build/ +bin/ +.cache +test-output/ +classes/ +generated/ +.vagrant/ +target/ From ab5f60b10b2e878aa6b24278b2065c88e4bd3d5c Mon Sep 17 00:00:00 2001 From: Sagar Sane Date: Thu, 25 Aug 2016 14:22:02 -0600 Subject: [PATCH 2/2] Initial port .. more work required --- .travis.yml | 27 ++ build.gradle | 226 ++++++++++ buildSrc/build.gradle | 11 + .../com/twcable/spring/batch/Version.groovy | 65 +++ .../twcable/spring/batch/VersionSpec.groovy | 41 ++ gradle.properties | 50 +++ gradle/bundle.gradle | 60 +++ gradle/dependencies.gradle | 102 +++++ gradle/idea.gradle | 16 + gradle/packageExclusions.gradle | 45 ++ gradle/utils.gradle | 11 + gradle/verifyComponentConfig.gradle | 420 +++++++++++++++++ gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 164 +++++++ gradlew.bat | 90 ++++ settings.gradle | 1 + src/main/content/META-INF/vault/config.xml | 77 ++++ .../META-INF/vault/definition/.content.xml | 23 + src/main/content/META-INF/vault/filter.xml | 7 + .../content/META-INF/vault/properties.xml | 12 + src/main/content/META-INF/vault/settings.xml | 4 + ...bbit.resources.GrabbitResourceProvider.xml | 9 + ...onfig-com.twcable.grabbit.client.batch.xml | 26 ++ ...onfig-com.twcable.grabbit.server.batch.xml | 26 ++ ...org.apache.jackrabbit.core.SessionImpl.xml | 31 ++ .../batch/repository/AbstractJcrDao.groovy | 32 ++ .../GrabbitExecutionContextDao.groovy | 40 ++ .../repository/GrabbitJobExecution.groovy | 40 ++ .../repository/GrabbitJobExecutionDao.groovy | 39 ++ .../repository/GrabbitJobInstanceDao.groovy | 34 ++ .../repository/GrabbitStepExecutionDao.groovy | 33 ++ .../JcrGrabbitExecutionContextDao.groovy | 358 +++++++++++++++ .../JcrGrabbitJobExecutionDao.groovy | 424 ++++++++++++++++++ .../JcrGrabbitJobInstanceDao.groovy | 288 ++++++++++++ .../JcrGrabbitStepExecutionDao.groovy | 325 ++++++++++++++ .../JcrJobExplorerFactoryBean.groovy | 92 ++++ .../JcrJobRepositoryFactoryBean.groovy | 133 ++++++ .../services/CleanJobRepository.groovy | 28 ++ .../impl/DefaultCleanJobRepository.groovy | 115 +++++ .../GrabbitCleanJobRepositoryServlet.groovy | 59 +++ .../JcrGrabbitExecutionContextDaoSpec.groovy | 139 ++++++ .../JcrGrabbitJobExecutionDaoSpec.groovy | 200 +++++++++ .../JcrGrabbitJobInstanceDaoSpec.groovy | 184 ++++++++ .../JcrGrabbitStepExecutionDaoSpec.groovy | 104 +++++ ...rabbitCleanJobRepositoryServletSpec.groovy | 62 +++ .../twcable/grabbit/client/test_config.yaml | 9 + testutils/.gitignore | 1 + testutils/build.gradle | 36 ++ testutils/gradle/testing.gradle | 100 +++++ 49 files changed, 4425 insertions(+) create mode 100644 .travis.yml create mode 100644 build.gradle create mode 100644 buildSrc/build.gradle create mode 100644 buildSrc/src/main/groovy/com/twcable/spring/batch/Version.groovy create mode 100644 buildSrc/src/test/groovy/com/twcable/spring/batch/VersionSpec.groovy create mode 100644 gradle.properties create mode 100644 gradle/bundle.gradle create mode 100644 gradle/dependencies.gradle create mode 100644 gradle/idea.gradle create mode 100644 gradle/packageExclusions.gradle create mode 100644 gradle/utils.gradle create mode 100644 gradle/verifyComponentConfig.gradle create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 src/main/content/META-INF/vault/config.xml create mode 100644 src/main/content/META-INF/vault/definition/.content.xml create mode 100644 src/main/content/META-INF/vault/filter.xml create mode 100644 src/main/content/META-INF/vault/properties.xml create mode 100644 src/main/content/META-INF/vault/settings.xml create mode 100644 src/main/content/SLING-INF/content/apps/grabbit/config/com.twcable.grabbit.resources.GrabbitResourceProvider.xml create mode 100644 src/main/content/SLING-INF/content/apps/grabbit/config/org.apache.sling.commons.log.LogManager.factory.config-com.twcable.grabbit.client.batch.xml create mode 100644 src/main/content/SLING-INF/content/apps/grabbit/config/org.apache.sling.commons.log.LogManager.factory.config-com.twcable.grabbit.server.batch.xml create mode 100644 src/main/content/SLING-INF/content/apps/grabbit/config/org.apache.sling.commons.log.LogManager.factory.config-org.apache.jackrabbit.core.SessionImpl.xml create mode 100644 src/main/groovy/com/twcable/spring/batch/repository/AbstractJcrDao.groovy create mode 100644 src/main/groovy/com/twcable/spring/batch/repository/GrabbitExecutionContextDao.groovy create mode 100644 src/main/groovy/com/twcable/spring/batch/repository/GrabbitJobExecution.groovy create mode 100644 src/main/groovy/com/twcable/spring/batch/repository/GrabbitJobExecutionDao.groovy create mode 100644 src/main/groovy/com/twcable/spring/batch/repository/GrabbitJobInstanceDao.groovy create mode 100644 src/main/groovy/com/twcable/spring/batch/repository/GrabbitStepExecutionDao.groovy create mode 100644 src/main/groovy/com/twcable/spring/batch/repository/JcrGrabbitExecutionContextDao.groovy create mode 100644 src/main/groovy/com/twcable/spring/batch/repository/JcrGrabbitJobExecutionDao.groovy create mode 100644 src/main/groovy/com/twcable/spring/batch/repository/JcrGrabbitJobInstanceDao.groovy create mode 100644 src/main/groovy/com/twcable/spring/batch/repository/JcrGrabbitStepExecutionDao.groovy create mode 100644 src/main/groovy/com/twcable/spring/batch/repository/JcrJobExplorerFactoryBean.groovy create mode 100644 src/main/groovy/com/twcable/spring/batch/repository/JcrJobRepositoryFactoryBean.groovy create mode 100644 src/main/groovy/com/twcable/spring/batch/repository/services/CleanJobRepository.groovy create mode 100644 src/main/groovy/com/twcable/spring/batch/repository/services/impl/DefaultCleanJobRepository.groovy create mode 100644 src/main/groovy/com/twcable/spring/batch/repository/servlets/GrabbitCleanJobRepositoryServlet.groovy create mode 100644 src/test/groovy/com/twcable/spring/batch/repository/JcrGrabbitExecutionContextDaoSpec.groovy create mode 100644 src/test/groovy/com/twcable/spring/batch/repository/JcrGrabbitJobExecutionDaoSpec.groovy create mode 100644 src/test/groovy/com/twcable/spring/batch/repository/JcrGrabbitJobInstanceDaoSpec.groovy create mode 100644 src/test/groovy/com/twcable/spring/batch/repository/JcrGrabbitStepExecutionDaoSpec.groovy create mode 100644 src/test/groovy/com/twcable/spring/batch/repository/servlets/GrabbitCleanJobRepositoryServletSpec.groovy create mode 100644 src/test/resources/com/twcable/grabbit/client/test_config.yaml create mode 100644 testutils/.gitignore create mode 100644 testutils/build.gradle create mode 100644 testutils/gradle/testing.gradle diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..d3beb83 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,27 @@ +language: groovy + +#Yeah, we are going to want to cache our build dependencies so our builds aren't held up downloading for every container +cache: + directories: + - $HOME/.gradle + +jdk: + - oraclejdk7 + - oraclejdk8 + - openjdk7 + +# Allow running on a container instead of a traditional VM +sudo: false + +#There is currently an issue in Gradle where a forked JVM will ignore any no-daemon flag (--no-daemon, org.gradle.daemon) for the forked process if jvm args are supplied. +#We remove them to get around this +env: + global: + - GRADLE_OPTS="-Dorg.gradle.daemon=false -Dorg.gradle.jvmargs=''" + +before_install: + - bash install_protoc_from_source.sh +install: ./gradlew assemble +script: ./gradlew check + + diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..20eecff --- /dev/null +++ b/build.gradle @@ -0,0 +1,226 @@ +buildscript { + repositories { + jcenter() + + //Provides cq-gradle-plugins and jackalope + maven { + url "http://dl.bintray.com/twcable/aem" + } + mavenLocal() + } + + dependencies { + classpath "com.twcable.gradle:cq-gradle-plugins:${gradle_plugins_version}" + + + classpath "commons-io:commons-io:2.4" + } +} + +plugins { + id "com.jfrog.bintray" version "1.3.1" +} + + apply plugin: 'idea' + apply plugin: 'eclipse' + apply plugin: 'maven' + + repositories { + jcenter() + + //Provides cq-gradle-plugins and jackalope + maven { + url "http://dl.bintray.com/twcable/aem" + } + + maven { + url "http://dl.bintray.com/twcable/osgi" + } + + //Provides all Adobe AEM specific repos (like cq-workflow-console) + maven { + url "http://repo.adobe.com/nexus/content/groups/public" + } + + // hack to allow getting to a working version of AEM 6.1 workflow-console jar + // without running afoul of licensing restrictions + flatDir { dirs rootProject.projectDir } + } + +ext.vendor = 'Time Warner Cable - Converged Technology Group - CMS Team' +ext.organization = "Time Warner Cable" +ext.providerUrl = "https://www.timewarnercable.com" +ext.providerLink = "https://github.com/TWCable/grabbit" + + apply plugin: 'groovy' + apply from: "${rootProject.projectDir}/gradle/dependencies.gradle" + apply from: "${rootProject.projectDir}/testutils/gradle/testing.gradle" + + jar.manifest { + attributes 'Specification-Version': project.version + attributes 'Bundle-Vendor': project.vendor + attributes 'Implementation-Title': project.name + attributes 'Implementation-Version': project.version + attributes 'Implementation-Vendor': project.vendor + + instruction 'Import-Package', 'org.joda.time; version="[2,3)"' + instruction 'Import-Package', 'org.joda.time.base; version="[2,3)"' + instruction 'Import-Package', 'org.joda.time.format; version="[2,3)"' + } + + afterEvaluate { + if (project.hasProperty('sourceCompatibility')) { + project.sourceCompatibility = 1.7 + } + if (project.hasProperty('targetCompatibility')) { + project.targetCompatibility = 1.7 + } + if (project.hasProperty('bundleName')) { + jar.manifest { + attributes 'Bundle-Name': project.bundleName + attributes 'Bundle-SymbolicName': project.symbolicName + attributes 'Bundle-Description': project.bundleDescription + } + } + + if(project.hasProperty('slingServers')) { + project.slingServers.retryWaitMs = 500 + project.slingServers.maxWaitValidateBundlesMs = 90000 + } + // http://forums.gradle.org/gradle/topics/classpath_order_generated_by_eclipse_plugin_causes_problems + if (project.plugins.hasPlugin('java')) { + project.eclipse.classpath.file.whenMerged { classpath -> + def projectRefs = classpath.entries.findAll { entry -> entry.kind == 'src' && entry.path.startsWith('/') } + //move the project references to the end of the list: + classpath.entries.removeAll(projectRefs) + classpath.entries.addAll(projectRefs) + } + } + } + +idea.project.ipr { + withXml { provider -> + provider.node.component.find { it.@name == 'VcsDirectoryMappings' }.mapping.@vcs = 'Git' + def codeStyleNode = provider.node.component.find { it.@name == 'ProjectCodeStyleSettingsManager' } + if (codeStyleNode == null) { + codeStyleNode = provider.node.appendNode('component', [name: 'ProjectCodeStyleSettingsManager']) + } + codeStyleNode.replaceNode { node -> + component(name: 'ProjectCodeStyleSettingsManager') { + option(name: "PER_PROJECT_SETTINGS") { + value { + option(name: "OTHER_INDENT_OPTIONS") { + value { + option(name: "INDENT_SIZE", value: "4") + option(name: "CONTINUATION_INDENT_SIZE", value: "4") + option(name: "TAB_SIZE", value: "4") + option(name: "USE_TAB_CHARACTER", value: "false") + option(name: "SMART_TABS", value: "false") + option(name: "LABEL_INDENT_SIZE", value: "0") + option(name: "LABEL_INDENT_ABSOLUTE", value: "false") + option(name: "USE_RELATIVE_INDENTS", value: "false") + } + } + option(name: "CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND", value: "9999") + option(name: "NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND", value: "9999") + XML { + option(name: "XML_LEGACY_SETTINGS_IMPORTED", value: "true") + } + + // this is needed in addition to the one below, for import settings + GroovyCodeStyleSettings { + option(name: "CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND", value: "9999") + option(name: "NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND", value: "9999") + } + + // oddly, both "JAVA" and "Java" are used... + ['Groovy', 'JAVA', 'Java', 'Scala'].each { + codeStyleSettings(language: it) { + option(name: "CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND", value: "9999") + option(name: "NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND", value: "9999") + option(name: "BLANK_LINES_AROUND_METHOD", value: "2") + //option(name: "BLANK_LINES_BEFORE_METHOD_BODY", value: "1") + option(name: "ELSE_ON_NEW_LINE", value: "true") + option(name: "CATCH_ON_NEW_LINE", value: "true") + option(name: "FINALLY_ON_NEW_LINE", value: "true") + option(name: "SPACE_AFTER_TYPE_CAST", value: "false") + option(name: "INDENT_SIZE", value: "2") + option(name: "TAB_SIZE", value: "4") + + // both this level and 'indentOptions' are used + option(name: "CONTINUATION_INDENT_SIZE", value: "4") + indentOptions { + option(name: "CONTINUATION_INDENT_SIZE", value: "4") + } + } + } + } + } + option(name: "USE_PER_PROJECT_SETTINGS0", value: "true") + } + } + } +} + +apply from: "${rootProject.projectDir}/gradle/verifyComponentConfig.gradle" +apply from: "${rootProject.projectDir}/gradle/utils.gradle" + +apply plugin: 'osgi' +apply plugin: 'scr' +apply plugin: 'sling-bundle' + +// remove "mvn install" since it's not needed and causes a conflict +def installTask = tasks.findByPath('install') +if (installTask != null) tasks.remove(installTask) + +apply from: "${rootProject.projectDir}/gradle/bundle.gradle" +apply from: "${rootProject.projectDir}/gradle/packageExclusions.gradle" +apply from: "${rootProject.projectDir}/gradle/idea.gradle" + +gradle.taskGraph.whenReady { taskGraph -> + if (taskGraph.hasTask(bintrayUpload)) { + if (!project.hasProperty('bintray.user') || !project.hasProperty('bintray.key')) { + throw new IllegalArgumentException((String)"Please define 'bintray.user' and " + + "'bintray.key' properties. (Such as in ~/.gradle/gradle.properties)") + } + } +} + +version = new com.twcable.grabbit.Version(version as String) + +bintray { + user = project.properties['bintray.user'] + key = project.properties['bintray.key'] + filesSpec { + from tasks.getByPath('createPackage').archivePath + into '.' + } + + publish = version.status == 'release' + + pkg { + userOrg = 'twcable' + repo = 'aem' + name = 'Grabbit' + + desc = 'The purpose of this project is to provide a reliable and fast solution for copying content from a Source to Destination. Source and destination can be any AEM instances.' + + websiteUrl = 'https://github.com/TWCable/grabbit' + issueTrackerUrl = 'https://github.com/TWCable/grabbit/issues' + vcsUrl = 'https://github.com/TWCable/grabbit.git' + licenses = ['Apache-2.0'] + labels = ['AEM', 'CQ', 'Content Copy', 'Data Migration'] + attributes = ['plat': ['aem', 'cq']] + + publicDownloadNumbers = true + + //noinspection GroovyAssignabilityCheck + version { + //noinspection GrReassignedInClosureLocalVar + name = project.version.bintrayVersion + vcsTag = 'v' + project.version + } + } +} + +bintrayUpload.dependsOn('createPackage') diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle new file mode 100644 index 0000000..084c439 --- /dev/null +++ b/buildSrc/build.gradle @@ -0,0 +1,11 @@ +apply plugin: 'groovy' + +dependencies { + testCompile('org.spockframework:spock-core:1.0-groovy-2.3') { + exclude module: 'groovy-all' + } +} + +repositories { + mavenCentral() +} diff --git a/buildSrc/src/main/groovy/com/twcable/spring/batch/Version.groovy b/buildSrc/src/main/groovy/com/twcable/spring/batch/Version.groovy new file mode 100644 index 0000000..f33061c --- /dev/null +++ b/buildSrc/src/main/groovy/com/twcable/spring/batch/Version.groovy @@ -0,0 +1,65 @@ +/** + * Copyright 2015 Time Warner Cable, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.twcable.spring.batch + +// ************************************************************************** +// +// VERSION CLASS +// +// ************************************************************************** + + +class Version { + String originalVersion + String thisVersion + String status + Date buildTime + + + Version(String versionValue) { + buildTime = new Date() + originalVersion = versionValue + if (originalVersion.endsWith('-SNAPSHOT')) { + status = 'integration' + thisVersion = originalVersion - 'SNAPSHOT' + getTimestamp() + } + else { + status = 'release' + thisVersion = versionValue + } + } + + + @SuppressWarnings("UnnecessaryQualifiedReference") + String getTimestamp() { + // Convert local file timestamp to UTC + def format = new java.text.SimpleDateFormat('yyyyMMddHHmmss') + format.setCalendar(Calendar.getInstance(TimeZone.getTimeZone('UTC'))); + return format.format(buildTime) + } + + + String toString() { + originalVersion + } + + + String getBintrayVersion() { + thisVersion + } + +} diff --git a/buildSrc/src/test/groovy/com/twcable/spring/batch/VersionSpec.groovy b/buildSrc/src/test/groovy/com/twcable/spring/batch/VersionSpec.groovy new file mode 100644 index 0000000..c67ec57 --- /dev/null +++ b/buildSrc/src/test/groovy/com/twcable/spring/batch/VersionSpec.groovy @@ -0,0 +1,41 @@ +/** + * Copyright 2015 Time Warner Cable, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.twcable.grabbit + +import spock.lang.Specification + +class VersionSpec extends Specification { + + def "version When Snapshot"(){ + given: + final String ver = "Test-SNAPSHOT" + when: + Version version = new Version(ver) + then: + version.thisVersion == ver - "SNAPSHOT" + version.getTimestamp() + } + + def "default Version"(){ + given: + final String ver = "1.0.5" + when: + Version version = new Version(ver) + then: + version.thisVersion == ver + } + +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..130479d --- /dev/null +++ b/gradle.properties @@ -0,0 +1,50 @@ +# suppress inspection "UnusedProperty" for whole file +org.gradle.daemon = true +org.gradle.jvmargs=-XX:MaxPermSize=512m -XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled -XX:+HeapDumpOnOutOfMemoryError -Xmx1024m -Dfile.encoding=utf-8 + +bundleInstallRoot = /apps/springbatch-jcrjobrepository/install + +group = com.twcable.spring.batch +version = 1.0.0 + +# Please keep alphabetical +cglib_nodep_version = 2.2.2 +commons_collections_version = 3.2.1 +commons_io_version = 1.4 +commons_lang_version = 3.3.2 +commons_text_version = 1.1.8 + +felix_osgi_version = 1.4.0 +gradle_plugins_version = 3.0.1 +granite_version = 5.12.2 +groovy_version = 2.3.6 +guava_version = 15.0 +httpcomponents_version = 4.3.4 +httpcomponents_client_version = 4.3.6 +httpcomponents_core_version = 4.3.2 +jackalope_version = 2.0.0 +jackrabbit_ocm_version = 1.5.3 +jackrabbit_version = 2.10.0 +jcr_version = 2.0 +jms_version = 3.1.1 +jsr305_version = 2.0.0 +logback_version = 1.0.4 +objenesis_version = 2.1 +protobuf_gradle_plugin_version = 0.9.1 +protobuf_version = 2.6.1 +scr_annotations_version = 1.7.0 +servlet_api_version = 2.5 +slf4j_version = 1.7.6 +sling_api_version = 2.9.0 +sling_commons_testing_version = 2.0.12 +sling_commons_version = 2.2.0 +sling_event_version = 3.1.4 +sling_jcr_api_version = 2.5.0 +sling_jcr_resource_version = 2.5.0 +sling_resourceresolver_version = 1.0.6 +sling_rewriter_version = 1.0.4 +spock_version = 0.7-groovy-2.0 +spring_batch_version = 2.2.7.RELEASE +spring_osgi_version = 2.0.0.M1 +woodstox_version = 4.2.0 +snakeyaml_version = 1.16 diff --git a/gradle/bundle.gradle b/gradle/bundle.gradle new file mode 100644 index 0000000..0e9f085 --- /dev/null +++ b/gradle/bundle.gradle @@ -0,0 +1,60 @@ +apply plugin: 'cqpackage' + +ext.packageName = project.name +ext.bundleName = project.name +ext.bundleDescription = 'SpringBatch-JcrJobRepository' +ext.symbolicName = "com.twcable.spring.batch" +jar.baseName = 'springbatch-jcrjobrepository' + +createPackage.bundleInstallRoot = '/apps/springbatch-jcrjobrepository/install' + +bundle.installPath = '/apps/springbatch-jcrjobrepository/install' + +jar { + from('src/main/content/SLING-INF/content', { + into 'SLING-INF/content' + }) +} + +verifyBundles.dependsOn 'jar' + +jar.manifest { + + attributes 'Sling-Initial-Content': project.configFolders.collect { + "SLING-INF/content/apps/springbatch-jcrjobrepository/${it}; overwrite:=true; uninstall:=true; path:=/apps/springbatch-jcrjobrepository/${it}" + }.join(', ') + + attributes 'Bundle-Name': project.bundleName + attributes 'Bundle-SymbolicName': project.symbolicName + attributes 'Bundle-Description': project.bundleDescription + instruction 'Import-Package', 'groovy.json; version="[2.3,3.0)"' + instruction 'Import-Package', 'groovy.json.internal; version="[2.3,3.0)"' + instruction 'Import-Package', 'org.springframework.batch.core.scope; version="[2.2,3.0)"' + instruction 'Import-Package', 'org.springframework.batch.item.file.transform; version="[2.2,3.0)"' + instruction 'Import-Package', 'org.springframework.batch.item.file; version="[2.2,3.0)"' + instruction 'Import-Package', 'org.springframework.batch.item.support; version="[2.2,3.0)"' + instruction 'Import-Package', 'org.springframework.batch.core.configuration.xml; version="[2.2,3.0)"' + instruction 'Import-Package', 'org.springframework.batch.core.configuration.support; version="[2.2,3.0)"' + instruction 'Import-Package', 'org.springframework.batch.core.explore.support; version="[2.2,3.0)"' + instruction 'Import-Package', 'org.springframework.batch.core.repository.support; version="[2.2,3.0)"' + instruction 'Import-Package', 'org.springframework.batch.core.repository; version="[2.2,3.0)"' + instruction 'Import-Package', 'org.springframework.batch.core.launch.support; version="[2.2,3.0)"' + instruction 'Import-Package', 'org.springframework.batch.core.job.flow.support; version="[2.2,3.0)"' + instruction 'Import-Package', 'org.springframework.batch.core.job.flow.support.state; version="[2.2,3.0)"' + instruction 'Import-Package', 'org.springframework.batch.core.repository.dao; version="[2.2,3.0)"' + instruction 'Import-Package', 'org.springframework.batch.support.transaction; version="[2.2,3.0)"' + instruction 'Import-Package', 'org.springframework.beans.factory.config; version="[3.1,4.0)"' + instruction 'Import-Package', 'org.springframework.core.io; version="[3.1,4.0)"' + instruction 'Import-Package', 'org.springframework.core.task; version="[3.1,4.0)"' + instruction 'Import-Package', 'org.springframework.aop; version="[3.1,4.0)"' + instruction 'Import-Package', 'org.springframework.aop.framework; version="[3.1,4.0)"' + instruction 'Import-Package', 'org.springframework.aop.scope; version="[3.1,4.0)"' + instruction 'Import-Package', 'org.springframework.scheduling.concurrent; version="[3.1,4.0)"' + instruction 'Import-Package', "org.aopalliance.aop;version=1.0.0" + instruction 'Import-Package', "sun.misc" + instruction 'Import-Package', '*' + + // export everything to OSGi except *.impl packages + instruction 'Export-Package', "!*.impl, *;-noimport:=false;version=${version}" +} + diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle new file mode 100644 index 0000000..332bf76 --- /dev/null +++ b/gradle/dependencies.gradle @@ -0,0 +1,102 @@ +ext.spring_version = "3.2.9.RELEASE_1" + +//noinspection GroovyAssignabilityCheck +dependencies { + // Library for Groovy + compile "org.codehaus.groovy:groovy-all:${groovy_version}" + + // Used as part of the build to create the OSGI-INF/serviceComponents.xml file + compile "org.apache.felix:org.apache.felix.scr.annotations:${scr_annotations_version}" + + //Google libraries + compile "com.google.guava:guava:${guava_version}" + + //For annotations like @Nonnull, etc. + compile "com.google.code.findbugs:jsr305:${jsr305_version}" + + // Adobe AEM Libraries + compile "com.day.cq.workflow:cq-workflow-console:${cq_workflow_console_version}" + + // Apache Sling libraries + compile "org.apache.sling:org.apache.sling.api:${sling_api_version}" + compile "org.apache.sling:org.apache.sling.jcr.resource:${sling_jcr_resource_version}" + + // Apache Felix libraries + compile "org.apache.felix:org.osgi.core:${felix_osgi_version}" + compile "org.apache.felix:org.osgi.compendium:${felix_osgi_version}" + + compile "joda-time:joda-time:2.7" + + // Need the "javax.jms" library for Spring JMS + compile "org.glassfish:javax.jms:${jms_version}" + + // Working with the JCR + compile "javax.jcr:jcr:${jcr_version}" + compile "org.apache.jackrabbit:jackrabbit-jcr-commons:${jackrabbit_version}" + compile "org.apache.sling:org.apache.sling.jcr.api:${sling_commons_version}" + + // Logging + compile "org.slf4j:slf4j-api:${slf4j_version}" + compile "org.slf4j:jcl-over-slf4j:${slf4j_version}" + + // Servlet/JSP libraries + compile "javax.servlet:servlet-api:${servlet_api_version}" + + // This version of Spring Batch brings in non-OSGi transitive dependencies, so they need to be overrriden + compile "org.springframework.batch:spring-batch-core:${spring_batch_version}", { + exclude group: "org.springframework", module: "spring-core" + exclude group: "org.springframework", module: "spring-beans" + exclude group: "org.springframework", module: "spring-aop" + exclude group: "org.springframework", module: "spring-expression" + exclude group: "org.springframework", module: "spring-context" + exclude group: "org.springframework", module: "spring-tx" + exclude group: "com.thoughtworks.xstream", module: "xstream" + } + + // Spring libraries + compile "org.apache.servicemix.bundles:org.apache.servicemix.bundles.spring-aop:${spring_version}" + compile "org.apache.servicemix.bundles:org.apache.servicemix.bundles.spring-beans:${spring_version}" + compile "org.apache.servicemix.bundles:org.apache.servicemix.bundles.spring-core:${spring_version}" + compile "org.apache.servicemix.bundles:org.apache.servicemix.bundles.spring-context:${spring_version}" + compile "org.apache.servicemix.bundles:org.apache.servicemix.bundles.spring-tx:${spring_version}" + compile "org.apache.servicemix.bundles:org.apache.servicemix.bundles.spring-expression:${spring_version}" + compile "org.aopalliance:com.springsource.org.aopalliance:1.0.0" + compile "org.springframework.osgi:spring-osgi-extender:${spring_osgi_version}", { + exclude group: "org.springframework", module: "org.springframework.core" + exclude group: "org.springframework", module: "org.springframework.context" + exclude group: "org.springframework", module: "org.springframework.beans" + exclude group: "org.springframework", module: "org.springframework.asm" + exclude group: "org.springframework", module: "org.springframework.expression" + exclude group: "org.springframework", module: "org.springframework.aop" + } + + compile "org.apache.commons:commons-lang3:${commons_lang_version}" + compile "commons-io:commons-io:${commons_io_version}" + + // HTTP components libraries + compile "org.apache.httpcomponents:httpclient-osgi:${httpcomponents_client_version}", { + exclude module: "httpclient" + exclude module: "httpcore" + exclude module: "httpclient-cache" + exclude module: "httpmime" + exclude module: "fluent-hc" + } + compile "org.apache.httpcomponents:httpcore-osgi:${httpcomponents_core_version}", { + exclude module: "httpclient-cache" + exclude module: "httpcore" + exclude module: "httpcore-nio" + exclude module: "httpmime" + exclude module: "fluent-hc" + } + + compile "org.yaml:snakeyaml:${snakeyaml_version}" + + compile "com.twc.osgi:protobuf-java:${protobuf_version}" + //For cleaner mocking + testCompile "org.objenesis:objenesis:${objenesis_version}" + +} + +configurations.compile { + exclude group: 'commons-logging', module: 'commons-logging' +} diff --git a/gradle/idea.gradle b/gradle/idea.gradle new file mode 100644 index 0000000..53b233b --- /dev/null +++ b/gradle/idea.gradle @@ -0,0 +1,16 @@ +idea { + module { + // The whole build dir is excluded by default, but we need build/generated-sources, + // which contains the generated proto classes. + excludeDirs = [ + file("$buildDir/classes"), + file("$buildDir/docs"), + file("$buildDir/dependency-cache"), + file("$buildDir/libs"), + file("$buildDir/reports"), + file("$buildDir/resources"), + file("$buildDir/test-results"), + file("$buildDir/tmp"), + ] + } +} diff --git a/gradle/packageExclusions.gradle b/gradle/packageExclusions.gradle new file mode 100644 index 0000000..5adfb2a --- /dev/null +++ b/gradle/packageExclusions.gradle @@ -0,0 +1,45 @@ +apply plugin: 'cqpackage' +// exclude bundles provided by CQ +configurations.cq_package { + exclude group: 'javax.servlet', module: 'servlet-api' + exclude group: 'commons-logging', module: 'commons-logging' + exclude group: 'commons-httpclient', module: 'commons-httpclient' + exclude group: 'com.sun.xml.bind' + exclude group: 'aopalliance', module: 'aopalliance' + exclude group: 'javax.xml.stream' + exclude group: 'javax.jcr', module: 'jcr' + + // use AEM's logging, not logback + exclude group: 'ch.qos.logback', module: 'logback-classic' + exclude group: 'ch.qos.logback', module: 'logback-core' + + // Slf4j is provided by AEM + exclude group: 'org.slf4j', module: 'slf4j-api' + exclude group: 'org.slf4j', module: 'jcl-over-slf4j' + + // excluding Joda Time too as AEM provides its own in com.day.commons.osgi.wrapper.joda-time + exclude group: 'joda-time', module: 'joda-time' + + exclude group: 'com.google.code.findbugs', module: 'jsr305' + + exclude group: 'com.day.cq.workflow', module: 'cq-workflow-console' + + exclude group: 'org.apache.felix', module: 'org.apache.felix.scr.annotations' + exclude group: 'org.apache.felix', module: 'org.osgi.core' + exclude group: 'org.apache.felix', module: 'org.osgi.compendium' + + exclude group: 'org.apache.commons', module: 'commons-lang3' + exclude group: 'org.apache.jackrabbit', module:'jackrabbit-jcr-commons' + exclude group: 'commons-io', module: 'commons-io' + + //Exclude Apache Sling Libraries + exclude group: 'org.apache.sling', module: 'org.apache.sling.api' + exclude group: 'org.apache.sling', module:'org.apache.sling.jcr.resource' + exclude group: 'org.apache.sling', module: 'org.apache.sling.jcr.api' + + // 6.x exclude bundles + exclude group: 'com.google.guava', module: 'guava' + exclude group: 'org.apache.httpcomponents', module: 'httpclient-osgi' + exclude group: 'org.apache.httpcomponents', module: 'httpcore-osgi' + exclude module: 'cq-workflow-console' +} diff --git a/gradle/utils.gradle b/gradle/utils.gradle new file mode 100644 index 0000000..03a0244 --- /dev/null +++ b/gradle/utils.gradle @@ -0,0 +1,11 @@ +task getConfigFolders { + def configBaseDir = new File(project.projectDir, 'src/main/content/SLING-INF/content/apps/grabbit/') + project.ext.configFolders = [] + configBaseDir.eachDir { File dir -> + if (dir.name ==~ /config\.?.*/) { + configFolders.add(dir.name) + } + } +} + +jar.dependsOn += getConfigFolders diff --git a/gradle/verifyComponentConfig.gradle b/gradle/verifyComponentConfig.gradle new file mode 100644 index 0000000..1627c2b --- /dev/null +++ b/gradle/verifyComponentConfig.gradle @@ -0,0 +1,420 @@ +buildscript { + repositories { + jcenter() + } + dependencies { + classpath "com.google.guava:guava:${guava_version}" + } +} +apply plugin: 'groovy' +apply plugin: 'scr' + +import groovy.transform.ToString +import groovy.util.logging.Slf4j +import groovy.xml.Namespace +import groovy.xml.QName +import java.util.regex.Matcher +import com.google.common.base.Strings +import static Empty.EMPTY + +task verifyComponentConfig(type: VerifyComponentConfig, dependsOn: [processScrAnnotations]) + +// ************************************************************************** +// +// HELPER CLASSES +// +// ************************************************************************** + +class VerifyComponentConfig extends DefaultTask { + + VerifyComponentConfig() { + doLast { + def verifyFlag = false + if (!missingConfigs.isEmpty()) { + final compNames = missingConfigs.collect { Component comp -> comp.name }.sort() + project.logger.lifecycle("\nServices that do not yet have config nodes: ${compNames}\n") + verifyFlag = true + } + + if (!missingDefaultConfigNodes.isEmpty()) { + final compNames = missingDefaultConfigNodes.collect { Component comp -> comp.name }.sort() + project.logger.lifecycle("\nMissing default configuration but has runmode specific configs: ${compNames}\n") + verifyFlag = true + } + + if (!configDifferences.isEmpty()) { + StringBuilder builder = new StringBuilder("\nHas different configuration properties between nodes:\n") + configDifferences.sort().each { Component component -> + builder.append(" ${component.name}:\n") + component.configDifferences.each { ConfigDifference confDiff -> + appendDiff(builder, confDiff.leftSide, confDiff.rightSide) + appendDiff(builder, confDiff.rightSide, confDiff.leftSide) + } + } + project.logger.lifecycle(builder.toString()) + verifyFlag = true + } + if(verifyFlag) throw new IllegalStateException("Please fix config nodes issues to proceed with the build") + } + } + + static void appendDiff(StringBuilder builder, ConfigFile leftSide, ConfigFile rightSide) { + final leftDiff = leftSide.componentProperties.configProperties.propertyNames - + rightSide.componentProperties.configProperties.propertyNames + if (!leftDiff.isEmpty()) { + builder.append " ${runmodesStr(leftSide.runmodes)} has properties not in ${runmodesStr(rightSide.runmodes)}\n" + leftDiff.each { + builder.append " - ${it}\n" + } + } + } + + static String runmodesStr(Set rm) { + rm.isEmpty() ? "DEFAULT" : rm.join('.') + } + + @Lazy Components components = { + def serviceComponentsFile = new File((File)project.sourceSets.main.output.classesDir, 'OSGI-INF/serviceComponents.xml') + assert serviceComponentsFile.exists() + + def ns = new Namespace("http://www.osgi.org/xmlns/scr/v1.1.0", 'scr') + final xml = new XmlParser().parse(serviceComponentsFile) + final compColl = xml[ns.component].collect { Node node -> + String compName = node.'@name' + Map props = [:] + node['property'].each { Node propNode -> + String name = propNode.attribute('name') as String + def value = propNode.attribute('value') ?: EMPTY + props[name] = value + } + new Component(compName, props, project.projectDir) + } + new Components(compColl) + }() + + @Lazy Components missingConfigs = { + new Components(components.componentsNeedingConfig.findAll { it.isMissingConfigFile }) + }() + + @Lazy Components missingDefaultConfigNodes = { + new Components(components.componentsNeedingConfig.findAll { it.isMissingDefaultConfigFile }) + }() + + @Lazy Components configDifferences = { + new Components(components.componentsNeedingConfig.findAll { !it.configDifferences.isEmpty() }) + }() +} + +@ToString +class Components { + private final Set comps + final Set componentsNeedingConfig + final Set cleanComponents + final Set uncleanComponents + + Components(Collection comps) { + if (comps == null) throw new IllegalArgumentException('comps == null') + this.comps = comps as Set + + componentsNeedingConfig = comps.findAll { it.needsConfigFile } + cleanComponents = componentsNeedingConfig.findAll { it.configProperties.isClean } + uncleanComponents = componentsNeedingConfig.findAll { !it.configProperties.isClean } + } + + boolean isEmpty() { + comps.isEmpty() + } + + Collection collect(Closure closure) { + comps.collect(closure) + } + + List sort() { + comps.sort() + } +} + +@Slf4j +@ToString +class Component implements Comparable { + final String name + final ComponentProperties componentProperties + final File projectDir + + Component(String name, Map allProperties, File projectDir) { + if (name == null) throw new IllegalArgumentException('name == null') + if (allProperties == null) throw new IllegalArgumentException('allProperties == null') + this.name = name + this.componentProperties = new ComponentProperties(allProperties) + this.projectDir = projectDir + } + + ConfigComponentProperties getConfigProperties() { + componentProperties.configProperties + } + + boolean equals(o) { + if (this.is(o)) return true + if (getClass() != o.class) return false + + Component component = (Component)o + + if (name != component.name) return false + + return true + } + + int hashCode() { + return name.hashCode() + } + + @Lazy Collection configFiles = { + def configBaseFolder = "/src/main/content/SLING-INF/content/apps/grabbit/" + File baseDir = new File(projectDir, configBaseFolder) + if(!baseDir.exists()) { + throw new Exception("Config nodes folder ${projectDir}${configBaseFolder} missing"); + } + + final configFiles = [] + baseDir.eachDir { File dir -> + if (dir.name ==~ /config\.?.*/) { + File file = new File(dir, "${name}.xml") + if (file.exists()) { + log.info "$file exists" + configFiles.add(new ConfigFile(file)) + } + else { + log.debug "There is no $file" + } + } + } + configFiles + }() + + @Lazy boolean isMissingConfigFile = { + if (!needsConfigFile) { + return false + } + else { + if (!configFiles.isEmpty()) { + log.info "Has config file ${name}: ${configFiles}" + return false + } + else { + log.info "Missing config file ${name}: ${configFiles}" + return true + } + } + }() + + @Lazy boolean isMissingDefaultConfigFile = { + if (!needsConfigFile) { + return false + } + else { + // there's at least one config file, but none that are default + !configFiles.isEmpty() && !configFiles.any { it.runmodes.isEmpty() } + } + }() + + @Lazy boolean needsConfigFile = { + if (configProperties.isEmpty()) { + log.info "Has no config properties: ${name}" + return false + } + else { + log.debug "Has config properties: ${name}" + return true + } + }() + + @Lazy Set configDifferences = { + if (!needsConfigFile || configFiles.size() < 2) { + return Collections.emptySet() + } + else { + Set thePermutations = [] + configFiles.eachPermutation { + if (it[0].compareTo(it[1]) < 0) thePermutations << new ConfigDifference(it[0], it[1]) + } + + thePermutations.findAll { + it.leftSide.componentProperties.configProperties.propertyNames != + it.rightSide.componentProperties.configProperties.propertyNames + } + } + }() + + @Override + int compareTo(other) { + this.name <=> other.name + } +} + +class ConfigDifference { + final ConfigFile leftSide + final ConfigFile rightSide + + ConfigDifference(ConfigFile leftSide, ConfigFile rightSide) { + this.leftSide = leftSide + this.rightSide = rightSide + } + + boolean equals(o) { + if (this.is(o)) return true + if (getClass() != o.class) return false + + ConfigDifference that = (ConfigDifference)o + + if (leftSide != that.leftSide) return false + if (rightSide != that.rightSide) return false + + return true + } + + int hashCode() { + int result + result = leftSide.hashCode() + result = 31 * result + rightSide.hashCode() + return result + } + + String toString() { "ConfigDifference(left: ${leftSide}, right: ${rightSide})" } +} + + +@Slf4j +class ConfigFile implements Comparable { + final File file + final Set runmodes + + ConfigFile(File file) { + this.file = file + this.runmodes = determineRunmodes() + } + + + @Lazy ComponentProperties componentProperties = { + final xml = new XmlParser().parse(file) + if(xml != null && xml.name() == "node") { + return readSlingConfigXml(xml) + } else { + throw new Exception("Parent tag is not available for config file ${file}") + } + }() + + private ComponentProperties readSlingConfigXml(Node parentNode) { + if(parentNode.primaryNodeType.size() == 0) + throw new Exception("Missing config node") + else if (parentNode.property.size() == 0) + throw new Exception("Missing config node") + + Map configMap = new HashMap() + parentNode.property.each { nodes -> + if(nodes.name.size() == 0) + throw new Exception("Missing config node") + else if (nodes.value.size() == 0 && nodes.values.size() == 0) + throw new Exception("Missing or config node") + else if (nodes.values.size() != 0 && nodes.values.value.size() == 0) + throw new Exception("Missing config node") + + String name = nodes.name.text() + + def value + if(nodes.value.size() > 0) + value = nodes.value.text() + else if(nodes.values.size() > 0) + value = "[${nodes.values.value.collect{it.text()}.join(',')}]" + + String type = nodes.type.text() + + configMap.put(name, Strings.isNullOrEmpty(type) ? value : "{${type}}${value}") + } + new ComponentProperties(configMap) + } + + private Set determineRunmodes() { + String dirName = file.parentFile.name + if (!dirName.startsWith('config')) throw new IllegalStateException("${dirName} is not a config dir") + + Matcher matcher = dirName =~ /^config\.(.*)$/ + if (matcher.matches()) { + String runmodesStr = matcher[0][1] + return ((String)runmodesStr).split('\\.').toList() as Set + } + else { + return Collections.emptySet() + } + } + + String toString() { "ConfigFile(name: ${file.name}, runmodes: ${runmodes})" } + + @Override + int compareTo(Object t) { + this.file <=> ((ConfigFile)t).file + } + + boolean equals(o) { + if (this.is(o)) return true + if (getClass() != o.class) return false + + ConfigFile that = (ConfigFile)o + + if (file != that.file) return false + + return true + } + + int hashCode() { + return file.hashCode() + } +} + + +class Empty { + private Empty() {} + + static final Empty EMPTY = new Empty() + + String toString() { 'EMPTY' } +} + +@ToString +class ComponentProperties { + protected Map _allProperties + + ComponentProperties(Map props) { + _allProperties = props + } + + ConfigComponentProperties getConfigProperties() { + new ConfigComponentProperties(_allProperties.findAll { key, value -> + !(key.startsWith('sling.') || key.startsWith('service.') || key.startsWith('process.')) + }) + } + + boolean isEmpty() { + _allProperties.isEmpty() + } +} + +@ToString +class ConfigComponentProperties extends ComponentProperties { + + ConfigComponentProperties(Map props) { + super(props) + } + + /** + * "Clean" is defined as no properties that are not EMPTY + */ + boolean getIsClean() { + _allProperties.every { key, value -> value == EMPTY } + } + + Set getPropertyNames() { + _allProperties.keySet() + } +} + +jar.dependsOn += verifyComponentConfig diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..613ca65 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Jul 13 15:49:06 IST 2016 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.3-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..91a7e26 --- /dev/null +++ b/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..aec9973 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..3bd33be --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = "SpringBatch-JcrJobRepository" diff --git a/src/main/content/META-INF/vault/config.xml b/src/main/content/META-INF/vault/config.xml new file mode 100644 index 0000000..941af6c --- /dev/null +++ b/src/main/content/META-INF/vault/config.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/content/META-INF/vault/definition/.content.xml b/src/main/content/META-INF/vault/definition/.content.xml new file mode 100644 index 0000000..3c85322 --- /dev/null +++ b/src/main/content/META-INF/vault/definition/.content.xml @@ -0,0 +1,23 @@ + + + + + + + diff --git a/src/main/content/META-INF/vault/filter.xml b/src/main/content/META-INF/vault/filter.xml new file mode 100644 index 0000000..acdd788 --- /dev/null +++ b/src/main/content/META-INF/vault/filter.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/src/main/content/META-INF/vault/properties.xml b/src/main/content/META-INF/vault/properties.xml new file mode 100644 index 0000000..a5c6e60 --- /dev/null +++ b/src/main/content/META-INF/vault/properties.xml @@ -0,0 +1,12 @@ + + + +FileVault Package Properties +${project.packageName} +1 +${version} + +2 +${project.bundleDescription} +twc + diff --git a/src/main/content/META-INF/vault/settings.xml b/src/main/content/META-INF/vault/settings.xml new file mode 100644 index 0000000..7e2912d --- /dev/null +++ b/src/main/content/META-INF/vault/settings.xml @@ -0,0 +1,4 @@ + + + + diff --git a/src/main/content/SLING-INF/content/apps/grabbit/config/com.twcable.grabbit.resources.GrabbitResourceProvider.xml b/src/main/content/SLING-INF/content/apps/grabbit/config/com.twcable.grabbit.resources.GrabbitResourceProvider.xml new file mode 100644 index 0000000..b82be8d --- /dev/null +++ b/src/main/content/SLING-INF/content/apps/grabbit/config/com.twcable.grabbit.resources.GrabbitResourceProvider.xml @@ -0,0 +1,9 @@ + + sling:OsgiConfig + + provider.roots + /grabbit + String + + + diff --git a/src/main/content/SLING-INF/content/apps/grabbit/config/org.apache.sling.commons.log.LogManager.factory.config-com.twcable.grabbit.client.batch.xml b/src/main/content/SLING-INF/content/apps/grabbit/config/org.apache.sling.commons.log.LogManager.factory.config-com.twcable.grabbit.client.batch.xml new file mode 100644 index 0000000..e7739e2 --- /dev/null +++ b/src/main/content/SLING-INF/content/apps/grabbit/config/org.apache.sling.commons.log.LogManager.factory.config-com.twcable.grabbit.client.batch.xml @@ -0,0 +1,26 @@ + + sling:OsgiConfig + + org.apache.sling.commons.log.file + logs/batch-client.log + String + + + + org.apache.sling.commons.log.level + info + String + + + + org.apache.sling.commons.log.names + com.twcable.grabbit.client.batch + String + + + + org.apache.sling.commons.log.pattern + {0,date,dd.MM.yyyy HH:mm:ss.SSS} [{2}] {3} *{4}* {5} + String + + diff --git a/src/main/content/SLING-INF/content/apps/grabbit/config/org.apache.sling.commons.log.LogManager.factory.config-com.twcable.grabbit.server.batch.xml b/src/main/content/SLING-INF/content/apps/grabbit/config/org.apache.sling.commons.log.LogManager.factory.config-com.twcable.grabbit.server.batch.xml new file mode 100644 index 0000000..9fe6384 --- /dev/null +++ b/src/main/content/SLING-INF/content/apps/grabbit/config/org.apache.sling.commons.log.LogManager.factory.config-com.twcable.grabbit.server.batch.xml @@ -0,0 +1,26 @@ + + sling:OsgiConfig + + org.apache.sling.commons.log.file + logs/batch-server.log + String + + + + org.apache.sling.commons.log.level + info + String + + + + org.apache.sling.commons.log.names + com.twcable.grabbit.server.batch + String + + + + org.apache.sling.commons.log.pattern + {0,date,dd.MM.yyyy HH:mm:ss.SSS} [{2}] {3} *{4}* {5} + String + + diff --git a/src/main/content/SLING-INF/content/apps/grabbit/config/org.apache.sling.commons.log.LogManager.factory.config-org.apache.jackrabbit.core.SessionImpl.xml b/src/main/content/SLING-INF/content/apps/grabbit/config/org.apache.sling.commons.log.LogManager.factory.config-org.apache.jackrabbit.core.SessionImpl.xml new file mode 100644 index 0000000..90f2d25 --- /dev/null +++ b/src/main/content/SLING-INF/content/apps/grabbit/config/org.apache.sling.commons.log.LogManager.factory.config-org.apache.jackrabbit.core.SessionImpl.xml @@ -0,0 +1,31 @@ + + + sling:OsgiConfig + + org.apache.sling.commons.log.file + logs/jackrabbit-sessionImpl.log + String + + + + org.apache.sling.commons.log.level + info + String + + + + org.apache.sling.commons.log.names + org.apache.jackrabbit.core.SessionImpl + String + + + + org.apache.sling.commons.log.pattern + {0,date,dd.MM.yyyy HH:mm:ss.SSS} [{2}] {3} *{4}* {5} + String + + diff --git a/src/main/groovy/com/twcable/spring/batch/repository/AbstractJcrDao.groovy b/src/main/groovy/com/twcable/spring/batch/repository/AbstractJcrDao.groovy new file mode 100644 index 0000000..d9aed78 --- /dev/null +++ b/src/main/groovy/com/twcable/spring/batch/repository/AbstractJcrDao.groovy @@ -0,0 +1,32 @@ +/* + * Copyright 2015 Time Warner Cable, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.twcable.spring.batch.repository + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j + +/** + * A simple base class that is extended by all the DAOs + */ +@CompileStatic +@Slf4j +abstract class AbstractJcrDao { + + public static final String ROOT_RESOURCE_NAME = "/var/grabbit/job/repository" + + protected abstract void ensureRootResource() +} diff --git a/src/main/groovy/com/twcable/spring/batch/repository/GrabbitExecutionContextDao.groovy b/src/main/groovy/com/twcable/spring/batch/repository/GrabbitExecutionContextDao.groovy new file mode 100644 index 0000000..c6af91c --- /dev/null +++ b/src/main/groovy/com/twcable/spring/batch/repository/GrabbitExecutionContextDao.groovy @@ -0,0 +1,40 @@ +/* + * Copyright 2015 Time Warner Cable, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.twcable.spring.batch.repository + +import org.springframework.batch.core.repository.dao.ExecutionContextDao +import org.springframework.batch.item.ExecutionContext; + + +/** + * Modified DAO Interface for persisting and retrieving {@link ExecutionContext} + * @see ExecutionContextDao for more details + */ +interface GrabbitExecutionContextDao extends ExecutionContextDao { + + /** + * Returns job execution context paths by comparing "executionId" property on "executionContext/job/" with + * "executionId" property on JobExecutions for the @param jobExecutionResourcePaths + */ + public Collection getJobExecutionContextPaths(Collection jobExecutionResourcePaths) + + /** + * Returns step execution context paths by comparing "executionId" property on "executionContext/job/" with + * "id" property on StepExecutions for the @param stepExecutionResourcePaths + */ + public Collection getStepExecutionContextPaths(Collection stepExecutionResourcePaths) +} diff --git a/src/main/groovy/com/twcable/spring/batch/repository/GrabbitJobExecution.groovy b/src/main/groovy/com/twcable/spring/batch/repository/GrabbitJobExecution.groovy new file mode 100644 index 0000000..44ab117 --- /dev/null +++ b/src/main/groovy/com/twcable/spring/batch/repository/GrabbitJobExecution.groovy @@ -0,0 +1,40 @@ +/* + * Copyright 2015 Time Warner Cable, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.twcable.spring.batch.repository + +import groovy.transform.CompileStatic +import groovy.transform.InheritConstructors +import org.springframework.batch.core.JobExecution + +/** + * GrabbitJobExecution is a simple extension of {@Link JobExecution}. + * + *

+ * It simply provides a new property "transactionID." This transactionID allows us to associate a group + * of job executions together by their common run transaction (configuration run). For example, if I + * give Grabbit a configuration with two paths, two job executions will be started, and they could both then + * be grouped/referenced by their common transactionID. + *

+ * + * @see {@link JcrGrabbitJobExecutionDao} for a good handle on how we + * serve up this JobExecution during the Spring Batch lifecycle. + */ +@CompileStatic +@InheritConstructors +class GrabbitJobExecution extends JobExecution { + long transactionID +} diff --git a/src/main/groovy/com/twcable/spring/batch/repository/GrabbitJobExecutionDao.groovy b/src/main/groovy/com/twcable/spring/batch/repository/GrabbitJobExecutionDao.groovy new file mode 100644 index 0000000..5cd6e5a --- /dev/null +++ b/src/main/groovy/com/twcable/spring/batch/repository/GrabbitJobExecutionDao.groovy @@ -0,0 +1,39 @@ +/* + * Copyright 2015 Time Warner Cable, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.twcable.spring.batch.repository + +import org.springframework.batch.core.BatchStatus +import org.springframework.batch.core.JobExecution +import org.springframework.batch.core.repository.dao.JobExecutionDao + +/** + * Modified DAO Interface for persisting and retrieving {@link JobExecution} + * @see JobExecutionDao for more details + */ +interface GrabbitJobExecutionDao extends JobExecutionDao{ + + /** + * Returns job execution paths for given BatchStatuses + */ + public Collection getJobExecutions(Collection batchStatuses) + + /** + * Returns job execution paths which ended @param hours ago from "Now" + */ + public Collection getJobExecutions(int hours, Collection jobExecutionPaths) + +} diff --git a/src/main/groovy/com/twcable/spring/batch/repository/GrabbitJobInstanceDao.groovy b/src/main/groovy/com/twcable/spring/batch/repository/GrabbitJobInstanceDao.groovy new file mode 100644 index 0000000..711358d --- /dev/null +++ b/src/main/groovy/com/twcable/spring/batch/repository/GrabbitJobInstanceDao.groovy @@ -0,0 +1,34 @@ +/* + * Copyright 2015 Time Warner Cable, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.twcable.spring.batch.repository + +import org.springframework.batch.core.JobInstance +import org.springframework.batch.core.repository.dao.JobInstanceDao + +/** + * Modified DAO Interface for persisting and retrieving {@link JobInstance} + * @see JobInstanceDao for more details + */ +interface GrabbitJobInstanceDao extends JobInstanceDao { + + /** + * Returns job instance paths by comparing "id" property on "jobInstances" with + * "instanceId" property on JobExecutions for the @param jobExecutionResourcePaths + */ + public Collection getJobInstancePaths(Collection jobExecutionResourcePaths) + +} diff --git a/src/main/groovy/com/twcable/spring/batch/repository/GrabbitStepExecutionDao.groovy b/src/main/groovy/com/twcable/spring/batch/repository/GrabbitStepExecutionDao.groovy new file mode 100644 index 0000000..065f9c5 --- /dev/null +++ b/src/main/groovy/com/twcable/spring/batch/repository/GrabbitStepExecutionDao.groovy @@ -0,0 +1,33 @@ +/* + * Copyright 2015 Time Warner Cable, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.twcable.spring.batch.repository + +import org.springframework.batch.core.StepExecution +import org.springframework.batch.core.repository.dao.StepExecutionDao + +/** + * Modified DAO Interface for persisting and retrieving {@link StepExecution} + * @see StepExecutionDao for more details + */ +interface GrabbitStepExecutionDao extends StepExecutionDao { + + /** + * Returns step execution paths by comparing "jobExecutionId" property on "stepExecutions with + * "executionId" property on JobExecutions for the @param jobExecutionResourcePaths + */ + public Collection getStepExecutionPaths(Collection jobExecutionResourcePaths) +} diff --git a/src/main/groovy/com/twcable/spring/batch/repository/JcrGrabbitExecutionContextDao.groovy b/src/main/groovy/com/twcable/spring/batch/repository/JcrGrabbitExecutionContextDao.groovy new file mode 100644 index 0000000..6e2aa02 --- /dev/null +++ b/src/main/groovy/com/twcable/spring/batch/repository/JcrGrabbitExecutionContextDao.groovy @@ -0,0 +1,358 @@ +/* + * Copyright 2015 Time Warner Cable, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.twcable.spring.batch.repository + +import com.twcable.grabbit.jcr.JcrUtil +import com.twcable.grabbit.util.CryptoUtil +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import org.apache.sling.api.SlingException +import org.apache.sling.api.resource.* +import org.springframework.batch.core.JobExecution +import org.springframework.batch.core.StepExecution +import org.springframework.batch.core.repository.ExecutionContextSerializer +import org.springframework.batch.core.repository.dao.ExecutionContextDao +import org.springframework.batch.item.ExecutionContext + +import javax.annotation.Nonnull + +import static JcrGrabbitStepExecutionDao.ID +import static org.apache.jackrabbit.JcrConstants.NT_UNSTRUCTURED +import static org.apache.sling.api.resource.ResourceUtil.getOrCreateResource + +/** + * JCR Based implementation of {@link ExecutionContextDao} + * Uses {@link ResourceResolverFactory} Sling API to maintain ExecutionContext resources + */ +@CompileStatic +@Slf4j +class JcrGrabbitExecutionContextDao extends AbstractJcrDao implements GrabbitExecutionContextDao { + + public static final String EXECUTION_CONTEXT_ROOT = "${ROOT_RESOURCE_NAME}/executionContexts" + public static final String JOB_EXECUTION_CONTEXT_ROOT = "${EXECUTION_CONTEXT_ROOT}/job" + public static final String STEP_EXECUTION_CONTEXT_ROOT = "${EXECUTION_CONTEXT_ROOT}/step" + + public static final String EXECUTION_ID = "executionId" + public static final String EXECUTION_CONTEXT = "context" + + private ResourceResolverFactory resourceResolverFactory + private ExecutionContextSerializer contextSerializer + + + JcrGrabbitExecutionContextDao( + @Nonnull final ResourceResolverFactory rrf, @Nonnull final ExecutionContextSerializer serializer) { + this.resourceResolverFactory = rrf + this.contextSerializer = serializer + } + + /** + * Returns {@link ExecutionContext} for the given {@link JobExecution} + * The ExecutionContext is retrieved from {@link #JOB_EXECUTION_CONTEXT_ROOT} in JCR + * + * @see ExecutionContextDao#getExecutionContext(JobExecution) + */ + @Override + ExecutionContext getExecutionContext(@Nonnull final JobExecution jobExecution) { + if (!jobExecution) throw new IllegalArgumentException("jobExecution == null") + get(JOB_EXECUTION_CONTEXT_ROOT, jobExecution.id) + } + + /** + * Returns {@link ExecutionContext} for the given {@link StepExecution} + * The ExecutionContext is retrieved from {@link #STEP_EXECUTION_CONTEXT_ROOT} in JCR + * + * @see ExecutionContextDao#getExecutionContext(StepExecution) + */ + @Override + ExecutionContext getExecutionContext(@Nonnull final StepExecution stepExecution) { + if (!stepExecution) throw new IllegalArgumentException("stepExecution == null") + get(STEP_EXECUTION_CONTEXT_ROOT, stepExecution.id) + } + + /** + * Saves the {@link ExecutionContext} for given {@link JobExecution} + * The ExecutionContext is persisted under {@link #JOB_EXECUTION_CONTEXT_ROOT} in JCR + * + * @see ExecutionContextDao#saveExecutionContext(JobExecution) + */ + @Override + void saveExecutionContext(@Nonnull final JobExecution jobExecution) { + if (!jobExecution) throw new IllegalArgumentException("jobExecution == null") + //TODO: Persisted JobExecution Context SHOULD NOT exist + saveOrUpdate(JOB_EXECUTION_CONTEXT_ROOT, jobExecution.id, jobExecution.executionContext) + } + + /** + * Saves the {@link ExecutionContext} for given {@link StepExecution} + * The ExecutionContext is persisted under {@link #STEP_EXECUTION_CONTEXT_ROOT} in JCR + * + * @see ExecutionContextDao#saveExecutionContext(StepExecution) + */ + @Override + void saveExecutionContext(@Nonnull final StepExecution stepExecution) { + if (!stepExecution) throw new IllegalArgumentException("stepExecution == null") + //TODO: Persisted StepExecution Context SHOULD NOT exist + saveOrUpdate(STEP_EXECUTION_CONTEXT_ROOT, stepExecution.id, stepExecution.executionContext) + } + + /** + * Saves the {@link ExecutionContext}s for given {@link StepExecution}s + * + * @see #saveExecutionContext(StepExecution) + * @see #saveExecutionContext(JobExecution) + * @see ExecutionContextDao#saveExecutionContexts(java.util.Collection) + */ + @Override + void saveExecutionContexts(@Nonnull final Collection stepExecutions) { + if (!stepExecutions) throw new IllegalArgumentException("stepExecutions == null or empty") + //TODO : Persisted StepExecutions SHOULD NOT exist yet + + stepExecutions.each { StepExecution stepExecution -> + saveExecutionContext(stepExecution) + saveExecutionContext(stepExecution.jobExecution) + } + } + + /** + * Saves the Updated {@link ExecutionContext} for given {@link JobExecution} + * The Updated ExecutionContext is persisted in {@link #JOB_EXECUTION_CONTEXT_ROOT} in JCR + * + * @see ExecutionContextDao#updateExecutionContext(JobExecution) + */ + @Override + void updateExecutionContext(@Nonnull final JobExecution jobExecution) { + if (!jobExecution) throw new IllegalArgumentException("jobExecution == null") + //TODO : JobExecutionContext SHOULD exist already + saveOrUpdate(JOB_EXECUTION_CONTEXT_ROOT, jobExecution.id, jobExecution.executionContext) + } + + /** + * Saves the Updated {@link ExecutionContext} for given {@link StepExecution} + * the Updated ExecutionContext is persisted in {@link #STEP_EXECUTION_CONTEXT_ROOT} in JCR + * + * @see ExecutionContextDao#updateExecutionContext(StepExecution) + */ + @Override + void updateExecutionContext(@Nonnull final StepExecution stepExecution) { + if (!stepExecution) throw new IllegalArgumentException("stepExecution == null") + //TODO : StepExecutionContext SHOULD exist already + saveOrUpdate(STEP_EXECUTION_CONTEXT_ROOT, stepExecution.id, stepExecution.executionContext) + } + + /** + * Must be called when a new instance of JcrGrabbitExecutionContextDao is created. + * Ensures that {@link #STEP_EXECUTION_CONTEXT_ROOT} and {@link #JOB_EXECUTION_CONTEXT_ROOT} exist on initialization + */ + @Override + protected void ensureRootResource() { + JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + if (!getOrCreateResource(resolver, JOB_EXECUTION_CONTEXT_ROOT, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true)) { + //create the Root Resource + throw new IllegalStateException("Cannot get or create RootResource for : ${JOB_EXECUTION_CONTEXT_ROOT}") + } + if (!getOrCreateResource(resolver, STEP_EXECUTION_CONTEXT_ROOT, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true)) { + //create the Root Resource + throw new IllegalStateException("Cannot get or create RootResource for : ${STEP_EXECUTION_CONTEXT_ROOT}") + } + } + } + + /** + * Saves or Updates given ExecutionContext for the executionId under the rootResourceName + * + * Checks if A resource under either {@link #STEP_EXECUTION_CONTEXT_ROOT} or {@link #JOB_EXECUTION_CONTEXT_ROOT} exists + * which has the "executionId" property. If not present, creates a new resource with {@link com.twcable.grabbit.util.CryptoUtil#generateNextId()}. + * Else, updates existing resource + * + * @see #saveExecutionContext(JobExecution) + * @see #saveExecutionContext(StepExecution) + * @see #saveExecutionContexts(Collection) + * @see #updateExecutionContext(JobExecution) + * @see #updateExecutionContext(StepExecution) + */ + private void saveOrUpdate(@Nonnull final String rootResourceName, @Nonnull final Long executionId, + @Nonnull final ExecutionContext executionContext) { + if (!rootResourceName) throw new IllegalArgumentException("rootResourceName == null") + if (!executionId) throw new IllegalArgumentException("executionId == null") + if (!executionContext) throw new IllegalArgumentException("executionContext == null") + + JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + final rootResource = getOrCreateResource(resolver, rootResourceName, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true) + + final existingResource = rootResource.children.asList().find { Resource resource -> + final properties = resource.adaptTo(ValueMap) + (properties[EXECUTION_ID] as Long) == executionId + } + + //Retrieve all properties from the ExecutionContext in a map + final properties = contextAsResourceProperties(executionId, executionContext) + + if (!existingResource) { + //Resource doesn't exist. Creating it + log.debug "Resource for $executionId doesn't exist. Creating it..." + final createdResource = resolver.create(rootResource, "${CryptoUtil.generateNextId()}", properties) + resolver.commit() + log.debug "Resource Created : ${createdResource}" + } + else { + //Resource exists. Update its properties + ModifiableValueMap map = existingResource.adaptTo(ModifiableValueMap) + map.putAll(properties) + resolver.commit() + log.debug "Updated ExecutionContext : $existingResource" + } + } + } + + /** + * Returns a Map given the executionId and ExecutionContext + * Uses {@link #contextSerializer} to serialize the Context to String + * + * @see #contextFromResourceProperties(ValueMap) + * @see #saveOrUpdate(String, Long, ExecutionContext) + * @see org.springframework.batch.core.repository.dao.DefaultExecutionContextSerializer#serialize(Object, OutputStream) + */ + private Map contextAsResourceProperties(Long executionId, ExecutionContext context) { + final contextAsMap = context.entrySet().iterator().collectEntries { key, value -> [key, value] } as Map + + String contextAsString + try { + ByteArrayOutputStream out = new ByteArrayOutputStream() + contextSerializer.serialize(contextAsMap, out) + contextAsString = new String(out.toByteArray(), "ISO-8859-1") + } + catch (IOException ioe) { + throw new IllegalArgumentException("Could not serialize Execution Context: $context", ioe) + } + + final properties = ([ + (ResourceResolver.PROPERTY_RESOURCE_TYPE): NT_UNSTRUCTURED, + (EXECUTION_ID) : executionId, + (EXECUTION_CONTEXT) : contextAsString + ] as Map) + log.debug "Properties for ExecutionContext : ${context} : ${properties}" + properties + } + + /** + * Returns a Fully hydrated {@link ExecutionContext} for given rootResourceName and executionId + * + * @see #getExecutionContext(JobExecution) + * @see #getExecutionContext(StepExecution) + */ + private ExecutionContext get(@Nonnull final String rootResourceName, @Nonnull final Long executionId) { + if (!rootResourceName) throw new IllegalArgumentException("rootResourceName == null") + if (!executionId) throw new IllegalArgumentException("executionId == null") + + JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + final rootResource = getOrCreateResource(resolver, rootResourceName, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true) + + //find resource from the "rootResource" where the executionId property matches "executionid" + final contextResource = rootResource.children.asList().find { Resource resource -> + final properties = resource.adaptTo(ValueMap) + (properties[EXECUTION_ID] as Long) == executionId + } + if (!contextResource) { + log.error "Could not find executionContext : $executionId" + return null as ExecutionContext + } + + final properties = contextResource.adaptTo(ValueMap) + log.debug "Properties for ExecutionContext OF $executionId : $properties" + + final context = contextFromResourceProperties(properties) + log.debug "Context : ${context}" + return context + } + } + + /** + * Returns a new {@link ExecutionContext} given a {@link ValueMap} + * Uses {@link #contextSerializer} to deserialize the context from String back to Map + * + * @see #contextAsResourceProperties(Long, ExecutionContext) + * @see #get(String, Long) + * @see org.springframework.batch.core.repository.dao.DefaultExecutionContextSerializer#deserialize(InputStream) + */ + private ExecutionContext contextFromResourceProperties(ValueMap properties) { + final contextAsString = properties[EXECUTION_CONTEXT] as String + + Map contextAsMap + try { + ByteArrayInputStream input = new ByteArrayInputStream(contextAsString.getBytes("ISO-8859-1")) + contextAsMap = contextSerializer.deserialize(input) as Map + } + catch (IOException ioe) { + throw new IllegalArgumentException("Could not deserialize Execution Context: $contextAsString", ioe) + } + + ExecutionContext context = new ExecutionContext() + contextAsMap.each { key, value -> context.put(key, value) } + context + } + + @Override + Collection getJobExecutionContextPaths(Collection jobExecutionResourcePaths) { + JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + Collection jobExecutionContextPathsToRemove = [] + jobExecutionResourcePaths.each { String jobExecutionResourcePath -> + Resource jobExecutionResource = resolver.getResource(jobExecutionResourcePath) + ValueMap props = jobExecutionResource.adaptTo(ValueMap) + Long jobExecutionId = props[JcrGrabbitJobExecutionDao.EXECUTION_ID] as Long + String query = "select * from [nt:unstructured] as s " + + "where ISDESCENDANTNODE(s,'${JOB_EXECUTION_CONTEXT_ROOT}') AND ( s.${EXECUTION_ID} = ${jobExecutionId})" + try { + List jobExecutionContextPaths = resolver.findResources(query, "JCR-SQL2").toList().collect { it.path } + jobExecutionContextPathsToRemove.addAll(jobExecutionContextPaths) + } + catch(SlingException | IllegalStateException e) { + log.error "Exception when executing Query: ${query}. \nException - ", e + } + } + //There are 2 versions of Resources returned back by findResources + //One for JcrNodeResource and one for SocialResourceWrapper + //Hence, duplicates need to be removed + return jobExecutionContextPathsToRemove.unique() as Collection + } + } + + @Override + Collection getStepExecutionContextPaths(Collection stepExecutionResourcePaths) { + JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + Collection stepExecutionContextPathsToRemove = [] + stepExecutionResourcePaths.each { String stepExecutionResourcePath -> + Resource stepExecutionResource = resolver.getResource(stepExecutionResourcePath) + ValueMap props = stepExecutionResource.adaptTo(ValueMap) + Long stepExecutionId = props[ID] as Long + String query = "select * from [nt:unstructured] as s " + + "where ISDESCENDANTNODE(s,'${STEP_EXECUTION_CONTEXT_ROOT}') AND ( s.${EXECUTION_ID} = ${stepExecutionId})" + try { + List stepExecutionContextPaths = resolver.findResources(query, "JCR-SQL2").toList().collect { it.path } + stepExecutionContextPathsToRemove.addAll(stepExecutionContextPaths) + } catch (SlingException | IllegalStateException e) { + log.error "Exception when executing Query: ${query}. \nException - ", e + } + } + //There are 2 versions of Resources returned back by findResources + //One for JcrNodeResource and one for SocialResourceWrapper + //Hence, duplicates need to be removed + return stepExecutionContextPathsToRemove.unique() as Collection + + } + + } +} diff --git a/src/main/groovy/com/twcable/spring/batch/repository/JcrGrabbitJobExecutionDao.groovy b/src/main/groovy/com/twcable/spring/batch/repository/JcrGrabbitJobExecutionDao.groovy new file mode 100644 index 0000000..ba1e875 --- /dev/null +++ b/src/main/groovy/com/twcable/spring/batch/repository/JcrGrabbitJobExecutionDao.groovy @@ -0,0 +1,424 @@ +/* + * Copyright 2015 Time Warner Cable, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.twcable.spring.batch.repository + +import com.twcable.grabbit.DateUtil +import com.twcable.grabbit.client.batch.ClientBatchJob +import com.twcable.grabbit.jcr.JcrUtil +import com.twcable.grabbit.util.CryptoUtil +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import org.apache.commons.lang3.StringUtils +import org.apache.sling.api.resource.* +import org.springframework.batch.core.* +import org.springframework.batch.core.repository.dao.JobExecutionDao + +import javax.annotation.Nonnull + +import static JcrGrabbitJobInstanceDao.JOB_INSTANCE_ROOT +import static org.apache.jackrabbit.JcrConstants.NT_UNSTRUCTURED +import static org.apache.sling.api.resource.ResourceUtil.getOrCreateResource + +/** + * JCR Based implementation of {@link JobExecutionDao} + * Uses {@link ResourceResolverFactory} Sling API to maintain JobExecution resources + */ +@CompileStatic +@Slf4j +class JcrGrabbitJobExecutionDao extends AbstractJcrDao implements GrabbitJobExecutionDao { + + public static final String JOB_EXECUTION_ROOT = "${ROOT_RESOURCE_NAME}/jobExecutions" + + public static final String EXECUTION_ID = "executionId" + public static final String TRANSACTION_ID = "transactionId" + public static final String INSTANCE_ID = "instanceId" + public static final String START_TIME = "startTime" + public static final String END_TIME = "endTime" + public static final String STATUS = "status" + public static final String EXIT_CODE = "exitCode" + public static final String EXIT_MESSAGE = "exitMessage" + public static final String CREATE_TIME = "createTime" + public static final String LAST_UPDATED = "lastUpdated" + public static final String JOB_NAME = "jobName" + public static final String VERSION = "version" + + private ResourceResolverFactory resourceResolverFactory + + + JcrGrabbitJobExecutionDao(ResourceResolverFactory rrf) { + this.resourceResolverFactory = rrf + } + + /** + * Saves the {@link JobExecution} under {@link #JOB_EXECUTION_ROOT} in JCR + * + * @see JobExecutionDao#saveJobExecution(JobExecution) + */ + @Override + void saveJobExecution(@Nonnull final JobExecution jobExecution) { + if (!jobExecution) throw new IllegalArgumentException("jobExecution == null") + if (!jobExecution.jobInstance.id) throw new IllegalArgumentException("jobInstance for jobExecution must have an Id") + + //Create a new resource for the jobExecution (with the id) + jobExecution.incrementVersion() + JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + final id = CryptoUtil.generateNextId() + jobExecution.id = id + Resource rootResource = getOrCreateResource(resolver, JOB_EXECUTION_ROOT, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true) + final properties = getJobExecutionProperties(jobExecution) << ([ + (VERSION): jobExecution.version + ] as Map) + log.debug "Properties : ${properties}" + final createdJobExecution = resolver.create(rootResource, "${id}", properties) + resolver.commit() + log.debug "JobExecution Created : $createdJobExecution" + } + } + + /** + * Returns a Map given the JobExecution. Used as "properties map" when a new resource for given + * JobExecution is created + * + * @see #saveJobExecution(JobExecution) + * @see #updateJobExecution(JobExecution) + */ + private static Map getJobExecutionProperties(JobExecution execution) { + execution.with { + [ + (EXECUTION_ID): id, + (TRANSACTION_ID): execution.getJobParameters().getString(ClientBatchJob.TRANSACTION_ID), + (INSTANCE_ID) : jobId, + //Map implementation sets StartTime/EndTime to null .. but looks like I can't store nulls in JCR + (START_TIME) : startTime ? DateUtil.getISOStringFromDate(startTime) : "NULL", + (END_TIME) : endTime ? DateUtil.getISOStringFromDate(endTime) : "NULL", + (STATUS) : execution.status.toString(), + (EXIT_CODE) : execution.exitStatus.exitCode, + (EXIT_MESSAGE): execution.exitStatus.exitDescription, + (CREATE_TIME) : execution.createTime ? DateUtil.getISOStringFromDate(execution.createTime) : "NULL", + (LAST_UPDATED): execution.lastUpdated ? DateUtil.getISOStringFromDate(execution.lastUpdated) : "NULL", + (JOB_NAME) : execution.jobInstance.jobName + + ] as Map + } + } + + /** + * Updates the JobExecution resource under {@link #JOB_EXECUTION_ROOT} for given {@link JobExecution} + * + * @see JobExecutionDao#updateJobExecution(JobExecution) + */ + @Override + void updateJobExecution(@Nonnull final JobExecution jobExecution) { + if (!jobExecution) throw new IllegalArgumentException("jobExecution == null") + if (!jobExecution.id) throw new IllegalArgumentException("jobExecution must have an id") + + JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + final rootResource = getOrCreateResource(resolver, JOB_EXECUTION_ROOT, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true) + + //Get the Execution with jobExecution.id from jobExecution root resource + final jobExecutionResource = resolver.getResource(rootResource, "${jobExecution.id}") + + ModifiableValueMap map = jobExecutionResource.adaptTo(ModifiableValueMap) + + //Oh .. mutation :-) + jobExecution.incrementVersion() + + final properties = getJobExecutionProperties(jobExecution) << ([ + (VERSION): jobExecution.version + ] as Map) + + map.putAll(properties) + resolver.commit() + log.debug "Updated JobExecution $jobExecution with new properties : $properties" + } + } + + /** + * Returns all {@link JobExecution}s for given {@link JobInstance} + * The returned list is sorted backwards by creation time + * + * @see JobExecutionDao#findJobExecutions(JobInstance) + */ + @Override + List findJobExecutions(@Nonnull final JobInstance jobInstance) { + if (!jobInstance) throw new IllegalArgumentException("jobInstance == null") + + JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + final Resource jobExecutionRootResource = getOrCreateResource(resolver, JOB_EXECUTION_ROOT, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true) + List resources = jobExecutionRootResource?.children?.asList() + if (!resources) return Collections.EMPTY_LIST + + List neededResources = resources.findAll { Resource resource -> + final properties = resource.adaptTo(ValueMap) + (properties[INSTANCE_ID] as Long) == jobInstance.id + } as List + + final jobExecutions = neededResources.collect { Resource resource -> + //map jobExecution resources to the JobExecution POJO + final properties = resource.adaptTo(ValueMap) + final jobExecution = mapJobExecution(resolver, properties, jobInstance) + jobExecution + } as List + log.debug "JobExecutions : $jobExecutions" + + final sorted = jobExecutions.sort(false) { a, b -> + ((JobExecution)b).createTime <=> ((JobExecution)a).createTime + } + + return sorted + } + } + + /** + * Returns JobParameters using the given instanceId from {@link JcrGrabbitJobInstanceDao#JOB_INSTANCE_ROOT} + * + * @see #mapJobExecution(ResourceResolver, ValueMap, JobInstance) + */ + private static JobParameters getJobParameters(ResourceResolver resolver, Long instanceId) { + final jobInstanceRoot = getOrCreateResource(resolver, JOB_INSTANCE_ROOT, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true) + final instanceResource = resolver.getResource(jobInstanceRoot, "${instanceId}") + + final properties = instanceResource.adaptTo(ValueMap) + final params = properties[JcrGrabbitJobInstanceDao.PARAMETERS] as String[] + + final paramsMap = params?.collectEntries { String entry -> + final pair = entry.split("=") + if (pair.size() == 1) [pair[0], new JobParameter("No Value")] + else [pair[0], getJobParameter(pair[1])] + } + + paramsMap ? new JobParameters(paramsMap) : new JobParameters() + } + + /** + * Gets a single {@link JobParameter} instance given a String value + * Only Supports Long, String and Doubles right now. Does NOT support Date + * + * @see #getJobParameters(ResourceResolver, Long) + */ + private static JobParameter getJobParameter(String value) { + if (StringUtils.isNumeric(value)) { + return new JobParameter(Long.valueOf(value)) + } + else { + try { + final doubleValue = Double.parseDouble(value) + return new JobParameter(doubleValue) + } + catch (NumberFormatException ignored) { //not a double + + //Falling back to String + return new JobParameter(value) + + //TODO : Implement Conversion of String to Date + } + } + } + + /** + * Returns the Latest {@link JobExecution} created for given {@link JobInstance} + * + * @see JobExecutionDao#getLastJobExecution(JobInstance) + */ + @Override + JobExecution getLastJobExecution(@Nonnull final JobInstance jobInstance) { + if (!jobInstance) throw new IllegalArgumentException("jobInstance == null") + + final jobExecutions = findJobExecutions(jobInstance) + if (!jobExecutions) return null + + //JobExecutions should already return sorted by most recent first + jobExecutions.first() + } + + /** + * Returns all {@link JobExecution}s for given JobName that are running + * + * @see JobExecution#isRunning() + * @see JobExecutionDao#findRunningJobExecutions(String) + */ + @Override + Set findRunningJobExecutions(@Nonnull final String jobName) { + if (!jobName) throw new IllegalArgumentException("jobName == null") + + JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + final jobExecutionRoot = getOrCreateResource(resolver, JOB_EXECUTION_ROOT, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true) + + //find jobExecution root, then find all jobExecutions, then find the jobexecution + //whose, jobInstance.jobName == jobName + final jobExecutionResources = jobExecutionRoot?.children?.asList()?.findAll { Resource resource -> + final properties = resource.adaptTo(ValueMap) + properties[JOB_NAME] == jobName + } + if (!jobExecutionResources) return Collections.EMPTY_SET + + //Map to JobExecution POJO + final jobExecutions = jobExecutionResources.collect { Resource resource -> + final properties = resource.adaptTo(ValueMap) + final jobExecution = mapJobExecution(resolver, properties) + jobExecution + } + + //Find all jobExecution.isRunning() + final runningExecutions = jobExecutions.findAll { it.running } as Set + log.debug "All running executions : $runningExecutions" + return runningExecutions + } + } + + /** + * Returns a fully hydrated {@link JobExecution} for given executionId + * + * @see JobExecutionDao#getJobExecution(Long) + */ + @Override + JobExecution getJobExecution(@Nonnull final Long executionId) { + if (!executionId) throw new IllegalArgumentException("executionId == null") + + //find jobExecution root, then find jobExecution with node name : "jobExecution${executionId}" + JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + final jobExecutionRoot = getOrCreateResource(resolver, JOB_EXECUTION_ROOT, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true) + final jobExecutionResource = jobExecutionRoot?.children?.asList()?.find { Resource resource -> + final properties = resource.adaptTo(ValueMap) + (properties[EXECUTION_ID] as Long) == executionId + } + if (!jobExecutionResource) return null as JobExecution + + final properties = jobExecutionResource.adaptTo(ValueMap) + final jobExecution = mapJobExecution(resolver, properties) + log.debug "JobExecution : $jobExecution" + return jobExecution + } + } + + /** + * Maps given {@link ValueMap} to a new {@link JobExecution} instance + * + * @see #getJobExecution(Long) + * @see #findJobExecutions(JobInstance) + * @see #findRunningJobExecutions(String) + */ + private + static JobExecution mapJobExecution(ResourceResolver resolver, ValueMap properties, JobInstance jobInstance = null) { + + final id = properties[EXECUTION_ID] as Long + + GrabbitJobExecution jobExecution + if (!jobInstance) { + final JobInstance instance = new JobInstance(properties[INSTANCE_ID] as Long, properties[JOB_NAME] as String) + final jobParameters = getJobParameters(resolver, properties[INSTANCE_ID] as Long) + jobExecution = new GrabbitJobExecution(instance, id, jobParameters) + } + else { + final jobParameters = getJobParameters(resolver, jobInstance.id) + jobExecution = new GrabbitJobExecution(jobInstance, id, jobParameters) + } + + jobExecution.startTime = getDate(properties[START_TIME] as String) + jobExecution.endTime = getDate(properties[END_TIME] as String) + jobExecution.status = BatchStatus.valueOf(properties[STATUS] as String) + jobExecution.exitStatus = new ExitStatus(properties[EXIT_CODE] as String, properties[EXIT_MESSAGE] as String) + jobExecution.createTime = getDate(properties[CREATE_TIME] as String) + jobExecution.lastUpdated = getDate(properties[LAST_UPDATED] as String) + jobExecution.version = properties[VERSION] as Integer ?: 0 + jobExecution.transactionID = properties[TRANSACTION_ID] as Long ?: 0L + + jobExecution + } + + + private static Date getDate(String value) { + value == "NULL" ? null : DateUtil.getDateFromISOString(value) + } + + /** + * Because it may be possible that the status of a JobExecution is updated + * while running, the following method will synchronize only the status and + * version fields. + * + * @see JobExecutionDao#synchronizeStatus(JobExecution) + */ + @Override + void synchronizeStatus(@Nonnull final JobExecution jobExecution) { + if (!jobExecution) throw new IllegalArgumentException("jobExecution == null") + + final savedJobExecution = getJobExecution(jobExecution.id) + if (savedJobExecution.version != jobExecution.version) { + jobExecution.upgradeStatus(savedJobExecution.status) + jobExecution.version = savedJobExecution.version + log.info "Synchronized Status of ${jobExecution} with Saved Execution : ${savedJobExecution}" + } + } + + /** + * Must be called when a new instance of JcrGrabbitJobExecutionDao is created. + * Ensures that {@link #JOB_EXECUTION_ROOT} exists on initialization + */ + @Override + protected void ensureRootResource() { + JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + if (!getOrCreateResource(resolver, JOB_EXECUTION_ROOT, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true)) { + //create the Root Resource + throw new IllegalStateException("Cannot get or create RootResource for : ${JOB_EXECUTION_ROOT}") + } + if (!getOrCreateResource(resolver, JOB_INSTANCE_ROOT, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true)) { + //create the Root Resource + throw new IllegalStateException("Cannot get or create RootResource for : ${JOB_INSTANCE_ROOT}") + } + } + } + + @Override + Collection getJobExecutions(Collection batchStatuses) { + String statusPredicate = batchStatuses.collect { "s.status = '${it}'" }.join(' or ') + JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + String jobExecutionsQuery = "select * from [nt:unstructured] as s where " + + "ISDESCENDANTNODE(s,'${JOB_EXECUTION_ROOT}') AND ( ${statusPredicate} )" + Collection jobExecutions = resolver.findResources(jobExecutionsQuery, "JCR-SQL2") + .toList() + .collect { it.path } + .unique() as Collection + log.debug "JobExecutions: $jobExecutions, size: ${jobExecutions.size()}" + return jobExecutions + } + + } + + @Override + Collection getJobExecutions(int hours, Collection jobExecutions) { + JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + //Create a Date object that is "hours" ago from now + Calendar olderThanHours = Calendar.getInstance() + log.info "Current time: ${olderThanHours.time}" + olderThanHours.add(Calendar.HOUR, -hours) + log.info "Hours ${hours} .. OlderThanHours Time: ${olderThanHours.time}" + + //Find all resources that are older than "olderThanHours" Date + Collection olderResourcePaths = jobExecutions.findAll { String resourcePath -> + Resource resource = resolver.getResource(resourcePath) + ValueMap props = resource.adaptTo(ValueMap) + String dateInIsoString = props[END_TIME] as String + Date endTimeDate = DateUtil.getDateFromISOString(dateInIsoString) + olderThanHours.time.compareTo(endTimeDate) > 0 + } as Collection + log.debug "JobExecutionsOlder than ${hours} hours: $olderResourcePaths , length: ${olderResourcePaths.size()}" + return olderResourcePaths + + } + + } +} diff --git a/src/main/groovy/com/twcable/spring/batch/repository/JcrGrabbitJobInstanceDao.groovy b/src/main/groovy/com/twcable/spring/batch/repository/JcrGrabbitJobInstanceDao.groovy new file mode 100644 index 0000000..0602102 --- /dev/null +++ b/src/main/groovy/com/twcable/spring/batch/repository/JcrGrabbitJobInstanceDao.groovy @@ -0,0 +1,288 @@ +/* + * Copyright 2015 Time Warner Cable, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.twcable.spring.batch.repository + +import com.twcable.grabbit.jcr.JcrUtil +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import org.apache.sling.api.resource.Resource +import org.apache.sling.api.resource.ResourceResolver +import org.apache.sling.api.resource.ResourceResolverFactory +import org.apache.sling.api.resource.ValueMap +import org.springframework.batch.core.DefaultJobKeyGenerator +import org.springframework.batch.core.JobExecution +import org.springframework.batch.core.JobInstance +import org.springframework.batch.core.JobKeyGenerator +import org.springframework.batch.core.JobParameters +import org.springframework.batch.core.repository.dao.JobInstanceDao + +import javax.annotation.Nonnull + +import static org.apache.jackrabbit.JcrConstants.NT_UNSTRUCTURED +import static org.apache.sling.api.resource.ResourceUtil.getOrCreateResource + +/** + * JCR Based implementation of {@link JobInstanceDao} + * Uses {@link ResourceResolverFactory} Sling API to maintain JobInstance resources + */ +@CompileStatic +@Slf4j +class JcrGrabbitJobInstanceDao extends AbstractJcrDao implements GrabbitJobInstanceDao { + + public static final String JOB_INSTANCE_ROOT = "${ROOT_RESOURCE_NAME}/jobInstances" + + public static final String INSTANCE_ID = "id" + public static final String KEY = "key" + public static final String NAME = "name" + public static final String VERSION = "version" + public static final String PARAMETERS = "parameters" + + private ResourceResolverFactory resourceResolverFactory + + + JcrGrabbitJobInstanceDao(ResourceResolverFactory rrf) { + this.resourceResolverFactory = rrf + } + + /** + * Creates a new {@link JobInstance} in JCR under {@link #JOB_INSTANCE_ROOT} and returns the instance + * + * @see JobInstanceDao#createJobInstance(String, JobParameters) + */ + @Override + JobInstance createJobInstance(@Nonnull final String jobName, @Nonnull final JobParameters jobParameters) { + if (!jobName) throw new IllegalArgumentException("jobName == null") + if (!jobParameters) throw new IllegalArgumentException("jobParameters == null") + if (getJobInstance(jobName, jobParameters)) throw new IllegalStateException("A JobInstance for jobName: ${jobName} must not already exist") + + JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + + //TODO : This will need to be a "incrementing ID" after all and NOT use a UUID (for other APIs like + //getJobInstances which require result to be sorted backwards by "id". If we use UUID, then the possibility of + //Negative UUIDs will screw up the ordering) + final instanceId = getNextJobInstanceId(resolver) + + JobInstance jobInstance = new JobInstance(instanceId, jobName) + jobInstance.incrementVersion() + + JobKeyGenerator jobKeyGenerator = new DefaultJobKeyGenerator() + + final paramsStrings = jobParameters.parameters.collect { key, value -> "${key}=${value.toString()}" } as String[] + + final properties = [ + (KEY) : jobKeyGenerator.generateKey(jobParameters), + (NAME) : jobName, + (INSTANCE_ID): instanceId, + (VERSION) : jobInstance.version, + (PARAMETERS) : paramsStrings + ] as Map + + final rootResource = getOrCreateResource(resolver, JOB_INSTANCE_ROOT, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true) + final jobInstanceResource = resolver.create(rootResource, "${instanceId}", properties) + resolver.commit() + log.debug "JobInstance Resource : $jobInstanceResource" + return jobInstance + } + } + + /** + * Returns a fully hydrated {@link JobInstance} given JobName and {@link JobParameters} + * Uses a "JobKey" to identify the JobInstance + * @see DefaultJobKeyGenerator + * + * The {@link JobInstance} resource is retrieved from {@link #JOB_INSTANCE_ROOT} + * + * @see JobInstanceDao#getJobInstance(String, JobParameters) + */ + @Override + JobInstance getJobInstance(@Nonnull final String jobName, @Nonnull final JobParameters jobParameters) { + if (!jobName) throw new IllegalArgumentException("jobName == null") + if (!jobParameters) throw new IllegalArgumentException("jobParameters == null") + + final jobKey = new DefaultJobKeyGenerator().generateKey(jobParameters) + //Find a resource under "/jobInstances" for the jobKey above + JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + final rootResource = getOrCreateResource(resolver, JOB_INSTANCE_ROOT, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true) + final jobInstanceResource = rootResource?.children?.asList()?.find { Resource resource -> + final properties = resource.adaptTo(ValueMap) + (properties[KEY] as String) == jobKey + } + + if (!jobInstanceResource) return null as JobInstance + + //map that resource's id and jobName to a JobInstance instance + final jobId = jobInstanceResource.adaptTo(ValueMap).get(INSTANCE_ID) as Long + final JobInstance jobInstance = new JobInstance(jobId, jobName) + return jobInstance + } + } + + /** + * Returns a fully hydrated {@link JobInstance} given an instanceId + * The {@link JobInstance} resource is retrieved from {@link #JOB_INSTANCE_ROOT} + * + * @see JobInstanceDao#getJobInstance(Long) + */ + @Override + JobInstance getJobInstance(@Nonnull final Long instanceId) { + if (!instanceId) throw new IllegalArgumentException("instanceId == null") + //Get the "instanceId" node from JOB_INSTANCE_ROOT + JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + final rootResource = getOrCreateResource(resolver, JOB_INSTANCE_ROOT, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true) + final jobInstanceResource = rootResource?.children?.asList()?.find { Resource resource -> + final properties = resource.adaptTo(ValueMap) + (properties[INSTANCE_ID] as Long) == instanceId + } + + if (!jobInstanceResource) return null as JobInstance + + final jobName = jobInstanceResource.adaptTo(ValueMap).get(NAME) as String //Get the jobName Property from that + final jobInstance = new JobInstance(instanceId, jobName) + return jobInstance + } + } + + /** + * Returns a fully hydrated {@link JobInstance} given a {@link JobExecution} + * + * @see JobInstanceDao#getJobInstance(JobExecution) + */ + @Override + JobInstance getJobInstance(@Nonnull final JobExecution jobExecution) { + if (!jobExecution) throw new IllegalArgumentException("jobExecution == null") + jobExecution?.jobInstance + } + + /** + * Returns all {@link JobInstance}s for given jobName and a start index + count + * + * @see JobInstanceDao#getJobInstances(String, int, int) + * @see org.springframework.batch.core.repository.dao.MapJobInstanceDao#getJobInstances(String, int, int) + * @see org.springframework.batch.core.repository.dao.JdbcJobInstanceDao#getJobInstances(String, int, int) + */ + @Override + List getJobInstances( + @Nonnull final String jobName, @Nonnull final int start, @Nonnull final int count) { + if (!jobName) throw new IllegalArgumentException("jobName == null") + if (start == null) throw new IllegalArgumentException("start == null") + if (count == null) throw new IllegalArgumentException("count == null") + if ((start + count) < start) throw new IllegalArgumentException("start (${start}) + count (${count}) causes an int overflow") + + //Get All jobInstances in JOB_INSTANCE_ROOT with jobName property = $jobName + //Map jobInstances nodes to JobInstance object + JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + final rootResource = getOrCreateResource(resolver, JOB_INSTANCE_ROOT, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true) + final jobInstanceResource = rootResource?.children?.asList()?.findAll { Resource resource -> + final properties = resource.adaptTo(ValueMap) + properties[NAME] == jobName + } + final jobInstances = jobInstanceResource?.collect { Resource resource -> + final properties = resource.adaptTo(ValueMap) + final jobInstance = new JobInstance(properties[INSTANCE_ID] as Long, jobName) + jobInstance + } + + if (!jobInstances) return Collections.EMPTY_LIST + + if (jobInstances.size() == 1) { + //If only one jobInstance exists, no need to sort. Just return the list + return jobInstances + } + + final startIndex = Math.min(start, jobInstances.size()); + final endIndex = Math.min(start + count, jobInstances.size()); + + //TODO : This is going to + final sortedInstances = jobInstances.sort(false) { a, b -> + ((JobInstance)b).id <=> ((JobInstance)a).id + } as List + final subList = sortedInstances[startIndex..endIndex - 1] + return subList + } + } + + /** + * Returns all JobNames under {@link #JOB_INSTANCE_ROOT} + * + * @see JobInstanceDao#getJobNames() + */ + @Override + List getJobNames() { + JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + final jobInstanceResources = getOrCreateResource(resolver, JOB_INSTANCE_ROOT, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true)?.children?.asList() + if (!jobInstanceResources) return Collections.EMPTY_LIST + + final List jobNames = jobInstanceResources.collect { Resource r -> + final valueMap = r.adaptTo(ValueMap) + valueMap[NAME] as String + } as List + return jobNames + } + } + + /** + * Must be called when a new instance of JcrGrabbitJobInstanceDao is created. + * Ensures that {@link #JOB_INSTANCE_ROOT} exists on initialization + */ + @Override + protected void ensureRootResource() { + JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + if (!getOrCreateResource(resolver, JOB_INSTANCE_ROOT, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true)) { + //create the Root Resource + throw new IllegalStateException("Cannot get or create RootResource for : ${JOB_INSTANCE_ROOT}") + } + } + } + + /** + * Gets next Id to be assigned to JobInstanceId. This looks for Children under {@link #JOB_INSTANCE_ROOT} + * This is different from {@link com.twcable.grabbit.util.CryptoUtil#generateNextId()} and is only used for JobInstanceId + * @param resolver + */ + private static Long getNextJobInstanceId(@Nonnull ResourceResolver resolver) { + if (!resolver) throw new IllegalArgumentException("resolver == null") + + final rootResource = resolver.getResource(JOB_INSTANCE_ROOT) + final nextId = (rootResource.children.asList().size() + 1) as Long + log.debug "Next JobInstance Id : $nextId" + nextId + + } + + @Override + Collection getJobInstancePaths(Collection jobExecutionResourcePaths) { + JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + Collection jobInstancesToRemove = [] + jobExecutionResourcePaths.each { String jobExecutionResourcePath -> + Resource jobExecutionResource = resolver.getResource(jobExecutionResourcePath) + ValueMap props = jobExecutionResource.adaptTo(ValueMap) + Long instanceId = props[JcrGrabbitJobExecutionDao.INSTANCE_ID] as Long + String jobInstanceToRemoveResourcePath = "${JOB_INSTANCE_ROOT}/${instanceId}".toString() + Resource jobInstanceToRemove = resolver.getResource(jobInstanceToRemoveResourcePath) + if (!jobInstanceToRemove) { + log.info "JobInstance with id : ${instanceId} is already removed" + } else { + jobInstancesToRemove.add(jobInstanceToRemoveResourcePath) + } + } + return jobInstancesToRemove + + + } + + } +} diff --git a/src/main/groovy/com/twcable/spring/batch/repository/JcrGrabbitStepExecutionDao.groovy b/src/main/groovy/com/twcable/spring/batch/repository/JcrGrabbitStepExecutionDao.groovy new file mode 100644 index 0000000..f486259 --- /dev/null +++ b/src/main/groovy/com/twcable/spring/batch/repository/JcrGrabbitStepExecutionDao.groovy @@ -0,0 +1,325 @@ +/* + * Copyright 2015 Time Warner Cable, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.twcable.spring.batch.repository + +import com.twcable.grabbit.DateUtil +import com.twcable.grabbit.jcr.JcrUtil +import com.twcable.grabbit.util.CryptoUtil +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import org.apache.sling.api.SlingException +import org.apache.sling.api.resource.ModifiableValueMap +import org.apache.sling.api.resource.Resource +import org.apache.sling.api.resource.ResourceResolver +import org.apache.sling.api.resource.ResourceResolverFactory +import org.apache.sling.api.resource.ValueMap +import org.springframework.batch.core.BatchStatus +import org.springframework.batch.core.ExitStatus +import org.springframework.batch.core.JobExecution +import org.springframework.batch.core.StepExecution +import org.springframework.batch.core.repository.dao.StepExecutionDao + +import javax.annotation.Nonnull + +import static JcrGrabbitJobExecutionDao.EXECUTION_ID +import static org.apache.jackrabbit.JcrConstants.NT_UNSTRUCTURED +import static org.apache.sling.api.resource.ResourceUtil.getOrCreateResource + +/** + * JCR Based implementation of {@link StepExecutionDao} + * Uses {@link ResourceResolverFactory} Sling API to maintain StepExecution resources + */ +@CompileStatic +@Slf4j +public class JcrGrabbitStepExecutionDao extends AbstractJcrDao implements GrabbitStepExecutionDao { + + public static final String STEP_EXECUTION_ROOT = "${ROOT_RESOURCE_NAME}/stepExecutions" + + public static final String ID = "id" + public static final String NAME = "name" + public static final String JOB_EXECUTION_ID = "jobExecutionId" + public static final String START_TIME = "startTime" + public static final String END_TIME = "endTime" + public static final String STATUS = "status" + public static final String COMMIT_COUNT = "commitCount" + public static final String READ_COUNT = "readCount" + public static final String FILTER_COUNT = "filterCount" + public static final String WRITE_COUNT = "writeCount" + public static final String EXIT_CODE = "exitCode" + public static final String EXIT_MESSAGE = "exitMessage" + public static final String READ_SKIP_COUNT = "readSkipCount" + public static final String WRITE_SKIP_COUNT = "writeSkipCount" + public static final String PROCESS_SKIP_COUNT = "processSkipCount" + public static final String ROLL_BACK_COUNT = "rollbackCount" + public static final String LAST_UPDATED = "lastUpdated" + public static final String VERSION = "version" + + private ResourceResolverFactory resourceResolverFactory + + + JcrGrabbitStepExecutionDao(ResourceResolverFactory rrf) { + this.resourceResolverFactory = rrf + } + + /** + * Saves the {@link StepExecution} under {@link #STEP_EXECUTION_ROOT} in JCR + * + * @see StepExecutionDao#saveStepExecution(StepExecution) + */ + @Override + void saveStepExecution(@Nonnull final StepExecution stepExecution) { + if (!stepExecution) throw new IllegalArgumentException("stepExecution == null") + if (stepExecution.id != null) throw new IllegalStateException("stepExecution.id must be null") + + final id = CryptoUtil.generateNextId() + stepExecution.id = id + saveOrUpdate("${id}", stepExecution) + } + + /** + * Saves the {@link StepExecution}s under {@link #STEP_EXECUTION_ROOT} in JCR + * + * @see #saveStepExecution(StepExecution) + * @see StepExecutionDao#saveStepExecutions(Collection) + */ + @Override + void saveStepExecutions(@Nonnull final Collection stepExecutions) { + if (!stepExecutions) throw new IllegalArgumentException("stepExecutions == null or empty") + if (stepExecutions.any { it.id != null }) throw new IllegalStateException("All stepExecution Ids must be null") + stepExecutions.each { saveStepExecution(it) } + } + + /** + * Updates the {@link StepExecution} resource under {@link #STEP_EXECUTION_ROOT} + * + * @see StepExecutionDao#updateStepExecution(StepExecution) + */ + @Override + void updateStepExecution(@Nonnull final StepExecution stepExecution) { + if (!stepExecution) throw new IllegalArgumentException("stepExecution == null") + if (!stepExecution.id) throw new IllegalArgumentException("stepExecution == null") + if (!stepExecution.jobExecutionId) throw new IllegalStateException("stepExecution.jobExecutionId == null") + if (!stepExecution.stepName) throw new IllegalStateException("stepExecution.stepName == null") + if (!stepExecution.startTime) throw new IllegalStateException("stepExecution.startTime == null") + if (!stepExecution.status) throw new IllegalStateException("stepExecution.status == null") + saveOrUpdate("${stepExecution.id}", stepExecution) + } + + /** + * Returns a fully hydrated {@link StepExecution} instance given its parent {@link JobExecution} and a stepExecutionId + * + * @see StepExecutionDao#getStepExecution(JobExecution, Long) + */ + @Override + StepExecution getStepExecution(@Nonnull final JobExecution jobExecution, @Nonnull final Long stepExecutionId) { + if (!jobExecution) throw new IllegalArgumentException("jobExecution == null") + if (!stepExecutionId) throw new IllegalArgumentException("stepExecutionId == null") + + JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + final Resource rootResource = getOrCreateResource(resolver, STEP_EXECUTION_ROOT, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true) + final stepExecutionResource = rootResource?.children?.asList()?.find { Resource resource -> + final properties = resource.adaptTo(ValueMap) + (properties[ID] as Long) == stepExecutionId + } + if (!stepExecutionResource) return null as StepExecution + + final properties = stepExecutionResource.adaptTo(ValueMap) + + final stepExecution = mapStepExecution(properties, jobExecution) + return stepExecution + } + } + + /** + * Hydrates all the {@link JobExecution#stepExecutions} by calling {@link JobExecution#addStepExecutions(List)} + * + * Retrieves all {@link StepExecution}s under {@link #STEP_EXECUTION_ROOT} for given {@link JobExecution#id} + * + * @see StepExecutionDao#addStepExecutions(JobExecution) + */ + @Override + void addStepExecutions(@Nonnull final JobExecution jobExecution) { + if (!jobExecution) throw new IllegalArgumentException("jobExecution == null") + //TODO : JOB_EXECUTION's ID must exist + + JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + final rootResource = getOrCreateResource(resolver, STEP_EXECUTION_ROOT, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true) + final stepExecutionResources = rootResource.children.asList().findAll { Resource resource -> + final properties = resource.adaptTo(ValueMap) + (properties[JOB_EXECUTION_ID] as Long) == jobExecution.id + } + + final stepExecutions = stepExecutionResources.collect { Resource resource -> + final properties = resource.adaptTo(ValueMap) + mapStepExecution(properties, jobExecution) + } as List + + log.debug "StepExecutions for the JobExecution : ${jobExecution} ==> $stepExecutions" + + //THIS IS VERY WEIRD! + jobExecution.addStepExecutions(stepExecutions) + } + } + + /** + * Saves or Updates given StepExecution for the executionId under {@link #STEP_EXECUTION_ROOT} in JCR + * + * Checks if A resource for ExecutionId exists which has the "executionId" resource name. + * If not present, creates a new resource with executionId as name. + * Else, updates existing resource + * + * @see #saveStepExecution(StepExecution) + * @see #updateStepExecution(StepExecution) + */ + private void saveOrUpdate(@Nonnull final String executionId, @Nonnull final StepExecution execution) { + if (!executionId) throw new IllegalArgumentException("executionId == null") + if (!execution) throw new IllegalArgumentException("execution == null") + + JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + final rootResource = getOrCreateResource(resolver, STEP_EXECUTION_ROOT, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true) + final existingResource = resolver.getResource(rootResource, executionId) + + //Retrieve all properties from the ExecutionContext in a map + final properties = getStepExecutionProperties(execution) << ([ + (ResourceResolver.PROPERTY_RESOURCE_TYPE): NT_UNSTRUCTURED + ] as Map) + + if (!existingResource) { + //Resource doesn't exist. Creating it + + final createdResource = resolver.create(rootResource, executionId, properties) + resolver.commit() + log.debug "Resource Created : ${createdResource}" + } + else { + //Resource exists. Update its properties + + ModifiableValueMap map = existingResource.adaptTo(ModifiableValueMap) + map.putAll(properties) + resolver.commit() + log.debug "Updated StepExecution : $existingResource" + execution.incrementVersion() + } + } + } + + /** + * Returns a Map given a {@link StepExecution}. Used as Properties for a StepExecution resource in JCR + * + * @see #saveOrUpdate(String, StepExecution) + */ + private static Map getStepExecutionProperties(StepExecution execution) { + execution.incrementVersion() + [ + (ID) : execution.id, + (NAME) : execution.stepName, + (JOB_EXECUTION_ID) : execution.jobExecutionId, + //Map implementation sets StartTime/EndTime to null .. but looks like I can't store nulls in JCR + (START_TIME) : execution.startTime ? DateUtil.getISOStringFromDate(execution.startTime) : "NULL", + (END_TIME) : execution.endTime ? DateUtil.getISOStringFromDate(execution.endTime) : "NULL", + (STATUS) : execution.status.toString(), + (COMMIT_COUNT) : execution.commitCount, + (READ_COUNT) : execution.readCount, + (FILTER_COUNT) : execution.filterCount, + (WRITE_COUNT) : execution.writeCount, + (EXIT_CODE) : execution.exitStatus.exitCode, + (EXIT_MESSAGE) : execution.exitStatus.exitDescription, + (READ_SKIP_COUNT) : execution.readSkipCount, + (WRITE_SKIP_COUNT) : execution.writeSkipCount, + (PROCESS_SKIP_COUNT): execution.processSkipCount, + (ROLL_BACK_COUNT) : execution.rollbackCount, + (LAST_UPDATED) : execution.lastUpdated ? DateUtil.getISOStringFromDate(execution.lastUpdated) : "NULL", + (VERSION) : execution.version + + ] as Map + } + + /** + * Maps a {@link ValueMap} and {@link JobExecution} to a new {@link StepExecution} + * @param properties + * @param jobExecution + * @return + */ + private static StepExecution mapStepExecution(ValueMap properties, JobExecution jobExecution) { + + StepExecution stepExecution = new StepExecution(properties[NAME] as String, jobExecution, properties[ID] as Long) + stepExecution.startTime = getDate(properties[START_TIME] as String) + stepExecution.endTime = getDate(properties[END_TIME] as String) + stepExecution.status = BatchStatus.valueOf(properties[STATUS] as String) + stepExecution.commitCount = properties[COMMIT_COUNT] as Integer ?: 0 + stepExecution.readCount = properties[READ_COUNT] as Integer ?: 0 + stepExecution.filterCount = properties[FILTER_COUNT] as Integer ?: 0 + stepExecution.writeCount = properties[WRITE_COUNT] as Integer ?: 0 + stepExecution.exitStatus = new ExitStatus(properties[EXIT_CODE] as String, properties[EXIT_MESSAGE] as String) + stepExecution.readSkipCount = properties[READ_SKIP_COUNT] as Integer ?: 0 + stepExecution.writeSkipCount = properties[WRITE_SKIP_COUNT] as Integer ?: 0 + stepExecution.processSkipCount = properties[PROCESS_SKIP_COUNT] as Integer ?: 0 + stepExecution.rollbackCount = properties[ROLL_BACK_COUNT] as Integer ?: 0 + stepExecution.lastUpdated = getDate(properties[LAST_UPDATED] as String) + stepExecution.version = properties[VERSION] as Integer ?: 0 + log.debug "Mapped StepExecution : $stepExecution" + stepExecution + } + + + private static Date getDate(String value) { + value == "NULL" ? null : DateUtil.getDateFromISOString(value) + } + + /** + * Must be called when a new instance of JcrGrabbitStepExecutionDao is created. + * Ensures that {@link #STEP_EXECUTION_ROOT} exists on initialization + */ + @Override + protected void ensureRootResource() { + JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + //ResourceUtil.getOrCreateResource() + if (!getOrCreateResource(resolver, STEP_EXECUTION_ROOT, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true)) { + //create the Root Resource + throw new IllegalStateException("Cannot get or create RootResource for : ${STEP_EXECUTION_ROOT}") + } + if (!getOrCreateResource(resolver, JcrGrabbitJobExecutionDao.JOB_EXECUTION_ROOT, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true)) { + //create the Root Resource + throw new IllegalStateException("Cannot get or create RootResource for : ${JcrGrabbitJobExecutionDao.JOB_EXECUTION_ROOT}") + } + } + } + + @Override + Collection getStepExecutionPaths(Collection jobExecutionResourcePaths) { + JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + Collection stepExecutionsToRemove = [] + jobExecutionResourcePaths.each { String jobExecutionResourcePath -> + Resource jobExecutionResource = resolver.getResource(jobExecutionResourcePath) + ValueMap props = jobExecutionResource.adaptTo(ValueMap) + Long jobExecutionId = props[EXECUTION_ID] as Long + String query = "select * from [nt:unstructured] as s " + + "where ISDESCENDANTNODE(s,'${STEP_EXECUTION_ROOT}') AND ( s.${JOB_EXECUTION_ID} = ${jobExecutionId})" + try { + List stepExecutions = resolver.findResources(query, "JCR-SQL2").toList().collect { it.path } + stepExecutionsToRemove.addAll(stepExecutions) + } catch (SlingException | IllegalStateException e) { + log.error "Exception when executing Query: ${query}. \nException - ", e + } + } + //There are 2 versions of Resources returned back by findResources + //One for JcrNodeResource and one for SocialResourceWrapper + //Hence, duplicates need to be removed + return stepExecutionsToRemove.unique() as Collection + } + } +} diff --git a/src/main/groovy/com/twcable/spring/batch/repository/JcrJobExplorerFactoryBean.groovy b/src/main/groovy/com/twcable/spring/batch/repository/JcrJobExplorerFactoryBean.groovy new file mode 100644 index 0000000..60754b0 --- /dev/null +++ b/src/main/groovy/com/twcable/spring/batch/repository/JcrJobExplorerFactoryBean.groovy @@ -0,0 +1,92 @@ +/* + * Copyright 2015 Time Warner Cable, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.twcable.spring.batch.repository + +import groovy.transform.CompileStatic +import org.springframework.batch.core.explore.JobExplorer +import org.springframework.batch.core.explore.support.AbstractJobExplorerFactoryBean +import org.springframework.batch.core.explore.support.SimpleJobExplorer +import org.springframework.batch.core.repository.dao.ExecutionContextDao +import org.springframework.batch.core.repository.dao.JobExecutionDao +import org.springframework.batch.core.repository.dao.JobInstanceDao +import org.springframework.batch.core.repository.dao.StepExecutionDao +import org.springframework.beans.factory.InitializingBean +import org.springframework.util.Assert + +/** + * A {@link org.springframework.beans.factory.FactoryBean} that automates the creation of a + * {@link SimpleJobExplorer} using JCR DAO implementations. + * + * @see JcrJobRepositoryFactoryBean + */ +@CompileStatic +public class JcrJobExplorerFactoryBean extends AbstractJobExplorerFactoryBean implements InitializingBean { + + private JcrJobRepositoryFactoryBean repositoryFactory + + + public void setRepositoryFactory(JcrJobRepositoryFactoryBean repositoryFactory) { + this.repositoryFactory = repositoryFactory; + } + + + JcrJobExplorerFactoryBean(JcrJobRepositoryFactoryBean repositoryFactory) { + this.repositoryFactory = repositoryFactory + } + + + JcrJobExplorerFactoryBean() {} + + + @Override + protected JobInstanceDao createJobInstanceDao() throws Exception { + repositoryFactory.jobInstanceDao + } + + + @Override + protected JobExecutionDao createJobExecutionDao() throws Exception { + repositoryFactory.jobExecutionDao + } + + + @Override + protected StepExecutionDao createStepExecutionDao() throws Exception { + repositoryFactory.stepExecutionDao + } + + + @Override + protected ExecutionContextDao createExecutionContextDao() throws Exception { + repositoryFactory.executionContextDao + } + + + @Override + public JobExplorer getObject() throws Exception { + return new SimpleJobExplorer(createJobInstanceDao(), createJobExecutionDao(), createStepExecutionDao(), + createExecutionContextDao()); + + } + + + @Override + void afterPropertiesSet() throws Exception { + Assert.state(repositoryFactory != null, "A JcrJobRepositoryFactoryBean must be provided") + repositoryFactory.afterPropertiesSet() + } +} diff --git a/src/main/groovy/com/twcable/spring/batch/repository/JcrJobRepositoryFactoryBean.groovy b/src/main/groovy/com/twcable/spring/batch/repository/JcrJobRepositoryFactoryBean.groovy new file mode 100644 index 0000000..2aced9c --- /dev/null +++ b/src/main/groovy/com/twcable/spring/batch/repository/JcrJobRepositoryFactoryBean.groovy @@ -0,0 +1,133 @@ +/* + * Copyright 2015 Time Warner Cable, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.twcable.spring.batch.repository + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import org.apache.sling.api.resource.ResourceResolverFactory +import org.springframework.batch.core.repository.ExecutionContextSerializer +import org.springframework.batch.core.repository.dao.ExecutionContextDao +import org.springframework.batch.core.repository.dao.JobExecutionDao +import org.springframework.batch.core.repository.dao.JobInstanceDao +import org.springframework.batch.core.repository.dao.StepExecutionDao +import org.springframework.batch.core.repository.support.AbstractJobRepositoryFactoryBean +import org.springframework.batch.support.transaction.ResourcelessTransactionManager +import org.springframework.transaction.PlatformTransactionManager + + +/** + * A {@link org.springframework.beans.factory.FactoryBean} that automates the creation of a + * {@link org.springframework.batch.core.repository.support.SimpleJobRepository} using persistent JCR DAO + * implementations. Requires user to provide a reference to {@link ResourceResolverFactory} and + * a {@link ExecutionContextSerializer} + */ +@CompileStatic +@Slf4j +class JcrJobRepositoryFactoryBean extends AbstractJobRepositoryFactoryBean { + + private JcrGrabbitJobExecutionDao jobExecutionDao + + private JcrGrabbitJobInstanceDao jobInstanceDao + + private JcrGrabbitStepExecutionDao stepExecutionDao + + private JcrGrabbitExecutionContextDao executionContextDao + + private ResourceResolverFactory resourceResolverFactory + + private ExecutionContextSerializer executionContextSerializer + + /** + * Create a new instance with a {@link org.springframework.batch.support.transaction.ResourcelessTransactionManager}. + */ + public JcrJobRepositoryFactoryBean() { + this(new ResourcelessTransactionManager()) + } + + + public JcrJobRepositoryFactoryBean(PlatformTransactionManager transactionManager) { + setTransactionManager(transactionManager) + } + + + void setResourceResolverFactory(ResourceResolverFactory resourceResolverFactory) { + log.info "Setting RRF : ${resourceResolverFactory}" + this.resourceResolverFactory = resourceResolverFactory + } + + + void setExecutionContextSerializer(ExecutionContextSerializer executionContextSerializer) { + log.info "Setting ExecutionContextSerializer : ${executionContextSerializer}" + this.executionContextSerializer = executionContextSerializer + } + + + GrabbitJobExecutionDao getJobExecutionDao() { + jobExecutionDao + } + + + GrabbitJobInstanceDao getJobInstanceDao() { + jobInstanceDao + } + + + GrabbitStepExecutionDao getStepExecutionDao() { + stepExecutionDao + } + + + GrabbitExecutionContextDao getExecutionContextDao() { + executionContextDao + } + + + @Override + protected JobInstanceDao createJobInstanceDao() throws Exception { + log.info "Create JobInstance" + jobInstanceDao = new JcrGrabbitJobInstanceDao(resourceResolverFactory) + jobInstanceDao.ensureRootResource() + jobInstanceDao + } + + + @Override + protected JobExecutionDao createJobExecutionDao() throws Exception { + log.info "Create JobExecution" + jobExecutionDao = new JcrGrabbitJobExecutionDao(resourceResolverFactory) + jobExecutionDao.ensureRootResource() + jobExecutionDao + } + + + @Override + protected StepExecutionDao createStepExecutionDao() throws Exception { + log.info "Create StepExecution" + stepExecutionDao = new JcrGrabbitStepExecutionDao(resourceResolverFactory) + stepExecutionDao.ensureRootResource() + stepExecutionDao + } + + + @Override + protected ExecutionContextDao createExecutionContextDao() throws Exception { + log.info "Create ExecutionContext" + executionContextDao = new JcrGrabbitExecutionContextDao(resourceResolverFactory, executionContextSerializer) + executionContextDao.ensureRootResource() + executionContextDao + } +} diff --git a/src/main/groovy/com/twcable/spring/batch/repository/services/CleanJobRepository.groovy b/src/main/groovy/com/twcable/spring/batch/repository/services/CleanJobRepository.groovy new file mode 100644 index 0000000..abefa3b --- /dev/null +++ b/src/main/groovy/com/twcable/spring/batch/repository/services/CleanJobRepository.groovy @@ -0,0 +1,28 @@ +/* + * Copyright 2015 Time Warner Cable, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.twcable.spring.batch.repository.services + +interface CleanJobRepository { + + /** + * This API is used to clean Grabbit's JCR Job Repository. It removes all job executions and all associated + * job instances etc. that are @param hours older than NOW (time when this API is called) + * @return Collection of JobExecutionIds that were removed + */ + public Collection cleanJobRepository(int hours) + +} diff --git a/src/main/groovy/com/twcable/spring/batch/repository/services/impl/DefaultCleanJobRepository.groovy b/src/main/groovy/com/twcable/spring/batch/repository/services/impl/DefaultCleanJobRepository.groovy new file mode 100644 index 0000000..b3f5348 --- /dev/null +++ b/src/main/groovy/com/twcable/spring/batch/repository/services/impl/DefaultCleanJobRepository.groovy @@ -0,0 +1,115 @@ +/* + * Copyright 2015 Time Warner Cable, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.twcable.spring.batch.repository.services.impl + +import com.twcable.grabbit.jcr.JcrUtil +import com.twcable.spring.batch.repository.JcrJobRepositoryFactoryBean +import com.twcable.spring.batch.repository.services.CleanJobRepository +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import org.apache.felix.scr.annotations.Activate +import org.apache.felix.scr.annotations.Component +import org.apache.felix.scr.annotations.Reference +import org.apache.felix.scr.annotations.Service +import org.apache.sling.api.resource.PersistenceException +import org.apache.sling.api.resource.Resource +import org.apache.sling.api.resource.ResourceResolver +import org.apache.sling.api.resource.ResourceResolverFactory +import org.springframework.batch.core.BatchStatus +import org.springframework.context.ConfigurableApplicationContext + +@Slf4j +@CompileStatic +@Component(label = "Grabbit Clean Job Repository Service", description = "Grabbit Clean Job Repository Service", immediate = true, metatype = true, enabled = true) +@Service(CleanJobRepository) +@SuppressWarnings(['GroovyUnusedDeclaration', 'GrMethodMayBeStatic']) +class DefaultCleanJobRepository implements CleanJobRepository { + + @Reference + ResourceResolverFactory resourceResolverFactory + + @Reference + ConfigurableApplicationContext configurableApplicationContext + + @Activate + void activate() { + log.info "CleanJobRepository Service activated" + } + + @Override + Collection cleanJobRepository(int hours) { + JcrJobRepositoryFactoryBean jobRepositoryFactoryBean = configurableApplicationContext.getBean(JcrJobRepositoryFactoryBean) + + if(!jobRepositoryFactoryBean) { + log.error "Cannot get an instance of JcrJobRepositoryFactoryBean. Will not clean up Grabbit Jcr Job Repository" + return [] + } + + Collection jobExecutionPaths = jobRepositoryFactoryBean.jobExecutionDao.getJobExecutions([BatchStatus.FAILED, BatchStatus.COMPLETED]) + Collection olderThanHoursJobExecutions = jobRepositoryFactoryBean.jobExecutionDao.getJobExecutions(hours, jobExecutionPaths) + Collection jobInstancesToRemove = jobRepositoryFactoryBean.jobInstanceDao.getJobInstancePaths(olderThanHoursJobExecutions) + Collection stepExecutionsToRemove = jobRepositoryFactoryBean.stepExecutionDao.getStepExecutionPaths(olderThanHoursJobExecutions) + Collection jobExecutionContextsToRemove = jobRepositoryFactoryBean.executionContextDao.getJobExecutionContextPaths(olderThanHoursJobExecutions) + Collection stepExecutionContextsToRemove = jobRepositoryFactoryBean.executionContextDao.getStepExecutionContextPaths(stepExecutionsToRemove) + + JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + + log.debug "jobInstancesToRemove: $jobInstancesToRemove, size: ${jobInstancesToRemove.size()}" + log.debug "jobExecutionsToRemove: $olderThanHoursJobExecutions, size: ${olderThanHoursJobExecutions.size()}" + log.debug "stepExecutionsToRemove: $stepExecutionsToRemove, size: ${stepExecutionsToRemove.size()}" + log.debug "jobExecutionContextsToRemove: $jobExecutionContextsToRemove, size: ${jobExecutionContextsToRemove.size()}" + log.debug "stepExecutionContextsToResource: $stepExecutionContextsToRemove, size: ${stepExecutionContextsToRemove.size()}" + + log.info "Removing ${jobInstancesToRemove.size()} JobInstances" + removeResources(jobInstancesToRemove, resolver) + log.info "Removing ${olderThanHoursJobExecutions.size()} JobExecutions" + removeResources(olderThanHoursJobExecutions, resolver) + log.info "Removing ${stepExecutionsToRemove.size()} StepExecutions" + removeResources(stepExecutionsToRemove, resolver) + log.info "Removing ${jobExecutionContextsToRemove.size()} JobExecutionContexts" + removeResources(jobExecutionContextsToRemove, resolver) + log.info "Removing ${stepExecutionContextsToRemove.size()} StepExecutionContexts" + removeResources(stepExecutionContextsToRemove, resolver) + } + + Collection removedJobExecutionIds = olderThanHoursJobExecutions.collect { it.split("/").last() } + return removedJobExecutionIds + } + + private void removeResources(Collection resourcePathsToRemove, ResourceResolver resolver) { + try { + resourcePathsToRemove.each { + Resource resourceToDelete = resolver.getResource(it) + if(resourceToDelete) { + resolver.delete(resourceToDelete) + log.debug "Resource ${it} will be removed" + } + else { + log.warn "Resource ${it} doesn't exist. So cannot remove it" + } + } + resolver.commit() + resolver.refresh() + } + catch(PersistenceException e) { + log.error "Exception while removing resources :", e + if(resolver.hasChanges()) { + resolver.revert() + } + } + } +} diff --git a/src/main/groovy/com/twcable/spring/batch/repository/servlets/GrabbitCleanJobRepositoryServlet.groovy b/src/main/groovy/com/twcable/spring/batch/repository/servlets/GrabbitCleanJobRepositoryServlet.groovy new file mode 100644 index 0000000..f22f380 --- /dev/null +++ b/src/main/groovy/com/twcable/spring/batch/repository/servlets/GrabbitCleanJobRepositoryServlet.groovy @@ -0,0 +1,59 @@ +/* + * Copyright 2015 Time Warner Cable, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.twcable.spring.batch.repository.servlets + +import com.twcable.spring.batch.repository.services.CleanJobRepository +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import org.apache.felix.scr.annotations.Reference +import org.apache.felix.scr.annotations.sling.SlingServlet +import org.apache.sling.api.SlingHttpServletRequest +import org.apache.sling.api.SlingHttpServletResponse +import org.apache.sling.api.servlets.SlingAllMethodsServlet + +import javax.annotation.Nonnull +import javax.servlet.http.HttpServletResponse + +/** + * This servlet is used for cleanup of the Grabbit's JobRepository stored under /var/grabbit/job. + * + * It is a handler for the {@link com.twcable.grabbit.resources.CleanJobRepositoryResource} resource. + */ +@Slf4j +@CompileStatic +@SlingServlet(methods = ['POST'], resourceTypes = ['twcable:grabbit/jobrepository/clean']) +class GrabbitCleanJobRepositoryServlet extends SlingAllMethodsServlet { + + @Reference + CleanJobRepository cleanJobRepository + + @Override + protected void doPost( @Nonnull SlingHttpServletRequest request, @Nonnull SlingHttpServletResponse response) { + String hoursParam = request.getParameter("hours") ?: "" + if(!hoursParam.isInteger()) { + log.warn "Parameter 'hours' must be an integer" + response.status = HttpServletResponse.SC_BAD_REQUEST + response.writer.write("Parameter 'hours' must be an integer") + return + } + int hours = hoursParam.toInteger() + Collection removedJobExecutions = cleanJobRepository.cleanJobRepository(hours) + response.status = HttpServletResponse.SC_OK + response.writer.write ("JobExecutions and the corresponding JobInstances, StepExecutions and ExecutionContexts " + + "were removed. JobExecutionsIds that were removed: ${removedJobExecutions}") + } +} diff --git a/src/test/groovy/com/twcable/spring/batch/repository/JcrGrabbitExecutionContextDaoSpec.groovy b/src/test/groovy/com/twcable/spring/batch/repository/JcrGrabbitExecutionContextDaoSpec.groovy new file mode 100644 index 0000000..56a6b0e --- /dev/null +++ b/src/test/groovy/com/twcable/spring/batch/repository/JcrGrabbitExecutionContextDaoSpec.groovy @@ -0,0 +1,139 @@ +/* + * Copyright 2015 Time Warner Cable, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.twcable.spring.batch.repository + +import com.twcable.jackalope.impl.sling.SimpleResourceResolverFactory +import org.apache.sling.api.resource.ResourceResolverFactory +import org.springframework.batch.core.JobExecution +import org.springframework.batch.core.StepExecution +import org.springframework.batch.core.repository.ExecutionContextSerializer +import spock.lang.Ignore +import spock.lang.Shared +import spock.lang.Specification +import spock.lang.Subject + +import static JcrGrabbitExecutionContextDao.EXECUTION_CONTEXT +import static JcrGrabbitExecutionContextDao.EXECUTION_ID +import static com.twcable.jackalope.JCRBuilder.* + +@Subject(JcrGrabbitExecutionContextDao) +class JcrGrabbitExecutionContextDaoSpec extends Specification { + + @Shared + ResourceResolverFactory mockFactory + + @Shared + ExecutionContextSerializer stubSerializer + + + def setupSpec() { + final builder = + node("var", + node("grabbit", + node("job", + node("repository", + node("executionContexts", + node("job", + node("1", + property(EXECUTION_ID, 1), + property(EXECUTION_CONTEXT, "SomeThing") + ) + ), + node("step", + node("1", + property(EXECUTION_ID, 1), + property(EXECUTION_CONTEXT, "SomeThing") + ) + ) + ) + ) + ) + ) + ) + mockFactory = new SimpleResourceResolverFactory(repository(builder).build()) + stubSerializer = new StubExecutionContextSerializer() + } + + + def "EnsureRootResource for JcrGrabbitExecutionContextDao"() { + when: + final executionContextDao = new JcrGrabbitExecutionContextDao(mockFactory, stubSerializer) + executionContextDao.ensureRootResource() + + then: + notThrown(IllegalStateException) + + } + + + def "GetExecutionContext for a JobExecution"() { + when: + final executionContextDao = new JcrGrabbitExecutionContextDao(mockFactory, stubSerializer) + final result = executionContextDao.getExecutionContext(new JobExecution(1)) + + then: + result != null + result.containsKey("deserialized") + } + + + def "GetExecutionContext for a StepExecution"() { + when: + final executionContextDao = new JcrGrabbitExecutionContextDao(mockFactory, stubSerializer) + final result = executionContextDao.getExecutionContext(new StepExecution("someStep", new JobExecution(1), 1)) + + then: + result != null + result.containsKey("deserialized") + } + + @Ignore('TODO: Implement this test when Jackalope implements resourceResolver.findResources() API') + def "GetJobExecutionContextPaths for JobExecutionPaths"() { + when: + final executionContextDao = new JcrGrabbitExecutionContextDao(mockFactory, stubSerializer) + final result = executionContextDao.getJobExecutionContextPaths([]) + + then: + 1 == 1 + } + + @Ignore('TODO: Implement this test when Jackalope implements resourceResolver.findResources() API') + def "GetStepExecutionContextPaths for JobExecutionPaths"() { + when: + final executionContextDao = new JcrGrabbitExecutionContextDao(mockFactory, stubSerializer) + final result = executionContextDao.getStepExecutionContextPaths([]) + + then: + 1 == 1 + + } + + class StubExecutionContextSerializer implements ExecutionContextSerializer { + + @Override + Object deserialize(InputStream inputStream) throws IOException { + [deserialized: new String("Deserialized")] + } + + + @Override + void serialize(Object object, OutputStream outputStream) throws IOException { + outputStream.write("Serialized".bytes) + } + } + +} diff --git a/src/test/groovy/com/twcable/spring/batch/repository/JcrGrabbitJobExecutionDaoSpec.groovy b/src/test/groovy/com/twcable/spring/batch/repository/JcrGrabbitJobExecutionDaoSpec.groovy new file mode 100644 index 0000000..1f81144 --- /dev/null +++ b/src/test/groovy/com/twcable/spring/batch/repository/JcrGrabbitJobExecutionDaoSpec.groovy @@ -0,0 +1,200 @@ +/* + * Copyright 2015 Time Warner Cable, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.twcable.spring.batch.repository + +import com.twcable.jackalope.impl.sling.SimpleResourceResolverFactory +import org.apache.sling.api.resource.ResourceResolverFactory +import org.springframework.batch.core.BatchStatus +import org.springframework.batch.core.JobExecution +import org.springframework.batch.core.JobInstance +import spock.lang.Shared +import spock.lang.Specification +import spock.lang.Subject + +import static JcrGrabbitJobExecutionDao.* +import static com.twcable.jackalope.JCRBuilder.* + +@Subject(JcrGrabbitJobExecutionDao) +class JcrGrabbitJobExecutionDaoSpec extends Specification { + + @Shared + ResourceResolverFactory mockFactory + + + def setupSpec() { + final builder = + node("var", + node("grabbit", + node("job", + node("repository", + node("jobExecutions", + node("1", + property(INSTANCE_ID, 1), + property(EXECUTION_ID, 1), + property(TRANSACTION_ID, 5), + property(STATUS, "COMPLETED"), + property(EXIT_CODE, "code"), + property(EXIT_MESSAGE, "message"), + property(CREATE_TIME, "2014-12-27T16:59:18.669-05:00"), + property(END_TIME, "2014-12-29T16:59:18.669-05:00"), + property(JOB_NAME, "someJob"), + property(VERSION, 1) + ), + node("2", + property(INSTANCE_ID, 1), + property(EXECUTION_ID, 2), + property(TRANSACTION_ID, 5), + property(STATUS, "STARTED"), + property(EXIT_CODE, "code"), + property(EXIT_MESSAGE, "message"), + property(CREATE_TIME, "2014-12-28T16:59:18.669-05:00"), + property(END_TIME, "NULL"), + property(JOB_NAME, "someJob") + ), + node("3", + property(INSTANCE_ID, 2), + property(EXECUTION_ID, 3), + property(TRANSACTION_ID, 5), + property(STATUS, "STARTED"), + property(EXIT_CODE, "code"), + property(EXIT_MESSAGE, "message"), + property(CREATE_TIME, "2014-12-29T16:59:18.669-05:00"), + property(END_TIME, "NULL"), + property(JOB_NAME, "someOtherJob") + ), + node("4", + property(INSTANCE_ID, 1), + property(EXECUTION_ID, 1), + property(TRANSACTION_ID, 5), + property(STATUS, "FAILED"), + property(EXIT_CODE, "code"), + property(EXIT_MESSAGE, "message"), + property(CREATE_TIME, "2014-12-27T16:59:18.669-05:00"), + property(END_TIME, "2015-12-29T16:59:18.669-05:00"), + property(JOB_NAME, "someJob"), + property(VERSION, 1) + + ), + ), + node("jobInstances", + node("1")) + ) + ) + ) + ) + mockFactory = new SimpleResourceResolverFactory(repository(builder).build()) + } + + + def "EnsureRootResource for JcrGrabbitJobExecutionDao"() { + when: + final jobExecutionDao = new JcrGrabbitJobExecutionDao(mockFactory) + jobExecutionDao.ensureRootResource() + + then: + notThrown(IllegalStateException) + + } + + + def "FindJobExecutions for given JobInstance"() { + when: + final jobExecutionDao = new JcrGrabbitJobExecutionDao(mockFactory) + final result = jobExecutionDao.findJobExecutions(new JobInstance(1, "someJob")) + + then: + result != null + result.size() == 3 + result.first().id == 2 + } + + + def "GetLastJobExecution for given JobInstance"() { + when: + final jobExecutionDao = new JcrGrabbitJobExecutionDao(mockFactory) + final result = jobExecutionDao.getLastJobExecution(new JobInstance(1, "someJob")) + + then: + result != null + result.id == 2 + + } + + + def "FindRunningJobExecutions for given Job Name"() { + when: + final jobExecutionDao = new JcrGrabbitJobExecutionDao(mockFactory) + final result = jobExecutionDao.findRunningJobExecutions("someJob") + + then: + result != null + result.size() == 1 + result.first().id == 2 + } + + + def "GetJobExecution for given JobExecution id"() { + when: + final jobExecutionDao = new JcrGrabbitJobExecutionDao(mockFactory) + final result = jobExecutionDao.getJobExecution(2) + + then: + result != null + result.jobId == 1 + result.id == 2 + result.status == BatchStatus.valueOf("STARTED") + } + + + def "Get a transaction ID for a given job execution"() { + when: + final jobExecutionDao = new JcrGrabbitJobExecutionDao(mockFactory) + final jobExecution = jobExecutionDao.getJobExecution(2) as GrabbitJobExecution + + then: + jobExecution.transactionID == 5 + } + + + def "SynchronizeStatus for a given JobExecution"() { + when: + final jobExecutionDao = new JcrGrabbitJobExecutionDao(mockFactory) + def unsyncronized = new JobExecution(1) + unsyncronized.setVersion(0) + unsyncronized.setStatus(BatchStatus.STARTED) + jobExecutionDao.synchronizeStatus(unsyncronized) + + then: + unsyncronized.version == 1 + unsyncronized.status == BatchStatus.FAILED + } + + def "GetJobExecutions for hours and jobExecutions"() { + when: + final jobExecutionDao = new JcrGrabbitJobExecutionDao(mockFactory) + final jobExecutionPaths = [ + "/var/grabbit/job/repository/jobExecutions/1", + "/var/grabbit/job/repository/jobExecutions/4" + ] + final result = jobExecutionDao.getJobExecutions(1, jobExecutionPaths) + + then: + result != null + result.size() == 2 + } + +} diff --git a/src/test/groovy/com/twcable/spring/batch/repository/JcrGrabbitJobInstanceDaoSpec.groovy b/src/test/groovy/com/twcable/spring/batch/repository/JcrGrabbitJobInstanceDaoSpec.groovy new file mode 100644 index 0000000..5374dee --- /dev/null +++ b/src/test/groovy/com/twcable/spring/batch/repository/JcrGrabbitJobInstanceDaoSpec.groovy @@ -0,0 +1,184 @@ +/* + * Copyright 2015 Time Warner Cable, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.twcable.spring.batch.repository + +import com.twcable.jackalope.impl.sling.SimpleResourceResolverFactory +import org.apache.sling.api.resource.ResourceResolverFactory +import org.springframework.batch.core.* +import spock.lang.Shared +import spock.lang.Specification +import spock.lang.Subject +import spock.lang.Unroll + +import static JcrGrabbitJobInstanceDao.* +import static com.twcable.jackalope.JCRBuilder.* + +@Subject(JcrGrabbitJobInstanceDao) +class JcrGrabbitJobInstanceDaoSpec extends Specification { + + @Shared + ResourceResolverFactory mockFactory + + + def setupSpec() { + final builder = + node("var", + node("grabbit", + node("job", + node("repository", + node("jobInstances", + node("1", + property(INSTANCE_ID, 1), + property(NAME, "someJob"), + property(KEY, new DefaultJobKeyGenerator().generateKey(new JobParameters([someKey: new JobParameter("someValue")]))) + ), + node("2", + property(INSTANCE_ID, 2), + property(NAME, "someOtherJob"), + ), + node("3", + property(INSTANCE_ID, 3), + property(NAME, "someOtherJob"), + ), + node("4", + property(INSTANCE_ID, 4), + property(NAME, "someOtherJob"), + ) + ), + node("jobExecutions", + node("1", + property(JcrGrabbitJobExecutionDao.INSTANCE_ID, 1), + property(JcrGrabbitJobExecutionDao.EXECUTION_ID, 1), + property(JcrGrabbitJobExecutionDao.TRANSACTION_ID, 5), + property(JcrGrabbitJobExecutionDao.STATUS, "COMPLETED"), + property(JcrGrabbitJobExecutionDao.EXIT_CODE, "code"), + property(JcrGrabbitJobExecutionDao.EXIT_MESSAGE, "message"), + property(JcrGrabbitJobExecutionDao.CREATE_TIME, "2014-12-27T16:59:18.669-05:00"), + property(JcrGrabbitJobExecutionDao.END_TIME, "2014-12-29T16:59:18.669-05:00"), + property(JcrGrabbitJobExecutionDao.JOB_NAME, "someJob"), + property(JcrGrabbitJobExecutionDao.VERSION, 1) + ), + node("4", + property(JcrGrabbitJobExecutionDao.INSTANCE_ID, 4), + property(JcrGrabbitJobExecutionDao.EXECUTION_ID, 1), + property(JcrGrabbitJobExecutionDao.TRANSACTION_ID, 5), + property(JcrGrabbitJobExecutionDao.STATUS, "FAILED"), + property(JcrGrabbitJobExecutionDao.EXIT_CODE, "code"), + property(JcrGrabbitJobExecutionDao.EXIT_MESSAGE, "message"), + property(JcrGrabbitJobExecutionDao.CREATE_TIME, "2014-12-27T16:59:18.669-05:00"), + property(JcrGrabbitJobExecutionDao.END_TIME, "2015-12-29T16:59:18.669-05:00"), + property(JcrGrabbitJobExecutionDao.JOB_NAME, "someJob"), + property(JcrGrabbitJobExecutionDao.VERSION, 1) + + ), + ), + ) + ) + ) + ) + mockFactory = new SimpleResourceResolverFactory(repository(builder).build()) + } + + + def "EnsureRootResource for JcrGrabbitJobInstanceDao"() { + when: + final jobInstanceDao = new JcrGrabbitJobInstanceDao(mockFactory) + jobInstanceDao.ensureRootResource() + + then: + notThrown(IllegalStateException) + } + + + def "GetJobInstance for given JobExecution"() { + when: + final jobInstanceDao = new JcrGrabbitJobInstanceDao(mockFactory) + final result = jobInstanceDao.getJobInstance(new JobExecution(new JobInstance(1, "someJob"), new JobParameters())) + + then: + result != null + result.id == 1 + + } + + + def "GetJobInstance for given InstanceId"() { + when: + final jobInstanceDao = new JcrGrabbitJobInstanceDao(mockFactory) + final result = jobInstanceDao.getJobInstance(1) + + then: + result != null + result.jobName == "someJob" + + } + + + def "GetJobInstance for given Job Name and Job parameters"() { + when: + final jobInstanceDao = new JcrGrabbitJobInstanceDao(mockFactory) + final result = jobInstanceDao.getJobInstance("someJob", new JobParameters([someKey: new JobParameter("someValue")])) + + then: + result != null + result.id == 1 + } + + + @Unroll + def "GetJobInstances for given Job Name #jobName, a start index and count"() { + when: + final jobInstanceDao = new JcrGrabbitJobInstanceDao(mockFactory) + final result = jobInstanceDao.getJobInstances(jobName, 0, Integer.MAX_VALUE) + + then: + result != null + result.size() == size + result.first().id == firstId + + where: + jobName | size | firstId + "someJob" | 1 | 1 + "someOtherJob" | 3 | 4 + } + + + def "GetJobNames for Job Instances"() { + when: + final jobInstanceDao = new JcrGrabbitJobInstanceDao(mockFactory) + final result = jobInstanceDao.jobNames + + then: + result.containsAll(["someJob", "someOtherJob"]) + } + + def "GetJobInstancePaths for jobExecutions"() { + when: + final jobInstanceDao = new JcrGrabbitJobInstanceDao(mockFactory) + final jobExecutionPaths = [ + "/var/grabbit/job/repository/jobExecutions/1", + "/var/grabbit/job/repository/jobExecutions/4" + ] + + final result = jobInstanceDao.getJobInstancePaths(jobExecutionPaths) + + then: + result != null + result.size() == 2 + } + +} diff --git a/src/test/groovy/com/twcable/spring/batch/repository/JcrGrabbitStepExecutionDaoSpec.groovy b/src/test/groovy/com/twcable/spring/batch/repository/JcrGrabbitStepExecutionDaoSpec.groovy new file mode 100644 index 0000000..efc54f2 --- /dev/null +++ b/src/test/groovy/com/twcable/spring/batch/repository/JcrGrabbitStepExecutionDaoSpec.groovy @@ -0,0 +1,104 @@ +/* + * Copyright 2015 Time Warner Cable, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.twcable.spring.batch.repository + +import com.twcable.jackalope.impl.sling.SimpleResourceResolverFactory +import org.apache.sling.api.resource.ResourceResolverFactory +import org.springframework.batch.core.BatchStatus +import org.springframework.batch.core.JobExecution +import org.springframework.batch.core.JobInstance +import org.springframework.batch.core.JobParameters +import spock.lang.* + +import static JcrGrabbitStepExecutionDao.* +import static com.twcable.jackalope.JCRBuilder.* + +@Subject(JcrGrabbitStepExecutionDao) +class JcrGrabbitStepExecutionDaoSpec extends Specification { + + @Shared + ResourceResolverFactory mockFactory + + + def setupSpec() { + final builder = + node("var", + node("grabbit", + node("job", + node("repository", + node("jobExecutions"), + node("stepExecutions", + node("1", + property(ID, 1), + property(NAME, "someStep"), + property(JOB_EXECUTION_ID, 1), + property(STATUS, "COMPLETED"), + ), + node("5", + property(ID, 5), + property(NAME, "someOtherStep"), + property(JOB_EXECUTION_ID, 3), + property(STATUS, "STARTED"), + ) + ) + ) + ) + ) + ) + mockFactory = new SimpleResourceResolverFactory(repository(builder).build()) + } + + + def "EnsureRootResource for JcrGrabbitStepExecutionDao"() { + when: + final stepExecutionDao = new JcrGrabbitStepExecutionDao(mockFactory) + stepExecutionDao.ensureRootResource() + + then: + notThrown(IllegalStateException) + } + + + @Unroll + def "GetStepExecution for a given JobExecution and a StepExecution id #stepExecutionId"() { + when: + final stepExecutionDao = new JcrGrabbitStepExecutionDao(mockFactory) + final result = stepExecutionDao.getStepExecution(new JobExecution(new JobInstance(1, "someJob"), jobExecutionId, new JobParameters()), stepExecutionId) + + then: + result != null + result.jobExecutionId == jobExecutionId + result.status == stepStatus + + where: + stepExecutionId | jobExecutionId | stepStatus + 1 | 1 | BatchStatus.COMPLETED + 5 | 3 | BatchStatus.STARTED + + } + + @Ignore('TODO: Implement this test when Jackalope implements resourceResolver.findResources() API') + def "GetStepExecutionPaths for jobResourcePaths"() { + when: + final stepExecutionDao = new JcrGrabbitStepExecutionDao(mockFactory) + final result = stepExecutionDao.getStepExecutionPaths([]) + + then: + 1 == 1 + + } +} diff --git a/src/test/groovy/com/twcable/spring/batch/repository/servlets/GrabbitCleanJobRepositoryServletSpec.groovy b/src/test/groovy/com/twcable/spring/batch/repository/servlets/GrabbitCleanJobRepositoryServletSpec.groovy new file mode 100644 index 0000000..6f6a92b --- /dev/null +++ b/src/test/groovy/com/twcable/spring/batch/repository/servlets/GrabbitCleanJobRepositoryServletSpec.groovy @@ -0,0 +1,62 @@ +/* + * Copyright 2015 Time Warner Cable, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.twcable.spring.batch.repository.servlets + +import com.twcable.spring.batch.repository.services.CleanJobRepository +import org.apache.sling.api.SlingHttpServletRequest +import org.apache.sling.api.SlingHttpServletResponse +import spock.lang.Specification +import spock.lang.Subject + +@Subject(GrabbitCleanJobRepositoryServlet) +class GrabbitCleanJobRepositoryServletSpec extends Specification { + + def "Servlet handles the case when hours parameter is not passed"() { + given: + def servlet = new GrabbitCleanJobRepositoryServlet(cleanJobRepository: Mock(CleanJobRepository)) + def request = Mock(SlingHttpServletRequest) + request.getParameter("hours") >> null + def response = Mock(SlingHttpServletResponse) + def writer = new StringWriter() + response.getWriter() >> new PrintWriter(writer) + when: + servlet.doPost(request, response) + + then: + writer != null + writer.toString() == "Parameter 'hours' must be an integer" + } + + def "Servlet handles the case when hours parameter is correctly passed"() { + given: + def clientJobRepository = Mock(CleanJobRepository) + clientJobRepository.cleanJobRepository(_) >> (["id1","id2","id3"] as List) + def servlet = new GrabbitCleanJobRepositoryServlet(cleanJobRepository: clientJobRepository) + def request = Mock(SlingHttpServletRequest) + request.getParameter("hours") >> 5 + def response = Mock(SlingHttpServletResponse) + def writer = new StringWriter() + response.getWriter() >> new PrintWriter(writer) + when: + servlet.doPost(request, response) + + then: + writer != null + writer.toString().contains("JobExecutionsIds that were removed") + writer.toString().contains("[id1, id2, id3]") + } +} diff --git a/src/test/resources/com/twcable/grabbit/client/test_config.yaml b/src/test/resources/com/twcable/grabbit/client/test_config.yaml new file mode 100644 index 0000000..3aa24d7 --- /dev/null +++ b/src/test/resources/com/twcable/grabbit/client/test_config.yaml @@ -0,0 +1,9 @@ +serverUsername: "username" +serverPassword: "password" +serverHost: "host" +serverPort: 4502 +deltaContent: true +pathConfigurations: + - path: "/content/testpath" + workflowConfigIds: + - '/etc/workflow/launcher/config/update_asset_mod' diff --git a/testutils/.gitignore b/testutils/.gitignore new file mode 100644 index 0000000..5e56e04 --- /dev/null +++ b/testutils/.gitignore @@ -0,0 +1 @@ +/bin diff --git a/testutils/build.gradle b/testutils/build.gradle new file mode 100644 index 0000000..920bf0a --- /dev/null +++ b/testutils/build.gradle @@ -0,0 +1,36 @@ +apply plugin: 'java' +apply plugin: 'osgi' +apply plugin: 'groovy' + +group = 'com.twcable.grabbit' +description = 'Test Utilities' + +dependencies { + + compile project(":commons-utils") + // Library for Groovy + compile "org.codehaus.groovy:groovy-all:${groovy_version}" + compile "org.spockframework:spock-core:${spock_version}" + + compile "javax.servlet:servlet-api:${servlet_api_version}" + + compile "org.apache.sling:org.apache.sling.commons.testing:${sling_commons_testing_version}", { + exclude group: 'org.apache.tika', module: 'tika-parsers' + exclude group: 'org.apache.pdfbox', module: 'pdfbox' + exclude group: 'org.slf4j', module: 'slf4j-simple' + exclude group: 'junit', module: 'junit' + exclude group: 'org.jmock', module: 'jmock-junit4' + exclude group: 'rhino', module: 'js' + } + + compile ":cq-workflow-console:${cq_workflow_console_version}" + + compile "org.slf4j:slf4j-api:${slf4j_version}" + compile "org.mockito:mockito-all:${mockito_version}" + compile "org.apache.commons:commons-lang3:${commons_lang_version}" + compile "javax.servlet:servlet-api:${servlet_api_version}" + compile "org.apache.felix:org.osgi.core:${felix_osgi_version}" + compile "org.apache.felix:org.osgi.compendium:${felix_osgi_version}" + + compile "com.google.guava:guava:${guava_version}" +} diff --git a/testutils/gradle/testing.gradle b/testutils/gradle/testing.gradle new file mode 100644 index 0000000..b0e541c --- /dev/null +++ b/testutils/gradle/testing.gradle @@ -0,0 +1,100 @@ +buildscript { + repositories { + jcenter() + + //Provides cq-gradle-plugins and jackalope + maven { + url "http://dl.bintray.com/twcable/aem" + } + mavenLocal() + } + + dependencies { + classpath "com.twcable.gradle:cq-gradle-plugins:${gradle_plugins_version}" + + classpath "commons-io:commons-io:2.4" + } +} + +apply plugin: 'groovy' +apply plugin: 'osgi' + +dependencies { + testCompile "org.spockframework:spock-core:${spock_version}" + testCompile "org.spockframework:spock-spring:${spock_version}" + testCompile "org.springframework.batch:spring-batch-test:${spring_batch_version}" + testCompile "org.springframework:spring-test:3.2.9.RELEASE" + + + testCompile "com.twcable.jackalope:jackalope:${jackalope_version}" + + testRuntime "cglib:cglib-nodep:${cglib_nodep_version}" +} +// ******************************************************************** +// +// Testing +// +// ******************************************************************** +task testReport(type: TestReport, dependsOn: test) { + destinationDir = file("$buildDir/reports/tests/") + reportOn file("$buildDir/test-results/binary/test/") +} + +check.dependsOn testReport + +// +// Add integration testing support +// +sourceSets { + integrationTest { + compileClasspath = sourceSets.main.output + sourceSets.test.output + configurations.testRuntime + runtimeClasspath = output + sourceSets.main.output + sourceSets.test.output + configurations.testRuntime + groovy.srcDir file('src/integrationTest/groovy') + java.srcDir file('src/integrationTest/java') + } + + functionalTest { + compileClasspath = sourceSets.main.output + sourceSets.test.output + configurations.testRuntime + runtimeClasspath = output + sourceSets.main.output + sourceSets.test.output + configurations.testRuntime + groovy.srcDir file('src/functionalTest/groovy') + java.srcDir file('src/functionalTest/java') + } +} + +task integrationTest(type: Test) { + testClassesDir = sourceSets.integrationTest.output.classesDir + classpath = sourceSets.integrationTest.runtimeClasspath +} + +task integrationTestReport(type: TestReport, dependsOn: integrationTest) { + destinationDir = file("$buildDir/reports/integrationTests/") + reportOn file("$buildDir/test-results/binary/integrationTest/") +} + +task functionalTest(type: Test) { + testClassesDir = sourceSets.functionalTest.output.classesDir + maxParallelForks = 2 + System.properties.each { String key, String value -> + if (key.startsWith('test.')) { + systemProperty((key - 'test.'), value) + } + } + classpath = sourceSets.functionalTest.runtimeClasspath +} + +task functionalTestReport(type: TestReport, dependsOn: functionalTest) { + destinationDir = file("$buildDir/reports/functionalTests/") + reportOn file("$buildDir/test-results/binary/functionalTest/") +} + + +[test, integrationTest]*.systemProperty 'java.awt.headless', true +[compileGroovy, compileTestGroovy, compileIntegrationTestGroovy]*.options*.forkOptions*.memoryMaximumSize = '2048m' + +idea.module.testSourceDirs += sourceSets.integrationTest.groovy.srcDirs +idea.module.testSourceDirs += sourceSets.integrationTest.java.srcDirs +idea.module.testSourceDirs += sourceSets.functionalTest.groovy.srcDirs +idea.module.testSourceDirs += sourceSets.functionalTest.java.srcDirs + +// workaround for http://issues.gradle.org/browse/GRADLE-1682 +configurations.testRuntime.exclude group: 'org.apache.felix', module: 'org.osgi.foundation'