When we develop applications, we usually use an Integrated Development Environment (IDE). An IDE provides support for writing code for our applications. We can write our code in Java, Groovy, or Scala. We have seen how we can use Gradle to define, for example, library dependencies to compile the code. We want to use the same information that we have defined in a Gradle build file in a project in our favorite IDE.
In this chapter, we will learn how we can use Gradle plugins to generate the project files with classpath dependencies for Eclipse and JetBrains IntelliJ IDEA. We will also learn how we can customize the file generation to add extra configuration data.
Next, we will see the Eclipse and IntelliJ IDEA support for running Gradle tasks from within the IDE.
The Eclipse plugin can generate the project files necessary to import the project in Eclipse. In this section, we will see which tasks are added by the plugin and how we can customize the generated output.
If we have a Java project and want to import the project into Eclipse, we must use the Eclipse plugin to generate the Eclipse project files. Each Eclipse project has as minimum a .project
file and a .classpath
file. The .project
file contains metadata about the project, such as the project name. The .classpath
file contains classpath entries for the project. Eclipse needs this to be able to compile the source files in the project. The Eclipse plugin will try to download the artifact with source files belonging to a dependency as well. So, if we import the project into Eclipse and the source files are available, we can directly see the source of dependent class files.
For a Java project, an additional Java Development Tools
(JDT) configuration file is created in the .settings
folder. The name of the file is org.eclipse.jdt.core.prefs
.
Let's create a simple Gradle build file for a Java project. The code for the build file is shown in the following code snippet:
apply plugin: 'java' apply plugin: 'eclipse' version = 1.0 sourceCompatibility = 1.6 targetCompatibility = 1.6 description = 'Sample project' ext { slf4jVersion = '1.6.6' slf4jGroup = 'org.slf4j' } configurations { extraLib } repositories { mavenCentral() } dependencies { testCompile 'junit:junit:4.8' extraLib "$slf4jGroup:slf4j-api:$slf4jVersion", "$slf4jGroup:slf4j-simple:$slf4jVersion" }
We apply the Java and Eclipse plugins for our project. We set some project properties, such as version, description, source, and target compatibility. We define a dependency on JUnit for the testCompile
configuration. Also, we add an extra custom configuration with a dependency on the slf4j
logging library.
First, let's see which tasks are added to our project by the Eclipse plugin. We invoke the tasks
task and look at all the tasks in our plugin, shown in the following code snippet:
$ gradle tasks --all ... IDE tasks --------- cleanEclipse - Cleans all Eclipse files. cleanEclipseClasspath cleanEclipseJdt cleanEclipseProject eclipse - Generates all Eclipse files. eclipseClasspath - Generates the Eclipse classpath file. eclipseJdt - Generates the Eclipse JDT settings file. eclipseProject - Generates the Eclipse project file.... ...
The eclipse
task is dependent on the following three tasks: eclipseClasspath
, eclipseJdt,
and eclipseProject
. Each task generates a single file. The eclipseClasspath
task generates the .classpath
file, eclipseProject
generates the .project
file, and eclipseJdt
generates org.eclipse.jdt.core.prefs
.
When we execute the eclipse
task from the command line, we get the following output:
$ gradle eclipse :eclipseClasspath Download http://repo1.maven.org/maven2/junit/junit/4.8/junit-4.8.pom Download http://repo1.maven.org/maven2/junit/junit/4.8/junit-4.8-sources.jar Download http://repo1.maven.org/maven2/junit/junit/4.8/junit-4.8.jar :eclipseJdt :eclipseProject :eclipse BUILD SUCCESSFUL Total time: 8.672 secs
Note that the sources of the JUnit library are downloaded. We now have the .classpath
and .project
files in our project folder. In the .settings
folder we have the org.eclipse.jdt.core.prefs
file.
The .project
file has the following contents:
<?xml version="1.0" encoding="UTF-8"?> <projectDescription> <name>chapter12</name> <comment>Sample project</comment> <projects/> <natures> <nature>org.eclipse.jdt.core.javanature</nature> </natures> <buildSpec> <buildCommand> <name>org.eclipse.jdt.core.javabuilder</name> <arguments/> </buildCommand> </buildSpec> <linkedResources/> </projectDescription>
The name
element is filled with the project's folder name. We will learn how to change this later in the chapter. The comment
element contains our project description. We have applied the Java plugin in our project, and hence the Java nature and build command are added to the project configuration.
If we look at the .classpath
file, we can see a classpathentry
element with the JUnit dependency, as shown in the following code snippet:
<?xml version="1.0" encoding="UTF-8"?> <classpath> <classpathentry kind="output" path="bin"/> <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER" exported="true"/> <classpathentry sourcepath="/Users/mrhaki/.gradle/caches/artifacts-14/filestore/junit/junit/4.8/source/abe171e0fc1242d1fe10e8dc43bce031e3f65560/junit-4.8-sources.jar" kind="lib" path="/Users/mrhaki/.gradle/caches/artifacts-14/filestore/junit/junit/4.8/jar/4150c00c5706306ef0f8f1410e70c8ff12757922/junit-4.8.jar" exported="true"/> </classpath>
The classpathentry
element has a reference to the location in the Gradle cache of the downloaded JUnit library. Note that the sourcepath
attribute references the source files.
The last generated org.eclipse.jdt.core.prefs
file has the following contents:
# #Thu Aug 21 09:36:25 CEST 2012 org.eclipse.jdt.core.compiler.debug.localVariable=generate org.eclipse.jdt.core.compiler.compliance=1.6 org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve org.eclipse.jdt.core.compiler.debug.sourceFile=generate org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 org.eclipse.jdt.core.compiler.problem.enumIdentifier=error org.eclipse.jdt.core.compiler.debug.lineNumber=generate eclipse.preferences.version=1 org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled org.eclipse.jdt.core.compiler.source=1.6 org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
We can see that the source and target compatibility we defined in the Gradle build file are used for the properties org.eclipse.jdt.core.compiler.source
, org.eclipse.jdt.core.compiler.codegen.targetPlatform
, and org.eclipse.jdt.core.compiler.compliance
.
We have added the Java plugin to our project and the Eclipse plugin knows this, so the Java nature and builder are added to the generated .project
file. If we use the Groovy and Scala plugins, the Eclipse plugin will add the correct nature and build configurations to the .project
file.
We have several options to customize the configuration in the generated files. The Eclipse plugin adds a DSL to configure model objects that represent Eclipse configuration objects. If we use the DSL to configure the objects, these newly configured objects are merged with existing configuration before the file is generated. We can also hook into the generation process and work directly on the model objects before and after the configuration is merged and the file is generated. Finally, we can even use a hook to work directly on the XML structure before the configuration file is generated.
The following steps describe the complete configuration file generation lifecycle:
beforeMerge
hook is invoked. The hook accepts the model object for the configuration file as an argument.whenMerged
hook is executed. The hook accepts the model object for the configuration file as an argument.withXml
hook is invoked. XML manipulation can happen here before the file is written to disk.When the Eclipse plugin generates the files, it will look in the Gradle build file for the necessary information. For example, if we set the description
property of the Project
object, the comment section in the .project
file is filled with the value of that property
:
The Eclipse plugin also adds a configuration script block with the name eclipse
. The configuration can be described using a simple DSL. At the top level we can add path variables that will be used for replacing absolute paths in classpath entries. The org.gradle.plugins.ide.eclipse.model.EclipseModel
object is used and the pathVariables()
method of this class must be used to define a path variable.
Next, we can define the configuration information for the .project
file in the project
section. The model object org.gradle.plugins.ide.eclipse.model.EclipseProject
is used to model the Eclipse project configuration. We can, for example, use the name
property to change the name of the project in the generated .project
file. It is good to know that Gradle can generate unique project names for a multi-project build. A unique name is necessary to import the projects into Eclipse. During the .project
file generation, all projects that are part of the multi-project must be known. So, it is best to run the eclipse
or eclipseProject
task from the root of the project. Also, methods for adding project natures and new build commands are available.
To customize the .classpath
file generation, we can use the classpath section of the eclipse configuration closure. Here, the org.gradle.plugins.ide.eclipse.model.EclipseClasspath
object is used to model the classpath entries of the Eclipse project. We can use the properties plusConfigurations
and minusConfigurations
to add or remove dependency configurations from the generated .classpath
file. By default, the associated source files for a dependency are downloaded, but we can also set the downloadJavadoc
property to true
to download the Javadoc associated with the dependency.
The jdt
section of the eclipse configuration closure can be used to change the source and target compatibility versions. By default, the Gradle Java plugin settings are used, but we can overrride it here. The org.gradle.plugins.ide.eclipse.model.EclipseJdt
object is used to model the Eclipse configuration.
In the following build file, we see an example of all the possible methods and properties we can use with the DSL to customize the generated .project
file:
apply plugin: 'java' apply plugin: 'eclipse' eclipse { pathVariables 'APPSERVER_HOME': file('/apps/appserver/1.0') project { name = 'sample-eclipse' comment = 'Eclipse project file build by Gradle' // Add new natures like Spring nature. natures 'org.springframework.ide.eclipse.core.springnature' // Add build command for Spring. buildCommand 'org.springframework.ide.eclipse.core.springbuilder' // If using location attribute then type 1 is file, 2 is folder linkedResource name: 'config', type: '2', location: file('/opt/local/config') // If using locationUri attribute then type 1 for file/folder, 2 is virtual folder linkedResource name: 'config2', type: '1', locationUri: 'file:../config' // Define reference to other project. This is not // a build path reference. referencedProjects 'other-project' } }
In the following example build file, we see the options to change the.classpath
file:
apply plugin: 'java' apply plugin: 'eclipse' eclipse { classpath { // Add extra dependency configurations. plusConfigurations += configurations.extraLib // Remove dependency configurations. minusConfigurations += configurations.testCompile // Included configurations are not exported. noExportConfigurations += configurations.testCompile // Download associated source files. downloadSources = true // Download Javadoc for dependencies. downloadJavadoc = true // Add extra containers. containers 'ApacheCommons' // Change default output dir (${projectDir}/bin) defaultOutputDir file("$buildDir/eclipse-classes") } }
The following example build file shows the configuration options to generate the org.eclipse.jdt.core.prefs
file:
apply plugin: 'java' apply plugin: 'eclipse' eclipse { jdt { sourceCompatibility = 1.6 targetCompatibility = 1.6 } }
Using the DSL to customize file generation is very elegant. Remember from the configuration file generation steps that this information is used right after the beforeMerged
and before the whenMerged
hooks. These hooks take a model object as an argument that we can use to customize. We can use the merge hooks if we want to do something that is not possible using the project configuration or DSL.
The merge hooks can be defined in the eclipse configuration closure. For each file, we can define a configuration closure for the beforeMerged
and whenMerged
hooks. These methods are part of the org.gradle.plugins.ide.api.XmlFileContentMerger
class. Gradle will delegate the configuration closures to the methods of this class. The beforeMerged
hook is useful to overwrite or change existing sections in the configuraton file. The cleanEclipse
task cleans all the sections in a configuration file, and by using the
beforeMerged
hook we can ourselves define which parts need to be cleaned or overwritten.
The
whenMerged
hook is the preferred way of changing the model object. When this hook is invoked, the model object is already configured with all settings from the project configuration and DSL.
Each file is represented by the file
property of the eclipse configuration closures. For example, to add a merge hook to the .project
file generation, we define it 353using the eclipse.project.file
property.
The following table shows the class that is passed as an argument for the merge hooks closures:
Model |
Merge hook argument |
Description |
---|---|---|
Project |
|
Model object with properties for |
Classpath |
|
Model object with properties for |
Jdt |
|
Model object with properties for |
For the Jdt model, we have an additional method named withProperties()
, to change the contents of the file. This method has a closure with an argument of type java.util.Properties
.
In the following example build file, we use the merged hooks to change the configuration in the .project
file:
apply { plugin 'java' plugin 'eclipse' } eclipse { project { file { beforeMerged { project -> // We can access the internal object structure // using merge hooks. project.natures.clear() } afterMerged { project -> project.name = 'sample-eclipse' project.comment = 'Eclipse project file build by Gradle' project.natures.add 'org.springframework.ide.eclipse.core.springnature' buildCommand.add 'org.springframework.ide.eclipse.core.springbuilder' linkedResources.add name: 'config', type: '2', location: 'file:/opt/local' referencedProjects.add 'other-project' } } } }
In the following example build, we use the merged hooks to change the .classpath
and org.eclipse.jdt.core.prefs
files:
apply { plugin 'java' plugin 'eclipse' } eclipse { classpath { file { beforeMerged { classpath -> // Remove lib classpath entries. classpath.entries.removeAll { it.kind == 'lib' } } whenMerged { classpath -> classpath.entries.add kind: 'output', path: "$buildDir/eclipse-classes" } } } jdt { file { beforeMerged { jdt -> } whenMerged { jdt -> jdt.sourceCompatibility = 1.6 jdt.targetCompatibility = 1.6 } whenProperties { properties -> properties.extraProperty = 'value' } } } }
We have seen how to customize the configuration file generation with project configuration, DSL, and the merge hooks. At the lowest level, there is a hook to change the XML structure before it is written to disk. Therefore, we must implement the withXml
hook. We define a closure, and the first argument of the closure is of type org.gradle.api.XmlProvider
. The class has the
asNode()
method, which returns the root of the XML as a Groovy node. This is the easiest object with which to alter the XML contents. The
asString()
method returns a StringBuilder
instance with the XML contents. Finally, the
asElement()
method returns an org.w3c.dom.Element
object.
The
asNode()
method returns the Groovy groovy.util.Node
class. With this node class, we can easily add, replace, or remove nodes and attributes.
In the following example build file, we can see different ways to manipulate the XML structure:
apply { plugin 'java' plugin 'eclipse' } eclipse { project { file { withXml { xml -> def projectXml = xml.asNode() projectXml.name = 'sample-eclipse' def natures = projectXml.natures natures.plus { nature { 'org.springframework.ide.eclipse.core.springnature' } } } } } classpath { file { withXml { xml -> def classpathXml = xml.asNode() classpathXml.classpathentry.findAll { it.@kind == 'con' }*.@exported = 'true' } } } }
We have seen all the different options to change the configuration files. Configuration changes, which we would normally make in Eclipse, can now be done programmatically in a Gradle build file.
If a file already exists, Gradle will try to merge extra information with the existing information. Depending on the section, the information will be amended to existing configuration data or will replace existing configuration data. This means that if we make changes to our project settings in Eclipse, they will not be overwritten even if we invoke one of the eclipse tasks.
To completely rebuild the project files, we must use the cleanEclipse
tasks. For each project file, there is a corresponding cleanEclipse
task. For example, to rebuild the .project
file, we invoke the cleanEclipseProject
task before eclipseProject
. Any changes we have made manually are removed, and a new .project
file is generated by Gradle, with the settings from our Gradle build file.
We can add Web Tools Platform (WTP) to Eclipse, to add support for Java enterprise applications. We get support for web applications (WAR) and enterprise applications (EAR). To generate the correct configuration files, we must add another plugin to our Gradle build file. We add the Eclipse WTP plugin to the project and also the War or Ear plugin.
Let's create a build file and add the War and Eclipse WTP plugins, as follows:
apply plugin: 'java' apply plugin: 'war' apply plugin: 'eclipse-wtp' version = 1.0 description = 'Sample project' repositories { mavenCentral() } dependencies { testCompile 'junit:junit:4.8' }
The Eclipse WTP plugin adds several new tasks to our Gradle build. In the following snippet, we invoke the tasks
task to see which tasks are added:
$ gradle tasks --all ... IDE tasks --------- cleanEclipse - Cleans all Eclipse files. [cleanEclipseWtp] cleanEclipseClasspath cleanEclipseJdt cleanEclipseProject cleanEclipseWtp - Cleans Eclipse wtp configuration files. cleanEclipseWtpComponent cleanEclipseWtpFacet eclipse - Generates all Eclipse files. [eclipseWtp] eclipseClasspath - Generates the Eclipse classpath file. eclipseJdt - Generates the Eclipse JDT settings file. eclipseProject - Generates the Eclipse project file. eclipseWtp - Generates Eclipse wtp configuration files. eclipseWtpComponent - Generates the Eclipse WTP component settings file. eclipseWtpFacet - Generates the Eclipse WTP facet settings file. ...
The Eclipse WTP plugin includes the Eclipse plugin as well. We get all the tasks that we have seen earlier, but new tasks are also added for WTP configuration files. The task eclipseWtp
depends on eclipseWtpComponent
and eclipseWtpFacet
, to generate the corresponding configuration files. Note that the eclipse
task itself also now depends on eclipseWtp
.
For each of these tasks, there is a corresponding clean task. These clean tasks will delete the configuration files.
If we execute the eclipse
task, we get the following configuration files: .project
, .classpath
, and org.eclipse.jdt.core.prefs
. We also get additional configuration files in the .settings
folder, with the names org.eclipse.wst.common.component
and org.eclipse.wst.common.project.facet.core.xml
.
$ gradle eclipse :eclipseClasspath :eclipseJdt :eclipseProject :eclipseWtpComponent :eclipseWtpFacet :eclipseWtp :eclipse BUILD SUCCESSFUL Total time: 4.264 secs
The contents of the .project
file show that the Eclipse WTP plugin added additional natures and build commands, which is shown as follows:
<?xml version="1.0" encoding="UTF-8"?> <projectDescription> <name>chapter12</name> <comment>Sample project</comment> <projects/> <natures> <nature>org.eclipse.jdt.core.javanature</nature> <nature>org.eclipse.wst.common.project.facet.core.nature</nature> <nature>org.eclipse.wst.common.modulecore.ModuleCoreNature</nature> <nature>org.eclipse.jem.workbench.JavaEMFNature</nature> </natures> <buildSpec> <buildCommand> <name>org.eclipse.jdt.core.javabuilder</name> <arguments/> </buildCommand> <buildCommand> <name>org.eclipse.wst.common.project.facet.core.builder</name> <arguments/> </buildCommand> <buildCommand> <name>org.eclipse.wst.validation.validationbuilder</name> <arguments/> </buildCommand> </buildSpec> <linkedResources/> </projectDescription>
In the .classpath
configuration file, an additional container named org.eclipse.jst.j2ee.internal.web.container
is added, as shown in the following code snippet:
<?xml version="1.0" encoding="UTF-8"?> <classpath> <classpathentry kind="output" path="bin"/> <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER" exported="true"/> <classpathentry kind="con" path="org.eclipse.jst.j2ee.internal.web.container" exported="true"/> <classpathentry sourcepath="/Users/mrhaki/.gradle/caches/artifacts-14/filestore/junit/junit/4.8/source/abe171e0fc1242d1fe10e8dc43bce031e3f65560/junit-4.8-sources.jar" kind="lib" path="/Users/mrhaki/.gradle/caches/artifacts-14/filestore/junit/junit/4.8/jar/4150c00c5706306ef0f8f1410e70c8ff12757922/junit-4.8.jar" exported="true"> <attributes> <attribute name="org.eclipse.jst.component.nondependency" value=""/> </attributes> </classpathentry> </classpath>
The contents of the org.eclipse.jdt.core.prefs
file in the .settings
folder are not different from the standard Eclipse plugin. The org.eclipse.wst.common.component
file has the following contents:
<?xml version="1.0" encoding="UTF-8"?> <project-modules id="moduleCoreId" project-version="2.0"> <wb-module deploy-name="chapter12"> <property name="context-root" value="chapter12"/> <wb-resource deploy-path="/" source-path="src/main/webapp"/> </wb-module> </project-modules>
Here, we find information for the web part of our project.
The last generated file in the .settings
folder is org.eclipse.wst.common.project.facet.core.xml
file, here we see the servlet and Java versions. The file has the following contents:
<?xml version="1.0" encoding="UTF-8"?> <faceted-project> <fixed facet="jst.java"/> <fixed facet="jst.web"/> <installed facet="jst.web" version="2.4"/> <installed facet="jst.java" version="6.0"/> </faceted-project>
The Eclipse WTP plugin uses the same configuration options as the standard Eclipse plugin. The plugin uses project information to set the value in the configuration files. We can use a DSL to configure the values we want in the generated files. We can also use the merge hooks and work with model objects to change information. The XML structure can be changed using the withXml
hook with a configuration closure.
To use the DSL, we can add an additional wtp
script block to the eclipse
script block. In the wtp
script block, we can change the component configuration in a component configuration closure and the facet settings in the facet configuration closure.
In the following example build file, we see some of the options we can set by using the DSL:
apply plugin: 'java' apply plugin: 'war' apply plugin: 'eclipse-wtp' eclipse { wtp { component { // Change context path of the Web application. // Default value is project.war.baseName. contextPath = '/sample-web' // Customize wb-resource elements of type WbResource. // Default for war plugin is // [deployPath: '/', sourcePath: project.webAppDirName] and // for ear plugin is [] resources += [deployPath: '/css', sourcePath: 'src/main/css'] // We can also use the resource() method resource deployPath: '/css', sourcePath: 'src/main/css' // Remove configurations from // the deployed configurations. minusConfigurations += project.configurations.testCompile // Add dependency configurations to // the deployLibPath location. libConfigurations += project.configurations.testCompile // Extra source directory. sourceDirs += file('src/main/css') } facet { // Add extra facet via property. facets += [name: 'extra', version: '1.0'] // Or via facet() method. facet name: 'gradle', version: '1.1' } } }
Another method to customize file generation is by using the merge hooks. The beforeMerged
and whenMerged
hooks accept a configuration closure to set properties on a model object. In the following table, we see the types of the model object that is passed as argument to the closure:
Model |
Merge hook argument |
Description |
---|---|---|
Component |
|
Model object with properties for |
Facet |
|
Model object with properties for |
In the following example build file, we use the merge hooks to customize the configuration:
apply plugin: 'java' apply plugin: 'war' apply plugin: 'eclipse-wtp' version = 1.0 description = 'Sample project' repositories { mavenCentral() } dependencies { testCompile 'junit:junit:4.8' } eclipse { wtp { component { file { beforeMerged { wtpComponent -> wtpComponent.wbEntries.clear() } whenMerged { wtpComponent -> wtpComponent.contextPath = '/sample-web' wtpComponent.deployName = 'sample' } } } facet { file { beforeMerged { wtpFacet -> } whenMerged { wtpFacet -> def java = wtpFacet.facets.find { it.facet == 'jst.java' } java.version = '5.0' } } } } }
Finally, we can manipulate the XML with the withXml
hook. The argument is of type XmlProvider
, just like the standard plugin. In the closure, we can change nodes and attributes.
The following example build file shows how we can manipulate the XML:
apply plugin: 'java' apply plugin: 'war' apply plugin: 'eclipse-wtp' version = 1.0 description = 'Sample project' repositories { mavenCentral() } dependencies { testCompile 'junit:junit:4.8' } eclipse { wtp { component { file { withXml { componentXml -> def root = componentXml.asNode() root.'wb-module'.@'deploy-name' = 'sample' } } } facet { file { withXml { facetXml -> def root = facetXml.asNode() root.installed.find { it.@facet == 'jst.web' }.@version = '2.5' } } } } }
3.15.151.32