Best approach Gradle multi-module project: generate just one global javadoc

Hello Gradle community

I have a gradle multi-module project and I did realise if I execute gradle javadoc it generates the javadoc individually for each module. Well I don’t want that.

I did a research on Google and I got the following:

I have tried the third approach with the following (included within subproject {...} and I have the apply plugin: 'java' declared too within subproject {...}):

def exportedProjects = [
        ":web-27-config",
	":web-27-domain",
	":web-27-repository-api", 
	":web-27-service-api",
	":web-27-service-impl"
]

task alljavadoc(type: Javadoc) {
    source exportedProjects.collect { project(it).sourceSets.main.allJava }    											  
    classpath = files(exportedProjects.collect { project(it).sourceSets.main.compileClasspath })
    destinationDir = file("${buildDir}/docs/javadoc")
}

But when in my STS IDE I do right click in my project and Gradle -> Refresh Gradle Project I always get:

Caused by: groovy.lang.MissingPropertyException: Could not find property 'sourceSets' on project ':web-27-domain'.
	at org.gradle.api.internal.AbstractDynamicObject.propertyMissingException(AbstractDynamicObject.java:43)
	at org.gradle.api.internal.AbstractDynamicObject.getProperty(AbstractDynamicObject.java:35)
	at org.gradle.api.internal.CompositeDynamicObject.getProperty(CompositeDynamicObject.java:97)
	at org.gradle.api.internal.project.DefaultProject_Decorated.getProperty(Unknown Source)
....

Therefore:

  1. How I can fix the error thrown above? (What is missing or what is need it?)
  2. Currently for July 2016, what is the best and recommended approach
    (from Gradle itself) to accomplish the generation of just one javadoc merging all the sub modules?.

Thanks in advance

1 Like

The sourceSets property is contributed to the object model by the java plugin.

I would assume that the domain projects does not have the java plugin applied at the time you are configuring the alljavadoc task. An easy fix is to configure the task in afterEvaluate { ... } block.

Hello

Thanks by the reply.

  • Each module has its own build.grade but is empty
  • Exists in the root project the build.grade (I do all there) with the following (among other things):
subprojects {

    apply plugin: 'java'
    apply plugin: 'eclipse'
    apply plugin: 'war'
    sourceCompatibility = '1.8'
    targetCompatibility = '1.8'


def exportedProjects = [
        ":web-27-config",
	":web-27-domain",
	":web-27-repository-api",
	":web-27-service-api",
	":web-27-service-impl"
]

task alljavadoc(type: Javadoc) {
    source exportedProjects.collect { project(it).sourceSets.main.allJava }    											  
    classpath = files(exportedProjects.collect { project(it).sourceSets.main.compileClasspath })
    destinationDir = file("${buildDir}/docs/javadoc")
}

repositories {
	jcenter()
}

... etc

Therefore with subprojects each module has apply plugin: 'java'

The curious is that it just fails in the second item of the collection

Overall looks alright. It would be useful if you could share a complete runnable example. In this case the subprojects { ... } block is not closed, so can’t really figure what follows what.

Hello Dimitar

Understood…


version = '1.0.0'

allprojects {

}

subprojects {

    apply plugin: 'java'
    apply plugin: 'eclipse'
    apply plugin: 'war'
    sourceCompatibility = '1.8'
    targetCompatibility = '1.8'

    def exportedProjects = [
                ":web-27-config",
                ":web-27-controller",
		":web-27-domain",
		":web-27-repository-api",
		":web-27-service-api",
		":web-27-service-impl"
    ]

    task alljavadoc(type: Javadoc) {
          source exportedProjects.collect { project(it).sourceSets.main.allJava }    											  
          classpath = files(exportedProjects.collect { project(it).sourceSets.main.compileClasspath })
          destinationDir = file("${buildDir}/docs/javadoc")
    }

    repositories {
	jcenter()
    }

    ext {

	//war
	warBaseName = 'web-27'
	
	//Apache ActiveMQ - JMS
	activeMQVersion = '5.13.3'		

	...more 

	//JSTL
	jstlVersion='1.2'

         ...more		
   }

   dependencies {
	
	//Apache ActiveMQ - JMS
 	compile "org.apache.activemq:activemq-broker:$activeMQVersion"
	
	... more
	
	//JSTL
	compile "javax.servlet:jstl:$jstlVersion"
	
       ...more
	 															 
   }
	
} // close for subprojects

project(':web-27-config') {
	description 'Infrastructure'
}

project(':web-27-controller') {
	description 'Web - Controller'
	dependencies {
 	   compile project(':web-27-rest')
 	   compile project(':web-27-support')
	}
}

... more

And always the second item of the collection fails and it does not go forward anymore…

Caused by: groovy.lang.MissingPropertyException: Could not find property 'sourceSets' on project ':web-27-controller'.
	at org.gradle.api.internal.AbstractDynamicObject.propertyMissingException(AbstractDynamicObject.java:43)
	at org.gradle.api.internal.AbstractDynamicObject.getProperty(AbstractDynamicObject.java:35)
	at org.gradle.api.internal.CompositeDynamicObject.getProperty(CompositeDynamicObject.java:97)
	at org.gradle.api.internal.project.DefaultProject_Decorated.getProperty(Unknown Source)

I hope it gives you more clues.

Thank you.

You are currently adding an alljavadoc task to every single child project. When you add it to the first project, the second project hasn’t been configured yet, which means that its java plugin has not been applied yet, hence it doesn’t have sourceSets.

The fix would be to take alljavadoc out of the subprojects { ... } block.

The Nebula gradle-aggregate-javadocs-plugin provides a Javadocs aggregation task out-of-the-box. I’d give that plugin a shot.

Hello Benjamin

Yes, it is my latest resource or option, but I want use something by default or nothing external yet.
It in case that plugin close or something related…

Hello Dimitar

I understand, I have the following now:

version = '1.0.0'

apply plugin: 'java' // mandatory to be here to avoid the classic error message

def exportedProjects = [
        ":web-27-config",
        ":web-27-controller",
	":web-27-domain",
	":web-27-repository-api",
	":web-27-service-api",
	":web-27-service-impl"
]

task alljavadoc(type: Javadoc) {
       println 'alljavadoc working'
       source exportedProjects.collect { project(it).sourceSets.main.allJava }    											  
       classpath = files(exportedProjects.collect { project(it).sourceSets.main.compileClasspath })
      //options.memberLevel = JavadocMemberLevel.PRIVATE
      //classpath = configurations.compile
     destinationDir = file("${buildDir}/docs/javadoc")
}

allprojects {
   //nothing
}

subproject {

       //apply plugin: 'java'
	apply plugin: 'eclipse'
	apply plugin: 'war'
	sourceCompatibility = '1.8'
	targetCompatibility = '1.8'
        ...
}

Well after to do:

  • gradle clean
  • gradle build (no errors)

gradle alljavadoc runs

And I get the following:

Manuels-MacBook-Pro:web-27 manueljordan$ gradle alljavadoc
alljavadoc working
:alljavadoc UP-TO-DATE

BUILD SUCCESSFUL

Total time: 0.892 secs

but nothing is generated…

Even if I do:

  • gradle javadoc

and then gradle alljavadoc

Just curious if you can replicate the same instructions by your side…

Thanks

I notice you stopped applying the java plugin to the subprojects. Your original setup was good - the only thing needed was to take the alljavadoc task definition and put it below the subprojects section.

Hello Dimitar

I notice you stopped applying the javadoc plugin to the subprojects.

Yes, I thought was illegal have the apply plugin: 'java' twice

Even when I enable apply plugin: 'java' in subprojects{} all remains the same.

Now according with your suggestion/observation:

the only thing needed was to take the alljavadoc task definition and put it below the subproject section

Yes, now works now… thanks so much! …

Even when I am not an expert with Gradle

(1) Now I am wondered why this change of location was mandatory, I thought that for a task its location does not matter, it for a single module project. I never had some issue about this situation, but now seems that for a multi-module now has consequences.

(2) The code works fine even if I have commented apply plugin: 'java' in subprojects{}, what do you suggest? I don’t know if it has a negative consequence if is declared twice, but apply plugin: 'java' is mandatory in the root level of the build.gradle file. First time I have this situation.

BTW, I have assumed that subprojects{} in some way inherits the apply plugin: 'java'

Thanks by your valuable support.

I have a situation, with:

task alljavadoc(type: Javadoc) {
       println 'alljavadoc working'
      source exportedProjects.collect { project(it).sourceSets.main.allJava }    											  
      classpath = files(exportedProjects.collect { project(it).sourceSets.main.compileClasspath })
      options.memberLevel = JavadocMemberLevel.PRIVATE
      destinationDir = file("${buildDir}/docs/javadoc")
}

I can generate the javadoc for all the modules in one location, but I did realize the javadoc generated has the following format:

public class Persona
extends java.lang.Object
implements java.io.Serializable

or

public class JsonDateSerializer
extends com.fasterxml.jackson.databind.JsonSerializer<java.util.Date>

the extension and implements should be really links, I mean the normal is have something like the following (see each link for a better understanding):

Class TimeZone

public abstract class TimeZone
extends Object
implements Serializable, Cloneable

or

Class JtaTransactionManager

public class JtaTransactionManager
extends AbstractPlatformTransactionManager
implements TransactionFactory, InitializingBean, Serializable

So what extra line must be added in alljavadoc? Thanks.

(1) Now I am wondered why this change of location was mandatory, I thought that for a task its location does not matter, it for a single module project. I never had some issue about this situation, but now seems that for a multi-module now has consequences.

Well, in the end Gradle evaluates Groovy scripts that build an object graph. Most of the DSL is designed to be late bound (i.e. you express dependencies by create rules, rather than modifying data).

Because the project structure is frozen the moment settings.gradle is evaluated, the subprojects section merely spins a loop and applies the code within the block to every subproject. For comparison, other sections (in particular all and withType sections) will apply the block to any applicable targets and any future targets that will match that predicate.

In this case though you used plain Groovy to collect the all sourceSets present at the moment you configure the task.

exportedProjects.collect { project(it).sourceSets.main.allJava }

Thet’s why the configuration of alljavadocs has to be in program order after than the application of the java plugin.
Please note that imperative code like this is somewhat poor style, but is acceptable if you know what you are doing and just need to get the job done. I would assume that the plugin suggested by @bmuschko would set proper rules, and then you would be able to reorder without consequences.

(2) The code works fine even if I have commented apply plugin: ‘java’ in subprojects{}, what do you suggest? I don’t know if it has a negative consequence if is declared twice, but apply plugin: ‘java’ is mandatory in the root level of the build.gradle file. First time I have this situation.

In general, a well written plugin is idempotent - that is it can be applied multiple times without changing the build. Also, Gradle encourages plugin composition - i.e. if you use the war plugin, I would expect it to automatically apply java. You need to apply the Java plugin only if you have Java code in that project.

Hello Dimitrov

Really thanks by the valuable explanation, I appreciate your support.

just curious if you are able to help me in your convenience about how the javadoc is generated, post before of your reply

Best Regards

The Javadoc task has a pretty much one to one mapping between the options property and the documented options for the javadoc tool. These options are using an instance of [StandardJavadocDocletOptions] (https://docs.gradle.org/current/javadoc/org/gradle/external/javadoc/StandardJavadocDocletOptions.html).

In your Javadoc task, options.memberLevel is an example of configuring one of these options. Another option available is link, which allows you to specify the URLs of external Javadocs to use.

You can specify something like

options.links 'http://docs.oracle.com/javase/8/docs/api/'

in your Javadoc task for each external URL that you want to include the external links.

Hello James

Thanks so much by your valuable reply, now works

Finally I have the following:

//Be sure to be located ALWAYS below of subprojects{}
task alljavadoc(type: Javadoc, group: "Documentation") {
      description = 'Generates a global javadoc from all the modules'
      source exportedProjects.collect { project(it).sourceSets.main.allJava }    											  
      classpath = files(exportedProjects.collect { project(it).sourceSets.main.compileClasspath })
      options.memberLevel = JavadocMemberLevel.PRIVATE
      options.links = ['http://docs.oracle.com/javase/8/docs/api/','http://docs.spring.io/spring/docs/current/javadoc-api/']
      destinationDir = file("${buildDir}/docs/javadoc")
}

BTW [] must be used even for one element

Not necessarily, it just depends on whether you prefer the syntax that calls the links method vs. the setLinks method.

If you want to set options.links using the = operator, which calls the setter, you must use ‘’ because the setter requires a List.

If you call the options.links method (without using the = operator), you’ll call a method that takes a varargs String. This method can be called many times with one or more arguments if desired.

Hi James

Yes, you are correct, my mistake, sorry, just thinking as usual Java developer

The following works as you said:

//Be sure to be located ALWAYS below of subprojects{}
task alljavadoc(type: Javadoc, group: "Documentation") {
      description = 'Generates a global javadoc from all the modules'
      source exportedProjects.collect { project(it).sourceSets.main.allJava }    											  
      classpath = files(exportedProjects.collect { project(it).sourceSets.main.compileClasspath })
      options.memberLevel = JavadocMemberLevel.PRIVATE
      options.links 'http://docs.oracle.com/javase/8/docs/api/'
      options.links 'http://docs.spring.io/spring/docs/current/javadoc-api/'
      options.links 'http://fasterxml.github.io/jackson-databind/javadoc/2.8/'    				
      destinationDir = file("${buildDir}/docs/javadoc")
}

Thanks again by the clarification

Better change the comment to “Be sure that all projects have the ‘java’ plugin applied by the time this is configured”

One way to achieve this would be to put it at the end of the main build script, another - to put it in afterEvaluate block.

See https://docs.gradle.org/current/userguide/build_lifecycle.html#N11BAE

Hello

Thanks by the suggestion and link, I will do the respective research and improvement…

Thanks by all!