ciscoo / cxf-codegen-gradle

Gradle plugin to generate Java artifacts from WSDL
Apache License 2.0
26 stars 6 forks source link

Generating from multiple WSDL overwrites ObjectFactory #40

Closed dsuchy closed 2 years ago

dsuchy commented 2 years ago

Sample case: few services (each defined by different WSDL) are using common model files. ObjectFactory for the common package will get overwritten after every iteration and, in the end, will consist of bindings for the last imported WSDL only.

The deprecated nils wsdl2java utility seems to have had a property for merging those files in that case: https://github.com/nilsmagnus/wsdl2java/issues/39

Does this plugin support ObjectFactory merging?

ciscoo commented 2 years ago

Can you share your Gradle configuration?

dsuchy commented 2 years ago
plugins {
    id "io.mateo.cxf-codegen" version "1.0.1"
}

...

cxfCodegen {
    wsdl2java {
        service1 {
            wsdl = file("src/main/resources/soap-ws-service1.wsdl")
            outputDir = file("src/main/java")
            bindingFiles = ["src/main/resources/binding.xjb"]
            extraArgs = ["-client", "-verbose"]
        }
        service2 {
            wsdl = file("src/main/resources/soap-ws-service2.wsdl")
            outputDir = file("src/main/java")
            bindingFiles = ["src/main/resources/binding.xjb"]
            extraArgs = ["-client", "-verbose"]
        }
        ...
    }
    wsdl2java.configureEach {
        markGenerated = true
        extraArgs = ["-frontend", "jaxws21"]
    }
}

dependencies {
    cxfCodegen (
            'jakarta.xml.ws:jakarta.xml.ws-api:2.3.3',
            'jakarta.annotation:jakarta.annotation-api:1.3.5'
    )
...
}
ciscoo commented 2 years ago

Couple issues with that setup.

  1. The extension is deprecated and will be removed in future a release. See the migration guide
  2. Generating Java code to the same directory leads to the situation you are facing currently.

To further expand on (2), when service1 finishes generating, then service2 generates its code which will overwrite what was previously generated; vice versa. This is why the default output directory is unique for each task to avoid such situations and allow Gradle to cache outputs effectively.

The wsdl2java plugin by Nil's 'hooks' in to the code generation process. This plugin does no such thing and instead leaves all code generation to the wsdl2java tool itself. As such, modification of the code generated is out of scope for this plugin. This plugins goal is to provide an API over the wsdl2java CLI tool provided by the Apache CXF project.

Furthermore, the issue you are facing seems to be CXF-1340. From the issue comments:

This issue is very closely related to https://issues.apache.org/jira/browse/CXF-1662 and is a JAXB limitation/design that we have no control over. We just call off to JAXB to generate these files. Thus, we cannot do anything about it.

Basically, JAXB is pretty much COMPLETELY designed around the idea of namespace:package is a 1:1 mapping. If you try to map multiple schemas into the same package, it's very likely NOT to work.

Along with the ObjectFactory issue, the package-info.java file that is generated is also very specific to the schema that was being processed when generated. If you have multiple schemas generating into the package, the resulting XML is likely to have the wrong namespaces, wrong qualification, etc... set. Basically, invalid XML according to the schema.

My understanding of that is that this behavior is expected. Further more, from the comments in the issue you linked, a workaround for the limitations of JAXB is specifying the package for each service. So:

cxfCodegen {
    wsdl2java {
        register("service1") {
            packageNames.set(listOf("com.example.service1"))
        }
        register("service2") {
            packageNames.set(listOf("com.example.service2"))
        }
    }
}

But since the extension configuration is deprecated, the preferred way using the task:

tasks.register("service1", io.mateo.cxf.codegen.wsdl2java.Wsdl2Java::class) {
    toolOptions {
        wsdl.set(file("src/main/resources/soap-ws-service1.wsdl"))
        bindingFiles.set(listOf("src/main/resources/binding.xjb"))
        extraArgs.set(listOf("-client"))
        verbose.set(true)
    }
}

tasks.register("service2", io.mateo.cxf.codegen.wsdl2java.Wsdl2Java::class) {
    toolOptions {
        wsdl.set(file("src/main/resources/soap-ws-service2.wsdl"))
        bindingFiles.set(listOf("src/main/resources/binding.xjb"))
        extraArgs.set(listOf("-client"))
        verbose.set(true)
    }
}

tasks.withType(io.mateo.cxf.codegen.wsdl2java.Wsdl2Java::class).configureEach {
    toolOptions {
        markGenerated.set(true)
        frontend.set("jaxws21")
    }
}

The output directory by default is build/${taskName}-wsdl2java-generated-sources which is added to the main source set by default, so no need to generate to src/main either.

dsuchy commented 2 years ago

Thank you for your detailed answer.

I probably should have worded my question more precisely – I tried to avoid generating into multiple packages, because many classes are shared and therefore would get multiplicated. Some of them aren't identical between services though, that is the reason why I was searching for a way to recreate stabilizeAndMergeObjectFactory property with non-deprecated plugin despite Apache CFX tool limitations.

The wsdl2java plugin by Nil's 'hooks' in to the code generation process. This plugin does no such thing and instead leaves all code generation to the wsdl2java tool itself. As such, modification of the code generated is out of scope for this plugin. This plugins goal is to provide an API over the wsdl2java CLI tool provided by the Apache CXF project.

I understand that the goal of the project is to port the CLI tool, not to extend its functionality. Thanks again for your quick and thorough help.

dsuchy commented 2 years ago

many classes are shared and therefore would get multiplicated

The implication of that is, unfortunately, that generating to the default build/${taskName}-wsdl2java-generated-sources directory will result in several error: duplicate class compile errors

ciscoo commented 2 years ago

searching for a way to recreate stabilizeAndMergeObjectFactory property with non-deprecated plugin despite Apache CFX tool limitations.

One idea, although it will be a bit of trail/error, is to generate the Java code to its default output directory, but exclude them from the main source and then use the output in combination with logic from ObjectFactoryMerger

val sevice1 = tasks.register("service1", io.mateo.cxf.codegen.wsdl2java.Wsdl2Java::class) {
    addToMainSourceSet.set(false)
    toolOptions {
        wsdl.set(file("src/main/resources/soap-ws-service1.wsdl"))
    }
}

val service2 = tasks.register("service2", io.mateo.cxf.codegen.wsdl2java.Wsdl2Java::class) {
    addToMainSourceSet.set(false)
    toolOptions {
        wsdl.set(file("src/main/resources/soap-ws-service2.wsdl"))
    }
}

val copyCode = tasks.register("copyCode", Copy::class) {
    from(service1, service2)
    into(layout.buildDirectory.dir("merged-wsdl2java"))
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
    // Rest of code
    doLast {

   }
}

sourceSets {
    main {
        java {
            srcDir(copyCode)
        }
    }
}
ciscoo commented 2 years ago

Closing as nothing to do here.

davidlivingrooms commented 1 year ago

For future lost souls that go down the path of creating separate packages in Spring. Make sure your JaxB2Marshaller configuration does not scan all packages at once or you will still get an error on startup.

The following config will not work if you have com.mypackage.soapservicea, com.mypackage.soapserviceb with duplicated types. They may be separate packages, but it still fails because you are scanning all of them.

@Configuration
public class BaseConfiguration {

  @Bean
  public Jaxb2Marshaller xmlMarshaller() {
    Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
    marshaller.setPackagesToScan("com.mypackage");
    return marshaller;
  }

}

I had to do the following to get it to work.

@Configuration
public class BaseConfiguration {

  @Bean
  public Jaxb2Marshaller soapServiceAXmlMarshaller() {
    Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
    marshaller.setPackagesToScan("com.mypackage.soapservicea");
    return marshaller;
  }

  @Bean
  public Jaxb2Marshaller soapServiceBXmlMarshaller() {
    Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
    marshaller.setPackagesToScan("com.mypackage.soapserviceb");
    return marshaller;
  }
}

Not pretty, but it works.