Exception when filtering resources with project.properties as tokens


(Wujek Srujek) #1

I would like to configure the processResources task with filtering, and the tokens to be the project properties. Here is what I have:

import org.apache.tools.ant.filters.ReplaceTokens
processResources {
  filter(ReplaceTokens, tokens: project.properties)
}

This fails with an exception (the second most nested one):

Caused by: org.gradle.api.InvalidUserDataException: Error - Invalid filter specification for org.apache.tools.ant.filters.ReplaceTokens
 at org.gradle.api.internal.file.copy.FilterChain$1.transform(FilterChain.java:69)
 at org.gradle.api.internal.file.copy.FilterChain$1.transform(FilterChain.java:58)
 at org.gradle.api.internal.ChainingTransformer.transform(ChainingTransformer.java:37)

, with the cause being:

Caused by: org.codehaus.groovy.runtime.typehandling.GroovyCastException: Cannot cast object '{here project properties}' with class 'java.util.HashMap' to class 'java.util.Hashtable' due to: java.lang.NullPointerException
 at org.gradle.api.internal.BeanDynamicObject$MetaClassAdapter.setProperty(BeanDynamicObject.java:178)
 at org.gradle.api.internal.BeanDynamicObject.setProperty(BeanDynamicObject.java:107)
 at org.gradle.util.ConfigureUtil.configureByMap(ConfigureUtil.java:43)
 at org.gradle.api.internal.file.copy.FilterChain$1.transform(FilterChain.java:65)
 ... 105 more

However, this works:

import org.apache.tools.ant.filters.ReplaceTokens
processResources {
  filter(ReplaceTokens, tokens: [keyword: 'test'])
}
println([a: 1].getClass().name)
println project.properties.getClass().name

And the code prints: java.util.LinkedHashMap (for [a: 1]) java.util.HashMap (for project.properties)

What is so different about project.properties that it can’t be used that way? How should filtering with project.properties be done, so that I don’t have to repeat myself and specify all the tokens in an explicit map?

wujek


(Peter Niederwieser) #2

I wouldn’t recommend to use ‘project.properties’ in such a way as you’ll likely include too many tokens. I’d expect that the stack trace has another cause which explains what the problem is.


(Wujek Srujek) #3

Hi, thanks for your answer. The second stack trace snippet I pasted above is exactly the last few lines, and it ends with a cas exception. I actually did investigate further, and it turns out that: 1. ReplaceTokens requires all values to be Strings - it doesn’t cope well with primitives or object or whatever else, and project.properties is full of DefaultSourceSets and so on. It also doesn’t cope with GStrings, for that matter, which is completely out of bounds in Groovy. Using anything else than String as value results in one or another ClassCastException / GroovyCastException. 2. ReplaceTokens delimiters (startToken and endToken) are restricted to single characters only, making the very popular ${property} notation impossible.

It doesn’t really explain why project.properties is treated differently than an inline map, though.

I actually ditched ReplaceTokens due to these limitations (plus hideous syntax with the necessary import), and didn’t investigate any further. If you think I should issue a bug with a sample project, please say so and I will. I will try to create a short application to debug it today (it’s torrential rain here!) a bit, but I might come up with nothing due to my lack knowldge of Gradle internals.

Right before I started messing around with the filter closure myself, I found what I believe a much better way for this particular task: ‘expand project.properties’. It’s strange that pretty much all code snippets on the net use ReplaceTokens (including the nabble archives and stackoverflow, where even Gradle developers use RT) and there is very little info about expand (it is mentioned in the official user guide, though). It seems the better way to me, but it seems no to be promoted enough ;d Is there anything wrong with it? Or is it just newer than all of the code snippets I mentioned above?

What do you suggest I use instead of project.properties? Create an impromptu map? A separate properties file? These are actually not bad, but what I am trying to achieve is something similar to maven filtering. I am holding a presentation on Gradle next week at my company and one of the things I want to show that it can do what maven can, plus more. And maven does filtering this way, if I’m not mistaken. It might be sad, but if I don’t prove Gradle can be maven and more, it will be decided against it. Anyways, ‘expand’ should be more visible in the net as it seems more ‘Gradle’ and more flexible, at least to me.

wujek


(Wujek Srujek) #4

Please, give me some credit, I do know how to look for root causes.


(Wujek Srujek) #5

Ok, so here it goes. For a build script:

apply plugin: 'java'
ext { whatever = 17 }
import org.apache.tools.ant.filters.ReplaceTokens
processResources {
    filter ReplaceTokens, tokens: project.properties }
}

and a file src/main/resources/test.resource with the contents:

Name is: @name@. Whatever is: @whatever@.

the following happens:

$ ./gradlew clean processResources --stacktrace
:clean
:processResources FAILED
  FAILURE: Build failed with an exception.
  * What went wrong:
Execution failed for task ':processResources'.
> Could not copy file '/home/wujek/IdeaWorkspace/filter-test/src/main/resources/test.resource' to '/home/wujek/IdeaWorkspace/filter-test/build/resources/main/test.resource'.
  * Try:
Run with --info or --debug option to get more log output.
  * Exception is:
org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':processResources'.
    at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeActions(ExecuteActionsTaskExecuter.java:69)
    at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:46)
    at org.gradle.api.internal.tasks.execution.PostExecutionAnalysisTaskExecuter.execute(PostExecutionAnalysisTaskExecuter.java:35)
    at org.gradle.api.internal.changedetection.state.CacheLockReleasingTaskExecuter$1.run(CacheLockReleasingTaskExecuter.java:35)
    at org.gradle.internal.Factories$1.create(Factories.java:22)
    at org.gradle.cache.internal.DefaultCacheAccess.longRunningOperation(DefaultCacheAccess.java:179)
    at org.gradle.cache.internal.DefaultCacheAccess.longRunningOperation(DefaultCacheAccess.java:232)
    at org.gradle.cache.internal.DefaultPersistentDirectoryStore.longRunningOperation(DefaultPersistentDirectoryStore.java:142)
    at org.gradle.api.internal.changedetection.state.DefaultTaskArtifactStateCacheAccess.longRunningOperation(DefaultTaskArtifactStateCacheAccess.java:83)
    at org.gradle.api.internal.changedetection.state.CacheLockReleasingTaskExecuter.execute(CacheLockReleasingTaskExecuter.java:33)
    at org.gradle.api.internal.tasks.execution.SkipUpToDateTaskExecuter.execute(SkipUpToDateTaskExecuter.java:58)
    at org.gradle.api.internal.tasks.execution.ContextualisingTaskExecuter.execute(ContextualisingTaskExecuter.java:34)
    at org.gradle.api.internal.changedetection.state.CacheLockAcquiringTaskExecuter$1.run(CacheLockAcquiringTaskExecuter.java:39)
    at org.gradle.internal.Factories$1.create(Factories.java:22)
    at org.gradle.cache.internal.DefaultCacheAccess.useCache(DefaultCacheAccess.java:124)
    at org.gradle.cache.internal.DefaultCacheAccess.useCache(DefaultCacheAccess.java:112)
    at org.gradle.cache.internal.DefaultPersistentDirectoryStore.useCache(DefaultPersistentDirectoryStore.java:134)
    at org.gradle.api.internal.changedetection.state.DefaultTaskArtifactStateCacheAccess.useCache(DefaultTaskArtifactStateCacheAccess.java:79)
    at org.gradle.api.internal.changedetection.state.CacheLockAcquiringTaskExecuter.execute(CacheLockAcquiringTaskExecuter.java:37)
    at org.gradle.api.internal.tasks.execution.ValidatingTaskExecuter.execute(ValidatingTaskExecuter.java:57)
    at org.gradle.api.internal.tasks.execution.SkipEmptySourceFilesTaskExecuter.execute(SkipEmptySourceFilesTaskExecuter.java:41)
    at org.gradle.api.internal.tasks.execution.SkipTaskWithNoActionsExecuter.execute(SkipTaskWithNoActionsExecuter.java:51)
    at org.gradle.api.internal.tasks.execution.SkipOnlyIfTaskExecuter.execute(SkipOnlyIfTaskExecuter.java:52)
    at org.gradle.api.internal.tasks.execution.ExecuteAtMostOnceTaskExecuter.execute(ExecuteAtMostOnceTaskExecuter.java:42)
    at org.gradle.api.internal.AbstractTask.executeWithoutThrowingTaskFailure(AbstractTask.java:282)
    at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor.executeTask(DefaultTaskPlanExecutor.java:48)
    at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor.processTask(DefaultTaskPlanExecutor.java:34)
    at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor.process(DefaultTaskPlanExecutor.java:27)
    at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter.execute(DefaultTaskGraphExecuter.java:89)
    at org.gradle.execution.SelectedTaskExecutionAction.execute(SelectedTaskExecutionAction.java:29)
    at org.gradle.execution.DefaultBuildExecuter.execute(DefaultBuildExecuter.java:61)
    at org.gradle.execution.DefaultBuildExecuter.access$200(DefaultBuildExecuter.java:23)
    at org.gradle.execution.DefaultBuildExecuter$2.proceed(DefaultBuildExecuter.java:67)
    at org.gradle.api.internal.changedetection.state.TaskCacheLockHandlingBuildExecuter$1.run(TaskCacheLockHandlingBuildExecuter.java:31)
    at org.gradle.internal.Factories$1.create(Factories.java:22)
    at org.gradle.cache.internal.DefaultCacheAccess.useCache(DefaultCacheAccess.java:124)
    at org.gradle.cache.internal.DefaultCacheAccess.useCache(DefaultCacheAccess.java:112)
    at org.gradle.cache.internal.DefaultPersistentDirectoryStore.useCache(DefaultPersistentDirectoryStore.java:134)
    at org.gradle.api.internal.changedetection.state.DefaultTaskArtifactStateCacheAccess.useCache(DefaultTaskArtifactStateCacheAccess.java:79)
    at org.gradle.api.internal.changedetection.state.TaskCacheLockHandlingBuildExecuter.execute(TaskCacheLockHandlingBuildExecuter.java:29)
    at org.gradle.execution.DefaultBuildExecuter.execute(DefaultBuildExecuter.java:61)
    at org.gradle.execution.DefaultBuildExecuter.access$200(DefaultBuildExecuter.java:23)
    at org.gradle.execution.DefaultBuildExecuter$2.proceed(DefaultBuildExecuter.java:67)
    at org.gradle.execution.DryRunBuildExecutionAction.execute(DryRunBuildExecutionAction.java:32)
    at org.gradle.execution.DefaultBuildExecuter.execute(DefaultBuildExecuter.java:61)
    at org.gradle.execution.DefaultBuildExecuter.execute(DefaultBuildExecuter.java:54)
    at org.gradle.initialization.DefaultGradleLauncher.doBuildStages(DefaultGradleLauncher.java:166)
    at org.gradle.initialization.DefaultGradleLauncher.doBuild(DefaultGradleLauncher.java:113)
    at org.gradle.initialization.DefaultGradleLauncher.run(DefaultGradleLauncher.java:81)
    at org.gradle.launcher.exec.InProcessBuildActionExecuter$DefaultBuildController.run(InProcessBuildActionExecuter.java:64)
    at org.gradle.launcher.cli.ExecuteBuildAction.run(ExecuteBuildAction.java:33)
    at org.gradle.launcher.cli.ExecuteBuildAction.run(ExecuteBuildAction.java:24)
    at org.gradle.launcher.exec.InProcessBuildActionExecuter.execute(InProcessBuildActionExecuter.java:35)
    at org.gradle.launcher.exec.InProcessBuildActionExecuter.execute(InProcessBuildActionExecuter.java:26)
    at org.gradle.launcher.cli.RunBuildAction.run(RunBuildAction.java:50)
    at org.gradle.api.internal.Actions$RunnableActionAdapter.execute(Actions.java:171)
    at org.gradle.launcher.cli.CommandLineActionFactory$ParseAndBuildAction.execute(CommandLineActionFactory.java:201)
    at org.gradle.launcher.cli.CommandLineActionFactory$ParseAndBuildAction.execute(CommandLineActionFactory.java:174)
    at org.gradle.launcher.cli.CommandLineActionFactory$WithLogging.execute(CommandLineActionFactory.java:170)
    at org.gradle.launcher.cli.CommandLineActionFactory$WithLogging.execute(CommandLineActionFactory.java:139)
    at org.gradle.launcher.cli.ExceptionReportingAction.execute(ExceptionReportingAction.java:33)
    at org.gradle.launcher.cli.ExceptionReportingAction.execute(ExceptionReportingAction.java:22)
    at org.gradle.launcher.Main.doAction(Main.java:48)
    at org.gradle.launcher.bootstrap.EntryPoint.run(EntryPoint.java:45)
    at org.gradle.launcher.Main.main(Main.java:39)
    at org.gradle.launcher.bootstrap.ProcessBootstrap.runNoExit(ProcessBootstrap.java:50)
    at org.gradle.launcher.bootstrap.ProcessBootstrap.run(ProcessBootstrap.java:32)
    at org.gradle.launcher.GradleMain.main(GradleMain.java:26)
    at org.gradle.wrapper.BootstrapMainStarter.start(BootstrapMainStarter.java:33)
    at org.gradle.wrapper.WrapperExecutor.execute(WrapperExecutor.java:130)
    at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:48)
Caused by: org.gradle.api.GradleException: Could not copy file '/home/wujek/IdeaWorkspace/filter-test/src/main/resources/test.resource' to '/home/wujek/IdeaWorkspace/filter-test/build/resources/main/test.resource'.
    at org.gradle.api.internal.file.AbstractFileTreeElement.copyTo(AbstractFileTreeElement.java:70)
    at org.gradle.api.internal.file.copy.MappingCopySpecVisitor$FileVisitDetailsImpl.copyTo(MappingCopySpecVisitor.java:127)
    at org.gradle.api.internal.file.copy.FileCopySpecVisitor.copyFile(FileCopySpecVisitor.java:56)
    at org.gradle.api.internal.file.copy.FileCopySpecVisitor.visitFileOrDir(FileCopySpecVisitor.java:52)
    at org.gradle.api.internal.file.copy.FileCopySpecVisitor.visitFile(FileCopySpecVisitor.java:39)
    at org.gradle.api.internal.file.copy.NormalizingCopySpecVisitor.visitFile(NormalizingCopySpecVisitor.java:70)
    at org.gradle.api.internal.file.copy.MappingCopySpecVisitor.visitFile(MappingCopySpecVisitor.java:57)
    at org.gradle.api.internal.file.collections.DirectoryFileTree.walkDir(DirectoryFileTree.java:156)
    at org.gradle.api.internal.file.collections.DirectoryFileTree.visitFrom(DirectoryFileTree.java:124)
    at org.gradle.api.internal.file.collections.DirectoryFileTree.visit(DirectoryFileTree.java:114)
    at org.gradle.api.internal.file.collections.FileTreeAdapter.visit(FileTreeAdapter.java:96)
    at org.gradle.api.internal.file.CompositeFileTree.visit(CompositeFileTree.java:54)
    at org.gradle.api.internal.file.copy.CopyActionImpl.execute(CopyActionImpl.java:64)
    at org.gradle.api.tasks.AbstractCopyTask.copy(AbstractCopyTask.java:42)
    at org.gradle.language.jvm.tasks.ProcessResources.copy(ProcessResources.java:30)
    at org.gradle.api.internal.BeanDynamicObject$MetaClassAdapter.invokeMethod(BeanDynamicObject.java:216)
    at org.gradle.api.internal.BeanDynamicObject.invokeMethod(BeanDynamicObject.java:122)
    at org.gradle.api.internal.CompositeDynamicObject.invokeMethod(CompositeDynamicObject.java:147)
    at org.gradle.language.jvm.tasks.ProcessResources_Decorated.invokeMethod(Unknown Source)
    at org.gradle.util.ReflectionUtil.invoke(ReflectionUtil.groovy:23)
    at org.gradle.api.internal.project.taskfactory.AnnotationProcessingTaskFactory$StandardTaskAction.doExecute(AnnotationProcessingTaskFactory.java:217)
    at org.gradle.api.internal.project.taskfactory.AnnotationProcessingTaskFactory$StandardTaskAction.execute(AnnotationProcessingTaskFactory.java:210)
    at org.gradle.api.internal.project.taskfactory.AnnotationProcessingTaskFactory$StandardTaskAction.execute(AnnotationProcessingTaskFactory.java:199)
    at org.gradle.api.internal.AbstractTask$TaskActionWrapper.execute(AbstractTask.java:526)
    at org.gradle.api.internal.AbstractTask$TaskActionWrapper.execute(AbstractTask.java:509)
    at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeAction(ExecuteActionsTaskExecuter.java:80)
    at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeActions(ExecuteActionsTaskExecuter.java:61)
    ... 70 more
Caused by: org.gradle.api.InvalidUserDataException: Error - Invalid filter specification for org.apache.tools.ant.filters.ReplaceTokens
    at org.gradle.api.internal.file.copy.FilterChain$1.transform(FilterChain.java:69)
    at org.gradle.api.internal.file.copy.FilterChain$1.transform(FilterChain.java:58)
    at org.gradle.api.internal.ChainingTransformer.transform(ChainingTransformer.java:37)
    at org.gradle.api.internal.file.copy.FilterChain.transform(FilterChain.java:39)
    at org.gradle.api.internal.file.copy.FilterChain.transform(FilterChain.java:46)
    at org.gradle.api.internal.file.copy.MappingCopySpecVisitor$FileVisitDetailsImpl.open(MappingCopySpecVisitor.java:111)
    at org.gradle.api.internal.file.AbstractFileTreeElement.copyTo(AbstractFileTreeElement.java:47)
    at org.gradle.api.internal.file.copy.MappingCopySpecVisitor$FileVisitDetailsImpl.copyTo(MappingCopySpecVisitor.java:119)
    at org.gradle.api.internal.file.AbstractFileTreeElement.copyFile(AbstractFileTreeElement.java:88)
    at org.gradle.api.internal.file.AbstractFileTreeElement.copyTo(AbstractFileTreeElement.java:65)
    ... 96 more
Caused by: org.codehaus.groovy.runtime.typehandling.GroovyCastException: Cannot cast object '{resources=org.gradle.api.internal.resources.DefaultResourceHandler@7b5d82b7, distsDirName=distributions, fileResolver=org.gradle.api.internal.file.BaseDirFileResolver@3830fba7, buildFile=/home/wujek/IdeaWorkspace/filter-test/build.gradle, test=task ':test', javadoc=task ':javadoc', dependencyCacheDir=/home/wujek/IdeaWorkspace/filter-test/build/dependency-cache, extensions=org.gradle.api.internal.plugins.DefaultConvention@17d9adc3, rootDir=/home/wujek/IdeaWorkspace/filter-test, sources=[[source set 'main:java', source set 'main:resources'], [source set 'test:java', source set 'test:resources']], ext=org.gradle.api.internal.plugins.DefaultExtraPropertiesExtension@541ee970, processTestResources=task ':processTestResources', compileJava=task ':compileJava', defaultArtifacts=org.gradle.api.internal.plugins.DefaultArtifactPublicationSet_Decorated@396fe335, projectDir=/home/wujek/IdeaWorkspace/filter-test, description=null, jar=task ':jar', implicitTasks=[task ':dependencies', task ':dependencyInsight', task ':help', task ':projects', task ':properties', task ':tasks'], projectRegistry=org.gradle.api.internal.project.DefaultProjectRegistry@60adca08, ant=org.gradle.api.internal.project.DefaultAntBuilder@31ab04ce, compileTestJava=task ':compileTestJava', defaultTasks=[], buildDir=/home/wujek/IdeaWorkspace/filter-test/build, services=ProjectInternalServiceRegistry, status=integration, allprojects=[root project 'filter-test'], libsDirName=libs, archivesBaseName=filter-test, childProjects={}, distsDir=/home/wujek/IdeaWorkspace/filter-test/build/distributions, convention=org.gradle.api.internal.plugins.DefaultConvention@17d9adc3, parent=null, class=class org.gradle.api.internal.project.DefaultProject_Decorated, buildscript=org.gradle.api.internal.initialization.DefaultScriptHandler@52c889db, projectEvaluationBroadcaster=ProjectEvaluationListener broadcast, logging=org.gradle.logging.internal.DefaultLoggingManager@6bd2b352, properties=(this Map), docsDirName=docs, metaInf=[], depth=0, standardOutputCapture=org.gradle.logging.internal.DefaultLoggingManager@6bd2b352, project=root project 'filter-test', testResultsDir=/home/wujek/IdeaWorkspace/filter-test/build/test-results, subprojects=[], buildTasks=[build], targetCompatibility=1.7, sourceCompatibility=1.7, libsDir=/home/wujek/IdeaWorkspace/filter-test/build/libs, dependencyCacheDirName=dependency-cache, buildScriptSource=org.gradle.groovy.scripts.UriScriptSource@55fb03cb, gradle=build 'filter-test', reporting=org.gradle.api.reporting.ReportingExtension_Decorated@52b63086, components=[org.gradle.api.internal.java.JavaLibrary@372bd7d6], projectEvaluator=org.gradle.configuration.LifecycleProjectEvaluator@1f673ee6, sourceSets=[source set 'main', source set 'test'], buildDependents=task ':buildDependents', repositories=[], plugins=[org.gradle.api.plugins.BasePlugin@9b46a1a, org.gradle.api.plugins.ReportingBasePlugin@2ed5242b, org.gradle.language.base.plugins.LanguageBasePlugin@72f4cfd4, org.gradle.language.jvm.plugins.JvmLanguagePlugin@345077f2, org.gradle.api.plugins.JavaLanguagePlugin@698ceb26, org.gradle.api.plugins.JavaBasePlugin@7d07a550, org.gradle.api.plugins.JavaPlugin@6e4985b7, org.gradle.api.plugins.HelpTasksPlugin@26749cf7, org.gradle.buildsetup.plugins.BuildSetupPlugin@33cbcffe], dependsOnProjects=[], testReportDirName=tests, apiDocTitle=filter-test API, state=project state 'EXECUTED', manifest=org.gradle.api.java.archives.internal.DefaultManifest@33527e25, reportsDir=/home/wujek/IdeaWorkspace/filter-test/build/reports, reportsDirName=reports, build=task ':build', artifacts=org.gradle.api.internal.artifacts.dsl.DefaultArtifactHandler@6cd7c975, version=unspecified, metaClass=org.codehaus.groovy.runtime.HandleMetaClass@4aeb5826[groovy.lang.MetaClassImpl@4aeb5826[class org.gradle.api.internal.project.DefaultProject_Decorated]], testClasses=task ':testClasses', clean=task ':clean', dependencies=org.gradle.api.internal.artifacts.dsl.dependencies.DefaultDependencyHandler@20e84d37, name=filter-test, path=:, logger=org.gradle.api.logging.Logging$LoggerImpl@732566c1, parentIdentifier=null, processResources=task ':processResources', testReportDir=/home/wujek/IdeaWorkspace/filter-test/build/reports/tests, classes=task ':classes', buildNeeded=task ':buildNeeded', module=org.gradle.api.internal.artifacts.ProjectBackedModule@1c7d43f6, check=task ':check', binaries=[[binary 'main', binary 'test']], whatever=17, docsDir=/home/wujek/IdeaWorkspace/filter-test/build/docs, tasks=[task ':assemble', task ':build', task ':buildDependents', task ':buildNeeded', task ':check', task ':classes', task ':clean', task ':compileJava', task ':compileTestJava', task ':jar', task ':javadoc', task ':processResources', task ':processTestResources', task ':setupBuild', task ':test', task ':testClasses'], rootProject=root project 'filter-test', rebuildTasks=[clean, build], inheritedScope=org.gradle.api.internal.ExtensibleDynamicObject$InheritedDynamicObject@63f8da16, scriptPluginFactory=org.gradle.configuration.DefaultScriptPluginFactory@66b60da0, assemble=task ':assemble', asDynamicObject=org.gradle.api.internal.ExtensibleDynamicObject@3a38b80e, testResultsDirName=test-results, antBuilderFactory=org.gradle.api.internal.project.DefaultAntBuilderFactory@61bbbcfe, runtimeClasspath=file collection, configurations=[configuration ':archives', configuration ':compile', configuration ':default', configuration ':runtime', configuration ':testCompile', configuration ':testRuntime'], group=}' with class 'java.util.HashMap' to class 'java.util.Hashtable' due to: java.lang.NullPointerException
    at org.gradle.api.internal.BeanDynamicObject$MetaClassAdapter.setProperty(BeanDynamicObject.java:178)
    at org.gradle.api.internal.BeanDynamicObject.setProperty(BeanDynamicObject.java:107)
    at org.gradle.util.ConfigureUtil.configureByMap(ConfigureUtil.java:43)
    at org.gradle.api.internal.file.copy.FilterChain$1.transform(FilterChain.java:65)
    ... 105 more

As you can see, there is no more stack trace than I showed in the initial post.

Now, here is what happens:

  1. Project.properties is a HashMap and it contains keys that are mapped to null values - for example, ‘description’ if not set, ‘parent’ or ‘parentIdentifier’ (probably always for non-child builds) 2. ReplaceTokens.setTokens needs a Hashtable, so deep down (initiated by Gradle’s ConfigureUtil.configureByMap:43) Groovy transforms the HashMap to it by calling the Hashtable constructor with the HashMap as argument - DefaultTypeTransformation.castToType:331 3. the Hashtable constructor does nothing else than iterate over the map and fill itself, but it throws NPE when the value is null (Hashtable.put:542 in Oracle Java 7 u 21 64 bit)

So, to make this work the build script has to be changed to (or similar):

filter ReplaceTokens, tokens: project.properties.collectEntries { k, v -> [k, v ?: ''] }

results in (just the final bit, the root cause):

Caused by: java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
    at org.apache.tools.ant.filters.ReplaceTokens.read(ReplaceTokens.java:165)
    at org.apache.tools.ant.filters.BaseFilterReader.read(BaseFilterReader.java:83)
    at org.apache.tools.ant.util.ReaderInputStream.read(ReaderInputStream.java:117)
    at org.apache.commons.io.IOUtils.copyLarge(IOUtils.java:1025)
    at org.gradle.api.internal.file.AbstractFileTreeElement.copyTo(AbstractFileTreeElement.java:49)
    at org.gradle.api.internal.file.copy.MappingCopySpecVisitor$FileVisitDetailsImpl.copyTo(MappingCopySpecVisitor.java:119)
    at org.gradle.api.internal.file.AbstractFileTreeElement.copyFile(AbstractFileTreeElement.java:88)
    at org.gradle.api.internal.file.AbstractFileTreeElement.copyTo(AbstractFileTreeElement.java:65)
    ... 96 more

with the line in question ReplaceTokens.read:165 being:

final String replaceWith = (String) hash.get(key.toString());

Which makes me raise the question - do people really do filtering this way? This seems really limited and results in strange errors. It’s actually hard to blame any of the parties, it just the integration of these parts is unfortunate: Ant says in the ReplaceTokens.setTokens documentation explicitly that it accepts only Hashtable of String -> String, so the cast (instead of, for instance, invoking toString) is justified. Groovy can’t really know that the constructor it calls will fails because it doesn’t know that it is calling Hashtable() and it doesn’t like nulls - unless it creates another specific hardcoded case. Does Gradle really have to put have null values in the properties? (I’m not arguing that is shouldn’t, I just don’t know, just asking…)

I wonder if anybody reaches this line.

wujek