I have a structure of old bean object in a dedicate package
I want to copy them to a test folder so when I update them I can ensure that new version are compatible with new one
To avoid any naming issue old bean will be rename during copy
This make the copy but the class cannot compile because className != filename
task saveOldBean(type: Copy) {
from('src/main/java/project/bean/') {
include '**/*Bean.java'
}
into 'src/test/java/project/bean/'
rename '(.*).java', '$1Old.java'
}
So i try to replace ClassName in file using same kind of feature (ie regexp)
task saveOldBean(type: Copy) {
from('src/main/java/project/bean/') {
include '**/(.*Bean).java'
filter(ReplaceTokens, tokens: [$1: $1Old])
}
into 'src/test/java/project/bean/'
rename '(.*).java', '$1Old.java'
}
This fails, so if you have any suggestion to make this "rename" works, you are welcome
after some tries here is an implementation
task backupBean(type: Copy) {
def TAG_PREVIOUS="Backup";
def newEnd = "${TAG_PREVIOUS}.java";
from('src/main/java') {
include '**/*Bean.java'
}
into 'src/test/java'
rename { String fileName ->
fileName.replace('.java', newEnd)
}
eachFile { FileCopyDetails fileInfo ->
def fileName = fileInfo.name;
def oldClassName = fileName.replace(newEnd, "");
def newClassName = fileName.substring(0, fileName.indexOf(".java"));
filter{ it.replaceAll("$oldClassName","${newClassName}")}
filter{ it.replaceAll("public final class","/*${version}*/\npublic final class")}
println "save [$oldClassName] to [$newClassName]"
}
}
Related
This is the error I get when I try to create the class under test
Could not find matching constructor for: com.pittacode.apihelper.json.JsonObjectFlattener()
groovy.lang.GroovyRuntimeException: Could not find matching constructor for: com.pittacode.apihelper.json.JsonObjectFlattener() at com.pittacode.apihelper.json.JsonObjectFlattenerTest.flatten json object with one nested object(JsonObjectFlattenerTest.groovy:12)
This is my test class
package com.pittacode.apihelper.json
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import spock.lang.Specification
class JsonObjectFlattenerTest extends Specification {
def classUnderTest = new JsonObjectFlattener()
def "flatten json object with one nested object"() {
given:
def jsonString = """
{
"1-1": 11,
"1-2": {
"2-1": "21"
},
"1-3": 13
}
"""
def jsonObject = JsonParser.parseString(json).getAsJsonObject()
when:
JsonObject result = classUnderTest.flatten(jsonObject)
then:
result.keySet().containsAll(["1-1", "1-2", "1-3", "2-1"])
}
}
I have a gradle project with one subproject and a module-info.java
plugins {
id "groovy"
id "application"
id "org.beryx.jlink" version "2.25.0"
id "org.javamodularity.moduleplugin" version "1.8.10"
}
repositories {
mavenCentral()
}
ext {
log4jVersion = "2.17.2"
}
dependencies {
implementation("com.jayway.jsonpath:json-path:2.7.0") {
exclude group: "com.fasterxml.jackson.core"
exclude group: "com.google.gson"
}
implementation("com.fasterxml.jackson.core:jackson-databind:2.13.2")
implementation("com.google.code.gson:gson:2.9.0")
implementation("org.apache.logging.log4j:log4j-api:${log4jVersion}")
runtimeOnly("org.apache.logging.log4j:log4j-core:${log4jVersion}")
annotationProcessor("org.apache.logging.log4j:log4j-core:${log4jVersion}")
runtimeOnly("org.apache.logging.log4j:log4j-slf4j-impl:${log4jVersion}")
testImplementation("org.codehaus.groovy:groovy:3.0.9")
testImplementation("org.spockframework:spock-core:2.0-groovy-3.0")
}
application {
mainClass = "com.pittacode.apihelper.Runner"
mainModule = "com.pittacode.apihelper"
}
tasks.named("test") {
useJUnitPlatform()
}
jlink {
forceMerge "log4j", "jackson"
// options = ["--bind-services"] // makes jre bigger but has everything, good way to test stuff
launcher {
name = "apihelper"
jvmArgs = ["-Dlog4j.configurationFile=./log4j2.xml", "-Dlog4j2.debug=false"]
}
jpackage {
if (org.gradle.internal.os.OperatingSystem.current().windows) {
installerOptions += ["--win-per-user-install", "--win-dir-chooser", "--win-menu", "--win-shortcut"]
imageOptions += ["--win-console"]
}
}
}
tasks.jlink.doLast {
copy {
from("src/main/resources")
into("$buildDir/image/bin")
}
}
This is the class
package com.pittacode.apihelper.json;
import com.google.gson.JsonObject;
public final class JsonObjectFlattener {
public JsonObjectFlattener() {
}
public JsonObject flatten(JsonObject o) {
return null;
}
}
The weird thing is that in another specification class if I try to initiate another object (unrelated) it seems to create it just fine. That one has parameters so I tried adding some in the flattener as well but didn't seem to make a difference
Well this was silly,
Like I mentioned I am using java modules and it seems that I need to export the packages that contain the classes I want to test.
module com.pittacode.apihelper {
requires jdk.crypto.ec; // needed for ssl communication
requires org.slf4j;
requires java.net.http;
requires java.sql;
requires com.google.gson;
requires json.path;
requires org.apache.logging.log4j;
requires com.fasterxml.jackson.databind;
exports com.pittacode.apihelper;
exports com.pittacode.apihelper.json; // <-- this is the missing line
}
Java fails to launch when the classpath is too long. The length limit is particularly short on Windows.
Gradle seem uninterested in fixing the issue on their side (even though it's sort of their responsibility since they're the ones launching Java), so we ended up substituting the JavaExec task out with our own alternative.
The alternative works like this:
public class WorkingJavaExec extends JavaExec {
private static final String MATCH_CHUNKS_OF_70_CHARACTERS =
"(?<=\\G.{70})";
private final Logger logger = LoggerFactory.getLogger(getClass());
#Override
public void exec() {
FileCollection oldClasspath = getClasspath();
File jarFile = null;
try {
if (!oldClasspath.isEmpty()) {
try {
jarFile =
toJarWithClasspath(oldClasspath.getFiles());
setClasspath(getProject().files(jarFile));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
super.exec();
} finally {
setClasspath(oldClasspath);
if (jarFile != null) {
try {
Files.delete(jarFile.toPath());
} catch (Exception e) {
logger.warn("Couldn't delete: " + jarFile, e);
}
}
}
}
public static File toJarWithClasspath(Set<File> files)
throws IOException {
File jarFile = File.createTempFile("long-classpath", ".jar");
try (ZipOutputStream zip =
new ZipOutputStream(new FileOutputStream(jarFile))) {
zip.putNextEntry(new ZipEntry("META-INF/MANIFEST.MF"));
try (PrintWriter writer =
new PrintWriter(
new OutputStreamWriter(
zip, StandardCharsets.UTF_8))) {
writer.println("Manifest-Version: 1.0");
String classPath = files.stream().map(
file -> file.toURI().toString())
.collect(Collectors.joining(" "));
String classPathEntry = "Class-Path: " + classPath;
writer.println(Arrays.stream(
classPathEntry.split(MATCH_CHUNKS_OF_70_CHARACTERS))
.collect(Collectors.joining("\n ")));
}
}
return jarFile;
}
}
Using this is cumbersome, though, because everywhere someone might run JavaExec, I have to replace it with WorkingJavaExec. New developers also don't know that there is this pitfall in Gradle in the first place, so they don't even know it's something they have to work around.
In reading the internals of Gradle, I saw that JavaExec internally uses a JavaExecAction to do the actual exec.
I thought that maybe by replacing this, we could fix the problem as if Gradle had fixed it themselves, and maybe it would then also apply to other tasks, such as Test. But I haven't been able to find any examples anywhere. (Even in other large projects, which you would expect to have hit the same issue!)
Is it possible to substitute JavaExecAction, and if so, how?
I'm not sure you can "substitute" JavaExecAction because it is set during JavaExec task instanciation, but I think you can solve this problem in a nicer way, using a custom Plugin as follow:
class FixClasspathLimitPlugin implements Plugin<Project> {
#Override
void apply(Project project) {
// after project has been evaluated, hack into all tasks of type JavaExec declared.
project.afterEvaluate {
project.tasks.stream().filter { task -> task instanceof JavaExec }.forEach {
println "Reconfiguring classpath for : $it"
JavaExec javaExec = (JavaExec) it;
FileCollection oldClasspath = javaExec.getClasspath()
// insert an Action at first position, that will change classpath
javaExec.doFirst { task ->
((JavaExec) task).setClasspath(getProject().files(toJarWithClasspath(oldClasspath.getFiles())));
}
// optional - reset old classpath
javaExec.doLast { task ->
((JavaExec) task).setClasspath(oldClasspath)
}
}
}
}
public static File toJarWithClasspath(Set<File> files)
throws Exception {
// same method implementation as given in your question
}
This way, you won't have to replace JavaExec in all build scripts written by your team, you will only have to ensure that these scripts apply your plugin.
And if you use a custom distribution of Gradle and use wrapper in you enterprise, you can even include this plugin in this distribution as an Init Script, as explained here: https://docs.gradle.org/current/userguide/init_scripts.html#sec:using_an_init_script
Put a file that ends with .gradle in the GRADLE_HOME/init.d/ directory, in the Gradle distribution. This allows you to package up a custom Gradle distribution containing some custom build logic and plugins. You can combine this with the Gradle wrapper as a way to make custom logic available to all builds in your enterprise.
This way, the plugin will be applied in a "transparent" way.
Concerning the Test task: it does not use JavaExecAction, I think, but a similar solution could be applied, using a similar plugin.
You can use the jar task to add the class path to the manifest for you:
jar {
baseName = "my-app"
version = "1.0.0"
manifest {
attributes("Class-Path": configurations.compile.collect { it.getName() }.join(' '))
}
}
And then you can reference that jar when launching:
task run(type:JavaExec) {
classpath = jar.outputs.files
main = "myapp.MainClass"
}
That works around the command line path limit. You might also want to copy the dependency JARs to the output folder, so they will be available at runtime.
task copyDependencies(type: Copy, dependsOn: [ "build" ]) {
from configurations.runtime
into "./build/libs"
}
build.finalizedBy(copyDependencies)
Helpful?
I'm writing a gradle plugin that modifies class files. I want the task to run on all the generated class files and replace them. Here's what I have so far:
public class InstrumenterTask extends DefaultTask {
private File outputDir;
private SourceSet sourceSet;
public void setOutputDir(File outputDir) {
this.outputDir = outputDir;
}
public void setSourceSet(SourceSet sourceSet) {
this.sourceSet = sourceSet;
}
#TaskAction
public void processSourceSet() {
File classesDir = sourceSet.getOutput().getClassesDir();
Path classesPath = classesDir.toPath();
Files.walk(classesPath).forEach(this::processClassFile);
}
private void processClassFile(File inputFile) {
// Omitted for brevity. Result is in outputDir
}
}
and my buildscript
apply plugin: 'java'
apply plugin: 'application'
mainClassName = 'user.Main'
jar {
manifest {
attributes 'Main-Class': 'user.Main'
}
}
task(mainInstrumenter, type: InstrumenterTask) {
outputDir = new File(buildDir, 'instrumentedClasses')
sourceSet = sourceSets.main
}
mainInstrumenter.dependsOn classes
jar.dependsOn mainInstrumenter
sourceSets.main.output.dir new File(buildDir, 'instrumentedClasses')
The problem now is that in the compiled jar, the class is there two times. How can I tell gradle not to include the classes in the default classes dir?
Alternatively, can I tell gradle to compile the classes to a different directory and put my classes in the default location?
You can simply, at the end of your task action, call
sourceSet.getOutput().setClassesDir(outputDir);
and store your outputs in outputDir. All the following, dependant tasks will pick up on the newly set classesDir, especially jar and run when the task is a dependency of them.
I have the following gradle task:
class MyTranslateTask extends DefaultTask {
#InputFiles FileCollection srcFiles
#OutputDirectory File destDir
#TaskAction
def run() {
...
}
}
How can I get all the files from my srcFiles which have changed since the last run of this task?
Gradle 1.6 introduced an incubating feature called IncrementalTasksInputs that allows you access to files that were changed or removed since last task run.
ref: https://gradle.org/docs/current/dsl/org.gradle.api.tasks.incremental.IncrementalTaskInputs.html
class IncrementalReverseTask extends DefaultTask {
#InputDirectory
def File inputDir
#OutputDirectory
def File outputDir
#TaskAction
void execute(IncrementalTaskInputs inputs) {
inputs.outOfDate { change ->
def targetFile = project.file("$outputDir/${change.file.name}")
targetFile.text = change.file.text.reverse()
}
inputs.removed { change ->
def targetFile = project.file("$outputDir/${change.file.name}")
if (targetFile.exists()) {
targetFile.delete()
}
}
}
}
I have the following class in src/main/java/com/company/project/Config.java:
public class Config {
public static final boolean DEBUG = true;
...
}
so that in some other class I can do the following, knowing that the java compiler will strip the if() statement if it evaluates to false:
import static com.company.project.Config.DEBUG
if (DEBUG) {
client.sendMessage("something");
log.debug("something");
}
In Gradle, what is the best way to filter and change DEBUG value in Config.java at compile time without modifying the original file?
So far I'm thinking:
Create a task updateDebug(type:Copy) that filters DEBUG and copies Config.java to a temporary location
Exclude from sourceSets the original Config.java file and include the temporary one
Make compileJava.dependsOn updateDebug
Is the above possible?
Is there a better way?
To answer my own question, given the class src/main/java/com/company/project/Config.java:
public class Config {
public static final boolean DEBUG = true;
...
}
this is the Gradle code I came up with:
//
// Command line: gradle war -Production
//
boolean production = hasProperty("roduction");
//
// Import java regex
//
import java.util.regex.*
//
// Change Config.java DEBUG value based on the build type
//
String filterDebugHelper(String line) {
Pattern pattern = Pattern.compile("(boolean\\s+DEBUG\\s*=\\s*)(true|false)(\\s*;)");
Matcher matcher = pattern.matcher(line);
if (matcher.find()) {
line = matcher.replaceFirst("\$1"+(production? "false": "true")+"\$3");
}
return (line);
}
//
// Filter Config.java and inizialize 'DEBUG' according to the current build type
//
task filterDebug(type: Copy) {
from ("${projectDir}/src/main/java/com/company/project") {
include "Config.java"
filter { String line -> filterDebugHelper(line) }
}
into "${buildDir}/tmp/filterJava"
}
//
// Remove from compilation the original Config.java and add the filtered one
//
sourceSets {
main {
java {
srcDirs ("${projectDir}/src/main/java", "${buildDir}/tmp/filterJava")
exclude ("com/company/project/Config.java")
}
resources {
}
}
}
//
// Execute 'filterDebug' task before compiling
//
compileJava {
dependsOn filterDebug
}
It's admittedly a little hacky but it works, and it gives me the most efficient solution while still controlling development/production builds from a single point of entry (build.gradle).