-
Notifications
You must be signed in to change notification settings - Fork 4.6k
/
IdeaModule.java
671 lines (605 loc) · 22.9 KB
/
IdeaModule.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.gradle.plugins.ide.idea.model;
import com.google.common.base.Predicate;
import com.google.common.collect.Sets;
import groovy.lang.Closure;
import groovy.lang.DelegatesTo;
import org.gradle.api.Action;
import org.gradle.api.JavaVersion;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.internal.project.ProjectInternal;
import org.gradle.plugins.ide.idea.model.internal.IdeaDependenciesProvider;
import org.gradle.plugins.ide.internal.IdeArtifactRegistry;
import org.gradle.plugins.ide.internal.resolver.DefaultGradleApiSourcesResolver;
import javax.inject.Inject;
import java.io.File;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import static org.gradle.util.internal.ConfigureUtil.configure;
/**
* Enables fine-tuning module details (*.iml file) of the IDEA plugin.
* <p>
* Example of use with a blend of most possible properties.
* Typically you don't have to configure this model directly because Gradle configures it for you.
*
* <pre class='autoTested'>
* plugins {
* id 'java'
* id 'idea'
* }
*
* //for the sake of this example, let's introduce a 'performanceTestCompile' configuration
* configurations {
* performanceTestCompile
* performanceTestCompile.extendsFrom(testCompile)
* }
*
* dependencies {
* //performanceTestCompile "some.interesting:dependency:1.0"
* }
*
* idea {
*
* //if you want parts of paths in resulting files (*.iml, etc.) to be replaced by variables (Files)
* pathVariables GRADLE_HOME: file('~/cool-software/gradle')
*
* module {
* //if for some reason you want to add an extra sourceDirs
* sourceDirs += file('some-extra-source-folder')
*
* //and some extra test source dirs
* testSources.from(file('some-extra-test-dir'))
*
* //and some extra resource dirs
* resourceDirs += file('some-extra-resource-dir')
*
* //and some extra test resource dirs
* testResources.from(file('some-extra-test-resource-dir'))
*
* //and hint to mark some of existing source dirs as generated sources
* generatedSourceDirs += file('some-extra-source-folder')
*
* //and some extra dirs that should be excluded by IDEA
* excludeDirs += file('some-extra-exclude-dir')
*
* //if you don't like the name Gradle has chosen
* name = 'some-better-name'
*
* //if you prefer different output folders
* inheritOutputDirs = false
* outputDir = file('muchBetterOutputDir')
* testOutputDir = file('muchBetterTestOutputDir')
*
* //if you prefer different SDK than the one inherited from IDEA project
* jdkName = '1.6'
*
* //put our custom test dependencies onto IDEA's TEST scope
* scopes.TEST.plus += [ configurations.performanceTestCompile ]
*
* //if 'content root' (as IDEA calls it) of the module is different
* contentRoot = file('my-module-content-root')
*
* //if you love browsing Javadoc
* downloadJavadoc = true
*
* //and hate reading sources :)
* downloadSources = false
* }
* }
* </pre>
*
* For tackling edge cases, users can perform advanced configuration on the resulting XML file.
* It is also possible to affect the way the IDEA plugin merges the existing configuration
* via beforeMerged and whenMerged closures.
* <p>
* beforeMerged and whenMerged closures receive a {@link Module} parameter
* <p>
* Examples of advanced configuration:
*
* <pre class='autoTested'>
* plugins {
* id 'java'
* id 'idea'
* }
*
* idea {
* module {
* iml {
* //if you like to keep *.iml in a secret folder
* generateTo = file('secret-modules-folder')
*
* //if you want to mess with the resulting XML in whatever way you fancy
* withXml {
* def node = it.asNode()
* node.appendNode('iLoveGradle', 'true')
* node.appendNode('butAlso', 'I find increasing pleasure tinkering with output *.iml contents. Yeah!!!')
* }
*
* //closure executed after *.iml content is loaded from existing file
* //but before gradle build information is merged
* beforeMerged { module ->
* //if you want skip merging exclude dirs
* module.excludeFolders.clear()
* }
*
* //closure executed after *.iml content is loaded from existing file
* //and after gradle build information is merged
* whenMerged { module ->
* //you can tinker with {@link Module}
* }
* }
* }
* }
*
* </pre>
*/
public abstract class IdeaModule {
private String name;
private Set<File> sourceDirs;
private Set<File> generatedSourceDirs = new LinkedHashSet<>();
private Set<File> resourceDirs = new LinkedHashSet<>();
/**
* <strong>This field is {@code @Deprecated}, please use {@link #testResources} instead.</strong>
*/
@Deprecated
private Set<File> testResourceDirs = new LinkedHashSet<>();
private ConfigurableFileCollection testResources;
private Map<String, Map<String, Collection<Configuration>>> scopes = new LinkedHashMap<>();
private boolean downloadSources = true;
private boolean downloadJavadoc;
private File contentRoot;
/**
* <strong>This field is {@code @Deprecated}, please use {@link #testSources} instead.</strong>
*/
@Deprecated
private Set<File> testSourceDirs;
private ConfigurableFileCollection testSources;
private Set<File> excludeDirs;
private Boolean inheritOutputDirs;
private File outputDir;
private File testOutputDir;
private Map<String, File> pathVariables = new LinkedHashMap<>();
private String jdkName;
protected IdeaLanguageLevel languageLevel;
protected JavaVersion targetBytecodeVersion;
private final IdeaModuleIml iml;
private final Project project;
private PathFactory pathFactory;
private boolean offline;
private Map<String, Iterable<File>> singleEntryLibraries;
@Inject
public IdeaModule(Project project, IdeaModuleIml iml) {
this.project = project;
this.iml = iml;
this.testSources = project.getObjects().fileCollection();
this.testResources = project.getObjects().fileCollection();
testSources.from(project.provider(this::getTestSourceDirs));
testResources.from(project.provider(this::getTestResourceDirs));
}
/**
* Configures module name, that is the name of the *.iml file.
* <p>
* It's <b>optional</b> because the task should configure it correctly for you.
* By default it will try to use the <b>project.name</b> or prefix it with a part of a <b>project.path</b> to make
* sure the module name is unique in the scope of a multi-module build.
* The 'uniqueness' of a module name is required for correct import into IDEA and the task will make sure the name
* is unique.
* <p>
* <b>since</b> 1.0-milestone-2
* <p>
* If your project has problems with unique names it is recommended to always run <code>gradle idea</code> from the
* root, i.e. for all subprojects.
* If you run the generation of the IDEA module only for a single subproject then you may have different results
* because the unique names are calculated based on IDEA modules that are involved in the specific build run.
* <p>
* If you update the module names then make sure you run <code>gradle idea</code> from the root, e.g. for all
* subprojects, including generation of IDEA project.
* The reason is that there may be subprojects that depend on the subproject with amended module name.
* So you want them to be generated as well because the module dependencies need to refer to the amended project
* name.
* Basically, for non-trivial projects it is recommended to always run <code>gradle idea</code> from the root.
* <p>
* For example see docs for {@link IdeaModule}
*/
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
/**
* The directories containing the production sources.
*
* For example see docs for {@link IdeaModule}
*/
public Set<File> getSourceDirs() {
return sourceDirs;
}
public void setSourceDirs(Set<File> sourceDirs) {
this.sourceDirs = sourceDirs;
}
/**
* The directories containing the generated sources (both production and test sources).
* <p>
* For example see docs for {@link IdeaModule}
*/
public Set<File> getGeneratedSourceDirs() {
return generatedSourceDirs;
}
public void setGeneratedSourceDirs(Set<File> generatedSourceDirs) {
this.generatedSourceDirs = generatedSourceDirs;
}
/**
* The keys of this map are the IDEA scopes. Each key points to another map that has two keys, plus and minus.
* The values of those keys are collections of {@link org.gradle.api.artifacts.Configuration} objects. The files of the
* plus configurations are added minus the files from the minus configurations. See example below...
* <p>
* Example how to use scopes property to enable 'performanceTestCompile' dependencies in the output *.iml file:
* <pre class='autoTested'>
* plugins {
* id 'java'
* id 'idea'
* }
*
* configurations {
* performanceTestCompile
* performanceTestCompile.extendsFrom(testCompile)
* }
*
* dependencies {
* //performanceTestCompile "some.interesting:dependency:1.0"
* }
*
* idea {
* module {
* scopes.TEST.plus += [ configurations.performanceTestCompile ]
* }
* }
* </pre>
*/
public Map<String, Map<String, Collection<Configuration>>> getScopes() {
return scopes;
}
public void setScopes(Map<String, Map<String, Collection<Configuration>>> scopes) {
this.scopes = scopes;
}
/**
* Whether to download and add sources associated with the dependency jars. Defaults to true. <p> For example see docs for {@link IdeaModule}
*/
public boolean isDownloadSources() {
return downloadSources;
}
public void setDownloadSources(boolean downloadSources) {
this.downloadSources = downloadSources;
}
/**
* Whether to download and add javadoc associated with the dependency jars. Defaults to false. <p> For example see docs for {@link IdeaModule}
*/
public boolean isDownloadJavadoc() {
return downloadJavadoc;
}
public void setDownloadJavadoc(boolean downloadJavadoc) {
this.downloadJavadoc = downloadJavadoc;
}
/**
* The content root directory of the module. <p> For example see docs for {@link IdeaModule}
*/
public File getContentRoot() {
return contentRoot;
}
public void setContentRoot(File contentRoot) {
this.contentRoot = contentRoot;
}
/**
* The directories containing the test sources.
*
* <strong>Note that late changes to default test directories may NOT be reflected in this collection and {@link #getTestSources()} should be preferred.</strong>
*
* For example see docs for {@link IdeaModule}
*
* <strong>This field is {@code @Deprecated}, please use {@link #getTestSources()} instead.</strong>
*/
@Deprecated
public Set<File> getTestSourceDirs() {
return testSourceDirs;
}
/**
* <strong>This field is {@code @Deprecated}, please use {@link #getTestSources()} instead to access the new collection property.</strong>
*/
@Deprecated
public void setTestSourceDirs(Set<File> testSourceDirs) {
this.testSourceDirs = testSourceDirs;
}
/**
* The complete and up-to-date collection of test source directories
*
* This should be preferred to {@link #getTestSourceDirs()} as it will include late changes to default directories.
*
* @return lazily configurable collection of test source directories
* @since 7.4
*/
public ConfigurableFileCollection getTestSources() {
return testSources;
}
/**
* The directories containing resources. <p> For example see docs for {@link IdeaModule}
*
* @since 4.7
*/
public Set<File> getResourceDirs() {
return resourceDirs;
}
/**
* Sets the directories containing resources. <p> For example see docs for {@link IdeaModule}
*
* @since 4.7
*/
public void setResourceDirs(Set<File> resourceDirs) {
this.resourceDirs = resourceDirs;
}
/**
* The directories containing the test resources. <p> For example see docs for {@link IdeaModule}
*
* <strong>This field is {@code @Deprecated}, please use {@link #getTestResources()} instead.</strong>
*
* @since 4.7
*/
@Deprecated
public Set<File> getTestResourceDirs() {
return testResourceDirs;
}
/**
* Sets the directories containing the test resources. <p> For example see docs for {@link IdeaModule}
*
* <strong>This field is {@code @Deprecated}, please use {@link #getTestResources()} instead to access the new collection property.</strong>
*
* @since 4.7
*/
@Deprecated
public void setTestResourceDirs(Set<File> testResourceDirs) {
this.testResourceDirs = testResourceDirs;
}
/**
* The complete and up-to-date collection of test resource directories.
*
* This should be preferred to {@link #getTestResourceDirs()} as it will include late changes to default directories.
*
* @return lazily configurable collection of test resource directories
* @since 7.4
*/
public ConfigurableFileCollection getTestResources() {
return testResources;
}
/**
* Directories to be excluded. <p> For example see docs for {@link IdeaModule}
*/
public Set<File> getExcludeDirs() {
return excludeDirs;
}
public void setExcludeDirs(Set<File> excludeDirs) {
this.excludeDirs = excludeDirs;
}
/**
* If true, output directories for this module will be located below the output directory for the project;
* otherwise, they will be set to the directories specified by {@link #getOutputDir()} and {@link #getTestOutputDir()}.
* <p>
* For example see docs for {@link IdeaModule}
*/
public Boolean getInheritOutputDirs() {
return inheritOutputDirs;
}
public void setInheritOutputDirs(Boolean inheritOutputDirs) {
this.inheritOutputDirs = inheritOutputDirs;
}
/**
* The output directory for production classes.
* If {@code null}, no entry will be created.
* <p>
* For example see docs for {@link IdeaModule}
*/
public File getOutputDir() {
return outputDir;
}
public void setOutputDir(File outputDir) {
this.outputDir = outputDir;
}
/**
* The output directory for test classes.
* If {@code null}, no entry will be created.
* <p>
* For example see docs for {@link IdeaModule}
*/
public File getTestOutputDir() {
return testOutputDir;
}
public void setTestOutputDir(File testOutputDir) {
this.testOutputDir = testOutputDir;
}
/**
* The variables to be used for replacing absolute paths in the iml entries.
* For example, you might add a {@code GRADLE_USER_HOME} variable to point to the Gradle user home dir.
* <p>
* For example see docs for {@link IdeaModule}
*/
public Map<String, File> getPathVariables() {
return pathVariables;
}
public void setPathVariables(Map<String, File> pathVariables) {
this.pathVariables = pathVariables;
}
/**
* The JDK to use for this module.
* If {@code null}, the value of the existing or default ipr XML (inherited) is used.
* If it is set to <code>inherited</code>, the project SDK is used.
* Otherwise the SDK for the corresponding value of java version is used for this module.
* <p>
* For example see docs for {@link IdeaModule}
*/
public String getJdkName() {
return jdkName;
}
public void setJdkName(String jdkName) {
this.jdkName = jdkName;
}
/**
* The module specific language Level to use for this module.
* When {@code null}, the module will inherit the language level from the idea project.
* <p>
* The Idea module language level is based on the {@code sourceCompatibility} settings for the associated Gradle project.
*/
public IdeaLanguageLevel getLanguageLevel() {
return languageLevel;
}
public void setLanguageLevel(IdeaLanguageLevel languageLevel) {
this.languageLevel = languageLevel;
}
/**
* The module specific bytecode version to use for this module.
* When {@code null}, the module will inherit the bytecode version from the idea project.
* <p>
* The Idea module bytecode version is based on the {@code targetCompatibility} settings for the associated Gradle project.
*/
public JavaVersion getTargetBytecodeVersion() {
return targetBytecodeVersion;
}
public void setTargetBytecodeVersion(JavaVersion targetBytecodeVersion) {
this.targetBytecodeVersion = targetBytecodeVersion;
}
/**
* See {@link #iml(Action)}
*/
public IdeaModuleIml getIml() {
return iml;
}
/**
* An owner of this IDEA module.
* <p>
* If IdeaModule requires some information from gradle this field should not be used for this purpose.
* IdeaModule instances should be configured with all necessary information by the plugin or user.
*/
public Project getProject() {
return project;
}
public PathFactory getPathFactory() {
return pathFactory;
}
public void setPathFactory(PathFactory pathFactory) {
this.pathFactory = pathFactory;
}
/**
* If true then external artifacts (e.g. those found in repositories) will not be included in the resulting classpath (only project and local file dependencies will be included).
*/
public boolean isOffline() {
return offline;
}
public void setOffline(boolean offline) {
this.offline = offline;
}
public Map<String, Iterable<File>> getSingleEntryLibraries() {
return singleEntryLibraries;
}
public void setSingleEntryLibraries(Map<String, Iterable<File>> singleEntryLibraries) {
this.singleEntryLibraries = singleEntryLibraries;
}
/**
* Enables advanced configuration like tinkering with the output XML or affecting the way existing *.iml content is merged with gradle build information.
* <p>
* For example see docs for {@link IdeaModule}.
*/
public void iml(@SuppressWarnings("rawtypes") @DelegatesTo(IdeaModuleIml.class) Closure closure) {
configure(closure, getIml());
}
/**
* Enables advanced configuration like tinkering with the output XML or affecting the way existing *.iml content is merged with gradle build information.
* <p>
* For example see docs for {@link IdeaModule}.
*
* @since 3.5
*/
public void iml(Action<? super IdeaModuleIml> action) {
action.execute(iml);
}
/**
* Configures output *.iml file.
* It's <b>optional</b> because the task should configure it correctly for you (including making sure it is unique in the multi-module build).
* If you really need to change the output file name (or the module name) it is much easier to do it via the <b>moduleName</b> property!
* <p>
* Please refer to documentation on <b>moduleName</b> property.
* In IntelliJ IDEA the module name is the same as the name of the *.iml file.
*/
public File getOutputFile() {
return new File(iml.getGenerateTo(), getName() + ".iml");
}
public void setOutputFile(File newOutputFile) {
setName(newOutputFile.getName().replaceFirst("\\.iml$", ""));
getIml().setGenerateTo(newOutputFile.getParentFile());
}
/**
* Resolves and returns the module's dependencies.
*
* @return dependencies
*/
public Set<Dependency> resolveDependencies() {
ProjectInternal projectInternal = (ProjectInternal) project;
IdeArtifactRegistry ideArtifactRegistry = projectInternal.getServices().get(IdeArtifactRegistry.class);
IdeaDependenciesProvider ideaDependenciesProvider = new IdeaDependenciesProvider(projectInternal, ideArtifactRegistry, new DefaultGradleApiSourcesResolver(projectInternal.newDetachedResolver()));
return ideaDependenciesProvider.provide(this);
}
@SuppressWarnings("unchecked")
public void mergeXmlModule(Module xmlModule) {
iml.getBeforeMerged().execute(xmlModule);
Path contentRoot = getPathFactory().path(getContentRoot());
Set<Path> sourceFolders = pathsOf(existing(getSourceDirs()));
Set<Path> generatedSourceFolders = pathsOf(existing(getGeneratedSourceDirs()));
Set<Path> testSourceFolders = pathsOf(existing(getTestSources().getFiles()));
Set<Path> resourceFolders = pathsOf(existing(getResourceDirs()));
Set<Path> testResourceFolders = pathsOf(existing(getTestResources().getFiles()));
Set<Path> excludeFolders = pathsOf(getExcludeDirs());
Path outputDir = getOutputDir() != null ? getPathFactory().path(getOutputDir()) : null;
Path testOutputDir = getTestOutputDir() != null ? getPathFactory().path(getTestOutputDir()) : null;
Set<Dependency> dependencies = resolveDependencies();
String level = getLanguageLevel() != null ? getLanguageLevel().getLevel() : null;
xmlModule.configure(
contentRoot,
sourceFolders, testSourceFolders,
resourceFolders, testResourceFolders,
generatedSourceFolders,
excludeFolders,
getInheritOutputDirs(), outputDir, testOutputDir,
dependencies,
getJdkName(), level
);
iml.getWhenMerged().execute(xmlModule);
}
private Set<File> existing(Set<File> files) {
return Sets.filter(files, new Predicate<File>() {
@Override
public boolean apply(File file) {
return file.exists();
}
});
}
private Set<Path> pathsOf(Set<File> files) {
return files.stream().map(file -> getPathFactory().path(file)).collect(Collectors.toCollection(LinkedHashSet::new));
}
}