| @@ -0,0 +1,60 | |||||
|
|
1 | package org.implab.gradle.common; | |||
|
|
2 | ||||
|
|
3 | import org.gradle.api.Action; | |||
|
|
4 | import org.gradle.api.Plugin; | |||
|
|
5 | import org.gradle.api.Project; | |||
|
|
6 | import org.gradle.api.logging.Logger; | |||
|
|
7 | import org.gradle.api.logging.Logging; | |||
|
|
8 | import org.gradle.api.tasks.Copy; | |||
|
|
9 | import org.gradle.api.tasks.Delete; | |||
|
|
10 | import org.gradle.api.tasks.TaskContainer; | |||
|
|
11 | import org.gradle.api.tasks.TaskProvider; | |||
|
|
12 | import org.gradle.language.base.plugins.LifecycleBasePlugin; | |||
|
|
13 | import org.implab.gradle.common.files.GenericSourceSet; | |||
|
|
14 | import org.implab.gradle.common.utils.Strings; | |||
|
|
15 | ||||
|
|
16 | /** | |||
|
|
17 | * This plugin creates a {@code sources} extension which is | |||
|
|
18 | * a container for {@link GenericSourceSet}. | |||
|
|
19 | * | |||
|
|
20 | */ | |||
|
|
21 | public abstract class SourcesPlugin implements Plugin<Project> { | |||
|
|
22 | private final Logger logger = Logging.getLogger(SourcesPlugin.class); | |||
|
|
23 | ||||
|
|
24 | private final String POLICY_NAME = "SourcesPolicy"; | |||
|
|
25 | ||||
|
|
26 | private final String EXTENSION_NAME = "sources"; | |||
|
|
27 | ||||
|
|
28 | @Override | |||
|
|
29 | public void apply(Project target) { | |||
|
|
30 | target.getPlugins().apply(LifecycleBasePlugin.class); | |||
|
|
31 | ||||
|
|
32 | var sources = target.getObjects().domainObjectContainer(GenericSourceSet.class); | |||
|
|
33 | var tasks = target.getTasks(); | |||
|
|
34 | var clean = tasks.named(LifecycleBasePlugin.CLEAN_TASK_NAME); | |||
|
|
35 | ||||
|
|
36 | target.getExtensions().add(EXTENSION_NAME, sources); | |||
|
|
37 | ||||
|
|
38 | sources.configureEach(configureSourceSet(tasks, clean)); | |||
|
|
39 | } | |||
|
|
40 | ||||
|
|
41 | private Action<GenericSourceSet> configureSourceSet(TaskContainer tasks, TaskProvider<?> clean) { | |||
|
|
42 | return sourceSet -> { | |||
|
|
43 | var name = sourceSet.getName(); | |||
|
|
44 | ||||
|
|
45 | logger.info("{}: Processing source set '{}'", POLICY_NAME, name); | |||
|
|
46 | ||||
|
|
47 | var taskName = "clean" + Strings.capitalize(sourceSet.getName()); | |||
|
|
48 | ||||
|
|
49 | logger.info("{}: Register task '{}' of type [{}]", POLICY_NAME, taskName, Copy.class.getTypeName()); | |||
|
|
50 | ||||
|
|
51 | var task = tasks.register(taskName, Delete.class, self -> { | |||
|
|
52 | self.delete(sourceSet.getOutputsDir()); | |||
|
|
53 | }); | |||
|
|
54 | ||||
|
|
55 | clean.configure(self -> { | |||
|
|
56 | self.dependsOn(task); | |||
|
|
57 | }); | |||
|
|
58 | }; | |||
|
|
59 | } | |||
|
|
60 | } | |||
| @@ -0,0 +1,136 | |||||
|
|
1 | package org.implab.gradle.common.files; | |||
|
|
2 | ||||
|
|
3 | import java.io.File; | |||
|
|
4 | import java.nio.file.Paths; | |||
|
|
5 | import java.util.HashSet; | |||
|
|
6 | import java.util.List; | |||
|
|
7 | import java.util.Objects; | |||
|
|
8 | import java.util.Set; | |||
|
|
9 | import java.util.concurrent.Callable; | |||
|
|
10 | import java.util.stream.Collectors; | |||
|
|
11 | ||||
|
|
12 | import javax.inject.Inject; | |||
|
|
13 | ||||
|
|
14 | import org.gradle.api.Named; | |||
|
|
15 | import org.gradle.api.NamedDomainObjectContainer; | |||
|
|
16 | import org.gradle.api.file.DirectoryProperty; | |||
|
|
17 | import org.gradle.api.file.FileCollection; | |||
|
|
18 | import org.gradle.api.file.ProjectLayout; | |||
|
|
19 | import org.gradle.api.file.SourceDirectorySet; | |||
|
|
20 | import org.gradle.api.logging.Logger; | |||
|
|
21 | import org.gradle.api.logging.Logging; | |||
|
|
22 | import org.gradle.api.model.ObjectFactory; | |||
|
|
23 | import org.gradle.util.Configurable; | |||
|
|
24 | import org.implab.gradle.common.utils.Closures; | |||
|
|
25 | ||||
|
|
26 | import groovy.lang.Closure; | |||
|
|
27 | ||||
|
|
28 | public abstract class GenericSourceSet | |||
|
|
29 | implements Named, Configurable<GenericSourceSet> { | |||
|
|
30 | private final Logger logger = Logging.getLogger(GenericSourceSet.class); | |||
|
|
31 | ||||
|
|
32 | private final String name; | |||
|
|
33 | ||||
|
|
34 | private final NamedDomainObjectContainer<SourceDirectorySet> sourceDirectorySets; | |||
|
|
35 | ||||
|
|
36 | private final NamedDomainObjectContainer<GenericSourceSetOutput> outputs; | |||
|
|
37 | ||||
|
|
38 | private final FileCollection allOutputs; | |||
|
|
39 | ||||
|
|
40 | private final FileCollection allSourceDirectories; | |||
|
|
41 | ||||
|
|
42 | private final ObjectFactory objects; | |||
|
|
43 | ||||
|
|
44 | private final Set<String> declaredOutputs = new HashSet<>(); | |||
|
|
45 | ||||
|
|
46 | @Inject | |||
|
|
47 | public GenericSourceSet(String name, ObjectFactory objects, ProjectLayout layout) { | |||
|
|
48 | this.name = name; | |||
|
|
49 | this.objects = objects; | |||
|
|
50 | ||||
|
|
51 | sourceDirectorySets = objects.domainObjectContainer( | |||
|
|
52 | SourceDirectorySet.class, | |||
|
|
53 | this::createSourceDirectorySet); | |||
|
|
54 | ||||
|
|
55 | outputs = objects.domainObjectContainer(GenericSourceSetOutput.class); | |||
|
|
56 | ||||
|
|
57 | allSourceDirectories = objects.fileCollection().from(sourceDirectoriesProvider()); | |||
|
|
58 | ||||
|
|
59 | allOutputs = objects.fileCollection().from(outputsProvider()); | |||
|
|
60 | ||||
|
|
61 | outputs.addRule("Register a declared set on demand", this::registerOutputOnDemand); | |||
|
|
62 | ||||
|
|
63 | getSourceSetDir().convention(layout | |||
|
|
64 | .getProjectDirectory() | |||
|
|
65 | .dir(Paths.get("src", name).toString())); | |||
|
|
66 | ||||
|
|
67 | getOutputsDir().convention(layout | |||
|
|
68 | .getBuildDirectory() | |||
|
|
69 | .dir(name)); | |||
|
|
70 | } | |||
|
|
71 | ||||
|
|
72 | @Override | |||
|
|
73 | public String getName() { | |||
|
|
74 | return name; | |||
|
|
75 | } | |||
|
|
76 | ||||
|
|
77 | public abstract DirectoryProperty getSourceSetDir(); | |||
|
|
78 | ||||
|
|
79 | public abstract DirectoryProperty getOutputsDir(); | |||
|
|
80 | ||||
|
|
81 | public NamedDomainObjectContainer<SourceDirectorySet> getSets() { | |||
|
|
82 | return sourceDirectorySets; | |||
|
|
83 | } | |||
|
|
84 | ||||
|
|
85 | public NamedDomainObjectContainer<GenericSourceSetOutput> getOutputs() { | |||
|
|
86 | return outputs; | |||
|
|
87 | } | |||
|
|
88 | ||||
|
|
89 | public FileCollection getAllOutputs() { | |||
|
|
90 | return allOutputs; | |||
|
|
91 | } | |||
|
|
92 | ||||
|
|
93 | public FileCollection getAllSourceDirectories() { | |||
|
|
94 | return allSourceDirectories; | |||
|
|
95 | } | |||
|
|
96 | ||||
|
|
97 | public FileCollection output(String name) { | |||
|
|
98 | return getOutputs().getAt(name).getFileCollection(); | |||
|
|
99 | } | |||
|
|
100 | ||||
|
|
101 | public void declareOutputs(String name, String... extra) { | |||
|
|
102 | declaredOutputs.add(Objects.requireNonNull(name, "declareOutputs: The output name cannot be null")); | |||
|
|
103 | for (var x : extra) | |||
|
|
104 | declaredOutputs.add(Objects.requireNonNull(x, "declareOutputs: The output name cannot be null")); | |||
|
|
105 | } | |||
|
|
106 | ||||
|
|
107 | @Override | |||
|
|
108 | public GenericSourceSet configure(@SuppressWarnings("rawtypes") Closure configure) { | |||
|
|
109 | Closures.apply(configure, this); | |||
|
|
110 | return this; | |||
|
|
111 | } | |||
|
|
112 | ||||
|
|
113 | private void registerOutputOnDemand(String outputName) { | |||
|
|
114 | logger.info("SourceSet '{}': registerOutputOnDemand '{}'", name, outputName); | |||
|
|
115 | ||||
|
|
116 | if (declaredOutputs.contains(outputName)) | |||
|
|
117 | outputs.register(outputName); | |||
|
|
118 | } | |||
|
|
119 | ||||
|
|
120 | private SourceDirectorySet createSourceDirectorySet(String name) { | |||
|
|
121 | return objects.sourceDirectorySet(name, name); | |||
|
|
122 | } | |||
|
|
123 | ||||
|
|
124 | private Callable<List<? extends FileCollection>> outputsProvider() { | |||
|
|
125 | return () -> outputs.stream() | |||
|
|
126 | .map(GenericSourceSetOutput::getFileCollection) | |||
|
|
127 | .toList(); | |||
|
|
128 | } | |||
|
|
129 | ||||
|
|
130 | private Callable<Set<File>> sourceDirectoriesProvider() { | |||
|
|
131 | return () -> sourceDirectorySets.stream() | |||
|
|
132 | .flatMap(x -> x.getSrcDirs().stream()) | |||
|
|
133 | .collect(Collectors.toSet()); | |||
|
|
134 | } | |||
|
|
135 | ||||
|
|
136 | } | |||
| @@ -0,0 +1,46 | |||||
|
|
1 | package org.implab.gradle.common.files; | |||
|
|
2 | ||||
|
|
3 | import org.gradle.api.Action; | |||
|
|
4 | import org.gradle.api.Named; | |||
|
|
5 | import org.gradle.api.file.ConfigurableFileCollection; | |||
|
|
6 | import org.gradle.api.file.FileCollection; | |||
|
|
7 | import org.gradle.api.model.ObjectFactory; | |||
|
|
8 | import org.gradle.util.Configurable; | |||
|
|
9 | import org.implab.gradle.common.utils.Closures; | |||
|
|
10 | ||||
|
|
11 | import groovy.lang.Closure; | |||
|
|
12 | ||||
|
|
13 | /** Simple wrapper to add {@link Named} to {@link FileCollection} */ | |||
|
|
14 | public abstract class GenericSourceSetOutput | |||
|
|
15 | implements Named, Configurable<GenericSourceSetOutput> { | |||
|
|
16 | ||||
|
|
17 | private final String name; | |||
|
|
18 | ||||
|
|
19 | private final ConfigurableFileCollection outputFileCollection; | |||
|
|
20 | ||||
|
|
21 | public GenericSourceSetOutput(String name, ObjectFactory objects) { | |||
|
|
22 | this.name = name; | |||
|
|
23 | outputFileCollection = objects.fileCollection(); | |||
|
|
24 | } | |||
|
|
25 | ||||
|
|
26 | public FileCollection getFileCollection() { | |||
|
|
27 | return outputFileCollection; | |||
|
|
28 | } | |||
|
|
29 | ||||
|
|
30 | @Override | |||
|
|
31 | public String getName() { | |||
|
|
32 | return name; | |||
|
|
33 | } | |||
|
|
34 | ||||
|
|
35 | @Override | |||
|
|
36 | public GenericSourceSetOutput configure(@SuppressWarnings("rawtypes") Closure cl) { | |||
|
|
37 | Closures.apply(cl, outputFileCollection); | |||
|
|
38 | return this; | |||
|
|
39 | } | |||
|
|
40 | ||||
|
|
41 | public GenericSourceSetOutput configure(Action<ConfigurableFileCollection> cl) { | |||
|
|
42 | cl.execute(outputFileCollection); | |||
|
|
43 | return this; | |||
|
|
44 | } | |||
|
|
45 | ||||
|
|
46 | } | |||
| @@ -0,0 +1,29 | |||||
|
|
1 | package org.implab.gradle.common.json; | |||
|
|
2 | ||||
|
|
3 | ||||
|
|
4 | import org.gradle.api.provider.Provider; | |||
|
|
5 | ||||
|
|
6 | import java.util.ArrayList; | |||
|
|
7 | import java.util.Collections; | |||
|
|
8 | import java.util.List; | |||
|
|
9 | ||||
|
|
10 | ||||
|
|
11 | public class DefaultJsonArraySpec implements JsonArraySpec { | |||
|
|
12 | ||||
|
|
13 | private final List<Object> values = new ArrayList<>(); | |||
|
|
14 | ||||
|
|
15 | @Override | |||
|
|
16 | public void add(Object value) { | |||
|
|
17 | if (value instanceof Provider<?>) { | |||
|
|
18 | throw new IllegalArgumentException( | |||
|
|
19 | "Providers are not allowed inside JSON arrays; " + | |||
|
|
20 | "use top-level metadata.set(\"key\", provider) instead." | |||
|
|
21 | ); | |||
|
|
22 | } | |||
|
|
23 | values.add(value); | |||
|
|
24 | } | |||
|
|
25 | ||||
|
|
26 | public List<Object> toList() { | |||
|
|
27 | return Collections.unmodifiableList(values); | |||
|
|
28 | } | |||
|
|
29 | } No newline at end of file | |||
| @@ -0,0 +1,26 | |||||
|
|
1 | package org.implab.gradle.common.json; | |||
|
|
2 | ||||
|
|
3 | import org.gradle.api.provider.Provider; | |||
|
|
4 | ||||
|
|
5 | import java.util.Collections; | |||
|
|
6 | import java.util.LinkedHashMap; | |||
|
|
7 | import java.util.Map; | |||
|
|
8 | ||||
|
|
9 | public class DefaultJsonObjectSpec implements GroovyObjectSpec { | |||
|
|
10 | ||||
|
|
11 | private final Map<String, Object> values = new LinkedHashMap<>(); | |||
|
|
12 | ||||
|
|
13 | @Override | |||
|
|
14 | public void set(String key, Object value) { | |||
|
|
15 | if (value instanceof Provider<?>) { | |||
|
|
16 | throw new IllegalArgumentException( | |||
|
|
17 | "Providers are not allowed inside nested JSON objects; " + | |||
|
|
18 | "use top-level metadata.set(\"" + key + "\", provider) instead."); | |||
|
|
19 | } | |||
|
|
20 | values.put(key, value); | |||
|
|
21 | } | |||
|
|
22 | ||||
|
|
23 | public Map<String, Object> toMap() { | |||
|
|
24 | return Collections.unmodifiableMap(values); | |||
|
|
25 | } | |||
|
|
26 | } No newline at end of file | |||
| @@ -0,0 +1,38 | |||||
|
|
1 | package org.implab.gradle.common.json; | |||
|
|
2 | ||||
|
|
3 | import java.util.Arrays; | |||
|
|
4 | ||||
|
|
5 | import groovy.lang.Closure; | |||
|
|
6 | import groovy.lang.MissingMethodException; | |||
|
|
7 | ||||
|
|
8 | public interface GroovyObjectSpec extends JsonObjectSpec { | |||
|
|
9 | ||||
|
|
10 | default void propertyMissing(String name, Object value) { | |||
|
|
11 | set(name, value); | |||
|
|
12 | } | |||
|
|
13 | ||||
|
|
14 | default Object methodMissing(String name, Object args) { | |||
|
|
15 | Object[] arr = (Object[]) args; | |||
|
|
16 | ||||
|
|
17 | // author { ... }, repository { ... } | |||
|
|
18 | if (arr.length == 1 && arr[0] instanceof Closure<?>) { | |||
|
|
19 | DefaultJsonObjectSpec spec = new DefaultJsonObjectSpec(); | |||
|
|
20 | Closure<?> cl = (Closure<?>) arr[0]; | |||
|
|
21 | cl.setDelegate(spec); | |||
|
|
22 | cl.setResolveStrategy(Closure.DELEGATE_FIRST); | |||
|
|
23 | cl.call(); | |||
|
|
24 | set(name, spec.toMap()); | |||
|
|
25 | return null; | |||
|
|
26 | } else { | |||
|
|
27 | boolean hasClosure = Arrays.stream(arr) | |||
|
|
28 | .anyMatch(a -> a instanceof Closure<?>); | |||
|
|
29 | ||||
|
|
30 | if (!hasClosure) { | |||
|
|
31 | set(name, Arrays.asList(arr)); | |||
|
|
32 | return null; | |||
|
|
33 | } | |||
|
|
34 | } | |||
|
|
35 | ||||
|
|
36 | throw new MissingMethodException(name, getClass(), arr); | |||
|
|
37 | } | |||
|
|
38 | } | |||
| @@ -0,0 +1,20 | |||||
|
|
1 | package org.implab.gradle.common.json; | |||
|
|
2 | ||||
|
|
3 | import org.gradle.api.Action; | |||
|
|
4 | ||||
|
|
5 | public interface JsonArraySpec { | |||
|
|
6 | ||||
|
|
7 | void add(Object value); | |||
|
|
8 | ||||
|
|
9 | default void obj(Action<? super JsonObjectSpec> action) { | |||
|
|
10 | DefaultJsonObjectSpec child = new DefaultJsonObjectSpec(); | |||
|
|
11 | action.execute(child); | |||
|
|
12 | add(child.toMap()); | |||
|
|
13 | } | |||
|
|
14 | ||||
|
|
15 | default void arr(Action<? super JsonArraySpec> action) { | |||
|
|
16 | DefaultJsonArraySpec child = new DefaultJsonArraySpec(); | |||
|
|
17 | action.execute(child); | |||
|
|
18 | add(child.toList()); | |||
|
|
19 | } | |||
|
|
20 | } No newline at end of file | |||
| @@ -0,0 +1,23 | |||||
|
|
1 | package org.implab.gradle.common.json; | |||
|
|
2 | ||||
|
|
3 | import java.io.File; | |||
|
|
4 | ||||
|
|
5 | import org.gradle.api.Action; | |||
|
|
6 | import org.gradle.api.file.RegularFile; | |||
|
|
7 | import org.gradle.api.provider.Provider; | |||
|
|
8 | ||||
|
|
9 | public interface JsonMapSpec extends GroovyObjectSpec { | |||
|
|
10 | void from(File file, Action<? super MapImportSpec> action); | |||
|
|
11 | ||||
|
|
12 | default void from(File file) { | |||
|
|
13 | from(file, spec -> { | |||
|
|
14 | }); | |||
|
|
15 | } | |||
|
|
16 | ||||
|
|
17 | void from(Provider<RegularFile> fileProvider, Action<? super MapImportSpec> action); | |||
|
|
18 | ||||
|
|
19 | default void from(Provider<RegularFile> fileProvider) { | |||
|
|
20 | from(fileProvider, spec -> { | |||
|
|
21 | }); | |||
|
|
22 | } | |||
|
|
23 | } | |||
| @@ -0,0 +1,20 | |||||
|
|
1 | package org.implab.gradle.common.json; | |||
|
|
2 | ||||
|
|
3 | import org.gradle.api.Action; | |||
|
|
4 | ||||
|
|
5 | public interface JsonObjectSpec { | |||
|
|
6 | ||||
|
|
7 | void set(String key, Object value); | |||
|
|
8 | ||||
|
|
9 | default void obj(String key, Action<? super JsonObjectSpec> action) { | |||
|
|
10 | DefaultJsonObjectSpec child = new DefaultJsonObjectSpec(); | |||
|
|
11 | action.execute(child); | |||
|
|
12 | set(key, child.toMap()); | |||
|
|
13 | } | |||
|
|
14 | ||||
|
|
15 | default void arr(String key, Action<? super JsonArraySpec> action) { | |||
|
|
16 | DefaultJsonArraySpec child = new DefaultJsonArraySpec(); | |||
|
|
17 | action.execute(child); | |||
|
|
18 | set(key, child.toList()); | |||
|
|
19 | } | |||
|
|
20 | } No newline at end of file | |||
| @@ -0,0 +1,66 | |||||
|
|
1 | package org.implab.gradle.common.json; | |||
|
|
2 | ||||
|
|
3 | import java.util.Arrays; | |||
|
|
4 | import java.util.LinkedHashSet; | |||
|
|
5 | import java.util.Set; | |||
|
|
6 | import java.util.function.Predicate; | |||
|
|
7 | ||||
|
|
8 | import org.gradle.api.Action; | |||
|
|
9 | import org.implab.gradle.common.utils.Closures; | |||
|
|
10 | ||||
|
|
11 | import groovy.lang.Closure; | |||
|
|
12 | ||||
|
|
13 | /** | |||
|
|
14 | * Простая include/exclude-маска по ключам верхнего уровня. | |||
|
|
15 | */ | |||
|
|
16 | public class MapImportSpec { | |||
|
|
17 | ||||
|
|
18 | private final Set<String> includes = new LinkedHashSet<>(); | |||
|
|
19 | private final Set<String> excludes = new LinkedHashSet<>(); | |||
|
|
20 | ||||
|
|
21 | public void include(String... keys) { | |||
|
|
22 | includes.addAll(Arrays.asList(keys)); | |||
|
|
23 | } | |||
|
|
24 | ||||
|
|
25 | public void exclude(String... keys) { | |||
|
|
26 | excludes.addAll(Arrays.asList(keys)); | |||
|
|
27 | } | |||
|
|
28 | ||||
|
|
29 | public Set<String> getIncludes() { | |||
|
|
30 | return includes; | |||
|
|
31 | } | |||
|
|
32 | ||||
|
|
33 | public Set<String> getExcludes() { | |||
|
|
34 | return excludes; | |||
|
|
35 | } | |||
|
|
36 | ||||
|
|
37 | public boolean hasIncludes() { | |||
|
|
38 | return !includes.isEmpty(); | |||
|
|
39 | } | |||
|
|
40 | ||||
|
|
41 | public boolean isExcluded(String key) { | |||
|
|
42 | return excludes.contains(key); | |||
|
|
43 | } | |||
|
|
44 | ||||
|
|
45 | public boolean isIncluded(String key) { | |||
|
|
46 | return !hasIncludes() || includes.contains(key); | |||
|
|
47 | } | |||
|
|
48 | ||||
|
|
49 | public boolean matches(String key) { | |||
|
|
50 | return isIncluded(key) && !isExcluded(key); | |||
|
|
51 | } | |||
|
|
52 | ||||
|
|
53 | public Predicate<String> toPredicate() { | |||
|
|
54 | return this::matches; | |||
|
|
55 | } | |||
|
|
56 | ||||
|
|
57 | public static Predicate<String> buildPredicate(Action<? super MapImportSpec> action) { | |||
|
|
58 | var spec = new MapImportSpec(); | |||
|
|
59 | action.execute(spec); | |||
|
|
60 | return spec.toPredicate(); | |||
|
|
61 | } | |||
|
|
62 | ||||
|
|
63 | public static Predicate<String> buildPredicate(Closure<?> closure) { | |||
|
|
64 | return buildPredicate(Closures.action(closure)); | |||
|
|
65 | } | |||
|
|
66 | } No newline at end of file | |||
| @@ -0,0 +1,181 | |||||
|
|
1 | package org.implab.gradle.common.tasks; | |||
|
|
2 | ||||
|
|
3 | import java.util.LinkedHashMap; | |||
|
|
4 | import java.util.Map; | |||
|
|
5 | import java.util.Objects; | |||
|
|
6 | import java.util.function.Predicate; | |||
|
|
7 | import java.util.stream.Collectors; | |||
|
|
8 | ||||
|
|
9 | import javax.inject.Inject; | |||
|
|
10 | ||||
|
|
11 | import org.eclipse.jdt.annotation.NonNullByDefault; | |||
|
|
12 | import org.gradle.api.Action; | |||
|
|
13 | import org.gradle.api.DefaultTask; | |||
|
|
14 | import org.gradle.api.file.RegularFile; | |||
|
|
15 | import org.gradle.api.file.RegularFileProperty; | |||
|
|
16 | import org.gradle.api.provider.MapProperty; | |||
|
|
17 | import org.gradle.api.provider.Provider; | |||
|
|
18 | import org.gradle.api.provider.ProviderFactory; | |||
|
|
19 | import org.gradle.api.tasks.Input; | |||
|
|
20 | import org.gradle.api.tasks.Internal; | |||
|
|
21 | import org.gradle.api.tasks.Optional; | |||
|
|
22 | import org.gradle.api.tasks.OutputFile; | |||
|
|
23 | import org.gradle.api.tasks.SkipWhenEmpty; | |||
|
|
24 | import org.gradle.api.tasks.TaskAction; | |||
|
|
25 | import org.implab.gradle.common.json.MapImportSpec; | |||
|
|
26 | import org.implab.gradle.common.json.Json; | |||
|
|
27 | import org.implab.gradle.common.json.JsonObjectSpec; | |||
|
|
28 | import org.implab.gradle.common.utils.Closures; | |||
|
|
29 | import org.implab.gradle.common.utils.Properties; | |||
|
|
30 | ||||
|
|
31 | import groovy.lang.Closure; | |||
|
|
32 | ||||
|
|
33 | /** | |||
|
|
34 | * A Gradle task that writes JSON content to a file. | |||
|
|
35 | * | |||
|
|
36 | * This task allows you to build up a JSON object through various methods and | |||
|
|
37 | * write it to a file. | |||
|
|
38 | * You can add content directly, import from other JSON files, or merge in map | |||
|
|
39 | * data with optional filtering. | |||
|
|
40 | * | |||
|
|
41 | * <h3>Usage Example:</h3> | |||
|
|
42 | * | |||
|
|
43 | * <pre> | |||
|
|
44 | * tasks.register('generateConfig', WriteJson) { | |||
|
|
45 | * outputFile = file('build/config.json') | |||
|
|
46 | * content { | |||
|
|
47 | * version = '1.0.0' | |||
|
|
48 | * name = 'myapp' | |||
|
|
49 | * } | |||
|
|
50 | * from file('src/base-config.json') | |||
|
|
51 | * putAll providers.provider { ['debug': true] } | |||
|
|
52 | * } | |||
|
|
53 | * </pre> | |||
|
|
54 | * | |||
|
|
55 | * <h3>Properties:</h3> | |||
|
|
56 | * <ul> | |||
|
|
57 | * <li><strong>content</strong> - A map of string keys to object values that | |||
|
|
58 | * will be serialized to JSON</li> | |||
|
|
59 | * <li><strong>outputFile</strong> - The file where the JSON will be | |||
|
|
60 | * written</li> | |||
|
|
61 | * </ul> | |||
|
|
62 | * | |||
|
|
63 | * <h3>Methods:</h3> | |||
|
|
64 | * <ul> | |||
|
|
65 | * <li><strong>content()</strong> - Configure content using a closure or | |||
|
|
66 | * action</li> | |||
|
|
67 | * <li><strong>from()</strong> - Import JSON content from a file with optional | |||
|
|
68 | * key filtering</li> | |||
|
|
69 | * <li><strong>putAll()</strong> - Merge a map provider with optional key | |||
|
|
70 | * filtering</li> | |||
|
|
71 | * </ul> | |||
|
|
72 | * | |||
|
|
73 | * @since 1.0 | |||
|
|
74 | */ | |||
|
|
75 | @NonNullByDefault | |||
|
|
76 | public abstract class WriteJson extends DefaultTask { | |||
|
|
77 | ||||
|
|
78 | private final ProviderFactory providers; | |||
|
|
79 | ||||
|
|
80 | private final JsonObjectSpec contentSpec = new JsonObjectSpec() { | |||
|
|
81 | @Override | |||
|
|
82 | public void set(String key, Object value) { | |||
|
|
83 | Properties.putMapEntry(getContent(), key, value); | |||
|
|
84 | } | |||
|
|
85 | }; | |||
|
|
86 | ||||
|
|
87 | @Input | |||
|
|
88 | @Optional | |||
|
|
89 | public abstract MapProperty<String, Object> getContent(); | |||
|
|
90 | ||||
|
|
91 | @OutputFile | |||
|
|
92 | public abstract RegularFileProperty getOutputFile(); | |||
|
|
93 | ||||
|
|
94 | @Internal | |||
|
|
95 | public Provider<String> getJson() { | |||
|
|
96 | return getContent().map(Json::stringify); | |||
|
|
97 | } | |||
|
|
98 | ||||
|
|
99 | @Inject | |||
|
|
100 | public WriteJson(ProviderFactory providers) { | |||
|
|
101 | this.providers = providers; | |||
|
|
102 | } | |||
|
|
103 | ||||
|
|
104 | public void content(Action<? super JsonObjectSpec> configure) { | |||
|
|
105 | configure.execute(contentSpec); | |||
|
|
106 | } | |||
|
|
107 | ||||
|
|
108 | public void content(Closure<?> configure) { | |||
|
|
109 | Closures.apply(configure, contentSpec); | |||
|
|
110 | } | |||
|
|
111 | ||||
|
|
112 | public void from(RegularFile file, Closure<?> configure) { | |||
|
|
113 | importContents(file, MapImportSpec.buildPredicate(configure)); | |||
|
|
114 | } | |||
|
|
115 | ||||
|
|
116 | public void from(RegularFile file, Action<? super MapImportSpec> configure) { | |||
|
|
117 | importContents(file, MapImportSpec.buildPredicate(configure)); | |||
|
|
118 | } | |||
|
|
119 | ||||
|
|
120 | public void from(RegularFile file) { | |||
|
|
121 | importContents(file, x -> true); | |||
|
|
122 | } | |||
|
|
123 | ||||
|
|
124 | public void from(Provider<RegularFile> file, Closure<?> configure) { | |||
|
|
125 | importContents(file, MapImportSpec.buildPredicate(configure)); | |||
|
|
126 | } | |||
|
|
127 | ||||
|
|
128 | public void from(Provider<RegularFile> file, Action<? super MapImportSpec> configure) { | |||
|
|
129 | importContents(file, MapImportSpec.buildPredicate(configure)); | |||
|
|
130 | } | |||
|
|
131 | ||||
|
|
132 | public void from(Provider<RegularFile> file) { | |||
|
|
133 | importContents(file, x -> true); | |||
|
|
134 | } | |||
|
|
135 | ||||
|
|
136 | public void putAll(Provider<Map<String, Object>> mapProvider, Action<? super MapImportSpec> configure) { | |||
|
|
137 | importMap(mapProvider, MapImportSpec.buildPredicate(configure)); | |||
|
|
138 | } | |||
|
|
139 | ||||
|
|
140 | public void putAll(Provider<Map<String, Object>> mapProvider, Closure<?> configure) { | |||
|
|
141 | importMap(mapProvider, MapImportSpec.buildPredicate(configure)); | |||
|
|
142 | } | |||
|
|
143 | ||||
|
|
144 | public void putAll(Provider<Map<String, Object>> mapProvider) { | |||
|
|
145 | importMap(mapProvider, x -> true); | |||
|
|
146 | } | |||
|
|
147 | ||||
|
|
148 | private void importContents(Provider<RegularFile> file, Predicate<String> keyPredicate) { | |||
|
|
149 | var jsonProvider = providers.fileContents(file).getAsText() | |||
|
|
150 | .map(Json.mapParser(Object.class)::apply); | |||
|
|
151 | ||||
|
|
152 | importMap(jsonProvider, keyPredicate); | |||
|
|
153 | } | |||
|
|
154 | ||||
|
|
155 | private void importContents(RegularFile file, Predicate<String> keyPredicate) { | |||
|
|
156 | var jsonProvider = providers.fileContents(file).getAsText() | |||
|
|
157 | .map(Json.mapParser(Object.class)::apply); | |||
|
|
158 | ||||
|
|
159 | importMap(jsonProvider, keyPredicate); | |||
|
|
160 | } | |||
|
|
161 | ||||
|
|
162 | private void importMap(Provider<Map<String, Object>> mapProvider, Predicate<? super String> keyPredicate) { | |||
|
|
163 | var filteredProvider = mapProvider.map(v -> v.entrySet().stream() | |||
|
|
164 | .filter(pair -> keyPredicate.test(pair.getKey()) && Objects.nonNull(pair.getValue())) | |||
|
|
165 | .collect(Collectors.toMap( | |||
|
|
166 | Map.Entry::getKey, | |||
|
|
167 | Map.Entry::getValue, | |||
|
|
168 | (a, b) -> b, | |||
|
|
169 | LinkedHashMap::new))); | |||
|
|
170 | ||||
|
|
171 | getContent().putAll(filteredProvider); | |||
|
|
172 | } | |||
|
|
173 | ||||
|
|
174 | @TaskAction | |||
|
|
175 | public void run() { | |||
|
|
176 | var data = getContent().getOrElse(Map.of()); | |||
|
|
177 | var file = getOutputFile().getAsFile().get(); | |||
|
|
178 | Json.write(file, data); | |||
|
|
179 | } | |||
|
|
180 | ||||
|
|
181 | } | |||
| @@ -0,0 +1,15 | |||||
|
|
1 | package org.implab.gradle.common.utils; | |||
|
|
2 | ||||
|
|
3 | import java.util.function.Consumer; | |||
|
|
4 | ||||
|
|
5 | import org.gradle.api.plugins.ExtensionContainer; | |||
|
|
6 | ||||
|
|
7 | public final class Extensions { | |||
|
|
8 | private Extensions() { | |||
|
|
9 | } | |||
|
|
10 | ||||
|
|
11 | public static Consumer<Class<?>> registerClass(ExtensionContainer extensions) { | |||
|
|
12 | var extra = extensions.getExtraProperties(); | |||
|
|
13 | return clazz -> extra.set(clazz.getSimpleName(), clazz); | |||
|
|
14 | } | |||
|
|
15 | } | |||
| @@ -0,0 +1,316 | |||||
|
|
1 | package org.implab.gradle.common.utils; | |||
|
|
2 | ||||
|
|
3 | import com.fasterxml.jackson.annotation.JsonCreator; | |||
|
|
4 | import com.fasterxml.jackson.annotation.JsonValue; | |||
|
|
5 | ||||
|
|
6 | import java.util.Objects; | |||
|
|
7 | import java.util.Optional; | |||
|
|
8 | import java.util.regex.Matcher; | |||
|
|
9 | import java.util.regex.Pattern; | |||
|
|
10 | ||||
|
|
11 | /** | |||
|
|
12 | * Immutable Semantic Version (SemVer 2.0.0) of the form: | |||
|
|
13 | * | |||
|
|
14 | * <pre> | |||
|
|
15 | * MAJOR.MINOR.PATCH[-PRERELEASE][+BUILD] | |||
|
|
16 | * </pre> | |||
|
|
17 | * | |||
|
|
18 | * Ordering (as defined by {@link #compareTo(SemVersion)}) follows | |||
|
|
19 | * the SemVer 2.0.0 precedence rules: | |||
|
|
20 | * <ul> | |||
|
|
21 | * <li>Compare {@code major}, then {@code minor}, then {@code patch}.</li> | |||
|
|
22 | * <li>Pre-release versions have lower precedence than the corresponding | |||
|
|
23 | * normal version.</li> | |||
|
|
24 | * <li>Build metadata does not affect precedence.</li> | |||
|
|
25 | * </ul> | |||
|
|
26 | * | |||
|
|
27 | * Public API does not use {@code null} for semantic values: | |||
|
|
28 | * {@link Optional} is used for pre-release and build metadata. | |||
|
|
29 | */ | |||
|
|
30 | public record SemVersion( | |||
|
|
31 | int major, | |||
|
|
32 | int minor, | |||
|
|
33 | int patch, | |||
|
|
34 | Optional<String> preRelease, | |||
|
|
35 | Optional<String> buildMetadata) implements Comparable<SemVersion> { | |||
|
|
36 | ||||
|
|
37 | // Pattern close to the official SemVer 2.0.0 recommendation. | |||
|
|
38 | // Groups: | |||
|
|
39 | // 1 - major | |||
|
|
40 | // 2 - minor | |||
|
|
41 | // 3 - patch | |||
|
|
42 | // 4 - pre-release (without '-') | |||
|
|
43 | // 5 - build metadata (without '+') | |||
|
|
44 | private static final Pattern SEMVER_PATTERN = Pattern.compile( | |||
|
|
45 | "^(0|[1-9]\\d*)" + // major | |||
|
|
46 | "\\.(0|[1-9]\\d*)" + // minor | |||
|
|
47 | "\\.(0|[1-9]\\d*)" + // patch | |||
|
|
48 | "(?:-((?:0|[1-9]\\d*|[0-9A-Za-z-][0-9A-Za-z-]*)" + | |||
|
|
49 | "(?:\\.(?:0|[1-9]\\d*|[0-9A-Za-z-][0-9A-Za-z-]*))*" + | |||
|
|
50 | "))?" + // pre-release | |||
|
|
51 | "(?:\\+([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?$" // build metadata | |||
|
|
52 | ); | |||
|
|
53 | ||||
|
|
54 | /** | |||
|
|
55 | * Compact constructor with basic invariants. | |||
|
|
56 | */ | |||
|
|
57 | public SemVersion { | |||
|
|
58 | if (major < 0 || minor < 0 || patch < 0) { | |||
|
|
59 | throw new IllegalArgumentException("Version numbers must be >= 0"); | |||
|
|
60 | } | |||
|
|
61 | // Be tolerant internally, but never expose null Optionals. | |||
|
|
62 | preRelease = (preRelease != null) ? preRelease : Optional.empty(); | |||
|
|
63 | buildMetadata = (buildMetadata != null) ? buildMetadata : Optional.empty(); | |||
|
|
64 | } | |||
|
|
65 | ||||
|
|
66 | /** | |||
|
|
67 | * Creates a version without pre-release and build metadata. | |||
|
|
68 | * | |||
|
|
69 | * @param major non-negative MAJOR number | |||
|
|
70 | * @param minor non-negative MINOR number | |||
|
|
71 | * @param patch non-negative PATCH number | |||
|
|
72 | */ | |||
|
|
73 | public static SemVersion of(int major, int minor, int patch) { | |||
|
|
74 | return new SemVersion(major, minor, patch, Optional.empty(), Optional.empty()); | |||
|
|
75 | } | |||
|
|
76 | ||||
|
|
77 | /** | |||
|
|
78 | * Creates a version from components. | |||
|
|
79 | * | |||
|
|
80 | * @param major non-negative MAJOR number | |||
|
|
81 | * @param minor non-negative MINOR number | |||
|
|
82 | * @param patch non-negative PATCH number | |||
|
|
83 | * @param preRelease optional pre-release part (without '-') | |||
|
|
84 | * @param buildMetadata optional build metadata (without '+') | |||
|
|
85 | */ | |||
|
|
86 | public static SemVersion of( | |||
|
|
87 | int major, | |||
|
|
88 | int minor, | |||
|
|
89 | int patch, | |||
|
|
90 | Optional<String> preRelease, | |||
|
|
91 | Optional<String> buildMetadata) { | |||
|
|
92 | return new SemVersion( | |||
|
|
93 | major, | |||
|
|
94 | minor, | |||
|
|
95 | patch, | |||
|
|
96 | Objects.requireNonNull(preRelease, "preRelease"), | |||
|
|
97 | Objects.requireNonNull(buildMetadata, "buildMetadata")); | |||
|
|
98 | } | |||
|
|
99 | ||||
|
|
100 | /** | |||
|
|
101 | * Parses a SemVer string of the form | |||
|
|
102 | * {@code MAJOR.MINOR.PATCH[-PRERELEASE][+BUILD]}. | |||
|
|
103 | * | |||
|
|
104 | * @param value the string to parse (must not be {@code null}) | |||
|
|
105 | * @return a new {@code SemVersion} | |||
|
|
106 | * @throws IllegalArgumentException if the string is not a valid SemVer | |||
|
|
107 | */ | |||
|
|
108 | public static SemVersion parse(String value) { | |||
|
|
109 | Objects.requireNonNull(value, "value"); | |||
|
|
110 | ||||
|
|
111 | Matcher m = SEMVER_PATTERN.matcher(value); | |||
|
|
112 | if (!m.matches()) { | |||
|
|
113 | throw new IllegalArgumentException("Invalid SemVer: " + value); | |||
|
|
114 | } | |||
|
|
115 | ||||
|
|
116 | int major = Integer.parseInt(m.group(1)); | |||
|
|
117 | int minor = Integer.parseInt(m.group(2)); | |||
|
|
118 | int patch = Integer.parseInt(m.group(3)); | |||
|
|
119 | ||||
|
|
120 | String pre = m.group(4); | |||
|
|
121 | String meta = m.group(5); | |||
|
|
122 | ||||
|
|
123 | Optional<String> preOpt = Optional.ofNullable(pre); | |||
|
|
124 | Optional<String> metaOpt = Optional.ofNullable(meta); | |||
|
|
125 | ||||
|
|
126 | // Extra validation for numeric pre-release identifiers (no leading zeros) | |||
|
|
127 | preOpt.ifPresent(SemVersion::validatePreReleaseIdentifiers); | |||
|
|
128 | ||||
|
|
129 | return new SemVersion(major, minor, patch, preOpt, metaOpt); | |||
|
|
130 | } | |||
|
|
131 | ||||
|
|
132 | private static void validatePreReleaseIdentifiers(String preRelease) { | |||
|
|
133 | String[] parts = preRelease.split("\\."); | |||
|
|
134 | for (String p : parts) { | |||
|
|
135 | if (isNumericIdentifier(p)) { | |||
|
|
136 | // numeric identifiers must not have leading zeros (except "0") | |||
|
|
137 | if (p.length() > 1 && p.charAt(0) == '0') { | |||
|
|
138 | throw new IllegalArgumentException( | |||
|
|
139 | "Numeric pre-release identifier must not contain leading zeros: " + p); | |||
|
|
140 | } | |||
|
|
141 | } | |||
|
|
142 | } | |||
|
|
143 | } | |||
|
|
144 | ||||
|
|
145 | private static boolean isNumericIdentifier(String s) { | |||
|
|
146 | int len = s.length(); | |||
|
|
147 | if (len == 0) { | |||
|
|
148 | return false; | |||
|
|
149 | } | |||
|
|
150 | for (int i = 0; i < len; i++) { | |||
|
|
151 | char c = s.charAt(i); | |||
|
|
152 | if (c < '0' || c > '9') { | |||
|
|
153 | return false; | |||
|
|
154 | } | |||
|
|
155 | } | |||
|
|
156 | return true; | |||
|
|
157 | } | |||
|
|
158 | ||||
|
|
159 | /** | |||
|
|
160 | * Returns {@code true} if this version has a pre-release part. | |||
|
|
161 | * | |||
|
|
162 | * @return {@code true} if {@link #preRelease()} is present | |||
|
|
163 | */ | |||
|
|
164 | public boolean isPreRelease() { | |||
|
|
165 | return preRelease().isPresent(); | |||
|
|
166 | } | |||
|
|
167 | ||||
|
|
168 | /** | |||
|
|
169 | * Returns {@code true} if this version is considered "stable". | |||
|
|
170 | * <p> | |||
|
|
171 | * By convention: | |||
|
|
172 | * <ul> | |||
|
|
173 | * <li>MAJOR must be greater than 0</li> | |||
|
|
174 | * <li>No pre-release part is present</li> | |||
|
|
175 | * </ul> | |||
|
|
176 | * | |||
|
|
177 | * @return {@code true} if this version is a stable release | |||
|
|
178 | */ | |||
|
|
179 | public boolean isStable() { | |||
|
|
180 | return !isPreRelease() && major() > 0; | |||
|
|
181 | } | |||
|
|
182 | ||||
|
|
183 | /** | |||
|
|
184 | * Returns {@code true} if this version has strictly lower precedence | |||
|
|
185 | * than the given {@code other} version according to | |||
|
|
186 | * {@link #compareTo(SemVersion)}. | |||
|
|
187 | * | |||
|
|
188 | * @param other the version to compare to | |||
|
|
189 | * @return {@code true} if {@code this.compareTo(other) < 0} | |||
|
|
190 | */ | |||
|
|
191 | public boolean isBefore(SemVersion other) { | |||
|
|
192 | return compareTo(other) < 0; | |||
|
|
193 | } | |||
|
|
194 | ||||
|
|
195 | /** | |||
|
|
196 | * Returns {@code true} if this version has strictly higher precedence | |||
|
|
197 | * than the given {@code other} version according to | |||
|
|
198 | * {@link #compareTo(SemVersion)}. | |||
|
|
199 | * | |||
|
|
200 | * @param other the version to compare to | |||
|
|
201 | * @return {@code true} if {@code this.compareTo(other) > 0} | |||
|
|
202 | */ | |||
|
|
203 | public boolean isAfter(SemVersion other) { | |||
|
|
204 | return compareTo(other) > 0; | |||
|
|
205 | } | |||
|
|
206 | ||||
|
|
207 | /** | |||
|
|
208 | * Canonical SemVer string representation. | |||
|
|
209 | * <p> | |||
|
|
210 | * This method is also used by Jackson during serialization. | |||
|
|
211 | * | |||
|
|
212 | * @return canonical SemVer string, e.g. {@code "1.2.3-alpha+build.1"} | |||
|
|
213 | */ | |||
|
|
214 | @JsonValue | |||
|
|
215 | public String asString() { | |||
|
|
216 | StringBuilder sb = new StringBuilder() | |||
|
|
217 | .append(major).append('.') | |||
|
|
218 | .append(minor).append('.') | |||
|
|
219 | .append(patch); | |||
|
|
220 | ||||
|
|
221 | preRelease.ifPresent(pr -> sb.append('-').append(pr)); | |||
|
|
222 | buildMetadata.ifPresent(md -> sb.append('+').append(md)); | |||
|
|
223 | ||||
|
|
224 | return sb.toString(); | |||
|
|
225 | } | |||
|
|
226 | ||||
|
|
227 | /** | |||
|
|
228 | * Creates a {@code SemVersion} from a canonical SemVer string. | |||
|
|
229 | * <p> | |||
|
|
230 | * Jackson will use this factory method when deserializing from a JSON string. | |||
|
|
231 | * | |||
|
|
232 | * @param value canonical SemVer string | |||
|
|
233 | * @return parsed {@code SemVersion} | |||
|
|
234 | */ | |||
|
|
235 | @JsonCreator(mode = JsonCreator.Mode.DELEGATING) | |||
|
|
236 | public static SemVersion fromJson(String value) { | |||
|
|
237 | return parse(value); | |||
|
|
238 | } | |||
|
|
239 | ||||
|
|
240 | /** | |||
|
|
241 | * Compares this version to another {@link SemVersion} according to the | |||
|
|
242 | * SemVer 2.0.0 precedence rules. | |||
|
|
243 | * <p> | |||
|
|
244 | * Build metadata is ignored for ordering. | |||
|
|
245 | */ | |||
|
|
246 | @Override | |||
|
|
247 | public int compareTo(SemVersion other) { | |||
|
|
248 | // 1. major / minor / patch | |||
|
|
249 | int c = Integer.compare(this.major, other.major); | |||
|
|
250 | if (c != 0) | |||
|
|
251 | return c; | |||
|
|
252 | ||||
|
|
253 | c = Integer.compare(this.minor, other.minor); | |||
|
|
254 | if (c != 0) | |||
|
|
255 | return c; | |||
|
|
256 | ||||
|
|
257 | c = Integer.compare(this.patch, other.patch); | |||
|
|
258 | if (c != 0) | |||
|
|
259 | return c; | |||
|
|
260 | ||||
|
|
261 | // 2. pre-release (absence > presence) | |||
|
|
262 | boolean thisHasPre = this.preRelease.isPresent(); | |||
|
|
263 | boolean otherHasPre = other.preRelease.isPresent(); | |||
|
|
264 | ||||
|
|
265 | if (!thisHasPre && !otherHasPre) { | |||
|
|
266 | return 0; | |||
|
|
267 | } else if (!thisHasPre) { | |||
|
|
268 | // normal version > pre-release | |||
|
|
269 | return 1; | |||
|
|
270 | } else if (!otherHasPre) { | |||
|
|
271 | return -1; | |||
|
|
272 | } | |||
|
|
273 | ||||
|
|
274 | // 3. both have pre-release: compare identifiers | |||
|
|
275 | return comparePreRelease(this.preRelease.get(), other.preRelease.get()); | |||
|
|
276 | } | |||
|
|
277 | ||||
|
|
278 | private static int comparePreRelease(String a, String b) { | |||
|
|
279 | String[] aParts = a.split("\\."); | |||
|
|
280 | String[] bParts = b.split("\\."); | |||
|
|
281 | ||||
|
|
282 | int len = Math.min(aParts.length, bParts.length); | |||
|
|
283 | for (int i = 0; i < len; i++) { | |||
|
|
284 | String ai = aParts[i]; | |||
|
|
285 | String bi = bParts[i]; | |||
|
|
286 | ||||
|
|
287 | boolean aNum = isNumericIdentifier(ai); | |||
|
|
288 | boolean bNum = isNumericIdentifier(bi); | |||
|
|
289 | ||||
|
|
290 | if (aNum && bNum) { | |||
|
|
291 | int aiVal = Integer.parseInt(ai); | |||
|
|
292 | int biVal = Integer.parseInt(bi); | |||
|
|
293 | int c = Integer.compare(aiVal, biVal); | |||
|
|
294 | if (c != 0) | |||
|
|
295 | return c; | |||
|
|
296 | } else if (aNum && !bNum) { | |||
|
|
297 | // numeric identifiers have lower precedence than non-numeric | |||
|
|
298 | return -1; | |||
|
|
299 | } else if (!aNum && bNum) { | |||
|
|
300 | return 1; | |||
|
|
301 | } else { | |||
|
|
302 | int c = ai.compareTo(bi); | |||
|
|
303 | if (c != 0) | |||
|
|
304 | return c; | |||
|
|
305 | } | |||
|
|
306 | } | |||
|
|
307 | ||||
|
|
308 | // If all common identifiers are equal, the shorter one has lower precedence | |||
|
|
309 | return Integer.compare(aParts.length, bParts.length); | |||
|
|
310 | } | |||
|
|
311 | ||||
|
|
312 | @Override | |||
|
|
313 | public String toString() { | |||
|
|
314 | return asString(); | |||
|
|
315 | } | |||
|
|
316 | } | |||
| @@ -14,9 +14,8 java { | |||||
| 14 | dependencies { |
|
14 | dependencies { | |
| 15 | compileOnly libs.jdt.annotations |
|
15 | compileOnly libs.jdt.annotations | |
| 16 |
|
16 | |||
| 17 | implementation libs.bundles.jackson |
|
17 | api gradleApi(), | |
| 18 |
|
18 | libs.bundles.jackson | ||
| 19 | api gradleApi() |
|
|||
| 20 | } |
|
19 | } | |
| 21 |
|
20 | |||
| 22 | task printVersion{ |
|
21 | task printVersion{ | |
| @@ -6,9 +6,13 import java.util.Optional; | |||||
| 6 | import java.util.function.Supplier; |
|
6 | import java.util.function.Supplier; | |
| 7 |
|
7 | |||
| 8 | import org.gradle.api.provider.Provider; |
|
8 | import org.gradle.api.provider.Provider; | |
|
|
9 | import org.gradle.util.Configurable; | |||
| 9 | import org.implab.gradle.common.exec.RedirectFrom; |
|
10 | import org.implab.gradle.common.exec.RedirectFrom; | |
|
|
11 | import org.implab.gradle.common.utils.Closures; | |||
| 10 |
|
12 | |||
| 11 | public class RedirectFromSpec { |
|
13 | import groovy.lang.Closure; | |
|
|
14 | ||||
|
|
15 | public class RedirectFromSpec implements Configurable<RedirectFromSpec> { | |||
| 12 | private Supplier<RedirectFrom> streamRedirect; |
|
16 | private Supplier<RedirectFrom> streamRedirect; | |
| 13 |
|
17 | |||
| 14 | public boolean isRedirected() { |
|
18 | public boolean isRedirected() { | |
| @@ -46,4 +50,12 public class RedirectFromSpec { | |||||
| 46 | public void empty() { |
|
50 | public void empty() { | |
| 47 | streamRedirect = () -> null; |
|
51 | streamRedirect = () -> null; | |
| 48 | } |
|
52 | } | |
|
|
53 | ||||
|
|
54 | @Override | |||
|
|
55 | public RedirectFromSpec configure(Closure cl) { | |||
|
|
56 | Closures.apply(cl, this); | |||
|
|
57 | return this; | |||
|
|
58 | } | |||
|
|
59 | ||||
|
|
60 | ||||
| 49 | } |
|
61 | } | |
| @@ -9,10 +9,14 import java.util.function.Supplier; | |||||
| 9 | import org.eclipse.jdt.annotation.NonNullByDefault; |
|
9 | import org.eclipse.jdt.annotation.NonNullByDefault; | |
| 10 | import org.eclipse.jdt.annotation.Nullable; |
|
10 | import org.eclipse.jdt.annotation.Nullable; | |
| 11 | import org.gradle.api.provider.Provider; |
|
11 | import org.gradle.api.provider.Provider; | |
|
|
12 | import org.gradle.util.Configurable; | |||
| 12 | import org.implab.gradle.common.exec.RedirectTo; |
|
13 | import org.implab.gradle.common.exec.RedirectTo; | |
|
|
14 | import org.implab.gradle.common.utils.Closures; | |||
|
|
15 | ||||
|
|
16 | import groovy.lang.Closure; | |||
| 13 |
|
17 | |||
| 14 | @NonNullByDefault |
|
18 | @NonNullByDefault | |
| 15 | public class RedirectToSpec { |
|
19 | public class RedirectToSpec implements Configurable<RedirectToSpec> { | |
| 16 | private Supplier<RedirectTo> streamRedirect; |
|
20 | private Supplier<RedirectTo> streamRedirect; | |
| 17 |
|
21 | |||
| 18 | public boolean isRedirected() { |
|
22 | public boolean isRedirected() { | |
| @@ -62,4 +66,10 public class RedirectToSpec { | |||||
| 62 | public void discard() { |
|
66 | public void discard() { | |
| 63 | streamRedirect = () -> null; |
|
67 | streamRedirect = () -> null; | |
| 64 | } |
|
68 | } | |
|
|
69 | ||||
|
|
70 | @Override | |||
|
|
71 | public RedirectToSpec configure(Closure cl) { | |||
|
|
72 | Closures.apply(cl, this); | |||
|
|
73 | return this; | |||
|
|
74 | } | |||
| 65 | } |
|
75 | } | |
| @@ -1,8 +1,10 | |||||
| 1 |
package org.implab.gradle.common. |
|
1 | package org.implab.gradle.common.json; | |
| 2 |
|
2 | |||
| 3 | import java.io.File; |
|
3 | import java.io.File; | |
| 4 | import java.io.IOException; |
|
4 | import java.io.IOException; | |
|
|
5 | import java.util.Map; | |||
| 5 | import java.util.function.Consumer; |
|
6 | import java.util.function.Consumer; | |
|
|
7 | import java.util.function.Function; | |||
| 6 |
|
8 | |||
| 7 | import org.implab.gradle.common.utils.LazyValue; |
|
9 | import org.implab.gradle.common.utils.LazyValue; | |
| 8 |
|
10 | |||
| @@ -37,8 +39,8 public class Json { | |||||
| 37 | return mapper.get(); |
|
39 | return mapper.get(); | |
| 38 | } |
|
40 | } | |
| 39 |
|
41 | |||
| 40 |
public static Consumer< |
|
42 | public static Consumer<Object> fileWriter(File file) { | |
| 41 |
return |
|
43 | return value -> { | |
| 42 | try { |
|
44 | try { | |
| 43 | jsonMapper().writeValue(file, value); |
|
45 | jsonMapper().writeValue(file, value); | |
| 44 | } catch (IOException e) { |
|
46 | } catch (IOException e) { | |
| @@ -79,6 +81,15 public class Json { | |||||
| 79 | } |
|
81 | } | |
| 80 | } |
|
82 | } | |
| 81 |
|
83 | |||
|
|
84 | public static <V> Map<String, V> parseMap(String json, Class<V> clazz) { | |||
|
|
85 | return parse(json, new TypeReference<>() { | |||
|
|
86 | }); | |||
|
|
87 | } | |||
|
|
88 | ||||
|
|
89 | public static <V> Function<String, Map<String, V>> mapParser(Class<V> clazz) { | |||
|
|
90 | return json -> parseMap(json, clazz); | |||
|
|
91 | } | |||
|
|
92 | ||||
| 82 | public static <T> T read(File file, TypeReference<T> type) { |
|
93 | public static <T> T read(File file, TypeReference<T> type) { | |
| 83 | try { |
|
94 | try { | |
| 84 | return jsonMapper().readValue(file, type); |
|
95 | return jsonMapper().readValue(file, type); | |
| @@ -87,6 +98,11 public class Json { | |||||
| 87 | } |
|
98 | } | |
| 88 | } |
|
99 | } | |
| 89 |
|
100 | |||
|
|
101 | public static <V> Map<String, V> readMap(File file, Class<V> clazz) { | |||
|
|
102 | return read(file, new TypeReference<>() { | |||
|
|
103 | }); | |||
|
|
104 | } | |||
|
|
105 | ||||
| 90 | public static <T> T updateValue(T target, Object source) { |
|
106 | public static <T> T updateValue(T target, Object source) { | |
| 91 | try { |
|
107 | try { | |
| 92 | return jsonMapper().updateValue(target, source); |
|
108 | return jsonMapper().updateValue(target, source); | |
| @@ -27,6 +27,5 public final class Closures { | |||||
| 27 | c.setResolveStrategy(Closure.DELEGATE_FIRST); |
|
27 | c.setResolveStrategy(Closure.DELEGATE_FIRST); | |
| 28 | c.setDelegate(target); |
|
28 | c.setDelegate(target); | |
| 29 | c.call(target); |
|
29 | c.call(target); | |
| 30 |
|
||||
| 31 | } |
|
30 | } | |
| 32 | } |
|
31 | } | |
| @@ -7,8 +7,6 import org.implab.gradle.common.utils.os | |||||
| 7 |
|
7 | |||
| 8 | public interface OperatingSystem { |
|
8 | public interface OperatingSystem { | |
| 9 |
|
9 | |||
| 10 | public static OperatingSystem CURRENT = SystemResolver.current(); |
|
|||
| 11 |
|
||||
| 12 | public static final String WINDOWS_FAMILY = "windows"; |
|
10 | public static final String WINDOWS_FAMILY = "windows"; | |
| 13 |
|
11 | |||
| 14 | public static final String LINUX_FAMILY = "linux"; |
|
12 | public static final String LINUX_FAMILY = "linux"; | |
| @@ -30,6 +28,6 public interface OperatingSystem { | |||||
| 30 | Optional<File> which(String cmd, Iterable<? extends File> paths); |
|
28 | Optional<File> which(String cmd, Iterable<? extends File> paths); | |
| 31 |
|
29 | |||
| 32 | public static OperatingSystem current() { |
|
30 | public static OperatingSystem current() { | |
| 33 | return CURRENT; |
|
31 | return SystemResolver.current(); | |
| 34 | } |
|
32 | } | |
| 35 | } |
|
33 | } | |
| @@ -3,7 +3,6 package org.implab.gradle.common.utils; | |||||
| 3 | import java.util.HashMap; |
|
3 | import java.util.HashMap; | |
| 4 | import java.util.Map; |
|
4 | import java.util.Map; | |
| 5 | import java.util.function.Function; |
|
5 | import java.util.function.Function; | |
| 6 |
|
||||
| 7 | import org.gradle.api.Action; |
|
6 | import org.gradle.api.Action; | |
| 8 | import org.gradle.api.provider.ListProperty; |
|
7 | import org.gradle.api.provider.ListProperty; | |
| 9 | import org.gradle.api.provider.MapProperty; |
|
8 | import org.gradle.api.provider.MapProperty; | |
| @@ -17,7 +16,24 public final class Properties { | |||||
| 17 | mergeMap(property, map, Function.identity()); |
|
16 | mergeMap(property, map, Function.identity()); | |
| 18 | } |
|
17 | } | |
| 19 |
|
18 | |||
| 20 |
public static <K, V> void |
|
19 | public static <K, V> void putMapEntry(MapProperty<K, V> property, K key, Object value, | |
|
|
20 | Function<Object, ? extends V> mapper) { | |||
|
|
21 | if (value instanceof Provider<?>) | |||
|
|
22 | property.put(key, ((Provider<?>) value).map(mapper::apply)); | |||
|
|
23 | else | |||
|
|
24 | property.put(key, mapper.apply(value)); | |||
|
|
25 | } | |||
|
|
26 | ||||
|
|
27 | public static <K> void putMapEntry(MapProperty<K, Object> property, K key, Object value) { | |||
|
|
28 | putMapEntry(property, key, value, Function.identity()); | |||
|
|
29 | } | |||
|
|
30 | ||||
|
|
31 | public static <K, V> void putMapEntry(MapProperty<K, V> property, K key, Object value, Class<V> valueType) { | |||
|
|
32 | putMapEntry(property, key, value, valueType::cast); | |||
|
|
33 | } | |||
|
|
34 | ||||
|
|
35 | public static <K, V> void mergeMap(MapProperty<K, V> property, Map<K, ?> map, | |||
|
|
36 | Function<Object, ? extends V> mapper) { | |||
| 21 | map.forEach((k, v) -> { |
|
37 | map.forEach((k, v) -> { | |
| 22 | if (v instanceof Provider<?>) |
|
38 | if (v instanceof Provider<?>) | |
| 23 | property.put(k, ((Provider<?>) v).map(mapper::apply)); |
|
39 | property.put(k, ((Provider<?>) v).map(mapper::apply)); | |
| @@ -30,7 +46,8 public final class Properties { | |||||
| 30 | mergeList(property, values, Function.identity()); |
|
46 | mergeList(property, values, Function.identity()); | |
| 31 | } |
|
47 | } | |
| 32 |
|
48 | |||
| 33 |
public static <V> void mergeList(ListProperty<V> property, Iterable<Object> values, |
|
49 | public static <V> void mergeList(ListProperty<V> property, Iterable<Object> values, | |
|
|
50 | Function<Object, ? extends V> mapper) { | |||
| 34 | values.forEach(v -> { |
|
51 | values.forEach(v -> { | |
| 35 | if (v instanceof Provider<?>) |
|
52 | if (v instanceof Provider<?>) | |
| 36 | property.add(((Provider<?>) v).map(mapper::apply)); |
|
53 | property.add(((Provider<?>) v).map(mapper::apply)); | |
| @@ -39,11 +56,12 public final class Properties { | |||||
| 39 | }); |
|
56 | }); | |
| 40 | } |
|
57 | } | |
| 41 |
|
58 | |||
| 42 | public static <K> void configureMap(MapProperty<K, Object> prop, Action<Map<K, Object>> configure) { |
|
59 | public static <K> void configureMap(MapProperty<K, Object> prop, Action<? super Map<K, Object>> configure) { | |
| 43 | configureMap(prop, configure, Function.identity()); |
|
60 | configureMap(prop, configure, Function.identity()); | |
| 44 | } |
|
61 | } | |
| 45 |
|
62 | |||
| 46 |
public static <K, V> void configureMap(MapProperty<K, V> prop, Action<Map<K, Object>> configure, |
|
63 | public static <K, V> void configureMap(MapProperty<K, V> prop, Action<? super Map<K, Object>> configure, | |
|
|
64 | Function<Object, V> mapper) { | |||
| 47 | var map = new HashMap<K, Object>(); |
|
65 | var map = new HashMap<K, Object>(); | |
| 48 | configure.execute(map); |
|
66 | configure.execute(map); | |
| 49 | mergeMap(prop, map, mapper); |
|
67 | mergeMap(prop, map, mapper); | |
| @@ -4,10 +4,7 import java.text.MessageFormat; | |||||
| 4 | import java.util.Iterator; |
|
4 | import java.util.Iterator; | |
| 5 | import java.util.Spliterators; |
|
5 | import java.util.Spliterators; | |
| 6 | import java.util.Optional; |
|
6 | import java.util.Optional; | |
| 7 | import java.util.Set; |
|
|||
| 8 | import java.util.function.Function; |
|
|||
| 9 | import java.util.function.Supplier; |
|
7 | import java.util.function.Supplier; | |
| 10 | import java.util.stream.Collectors; |
|
|||
| 11 | import java.util.stream.Stream; |
|
8 | import java.util.stream.Stream; | |
| 12 | import java.util.stream.StreamSupport; |
|
9 | import java.util.stream.StreamSupport; | |
| 13 |
|
10 | |||
| @@ -60,6 +57,24 public final class Values { | |||||
| 60 | return provider.get(); |
|
57 | return provider.get(); | |
| 61 | } |
|
58 | } | |
| 62 |
|
59 | |||
|
|
60 | public static boolean parseBoolean(Object value) { | |||
|
|
61 | if (value instanceof Boolean) { | |||
|
|
62 | return (Boolean) value; | |||
|
|
63 | } else { | |||
|
|
64 | var text = toString(value); | |||
|
|
65 | switch (text != null ? text.toLowerCase() : "") { | |||
|
|
66 | case "true", "yes", "1" -> { | |||
|
|
67 | return true; | |||
|
|
68 | } | |||
|
|
69 | case "false", "no", "0", "" -> { | |||
|
|
70 | return false; | |||
|
|
71 | } | |||
|
|
72 | default -> throw new IllegalArgumentException( | |||
|
|
73 | MessageFormat.format("Cannot parse boolean value from ''{0}''", text)); | |||
|
|
74 | } | |||
|
|
75 | } | |||
|
|
76 | } | |||
|
|
77 | ||||
| 63 | private static class ArrayIterator<T> implements Iterator<T> { |
|
78 | private static class ArrayIterator<T> implements Iterator<T> { | |
| 64 | private final T[] data; |
|
79 | private final T[] data; | |
| 65 |
|
80 | |||
| @@ -1,15 +1,23 | |||||
| 1 | package org.implab.gradle.common.utils.os; |
|
1 | package org.implab.gradle.common.utils.os; | |
| 2 |
|
2 | |||
|
|
3 | import org.implab.gradle.common.utils.LazyValue; | |||
| 3 | import org.implab.gradle.common.utils.OperatingSystem; |
|
4 | import org.implab.gradle.common.utils.OperatingSystem; | |
| 4 |
|
5 | |||
| 5 | public class SystemResolver { |
|
6 | public class SystemResolver { | |
| 6 | public static OperatingSystem current() { |
|
7 | ||
|
|
8 | private final static LazyValue<OperatingSystem> current = new LazyValue<>(SystemResolver::resolveCurrent); | |||
|
|
9 | ||||
|
|
10 | private static OperatingSystem resolveCurrent() { | |||
| 7 | var osName = System.getProperty("os.name"); |
|
11 | var osName = System.getProperty("os.name"); | |
| 8 | var osVersion = System.getProperty("os.version"); |
|
12 | var osVersion = System.getProperty("os.version"); | |
| 9 |
|
13 | |||
| 10 | return forName(osName, osVersion); |
|
14 | return forName(osName, osVersion); | |
| 11 | } |
|
15 | } | |
| 12 |
|
16 | |||
|
|
17 | public static OperatingSystem current() { | |||
|
|
18 | return current.get(); | |||
|
|
19 | } | |||
|
|
20 | ||||
| 13 | public static OperatingSystem forName(String os, String version) { |
|
21 | public static OperatingSystem forName(String os, String version) { | |
| 14 | var osName = os.toLowerCase(); |
|
22 | var osName = os.toLowerCase(); | |
| 15 |
|
23 | |||
| 1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
| 1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
General Comments 0
You need to be logged in to leave comments.
Login now
