| @@ -1,7 +1,8 | |||
|
|
1 | 1 | { |
|
|
2 | 2 | "java.configuration.updateBuildConfiguration": "automatic", |
|
|
3 | 3 | "java.compile.nullAnalysis.mode": "automatic", |
|
|
4 | 4 | "cSpell.words": [ |
|
|
5 | "implab" | |
|
|
5 | "implab", | |
|
|
6 | "rawtypes" | |
|
|
6 | 7 | ] |
|
|
7 | 8 | } No newline at end of file |
| @@ -1,15 +1,15 | |||
|
|
1 |
package org.implab.gradle.common. |
|
|
|
1 | package org.implab.gradle.common.core.gradle; | |
|
|
2 | 2 | |
|
|
3 | 3 | import java.util.function.Consumer; |
|
|
4 | 4 | |
|
|
5 | 5 | import org.gradle.api.plugins.ExtensionContainer; |
|
|
6 | 6 | |
|
|
7 | 7 | public final class Extensions { |
|
|
8 | 8 | private Extensions() { |
|
|
9 | 9 | } |
|
|
10 | 10 | |
|
|
11 | 11 | public static Consumer<Class<?>> registerClass(ExtensionContainer extensions) { |
|
|
12 | 12 | var extra = extensions.getExtraProperties(); |
|
|
13 | 13 | return clazz -> extra.set(clazz.getSimpleName(), clazz); |
|
|
14 | 14 | } |
|
|
15 | 15 | } |
| @@ -1,69 +1,69 | |||
|
|
1 |
package org.implab.gradle.common. |
|
|
|
1 | package org.implab.gradle.common.core.gradle; | |
|
|
2 | 2 | |
|
|
3 | 3 | import java.util.HashMap; |
|
|
4 | 4 | import java.util.Map; |
|
|
5 | 5 | import java.util.function.Function; |
|
|
6 | 6 | import org.gradle.api.Action; |
|
|
7 | 7 | import org.gradle.api.provider.ListProperty; |
|
|
8 | 8 | import org.gradle.api.provider.MapProperty; |
|
|
9 | 9 | import org.gradle.api.provider.Provider; |
|
|
10 | 10 | |
|
|
11 | 11 | public final class Properties { |
|
|
12 | 12 | private Properties() { |
|
|
13 | 13 | } |
|
|
14 | 14 | |
|
|
15 | 15 | public static <K> void mergeMap(MapProperty<K, Object> property, Map<K, Object> map) { |
|
|
16 | 16 | mergeMap(property, map, Function.identity()); |
|
|
17 | 17 | } |
|
|
18 | 18 | |
|
|
19 | 19 | public static <K, V> void putMapEntry(MapProperty<K, V> property, K key, Object value, |
|
|
20 | 20 | Function<Object, ? extends V> mapper) { |
|
|
21 | 21 | if (value instanceof Provider<?>) |
|
|
22 | 22 | property.put(key, ((Provider<?>) value).map(mapper::apply)); |
|
|
23 | 23 | else |
|
|
24 | 24 | property.put(key, mapper.apply(value)); |
|
|
25 | 25 | } |
|
|
26 | 26 | |
|
|
27 | 27 | public static <K> void putMapEntry(MapProperty<K, Object> property, K key, Object value) { |
|
|
28 | 28 | putMapEntry(property, key, value, Function.identity()); |
|
|
29 | 29 | } |
|
|
30 | 30 | |
|
|
31 | 31 | public static <K, V> void putMapEntry(MapProperty<K, V> property, K key, Object value, Class<V> valueType) { |
|
|
32 | 32 | putMapEntry(property, key, value, valueType::cast); |
|
|
33 | 33 | } |
|
|
34 | 34 | |
|
|
35 | 35 | public static <K, V> void mergeMap(MapProperty<K, V> property, Map<K, ?> map, |
|
|
36 | 36 | Function<Object, ? extends V> mapper) { |
|
|
37 | 37 | map.forEach((k, v) -> { |
|
|
38 | 38 | if (v instanceof Provider<?>) |
|
|
39 | 39 | property.put(k, ((Provider<?>) v).map(mapper::apply)); |
|
|
40 | 40 | else |
|
|
41 | 41 | property.put(k, mapper.apply(v)); |
|
|
42 | 42 | }); |
|
|
43 | 43 | } |
|
|
44 | 44 | |
|
|
45 | 45 | public static void mergeList(ListProperty<Object> property, Iterable<Object> values) { |
|
|
46 | 46 | mergeList(property, values, Function.identity()); |
|
|
47 | 47 | } |
|
|
48 | 48 | |
|
|
49 | 49 | public static <V> void mergeList(ListProperty<V> property, Iterable<Object> values, |
|
|
50 | 50 | Function<Object, ? extends V> mapper) { |
|
|
51 | 51 | values.forEach(v -> { |
|
|
52 | 52 | if (v instanceof Provider<?>) |
|
|
53 | 53 | property.add(((Provider<?>) v).map(mapper::apply)); |
|
|
54 | 54 | else |
|
|
55 | 55 | property.add(mapper.apply(v)); |
|
|
56 | 56 | }); |
|
|
57 | 57 | } |
|
|
58 | 58 | |
|
|
59 | 59 | public static <K> void configureMap(MapProperty<K, Object> prop, Action<? super Map<K, Object>> configure) { |
|
|
60 | 60 | configureMap(prop, configure, Function.identity()); |
|
|
61 | 61 | } |
|
|
62 | 62 | |
|
|
63 | 63 | public static <K, V> void configureMap(MapProperty<K, V> prop, Action<? super Map<K, Object>> configure, |
|
|
64 | 64 | Function<Object, V> mapper) { |
|
|
65 | 65 | var map = new HashMap<K, Object>(); |
|
|
66 | 66 | configure.execute(map); |
|
|
67 | 67 | mergeMap(prop, map, mapper); |
|
|
68 | 68 | } |
|
|
69 | 69 | } |
| @@ -1,29 +1,29 | |||
|
|
1 |
package org.implab.gradle.common. |
|
|
|
1 | package org.implab.gradle.common.core.gradle; | |
|
|
2 | 2 | |
|
|
3 | 3 | import org.gradle.api.Project; |
|
|
4 | 4 | import org.gradle.api.Task; |
|
|
5 | 5 | import org.gradle.api.logging.Logger; |
|
|
6 | 6 | import org.gradle.api.specs.Spec; |
|
|
7 | 7 | import org.gradle.api.tasks.Internal; |
|
|
8 | 8 | |
|
|
9 | 9 | /** Task methods available by default, this interface is used by mixins to |
|
|
10 | 10 | * interact with their task. |
|
|
11 | 11 | */ |
|
|
12 | 12 | public interface TaskExtra { |
|
|
13 | 13 | @Internal |
|
|
14 | 14 | Project getProject(); |
|
|
15 | 15 | |
|
|
16 | 16 | void onlyIf(Spec<? super Task> spec); |
|
|
17 | 17 | |
|
|
18 | 18 | @Internal |
|
|
19 | 19 | Logger getLogger(); |
|
|
20 | 20 | |
|
|
21 | 21 | default void onlyIfReason(String skipReason, Spec<? super Task> spec) { |
|
|
22 | 22 | onlyIf(self -> { |
|
|
23 | 23 | var satisfied = spec.isSatisfiedBy(self); |
|
|
24 | 24 | if (!satisfied) |
|
|
25 | 25 | getLogger().info("SKIP: {}", skipReason); |
|
|
26 | 26 | return satisfied; |
|
|
27 | 27 | }); |
|
|
28 | 28 | } |
|
|
29 | 29 | } |
| @@ -1,31 +1,31 | |||
|
|
1 |
package org.implab.gradle.common. |
|
|
|
1 | package org.implab.gradle.common.core.lang; | |
|
|
2 | 2 | |
|
|
3 | 3 | import groovy.lang.Closure; |
|
|
4 | 4 | |
|
|
5 | 5 | import org.eclipse.jdt.annotation.NonNullByDefault; |
|
|
6 | 6 | import org.gradle.api.Action; |
|
|
7 | 7 | |
|
|
8 | 8 | @NonNullByDefault |
|
|
9 | 9 | public final class Closures { |
|
|
10 | 10 | private Closures() { |
|
|
11 | 11 | } |
|
|
12 | 12 | |
|
|
13 | 13 | /** |
|
|
14 | 14 | * Wraps {@link Action} around the specified closure. The parameter |
|
|
15 | 15 | * of the action will be used as delegate in the specified closure. |
|
|
16 | 16 | * |
|
|
17 | 17 | * @param <T> The type of the action parameter |
|
|
18 | 18 | * @param closure The closure |
|
|
19 | 19 | * @return |
|
|
20 | 20 | */ |
|
|
21 | 21 | public static <T> Action<T> action(Closure<?> closure) { |
|
|
22 | 22 | return arg -> apply(closure, arg); |
|
|
23 | 23 | } |
|
|
24 | 24 | |
|
|
25 | 25 | public static void apply(Closure<?> action, Object target) { |
|
|
26 | 26 | var c = (Closure<?>)action.clone(); |
|
|
27 | 27 | c.setResolveStrategy(Closure.DELEGATE_FIRST); |
|
|
28 | 28 | c.setDelegate(target); |
|
|
29 | 29 | c.call(target); |
|
|
30 | 30 | } |
|
|
31 | 31 | } |
| @@ -1,51 +1,50 | |||
|
|
1 |
package org.implab.gradle.common. |
|
|
|
1 | package org.implab.gradle.common.core.lang; | |
|
|
2 | 2 | |
|
|
3 | 3 | import java.util.function.Consumer; |
|
|
4 | 4 | import java.util.function.Function; |
|
|
5 | 5 | |
|
|
6 | 6 | public class Exceptions { |
|
|
7 | 7 | /** |
|
|
8 | 8 | * Helper function which declares that this block can throw the specified |
|
|
9 | 9 | * exception. |
|
|
10 | 10 | * |
|
|
11 | 11 | * @param <E> |
|
|
12 | 12 | * @param clazz |
|
|
13 | 13 | * @throws E |
|
|
14 | 14 | */ |
|
|
15 | @SuppressWarnings("unused") | |
|
|
16 | 15 | public static <E extends Throwable> void mayThrow(Class<E> clazz) throws E { |
|
|
17 | 16 | } |
|
|
18 | 17 | |
|
|
19 | 18 | @SuppressWarnings("unchecked") |
|
|
20 | 19 | public static <E extends Throwable> E sneakyThrow(Throwable t) throws E { |
|
|
21 | 20 | throw (E) t; |
|
|
22 | 21 | } |
|
|
23 | 22 | |
|
|
24 | 23 | public static <T, U> Function<T, U> unchecked(ThrowingFunction<T, U> fn) { |
|
|
25 | 24 | return val -> { |
|
|
26 | 25 | try { |
|
|
27 | 26 | return fn.apply(val); |
|
|
28 | 27 | } catch (Exception e) { |
|
|
29 | 28 | throw sneakyThrow(e); |
|
|
30 | 29 | } |
|
|
31 | 30 | }; |
|
|
32 | 31 | } |
|
|
33 | 32 | |
|
|
34 | 33 | public static <T> Consumer<T> unchecked(ThrowingConsumer<T> c) { |
|
|
35 | 34 | return val -> { |
|
|
36 | 35 | try { |
|
|
37 | 36 | c.accept(val); |
|
|
38 | 37 | } catch (Exception e) { |
|
|
39 | 38 | throw sneakyThrow(e); |
|
|
40 | 39 | } |
|
|
41 | 40 | }; |
|
|
42 | 41 | } |
|
|
43 | 42 | |
|
|
44 | 43 | public interface ThrowingConsumer<T> { |
|
|
45 | 44 | void accept(T value) throws Exception; |
|
|
46 | 45 | } |
|
|
47 | 46 | |
|
|
48 | 47 | public interface ThrowingFunction<T, U> { |
|
|
49 | 48 | U apply(T value) throws Exception; |
|
|
50 | 49 | } |
|
|
51 | 50 | } |
| @@ -1,29 +1,34 | |||
|
|
1 |
package org.implab.gradle.common. |
|
|
|
1 | package org.implab.gradle.common.core.lang; | |
|
|
2 | 2 | |
|
|
3 | import java.util.concurrent.atomic.AtomicReference; | |
|
|
3 | import java.util.Objects; | |
|
|
4 | 4 | import java.util.function.Supplier; |
|
|
5 | 5 | |
|
|
6 | 6 | public class LazyValue<T> implements Supplier<T> { |
|
|
7 | private final AtomicReference<T> reference = new AtomicReference<>(); | |
|
|
7 | private volatile T value; | |
|
|
8 | 8 | |
|
|
9 | 9 | private final Supplier<T> innerSupplier; |
|
|
10 | 10 | |
|
|
11 | 11 | public LazyValue(Supplier<T> supplier) { |
|
|
12 | 12 | this.innerSupplier = supplier; |
|
|
13 | 13 | } |
|
|
14 | 14 | |
|
|
15 | 15 | @Override |
|
|
16 | 16 | public T get() { |
|
|
17 |
var |
|
|
|
18 |
if ( |
|
|
|
19 |
return |
|
|
|
20 | return updateValue(); | |
|
|
21 | } | |
|
|
17 | var v = value; | |
|
|
18 | if (v != null) { | |
|
|
19 | return v; | |
|
|
20 | } | |
|
|
22 | 21 | |
|
|
23 | private T updateValue() { | |
|
|
24 | var v = innerSupplier.get(); | |
|
|
25 | var t = reference.compareAndExchange(null, v); | |
|
|
26 | return t != null ? t : v; | |
|
|
22 | synchronized (this) { | |
|
|
23 | v = value; | |
|
|
24 | if (v == null) { | |
|
|
25 | v = Objects.requireNonNull( | |
|
|
26 | innerSupplier.get(), | |
|
|
27 | "LazyValue supplier returned null"); | |
|
|
28 | value = v; | |
|
|
29 | } | |
|
|
30 | return v; | |
|
|
31 | } | |
|
|
27 | 32 | } |
|
|
28 | 33 | |
|
|
29 | 34 | } |
| @@ -1,316 +1,316 | |||
|
|
1 |
package org.implab.gradle.common. |
|
|
|
1 | package org.implab.gradle.common.core.lang; | |
|
|
2 | 2 | |
|
|
3 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; |
|
|
4 | 4 | import com.fasterxml.jackson.annotation.JsonValue; |
|
|
5 | 5 | |
|
|
6 | 6 | import java.util.Objects; |
|
|
7 | 7 | import java.util.Optional; |
|
|
8 | 8 | import java.util.regex.Matcher; |
|
|
9 | 9 | import java.util.regex.Pattern; |
|
|
10 | 10 | |
|
|
11 | 11 | /** |
|
|
12 | 12 | * Immutable Semantic Version (SemVer 2.0.0) of the form: |
|
|
13 | 13 | * |
|
|
14 | 14 | * <pre> |
|
|
15 | 15 | * MAJOR.MINOR.PATCH[-PRERELEASE][+BUILD] |
|
|
16 | 16 | * </pre> |
|
|
17 | 17 | * |
|
|
18 | 18 | * Ordering (as defined by {@link #compareTo(SemVersion)}) follows |
|
|
19 | 19 | * the SemVer 2.0.0 precedence rules: |
|
|
20 | 20 | * <ul> |
|
|
21 | 21 | * <li>Compare {@code major}, then {@code minor}, then {@code patch}.</li> |
|
|
22 | 22 | * <li>Pre-release versions have lower precedence than the corresponding |
|
|
23 | 23 | * normal version.</li> |
|
|
24 | 24 | * <li>Build metadata does not affect precedence.</li> |
|
|
25 | 25 | * </ul> |
|
|
26 | 26 | * |
|
|
27 | 27 | * Public API does not use {@code null} for semantic values: |
|
|
28 | 28 | * {@link Optional} is used for pre-release and build metadata. |
|
|
29 | 29 | */ |
|
|
30 | 30 | public record SemVersion( |
|
|
31 | 31 | int major, |
|
|
32 | 32 | int minor, |
|
|
33 | 33 | int patch, |
|
|
34 | 34 | Optional<String> preRelease, |
|
|
35 | 35 | Optional<String> buildMetadata) implements Comparable<SemVersion> { |
|
|
36 | 36 | |
|
|
37 | 37 | // Pattern close to the official SemVer 2.0.0 recommendation. |
|
|
38 | 38 | // Groups: |
|
|
39 | 39 | // 1 - major |
|
|
40 | 40 | // 2 - minor |
|
|
41 | 41 | // 3 - patch |
|
|
42 | 42 | // 4 - pre-release (without '-') |
|
|
43 | 43 | // 5 - build metadata (without '+') |
|
|
44 | 44 | private static final Pattern SEMVER_PATTERN = Pattern.compile( |
|
|
45 | 45 | "^(0|[1-9]\\d*)" + // major |
|
|
46 | 46 | "\\.(0|[1-9]\\d*)" + // minor |
|
|
47 | 47 | "\\.(0|[1-9]\\d*)" + // patch |
|
|
48 | 48 | "(?:-((?:0|[1-9]\\d*|[0-9A-Za-z-][0-9A-Za-z-]*)" + |
|
|
49 | 49 | "(?:\\.(?:0|[1-9]\\d*|[0-9A-Za-z-][0-9A-Za-z-]*))*" + |
|
|
50 | 50 | "))?" + // pre-release |
|
|
51 | 51 | "(?:\\+([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?$" // build metadata |
|
|
52 | 52 | ); |
|
|
53 | 53 | |
|
|
54 | 54 | /** |
|
|
55 | 55 | * Compact constructor with basic invariants. |
|
|
56 | 56 | */ |
|
|
57 | 57 | public SemVersion { |
|
|
58 | 58 | if (major < 0 || minor < 0 || patch < 0) { |
|
|
59 | 59 | throw new IllegalArgumentException("Version numbers must be >= 0"); |
|
|
60 | 60 | } |
|
|
61 | 61 | // Be tolerant internally, but never expose null Optionals. |
|
|
62 | 62 | preRelease = (preRelease != null) ? preRelease : Optional.empty(); |
|
|
63 | 63 | buildMetadata = (buildMetadata != null) ? buildMetadata : Optional.empty(); |
|
|
64 | 64 | } |
|
|
65 | 65 | |
|
|
66 | 66 | /** |
|
|
67 | 67 | * Creates a version without pre-release and build metadata. |
|
|
68 | 68 | * |
|
|
69 | 69 | * @param major non-negative MAJOR number |
|
|
70 | 70 | * @param minor non-negative MINOR number |
|
|
71 | 71 | * @param patch non-negative PATCH number |
|
|
72 | 72 | */ |
|
|
73 | 73 | public static SemVersion of(int major, int minor, int patch) { |
|
|
74 | 74 | return new SemVersion(major, minor, patch, Optional.empty(), Optional.empty()); |
|
|
75 | 75 | } |
|
|
76 | 76 | |
|
|
77 | 77 | /** |
|
|
78 | 78 | * Creates a version from components. |
|
|
79 | 79 | * |
|
|
80 | 80 | * @param major non-negative MAJOR number |
|
|
81 | 81 | * @param minor non-negative MINOR number |
|
|
82 | 82 | * @param patch non-negative PATCH number |
|
|
83 | 83 | * @param preRelease optional pre-release part (without '-') |
|
|
84 | 84 | * @param buildMetadata optional build metadata (without '+') |
|
|
85 | 85 | */ |
|
|
86 | 86 | public static SemVersion of( |
|
|
87 | 87 | int major, |
|
|
88 | 88 | int minor, |
|
|
89 | 89 | int patch, |
|
|
90 | 90 | Optional<String> preRelease, |
|
|
91 | 91 | Optional<String> buildMetadata) { |
|
|
92 | 92 | return new SemVersion( |
|
|
93 | 93 | major, |
|
|
94 | 94 | minor, |
|
|
95 | 95 | patch, |
|
|
96 | 96 | Objects.requireNonNull(preRelease, "preRelease"), |
|
|
97 | 97 | Objects.requireNonNull(buildMetadata, "buildMetadata")); |
|
|
98 | 98 | } |
|
|
99 | 99 | |
|
|
100 | 100 | /** |
|
|
101 | 101 | * Parses a SemVer string of the form |
|
|
102 | 102 | * {@code MAJOR.MINOR.PATCH[-PRERELEASE][+BUILD]}. |
|
|
103 | 103 | * |
|
|
104 | 104 | * @param value the string to parse (must not be {@code null}) |
|
|
105 | 105 | * @return a new {@code SemVersion} |
|
|
106 | 106 | * @throws IllegalArgumentException if the string is not a valid SemVer |
|
|
107 | 107 | */ |
|
|
108 | 108 | public static SemVersion parse(String value) { |
|
|
109 | 109 | Objects.requireNonNull(value, "value"); |
|
|
110 | 110 | |
|
|
111 | 111 | Matcher m = SEMVER_PATTERN.matcher(value); |
|
|
112 | 112 | if (!m.matches()) { |
|
|
113 | 113 | throw new IllegalArgumentException("Invalid SemVer: " + value); |
|
|
114 | 114 | } |
|
|
115 | 115 | |
|
|
116 | 116 | int major = Integer.parseInt(m.group(1)); |
|
|
117 | 117 | int minor = Integer.parseInt(m.group(2)); |
|
|
118 | 118 | int patch = Integer.parseInt(m.group(3)); |
|
|
119 | 119 | |
|
|
120 | 120 | String pre = m.group(4); |
|
|
121 | 121 | String meta = m.group(5); |
|
|
122 | 122 | |
|
|
123 | 123 | Optional<String> preOpt = Optional.ofNullable(pre); |
|
|
124 | 124 | Optional<String> metaOpt = Optional.ofNullable(meta); |
|
|
125 | 125 | |
|
|
126 | 126 | // Extra validation for numeric pre-release identifiers (no leading zeros) |
|
|
127 | 127 | preOpt.ifPresent(SemVersion::validatePreReleaseIdentifiers); |
|
|
128 | 128 | |
|
|
129 | 129 | return new SemVersion(major, minor, patch, preOpt, metaOpt); |
|
|
130 | 130 | } |
|
|
131 | 131 | |
|
|
132 | 132 | private static void validatePreReleaseIdentifiers(String preRelease) { |
|
|
133 | 133 | String[] parts = preRelease.split("\\."); |
|
|
134 | 134 | for (String p : parts) { |
|
|
135 | 135 | if (isNumericIdentifier(p)) { |
|
|
136 | 136 | // numeric identifiers must not have leading zeros (except "0") |
|
|
137 | 137 | if (p.length() > 1 && p.charAt(0) == '0') { |
|
|
138 | 138 | throw new IllegalArgumentException( |
|
|
139 | 139 | "Numeric pre-release identifier must not contain leading zeros: " + p); |
|
|
140 | 140 | } |
|
|
141 | 141 | } |
|
|
142 | 142 | } |
|
|
143 | 143 | } |
|
|
144 | 144 | |
|
|
145 | 145 | private static boolean isNumericIdentifier(String s) { |
|
|
146 | 146 | int len = s.length(); |
|
|
147 | 147 | if (len == 0) { |
|
|
148 | 148 | return false; |
|
|
149 | 149 | } |
|
|
150 | 150 | for (int i = 0; i < len; i++) { |
|
|
151 | 151 | char c = s.charAt(i); |
|
|
152 | 152 | if (c < '0' || c > '9') { |
|
|
153 | 153 | return false; |
|
|
154 | 154 | } |
|
|
155 | 155 | } |
|
|
156 | 156 | return true; |
|
|
157 | 157 | } |
|
|
158 | 158 | |
|
|
159 | 159 | /** |
|
|
160 | 160 | * Returns {@code true} if this version has a pre-release part. |
|
|
161 | 161 | * |
|
|
162 | 162 | * @return {@code true} if {@link #preRelease()} is present |
|
|
163 | 163 | */ |
|
|
164 | 164 | public boolean isPreRelease() { |
|
|
165 | 165 | return preRelease().isPresent(); |
|
|
166 | 166 | } |
|
|
167 | 167 | |
|
|
168 | 168 | /** |
|
|
169 | 169 | * Returns {@code true} if this version is considered "stable". |
|
|
170 | 170 | * <p> |
|
|
171 | 171 | * By convention: |
|
|
172 | 172 | * <ul> |
|
|
173 | 173 | * <li>MAJOR must be greater than 0</li> |
|
|
174 | 174 | * <li>No pre-release part is present</li> |
|
|
175 | 175 | * </ul> |
|
|
176 | 176 | * |
|
|
177 | 177 | * @return {@code true} if this version is a stable release |
|
|
178 | 178 | */ |
|
|
179 | 179 | public boolean isStable() { |
|
|
180 | 180 | return !isPreRelease() && major() > 0; |
|
|
181 | 181 | } |
|
|
182 | 182 | |
|
|
183 | 183 | /** |
|
|
184 | 184 | * Returns {@code true} if this version has strictly lower precedence |
|
|
185 | 185 | * than the given {@code other} version according to |
|
|
186 | 186 | * {@link #compareTo(SemVersion)}. |
|
|
187 | 187 | * |
|
|
188 | 188 | * @param other the version to compare to |
|
|
189 | 189 | * @return {@code true} if {@code this.compareTo(other) < 0} |
|
|
190 | 190 | */ |
|
|
191 | 191 | public boolean isBefore(SemVersion other) { |
|
|
192 | 192 | return compareTo(other) < 0; |
|
|
193 | 193 | } |
|
|
194 | 194 | |
|
|
195 | 195 | /** |
|
|
196 | 196 | * Returns {@code true} if this version has strictly higher precedence |
|
|
197 | 197 | * than the given {@code other} version according to |
|
|
198 | 198 | * {@link #compareTo(SemVersion)}. |
|
|
199 | 199 | * |
|
|
200 | 200 | * @param other the version to compare to |
|
|
201 | 201 | * @return {@code true} if {@code this.compareTo(other) > 0} |
|
|
202 | 202 | */ |
|
|
203 | 203 | public boolean isAfter(SemVersion other) { |
|
|
204 | 204 | return compareTo(other) > 0; |
|
|
205 | 205 | } |
|
|
206 | 206 | |
|
|
207 | 207 | /** |
|
|
208 | 208 | * Canonical SemVer string representation. |
|
|
209 | 209 | * <p> |
|
|
210 | 210 | * This method is also used by Jackson during serialization. |
|
|
211 | 211 | * |
|
|
212 | 212 | * @return canonical SemVer string, e.g. {@code "1.2.3-alpha+build.1"} |
|
|
213 | 213 | */ |
|
|
214 | 214 | @JsonValue |
|
|
215 | 215 | public String asString() { |
|
|
216 | 216 | StringBuilder sb = new StringBuilder() |
|
|
217 | 217 | .append(major).append('.') |
|
|
218 | 218 | .append(minor).append('.') |
|
|
219 | 219 | .append(patch); |
|
|
220 | 220 | |
|
|
221 | 221 | preRelease.ifPresent(pr -> sb.append('-').append(pr)); |
|
|
222 | 222 | buildMetadata.ifPresent(md -> sb.append('+').append(md)); |
|
|
223 | 223 | |
|
|
224 | 224 | return sb.toString(); |
|
|
225 | 225 | } |
|
|
226 | 226 | |
|
|
227 | 227 | /** |
|
|
228 | 228 | * Creates a {@code SemVersion} from a canonical SemVer string. |
|
|
229 | 229 | * <p> |
|
|
230 | 230 | * Jackson will use this factory method when deserializing from a JSON string. |
|
|
231 | 231 | * |
|
|
232 | 232 | * @param value canonical SemVer string |
|
|
233 | 233 | * @return parsed {@code SemVersion} |
|
|
234 | 234 | */ |
|
|
235 | 235 | @JsonCreator(mode = JsonCreator.Mode.DELEGATING) |
|
|
236 | 236 | public static SemVersion fromJson(String value) { |
|
|
237 | 237 | return parse(value); |
|
|
238 | 238 | } |
|
|
239 | 239 | |
|
|
240 | 240 | /** |
|
|
241 | 241 | * Compares this version to another {@link SemVersion} according to the |
|
|
242 | 242 | * SemVer 2.0.0 precedence rules. |
|
|
243 | 243 | * <p> |
|
|
244 | 244 | * Build metadata is ignored for ordering. |
|
|
245 | 245 | */ |
|
|
246 | 246 | @Override |
|
|
247 | 247 | public int compareTo(SemVersion other) { |
|
|
248 | 248 | // 1. major / minor / patch |
|
|
249 | 249 | int c = Integer.compare(this.major, other.major); |
|
|
250 | 250 | if (c != 0) |
|
|
251 | 251 | return c; |
|
|
252 | 252 | |
|
|
253 | 253 | c = Integer.compare(this.minor, other.minor); |
|
|
254 | 254 | if (c != 0) |
|
|
255 | 255 | return c; |
|
|
256 | 256 | |
|
|
257 | 257 | c = Integer.compare(this.patch, other.patch); |
|
|
258 | 258 | if (c != 0) |
|
|
259 | 259 | return c; |
|
|
260 | 260 | |
|
|
261 | 261 | // 2. pre-release (absence > presence) |
|
|
262 | 262 | boolean thisHasPre = this.preRelease.isPresent(); |
|
|
263 | 263 | boolean otherHasPre = other.preRelease.isPresent(); |
|
|
264 | 264 | |
|
|
265 | 265 | if (!thisHasPre && !otherHasPre) { |
|
|
266 | 266 | return 0; |
|
|
267 | 267 | } else if (!thisHasPre) { |
|
|
268 | 268 | // normal version > pre-release |
|
|
269 | 269 | return 1; |
|
|
270 | 270 | } else if (!otherHasPre) { |
|
|
271 | 271 | return -1; |
|
|
272 | 272 | } |
|
|
273 | 273 | |
|
|
274 | 274 | // 3. both have pre-release: compare identifiers |
|
|
275 | 275 | return comparePreRelease(this.preRelease.get(), other.preRelease.get()); |
|
|
276 | 276 | } |
|
|
277 | 277 | |
|
|
278 | 278 | private static int comparePreRelease(String a, String b) { |
|
|
279 | 279 | String[] aParts = a.split("\\."); |
|
|
280 | 280 | String[] bParts = b.split("\\."); |
|
|
281 | 281 | |
|
|
282 | 282 | int len = Math.min(aParts.length, bParts.length); |
|
|
283 | 283 | for (int i = 0; i < len; i++) { |
|
|
284 | 284 | String ai = aParts[i]; |
|
|
285 | 285 | String bi = bParts[i]; |
|
|
286 | 286 | |
|
|
287 | 287 | boolean aNum = isNumericIdentifier(ai); |
|
|
288 | 288 | boolean bNum = isNumericIdentifier(bi); |
|
|
289 | 289 | |
|
|
290 | 290 | if (aNum && bNum) { |
|
|
291 | 291 | int aiVal = Integer.parseInt(ai); |
|
|
292 | 292 | int biVal = Integer.parseInt(bi); |
|
|
293 | 293 | int c = Integer.compare(aiVal, biVal); |
|
|
294 | 294 | if (c != 0) |
|
|
295 | 295 | return c; |
|
|
296 | 296 | } else if (aNum && !bNum) { |
|
|
297 | 297 | // numeric identifiers have lower precedence than non-numeric |
|
|
298 | 298 | return -1; |
|
|
299 | 299 | } else if (!aNum && bNum) { |
|
|
300 | 300 | return 1; |
|
|
301 | 301 | } else { |
|
|
302 | 302 | int c = ai.compareTo(bi); |
|
|
303 | 303 | if (c != 0) |
|
|
304 | 304 | return c; |
|
|
305 | 305 | } |
|
|
306 | 306 | } |
|
|
307 | 307 | |
|
|
308 | 308 | // If all common identifiers are equal, the shorter one has lower precedence |
|
|
309 | 309 | return Integer.compare(aParts.length, bParts.length); |
|
|
310 | 310 | } |
|
|
311 | 311 | |
|
|
312 | 312 | @Override |
|
|
313 | 313 | public String toString() { |
|
|
314 | 314 | return asString(); |
|
|
315 | 315 | } |
|
|
316 | 316 | } |
| @@ -1,54 +1,54 | |||
|
|
1 |
package org.implab.gradle.common. |
|
|
|
1 | package org.implab.gradle.common.core.lang; | |
|
|
2 | 2 | |
|
|
3 | 3 | import java.util.regex.Pattern; |
|
|
4 | 4 | |
|
|
5 | 5 | import org.eclipse.jdt.annotation.NonNullByDefault; |
|
|
6 | 6 | import org.gradle.api.provider.Provider; |
|
|
7 | 7 | |
|
|
8 | 8 | @NonNullByDefault |
|
|
9 | 9 | public class Strings { |
|
|
10 | 10 | |
|
|
11 | 11 | private static final Pattern firstLetter = Pattern.compile("^\\w"); |
|
|
12 | 12 | |
|
|
13 | 13 | public static String capitalize(String string) { |
|
|
14 | 14 | return string == null ? null |
|
|
15 | 15 | : string.length() == 0 ? string |
|
|
16 | 16 | : firstLetter.matcher(string).replaceFirst(m -> m.group().toUpperCase()); |
|
|
17 | 17 | } |
|
|
18 | 18 | |
|
|
19 | 19 | public static String toCamelCase(String name) { |
|
|
20 | 20 | if (name == null || name.isEmpty()) |
|
|
21 | 21 | return name; |
|
|
22 | 22 | StringBuilder out = new StringBuilder(name.length()); |
|
|
23 | 23 | boolean up = false; |
|
|
24 | 24 | boolean first = true; |
|
|
25 | 25 | for (int i = 0; i < name.length(); i++) { |
|
|
26 | 26 | char c = name.charAt(i); |
|
|
27 | 27 | switch (c) { |
|
|
28 | 28 | case '-', '_', ' ', '.' -> up = true; |
|
|
29 | 29 | default -> { |
|
|
30 | 30 | out.append( |
|
|
31 | 31 | first ? Character.toLowerCase(c) |
|
|
32 | 32 | : up ? Character.toUpperCase(c): c); |
|
|
33 | 33 | up = false; |
|
|
34 | 34 | first = false; |
|
|
35 | 35 | } |
|
|
36 | 36 | } |
|
|
37 | 37 | } |
|
|
38 | 38 | return out.toString(); |
|
|
39 | 39 | } |
|
|
40 | 40 | |
|
|
41 | 41 | public static void argumentNotNullOrEmpty(String value, String argumentName) { |
|
|
42 | 42 | if (value == null || value.length() == 0) |
|
|
43 | 43 | throw new IllegalArgumentException(String.format("Argument %s can't be null or empty", argumentName)); |
|
|
44 | 44 | } |
|
|
45 | 45 | |
|
|
46 | 46 | public static String asString(Object value) { |
|
|
47 | 47 | if (value == null) |
|
|
48 | 48 | return null; |
|
|
49 | 49 | if (value instanceof Provider<?> provider) |
|
|
50 | 50 | return asString(provider.get()); |
|
|
51 | 51 | else |
|
|
52 | 52 | return value.toString(); |
|
|
53 | 53 | } |
|
|
54 | 54 | } |
| @@ -1,98 +1,98 | |||
|
|
1 |
package org.implab.gradle.common. |
|
|
|
1 | package org.implab.gradle.common.core.lang; | |
|
|
2 | 2 | |
|
|
3 | 3 | import java.text.MessageFormat; |
|
|
4 | 4 | import java.util.Iterator; |
|
|
5 | 5 | import java.util.Spliterators; |
|
|
6 | 6 | import java.util.Optional; |
|
|
7 | 7 | import java.util.function.Supplier; |
|
|
8 | 8 | import java.util.stream.Stream; |
|
|
9 | 9 | import java.util.stream.StreamSupport; |
|
|
10 | 10 | |
|
|
11 | 11 | import org.gradle.api.provider.Provider; |
|
|
12 | 12 | |
|
|
13 | 13 | public final class Values { |
|
|
14 | 14 | |
|
|
15 | 15 | private Values() { |
|
|
16 | 16 | } |
|
|
17 | 17 | |
|
|
18 | 18 | /** |
|
|
19 | 19 | * Converts the supplied value to a string. |
|
|
20 | 20 | */ |
|
|
21 | 21 | public static String toString(Object value) { |
|
|
22 | 22 | if (value == null) { |
|
|
23 | 23 | return null; |
|
|
24 | 24 | } else if (value instanceof String string) { |
|
|
25 | 25 | return string; |
|
|
26 | 26 | } else if (value instanceof Provider<?> provider) { |
|
|
27 | 27 | return toString(provider.getOrNull()); |
|
|
28 | 28 | } else if (value instanceof Supplier<?> supplier) { |
|
|
29 | 29 | return toString(supplier.get()); |
|
|
30 | 30 | } else { |
|
|
31 | 31 | return value.toString(); |
|
|
32 | 32 | } |
|
|
33 | 33 | } |
|
|
34 | 34 | |
|
|
35 | 35 | public static <T> Stream<T> stream(Iterator<T> remaining) { |
|
|
36 | 36 | return StreamSupport.stream( |
|
|
37 | 37 | Spliterators.spliteratorUnknownSize(remaining, 0), |
|
|
38 | 38 | false); |
|
|
39 | 39 | } |
|
|
40 | 40 | |
|
|
41 | 41 | public static <T> Optional<T> take(Iterator<T> iterator) { |
|
|
42 | 42 | return iterator.hasNext() ? Optional.of(iterator.next()) : Optional.empty(); |
|
|
43 | 43 | } |
|
|
44 | 44 | |
|
|
45 | 45 | public static <T> Iterable<T> iterable(T[] values) { |
|
|
46 | 46 | return () -> new ArrayIterator<>(values); |
|
|
47 | 47 | } |
|
|
48 | 48 | |
|
|
49 | 49 | public static <T> Optional<T> optional(Provider<T> provider) { |
|
|
50 | 50 | return provider.isPresent() ? Optional.of(provider.get()) : Optional.empty(); |
|
|
51 | 51 | } |
|
|
52 | 52 | |
|
|
53 | 53 | public static <T> T required(Provider<T> provider, String providerName) { |
|
|
54 | 54 | if (!provider.isPresent()) |
|
|
55 | 55 | throw new IllegalStateException( |
|
|
56 | 56 | MessageFormat.format("The value for the '{0}' provider must be specified", providerName)); |
|
|
57 | 57 | return provider.get(); |
|
|
58 | 58 | } |
|
|
59 | 59 | |
|
|
60 | 60 | public static boolean parseBoolean(Object value) { |
|
|
61 | 61 | if (value instanceof Boolean) { |
|
|
62 | 62 | return (Boolean) value; |
|
|
63 | 63 | } else { |
|
|
64 | 64 | var text = toString(value); |
|
|
65 | 65 | switch (text != null ? text.toLowerCase() : "") { |
|
|
66 | 66 | case "true", "yes", "1" -> { |
|
|
67 | 67 | return true; |
|
|
68 | 68 | } |
|
|
69 | 69 | case "false", "no", "0", "" -> { |
|
|
70 | 70 | return false; |
|
|
71 | 71 | } |
|
|
72 | 72 | default -> throw new IllegalArgumentException( |
|
|
73 | 73 | MessageFormat.format("Cannot parse boolean value from ''{0}''", text)); |
|
|
74 | 74 | } |
|
|
75 | 75 | } |
|
|
76 | 76 | } |
|
|
77 | 77 | |
|
|
78 | 78 | private static class ArrayIterator<T> implements Iterator<T> { |
|
|
79 | 79 | private final T[] data; |
|
|
80 | 80 | |
|
|
81 | 81 | private int pos = 0; |
|
|
82 | 82 | |
|
|
83 | 83 | ArrayIterator(T[] data) { |
|
|
84 | 84 | this.data = data; |
|
|
85 | 85 | } |
|
|
86 | 86 | |
|
|
87 | 87 | @Override |
|
|
88 | 88 | public boolean hasNext() { |
|
|
89 | 89 | return pos < data.length; |
|
|
90 | 90 | } |
|
|
91 | 91 | |
|
|
92 | 92 | @Override |
|
|
93 | 93 | public T next() { |
|
|
94 | 94 | return data[pos++]; |
|
|
95 | 95 | } |
|
|
96 | 96 | } |
|
|
97 | 97 | |
|
|
98 | 98 | } |
| @@ -1,16 +1,14 | |||
|
|
1 |
package org.implab.gradle.common. |
|
|
|
1 | package org.implab.gradle.common.core.os; | |
|
|
2 | 2 | |
|
|
3 | import org.implab.gradle.common.utils.OperatingSystem; | |
|
|
4 | ||
|
|
5 | public class FreeBsd extends GenericSystem{ | |
|
|
3 | class FreeBsd extends GenericSystem{ | |
|
|
6 | 4 | |
|
|
7 | 5 | FreeBsd(String name, String version) { |
|
|
8 | 6 | super(name, version); |
|
|
9 | 7 | } |
|
|
10 | 8 | |
|
|
11 | 9 | @Override |
|
|
12 | 10 | public String family() { |
|
|
13 | 11 | return OperatingSystem.FREE_BSD_FAMILY; |
|
|
14 | 12 | } |
|
|
15 | 13 | |
|
|
16 | 14 | } |
| @@ -1,64 +1,63 | |||
|
|
1 |
package org.implab.gradle.common. |
|
|
|
1 | package org.implab.gradle.common.core.os; | |
|
|
2 | 2 | |
|
|
3 | 3 | import java.io.File; |
|
|
4 | 4 | import java.util.Arrays; |
|
|
5 | 5 | import java.util.Optional; |
|
|
6 | 6 | import java.util.function.Function; |
|
|
7 | 7 | import java.util.regex.Pattern; |
|
|
8 | 8 | import java.util.stream.Stream; |
|
|
9 | 9 | |
|
|
10 |
import org.implab.gradle.common. |
|
|
|
11 | import org.implab.gradle.common.utils.Values; | |
|
|
10 | import org.implab.gradle.common.core.lang.Values; | |
|
|
12 | 11 | |
|
|
13 | 12 | class GenericSystem implements OperatingSystem { |
|
|
14 | 13 | |
|
|
15 | 14 | private final String name; |
|
|
16 | 15 | |
|
|
17 | 16 | private final String version; |
|
|
18 | 17 | |
|
|
19 | 18 | GenericSystem(String name, String version) { |
|
|
20 | 19 | this.name = name; |
|
|
21 | 20 | this.version = version; |
|
|
22 | 21 | } |
|
|
23 | 22 | |
|
|
24 | 23 | public String getPathVar() { |
|
|
25 | 24 | return "PATH"; |
|
|
26 | 25 | } |
|
|
27 | 26 | |
|
|
28 | 27 | @Override |
|
|
29 | 28 | public String family() { |
|
|
30 | 29 | return OperatingSystem.UNKNOWN_FAMILY; |
|
|
31 | 30 | } |
|
|
32 | 31 | |
|
|
33 | 32 | @Override |
|
|
34 | 33 | public String name() { |
|
|
35 | 34 | return name; |
|
|
36 | 35 | } |
|
|
37 | 36 | |
|
|
38 | 37 | @Override |
|
|
39 | 38 | public String version() { |
|
|
40 | 39 | return version; |
|
|
41 | 40 | } |
|
|
42 | 41 | |
|
|
43 | 42 | protected Stream<File> getPath() { |
|
|
44 | 43 | String path = System.getenv(getPathVar()); |
|
|
45 | 44 | return path == null |
|
|
46 | 45 | ? Stream.empty() |
|
|
47 | 46 | : Arrays.stream(path.split(Pattern.quote(File.pathSeparator))) |
|
|
48 | 47 | .map(File::new); |
|
|
49 | 48 | } |
|
|
50 | 49 | |
|
|
51 | 50 | protected Function<File, Stream<File>> candidates(String cmd) { |
|
|
52 | 51 | return base -> Stream.of(new File(base, cmd)); |
|
|
53 | 52 | } |
|
|
54 | 53 | |
|
|
55 | 54 | @Override |
|
|
56 | 55 | public Optional<File> which(String cmd) { |
|
|
57 | 56 | return getPath().flatMap(candidates(cmd)).filter(File::isFile).findAny(); |
|
|
58 | 57 | } |
|
|
59 | 58 | |
|
|
60 | 59 | @Override |
|
|
61 | 60 | public Optional<File> which(String cmd, Iterable<? extends File> paths) { |
|
|
62 | 61 | return Values.stream(paths.iterator()).flatMap(candidates(cmd)).filter(File::isFile).findAny(); |
|
|
63 | 62 | } |
|
|
64 | 63 | } |
| @@ -1,16 +1,14 | |||
|
|
1 |
package org.implab.gradle.common. |
|
|
|
1 | package org.implab.gradle.common.core.os; | |
|
|
2 | 2 | |
|
|
3 | import org.implab.gradle.common.utils.OperatingSystem; | |
|
|
4 | ||
|
|
5 | public class Linux extends GenericSystem { | |
|
|
3 | class Linux extends GenericSystem { | |
|
|
6 | 4 | |
|
|
7 | 5 | Linux(String name, String version) { |
|
|
8 | 6 | super(name, version); |
|
|
9 | 7 | } |
|
|
10 | 8 | |
|
|
11 | 9 | @Override |
|
|
12 | 10 | public String family() { |
|
|
13 | 11 | return OperatingSystem.LINUX_FAMILY; |
|
|
14 | 12 | } |
|
|
15 | 13 | |
|
|
16 | 14 | } |
| @@ -1,15 +1,13 | |||
|
|
1 |
package org.implab.gradle.common. |
|
|
|
1 | package org.implab.gradle.common.core.os; | |
|
|
2 | 2 | |
|
|
3 | import org.implab.gradle.common.utils.OperatingSystem; | |
|
|
4 | ||
|
|
5 | public class MacOs extends GenericSystem { | |
|
|
3 | class MacOs extends GenericSystem { | |
|
|
6 | 4 | |
|
|
7 | 5 | MacOs(String name, String version) { |
|
|
8 | 6 | super(name, version); |
|
|
9 | 7 | } |
|
|
10 | 8 | |
|
|
11 | 9 | @Override |
|
|
12 | 10 | public String family() { |
|
|
13 | 11 | return OperatingSystem.MAC_OS_FAMILY; |
|
|
14 | 12 | } |
|
|
15 | 13 | } |
| @@ -1,33 +1,31 | |||
|
|
1 |
package org.implab.gradle.common. |
|
|
|
1 | package org.implab.gradle.common.core.os; | |
|
|
2 | 2 | |
|
|
3 | 3 | import java.io.File; |
|
|
4 | 4 | import java.util.Optional; |
|
|
5 | 5 | |
|
|
6 | import org.implab.gradle.common.utils.os.SystemResolver; | |
|
|
7 | ||
|
|
8 | 6 | public interface OperatingSystem { |
|
|
9 | 7 | |
|
|
10 | 8 | public static final String WINDOWS_FAMILY = "windows"; |
|
|
11 | 9 | |
|
|
12 | 10 | public static final String LINUX_FAMILY = "linux"; |
|
|
13 | 11 | |
|
|
14 | 12 | public static final String FREE_BSD_FAMILY = "freebsd"; |
|
|
15 | 13 | |
|
|
16 | 14 | public static final String MAC_OS_FAMILY = "os x"; |
|
|
17 | 15 | |
|
|
18 | 16 | public static final String UNKNOWN_FAMILY = "unknown"; |
|
|
19 | 17 | |
|
|
20 | 18 | String family(); |
|
|
21 | 19 | |
|
|
22 | 20 | String name(); |
|
|
23 | 21 | |
|
|
24 | 22 | String version(); |
|
|
25 | 23 | |
|
|
26 | 24 | Optional<File> which(String cmd); |
|
|
27 | 25 | |
|
|
28 | 26 | Optional<File> which(String cmd, Iterable<? extends File> paths); |
|
|
29 | 27 | |
|
|
30 | 28 | public static OperatingSystem current() { |
|
|
31 | 29 | return SystemResolver.current(); |
|
|
32 | 30 | } |
|
|
33 | 31 | } |
| @@ -1,37 +1,36 | |||
|
|
1 |
package org.implab.gradle.common. |
|
|
|
1 | package org.implab.gradle.common.core.os; | |
|
|
2 | 2 | |
|
|
3 |
import org.implab.gradle.common. |
|
|
|
4 | import org.implab.gradle.common.utils.OperatingSystem; | |
|
|
3 | import org.implab.gradle.common.core.lang.LazyValue; | |
|
|
5 | 4 | |
|
|
6 |
|
|
|
|
5 | class SystemResolver { | |
|
|
7 | 6 | |
|
|
8 | 7 | private final static LazyValue<OperatingSystem> current = new LazyValue<>(SystemResolver::resolveCurrent); |
|
|
9 | 8 | |
|
|
10 | 9 | private static OperatingSystem resolveCurrent() { |
|
|
11 | 10 | var osName = System.getProperty("os.name"); |
|
|
12 | 11 | var osVersion = System.getProperty("os.version"); |
|
|
13 | 12 | |
|
|
14 | 13 | return forName(osName, osVersion); |
|
|
15 | 14 | } |
|
|
16 | 15 | |
|
|
17 | 16 | public static OperatingSystem current() { |
|
|
18 | 17 | return current.get(); |
|
|
19 | 18 | } |
|
|
20 | 19 | |
|
|
21 | 20 | public static OperatingSystem forName(String os, String version) { |
|
|
22 | 21 | var osName = os.toLowerCase(); |
|
|
23 | 22 | |
|
|
24 | 23 | if (osName.contains("windows")) { |
|
|
25 | 24 | return new Windows(osName, version); |
|
|
26 | 25 | } else if (osName.contains("mac os x") || osName.contains("darwin") || osName.contains("osx")) { |
|
|
27 | 26 | return new MacOs(osName, version); |
|
|
28 | 27 | } else if (osName.contains("linux")) { |
|
|
29 | 28 | return new Linux(osName, version); |
|
|
30 | 29 | } else if (osName.contains("freebsd")) { |
|
|
31 | 30 | return new FreeBsd(osName, version); |
|
|
32 | 31 | } else { |
|
|
33 | 32 | // Not strictly true |
|
|
34 | 33 | return new GenericSystem(osName, version); |
|
|
35 | 34 | } |
|
|
36 | 35 | } |
|
|
37 | 36 | } |
| @@ -1,26 +1,24 | |||
|
|
1 |
package org.implab.gradle.common. |
|
|
|
1 | package org.implab.gradle.common.core.os; | |
|
|
2 | 2 | |
|
|
3 | 3 | import java.io.File; |
|
|
4 | 4 | import java.util.function.Function; |
|
|
5 | 5 | import java.util.stream.Stream; |
|
|
6 | 6 | |
|
|
7 | import org.implab.gradle.common.utils.OperatingSystem; | |
|
|
8 | ||
|
|
9 | 7 | class Windows extends GenericSystem { |
|
|
10 | 8 | |
|
|
11 | 9 | private Stream<String> exeSuffixes = Stream.of(".cmd", ".bat", ".exe"); |
|
|
12 | 10 | |
|
|
13 | 11 | Windows(String name, String version) { |
|
|
14 | 12 | super(name, version); |
|
|
15 | 13 | } |
|
|
16 | 14 | |
|
|
17 | 15 | @Override |
|
|
18 | 16 | public String family() { |
|
|
19 | 17 | return OperatingSystem.WINDOWS_FAMILY; |
|
|
20 | 18 | } |
|
|
21 | 19 | |
|
|
22 | 20 | @Override |
|
|
23 | 21 | protected Function<File, Stream<File>> candidates(String cmd) { |
|
|
24 | 22 | return base -> exeSuffixes.map(suffix -> new File(base, cmd + suffix)); |
|
|
25 | 23 | } |
|
|
26 | 24 | } |
| @@ -1,61 +1,61 | |||
|
|
1 | package org.implab.gradle.common.dsl; | |
|
|
1 | package org.implab.gradle.common.exec.dsl; | |
|
|
2 | 2 | |
|
|
3 | 3 | import java.io.File; |
|
|
4 | 4 | import java.io.InputStream; |
|
|
5 | 5 | import java.util.Optional; |
|
|
6 | 6 | import java.util.function.Supplier; |
|
|
7 | 7 | |
|
|
8 | 8 | import org.gradle.api.provider.Provider; |
|
|
9 | 9 | import org.gradle.util.Configurable; |
|
|
10 |
import org.implab.gradle.common. |
|
|
|
11 |
import org.implab.gradle.common. |
|
|
|
10 | import org.implab.gradle.common.core.lang.Closures; | |
|
|
11 | import org.implab.gradle.common.exec.runtime.RedirectFrom; | |
|
|
12 | 12 | |
|
|
13 | 13 | import groovy.lang.Closure; |
|
|
14 | 14 | |
|
|
15 | 15 | public class RedirectFromSpec implements Configurable<RedirectFromSpec> { |
|
|
16 | 16 | private Supplier<RedirectFrom> streamRedirect; |
|
|
17 | 17 | |
|
|
18 | 18 | public boolean isRedirected() { |
|
|
19 | 19 | return streamRedirect != null; |
|
|
20 | 20 | } |
|
|
21 | 21 | |
|
|
22 | 22 | public Optional<RedirectFrom> getRedirection() { |
|
|
23 | 23 | return streamRedirect != null ? Optional.ofNullable(streamRedirect.get()) : Optional.empty(); |
|
|
24 | 24 | } |
|
|
25 | 25 | |
|
|
26 | 26 | public void fromFile(File file) { |
|
|
27 | 27 | streamRedirect = () -> RedirectFrom.file(file); |
|
|
28 | 28 | } |
|
|
29 | 29 | |
|
|
30 | 30 | public void fromFile(Provider<File> fileProvider) { |
|
|
31 | 31 | streamRedirect = fileProvider.map(RedirectFrom::file)::getOrNull; |
|
|
32 | 32 | } |
|
|
33 | 33 | |
|
|
34 | 34 | public void fromStream(InputStream stream) { |
|
|
35 | 35 | streamRedirect = () -> RedirectFrom.stream(stream); |
|
|
36 | 36 | } |
|
|
37 | 37 | |
|
|
38 | 38 | public void fromStream(Provider<InputStream> streamProvider) { |
|
|
39 | 39 | streamRedirect = streamProvider.map(RedirectFrom::stream)::getOrNull; |
|
|
40 | 40 | } |
|
|
41 | 41 | |
|
|
42 | 42 | public void from(Object input) { |
|
|
43 | 43 | if (input instanceof Provider<?> inputProvider) { |
|
|
44 | 44 | streamRedirect = inputProvider.map(RedirectFrom::any)::get; |
|
|
45 | 45 | } else { |
|
|
46 | 46 | streamRedirect = () -> RedirectFrom.any(input); |
|
|
47 | 47 | } |
|
|
48 | 48 | } |
|
|
49 | 49 | |
|
|
50 | 50 | public void empty() { |
|
|
51 | 51 | streamRedirect = () -> null; |
|
|
52 | 52 | } |
|
|
53 | 53 | |
|
|
54 | 54 | @Override |
|
|
55 | 55 | public RedirectFromSpec configure(Closure cl) { |
|
|
56 | 56 | Closures.apply(cl, this); |
|
|
57 | 57 | return this; |
|
|
58 | 58 | } |
|
|
59 | 59 | |
|
|
60 | 60 | |
|
|
61 | 61 | } |
| @@ -1,75 +1,75 | |||
|
|
1 | package org.implab.gradle.common.dsl; | |
|
|
1 | package org.implab.gradle.common.exec.dsl; | |
|
|
2 | 2 | |
|
|
3 | 3 | import java.io.File; |
|
|
4 | 4 | import java.io.OutputStream; |
|
|
5 | 5 | import java.util.Optional; |
|
|
6 | 6 | import java.util.function.Consumer; |
|
|
7 | 7 | import java.util.function.Supplier; |
|
|
8 | 8 | |
|
|
9 | 9 | import org.eclipse.jdt.annotation.NonNullByDefault; |
|
|
10 | 10 | import org.eclipse.jdt.annotation.Nullable; |
|
|
11 | 11 | import org.gradle.api.provider.Provider; |
|
|
12 | 12 | import org.gradle.util.Configurable; |
|
|
13 |
import org.implab.gradle.common. |
|
|
|
14 |
import org.implab.gradle.common. |
|
|
|
13 | import org.implab.gradle.common.core.lang.Closures; | |
|
|
14 | import org.implab.gradle.common.exec.runtime.RedirectTo; | |
|
|
15 | 15 | |
|
|
16 | 16 | import groovy.lang.Closure; |
|
|
17 | 17 | |
|
|
18 | 18 | @NonNullByDefault |
|
|
19 | 19 | public class RedirectToSpec implements Configurable<RedirectToSpec> { |
|
|
20 | 20 | private Supplier<RedirectTo> streamRedirect; |
|
|
21 | 21 | |
|
|
22 | 22 | public boolean isRedirected() { |
|
|
23 | 23 | return getRedirection().isPresent(); |
|
|
24 | 24 | } |
|
|
25 | 25 | |
|
|
26 | 26 | public Optional<RedirectTo> getRedirection() { |
|
|
27 | 27 | return streamRedirect != null ? Optional.ofNullable(streamRedirect.get()) : Optional.empty(); |
|
|
28 | 28 | } |
|
|
29 | 29 | |
|
|
30 | 30 | public @Nullable RedirectTo getRedirectionOrNull() { |
|
|
31 | 31 | return streamRedirect != null ? streamRedirect.get() : null; |
|
|
32 | 32 | } |
|
|
33 | 33 | |
|
|
34 | 34 | public void toFile(File file) { |
|
|
35 | 35 | streamRedirect = () -> RedirectTo.file(file); |
|
|
36 | 36 | } |
|
|
37 | 37 | |
|
|
38 | 38 | public void toFile(Provider<File> fileProvider) { |
|
|
39 | 39 | streamRedirect = fileProvider.map(RedirectTo::file)::getOrNull; |
|
|
40 | 40 | } |
|
|
41 | 41 | |
|
|
42 | 42 | public void toStream(OutputStream stream) { |
|
|
43 | 43 | streamRedirect = () -> RedirectTo.stream(stream); |
|
|
44 | 44 | } |
|
|
45 | 45 | |
|
|
46 | 46 | public void toStream(Provider<OutputStream> streamProvider) { |
|
|
47 | 47 | streamRedirect = streamProvider.map(RedirectTo::stream)::getOrNull; |
|
|
48 | 48 | } |
|
|
49 | 49 | |
|
|
50 | 50 | public void to(Object output) { |
|
|
51 | 51 | if (output instanceof Provider<?> outputProvider) { |
|
|
52 | 52 | streamRedirect = outputProvider.map(RedirectTo::any)::get; |
|
|
53 | 53 | } else { |
|
|
54 | 54 | streamRedirect = () -> RedirectTo.any(output); |
|
|
55 | 55 | } |
|
|
56 | 56 | } |
|
|
57 | 57 | |
|
|
58 | 58 | public void eachLine(Consumer<String> consumer) { |
|
|
59 | 59 | streamRedirect = () -> RedirectTo.eachLine(consumer); |
|
|
60 | 60 | } |
|
|
61 | 61 | |
|
|
62 | 62 | public void allText(Consumer<String> consumer) { |
|
|
63 | 63 | streamRedirect = () -> RedirectTo.allText(consumer); |
|
|
64 | 64 | } |
|
|
65 | 65 | |
|
|
66 | 66 | public void discard() { |
|
|
67 | 67 | streamRedirect = () -> null; |
|
|
68 | 68 | } |
|
|
69 | 69 | |
|
|
70 | 70 | @Override |
|
|
71 | 71 | public RedirectToSpec configure(Closure cl) { |
|
|
72 | 72 | Closures.apply(cl, this); |
|
|
73 | 73 | return this; |
|
|
74 | 74 | } |
|
|
75 | 75 | } |
| @@ -1,26 +1,26 | |||
|
|
1 | package org.implab.gradle.common.dsl; | |
|
|
1 | package org.implab.gradle.common.exec.dsl; | |
|
|
2 | 2 | |
|
|
3 | 3 | import java.util.stream.Stream; |
|
|
4 | 4 | |
|
|
5 | 5 | import org.gradle.api.provider.ListProperty; |
|
|
6 |
import org.implab.gradle.common. |
|
|
|
6 | import org.implab.gradle.common.core.gradle.Properties; | |
|
|
7 | 7 | |
|
|
8 | 8 | public interface TaskCommandSpecMixin { |
|
|
9 | 9 | ListProperty<String> getCommandLine(); |
|
|
10 | 10 | |
|
|
11 | 11 | default void commandLine(Object arg0, Object... args) { |
|
|
12 | 12 | getCommandLine().empty(); |
|
|
13 | 13 | Properties.mergeList( |
|
|
14 | 14 | getCommandLine(), |
|
|
15 | 15 | () -> Stream.concat(Stream.of(arg0), Stream.of(args)).iterator(), |
|
|
16 | 16 | Object::toString); |
|
|
17 | 17 | } |
|
|
18 | 18 | |
|
|
19 | 19 | default void args(Object arg0, Object... args) { |
|
|
20 | 20 | Properties.mergeList( |
|
|
21 | 21 | getCommandLine(), |
|
|
22 | 22 | () -> Stream.concat(Stream.of(arg0), Stream.of(args)).iterator(), |
|
|
23 | 23 | Object::toString); |
|
|
24 | 24 | } |
|
|
25 | 25 | |
|
|
26 | 26 | } |
| @@ -1,45 +1,45 | |||
|
|
1 | package org.implab.gradle.common.dsl; | |
|
|
1 | package org.implab.gradle.common.exec.dsl; | |
|
|
2 | 2 | |
|
|
3 | 3 | import java.util.Map; |
|
|
4 | 4 | |
|
|
5 | 5 | import org.gradle.api.Action; |
|
|
6 | 6 | import org.gradle.api.file.DirectoryProperty; |
|
|
7 | 7 | import org.gradle.api.provider.MapProperty; |
|
|
8 | 8 | import org.gradle.api.provider.Property; |
|
|
9 |
import org.implab.gradle.common. |
|
|
|
10 |
import org.implab.gradle.common. |
|
|
|
9 | import org.implab.gradle.common.core.gradle.Properties; | |
|
|
10 | import org.implab.gradle.common.core.lang.Closures; | |
|
|
11 | 11 | |
|
|
12 | 12 | import groovy.lang.Closure; |
|
|
13 | 13 | |
|
|
14 | 14 | /** |
|
|
15 | 15 | * Configuration properties of the execution shell. This object specifies a |
|
|
16 | 16 | * working directory and environment variables for the processes started within |
|
|
17 | 17 | * this shell. |
|
|
18 | 18 | */ |
|
|
19 | 19 | public interface TaskEnvSpecMixin { |
|
|
20 | 20 | |
|
|
21 | 21 | /** Inherit environment from current process */ |
|
|
22 | 22 | Property<Boolean> getInheritEnvironment(); |
|
|
23 | 23 | |
|
|
24 | 24 | /** Working directory */ |
|
|
25 | 25 | DirectoryProperty getWorkingDirectory(); |
|
|
26 | 26 | |
|
|
27 | 27 | /** Environment variables */ |
|
|
28 | 28 | MapProperty<String, String> getEnvironment(); |
|
|
29 | 29 | |
|
|
30 | 30 | /** |
|
|
31 | 31 | * Configures the environment variable using the specified action. The |
|
|
32 | 32 | * action is called when the value is calculated; |
|
|
33 | 33 | * |
|
|
34 | 34 | * <p> |
|
|
35 | 35 | * The configuration action is called immediately. To support lazy evaluation, |
|
|
36 | 36 | * properties may be assigned to providers. |
|
|
37 | 37 | */ |
|
|
38 | 38 | default void env(Action<Map<String, Object>> configure) { |
|
|
39 | 39 | Properties.configureMap(getEnvironment(), configure, Object::toString); |
|
|
40 | 40 | } |
|
|
41 | 41 | |
|
|
42 | 42 | default void env(Closure<?> configure) { |
|
|
43 | 43 | env(Closures.action(configure)); |
|
|
44 | 44 | } |
|
|
45 | 45 | } |
| @@ -1,12 +1,12 | |||
|
|
1 | package org.implab.gradle.common.dsl; | |
|
|
1 | package org.implab.gradle.common.exec.dsl; | |
|
|
2 | 2 | |
|
|
3 | 3 | import org.eclipse.jdt.annotation.NonNullByDefault; |
|
|
4 | 4 | |
|
|
5 | 5 | @NonNullByDefault |
|
|
6 | 6 | public interface TaskPipeSpecMixin { |
|
|
7 | 7 | RedirectToSpec getStdout(); |
|
|
8 | 8 | |
|
|
9 | 9 | RedirectToSpec getStderr(); |
|
|
10 | 10 | |
|
|
11 | 11 | RedirectFromSpec getStdin(); |
|
|
12 | 12 | } |
| @@ -1,52 +1,52 | |||
|
|
1 | package org.implab.gradle.common.exec; | |
|
|
1 | package org.implab.gradle.common.exec.model; | |
|
|
2 | 2 | |
|
|
3 | 3 | import static java.util.Objects.requireNonNull; |
|
|
4 | 4 | |
|
|
5 | 5 | import java.util.Optional; |
|
|
6 | 6 | |
|
|
7 | 7 | import org.eclipse.jdt.annotation.NonNullByDefault; |
|
|
8 |
import org.implab.gradle.common. |
|
|
|
8 | import org.implab.gradle.common.core.lang.Values; | |
|
|
9 | 9 | |
|
|
10 | 10 | @NonNullByDefault |
|
|
11 | 11 | public interface CommandArgumentsBuilder<S extends CommandArgumentsBuilder<S>> { |
|
|
12 | 12 | |
|
|
13 | 13 | S self(); |
|
|
14 | 14 | |
|
|
15 | 15 | default S flag(String name, boolean value) { |
|
|
16 | 16 | requireNonNull(name, "Parameter name cannot be null"); |
|
|
17 | 17 | if (value) |
|
|
18 | 18 | addArguments(name); |
|
|
19 | 19 | return self(); |
|
|
20 | 20 | } |
|
|
21 | 21 | |
|
|
22 | 22 | default S param(String name, String value) { |
|
|
23 | 23 | requireNonNull(name, "Parameter name cannot be null"); |
|
|
24 | 24 | requireNonNull(value, "Parameter value cannot be null"); |
|
|
25 | 25 | if (value != null && value.length() > 0) |
|
|
26 | 26 | addArguments(name, value); |
|
|
27 | 27 | return self(); |
|
|
28 | 28 | } |
|
|
29 | 29 | |
|
|
30 | 30 | default S param(String name, Optional<String> value) { |
|
|
31 | 31 | value.ifPresent(v -> param(name, v)); |
|
|
32 | 32 | return self(); |
|
|
33 | 33 | } |
|
|
34 | 34 | |
|
|
35 | 35 | /** Adds the specified arguments to this builder */ |
|
|
36 | 36 | S addArguments(String... args); |
|
|
37 | 37 | |
|
|
38 | 38 | default S addArguments(Iterable<String> args) { |
|
|
39 | 39 | for (String arg : args) |
|
|
40 | 40 | addArguments(arg); |
|
|
41 | 41 | |
|
|
42 | 42 | return self(); |
|
|
43 | 43 | } |
|
|
44 | 44 | |
|
|
45 | 45 | /** Replaces arguments in the builder with the specified one. */ |
|
|
46 | 46 | default S arguments(String... args) { |
|
|
47 | 47 | return arguments(Values.iterable(args)); |
|
|
48 | 48 | } |
|
|
49 | 49 | |
|
|
50 | 50 | /** Replaces arguments in the builder with the specified one. */ |
|
|
51 | 51 | S arguments(Iterable<String> args); |
|
|
52 | 52 | } |
| @@ -1,63 +1,63 | |||
|
|
1 | package org.implab.gradle.common.exec; | |
|
|
1 | package org.implab.gradle.common.exec.model; | |
|
|
2 | 2 | |
|
|
3 | 3 | import java.io.File; |
|
|
4 | 4 | |
|
|
5 | 5 | import org.eclipse.jdt.annotation.NonNullByDefault; |
|
|
6 |
import org.implab.gradle.common. |
|
|
|
6 | import org.implab.gradle.common.core.lang.Values; | |
|
|
7 | 7 | |
|
|
8 | 8 | /** Command builder interface, used to specify the executable and parameters */ |
|
|
9 | 9 | @NonNullByDefault |
|
|
10 | 10 | public interface CommandBuilder extends CommandArgumentsBuilder<CommandBuilder> { |
|
|
11 | 11 | |
|
|
12 | 12 | @Override |
|
|
13 | 13 | default CommandBuilder self() { |
|
|
14 | 14 | return this; |
|
|
15 | 15 | } |
|
|
16 | 16 | |
|
|
17 | 17 | /** Sets the executable, the parameters are left intact. */ |
|
|
18 | 18 | CommandBuilder executable(String executable); |
|
|
19 | 19 | |
|
|
20 | 20 | default CommandBuilder executable(File executable) { |
|
|
21 | 21 | executable(executable.toString()); |
|
|
22 | 22 | return this; |
|
|
23 | 23 | } |
|
|
24 | 24 | |
|
|
25 | 25 | /** |
|
|
26 | 26 | * Sets the specified executable and parameters, old executable and parameters |
|
|
27 | 27 | * are discarded. |
|
|
28 | 28 | */ |
|
|
29 | 29 | default CommandBuilder commandLine(String executable, String... args) { |
|
|
30 | 30 | return executable(executable) |
|
|
31 | 31 | .arguments(args); |
|
|
32 | 32 | } |
|
|
33 | 33 | |
|
|
34 | 34 | /** |
|
|
35 | 35 | * Sets the specified executable and parameters, old executable and parameters |
|
|
36 | 36 | * are discarded. |
|
|
37 | 37 | * |
|
|
38 | 38 | * @param command The command line. Must contain at least one element |
|
|
39 | 39 | * (executable). |
|
|
40 | 40 | * |
|
|
41 | 41 | */ |
|
|
42 | 42 | default CommandBuilder commandLine(Iterable<? extends String> command) { |
|
|
43 | 43 | var iterator = command.iterator(); |
|
|
44 | 44 | |
|
|
45 | 45 | // set executable |
|
|
46 | 46 | executable(Values.take(iterator).orElseThrow()); |
|
|
47 | 47 | // cleat arguments |
|
|
48 | 48 | arguments(); |
|
|
49 | 49 | |
|
|
50 | 50 | // set new arguments |
|
|
51 | 51 | while (iterator.hasNext()) |
|
|
52 | 52 | addArguments(iterator.next()); |
|
|
53 | 53 | |
|
|
54 | 54 | return this; |
|
|
55 | 55 | } |
|
|
56 | 56 | |
|
|
57 | 57 | default CommandBuilder from(CommandSpec commandSpec) { |
|
|
58 | 58 | return executable(commandSpec.executable()) |
|
|
59 | 59 | .arguments(commandSpec.arguments()); |
|
|
60 | 60 | } |
|
|
61 | 61 | |
|
|
62 | 62 | CommandSpec build(); |
|
|
63 | 63 | } |
| @@ -1,23 +1,23 | |||
|
|
1 | package org.implab.gradle.common.exec; | |
|
|
1 | package org.implab.gradle.common.exec.model; | |
|
|
2 | 2 | |
|
|
3 | 3 | import java.util.List; |
|
|
4 | 4 | import java.util.stream.Stream; |
|
|
5 | 5 | |
|
|
6 | 6 | import org.eclipse.jdt.annotation.NonNullByDefault; |
|
|
7 | 7 | |
|
|
8 | 8 | @NonNullByDefault |
|
|
9 | 9 | public interface CommandSpec { |
|
|
10 | 10 | |
|
|
11 | 11 | String executable(); |
|
|
12 | 12 | |
|
|
13 | 13 | List<String> arguments(); |
|
|
14 | 14 | |
|
|
15 | 15 | default List<String> commandLine() { |
|
|
16 | 16 | return Stream.concat(Stream.of(executable()), arguments().stream()).toList(); |
|
|
17 | 17 | } |
|
|
18 | 18 | |
|
|
19 | 19 | public static CommandBuilder builder() { |
|
|
20 | 20 | return new CommandSpecRecord.Builder(); |
|
|
21 | 21 | } |
|
|
22 | 22 | |
|
|
23 | 23 | } |
| @@ -1,51 +1,51 | |||
|
|
1 | package org.implab.gradle.common.exec; | |
|
|
1 | package org.implab.gradle.common.exec.model; | |
|
|
2 | 2 | |
|
|
3 | 3 | import java.util.List; |
|
|
4 | 4 | |
|
|
5 | 5 | import org.eclipse.jdt.annotation.NonNullByDefault; |
|
|
6 | 6 | |
|
|
7 | 7 | import static java.util.Objects.requireNonNull; |
|
|
8 | 8 | |
|
|
9 | 9 | import java.util.ArrayList; |
|
|
10 | 10 | |
|
|
11 | 11 | @NonNullByDefault |
|
|
12 | 12 | public record CommandSpecRecord(String executable, List<String> arguments) implements CommandSpec { |
|
|
13 | 13 | |
|
|
14 | 14 | static class Builder implements CommandBuilder { |
|
|
15 | 15 | |
|
|
16 | 16 | private String executable; |
|
|
17 | 17 | |
|
|
18 | 18 | private List<String> arguments = new ArrayList<>(); |
|
|
19 | 19 | |
|
|
20 | 20 | @Override |
|
|
21 | 21 | public CommandBuilder executable(String executable) { |
|
|
22 | 22 | requireNonNull(executable, "cmd can't be null"); |
|
|
23 | 23 | this.executable = executable; |
|
|
24 | 24 | return this; |
|
|
25 | 25 | } |
|
|
26 | 26 | |
|
|
27 | 27 | @Override |
|
|
28 | 28 | public CommandBuilder arguments(Iterable<String> args) { |
|
|
29 | 29 | requireNonNull(args, "Args must not be null"); |
|
|
30 | 30 | arguments.clear(); |
|
|
31 | 31 | for (var arg : args) |
|
|
32 | 32 | arguments.add(arg); |
|
|
33 | 33 | return this; |
|
|
34 | 34 | } |
|
|
35 | 35 | |
|
|
36 | 36 | @Override |
|
|
37 | 37 | public CommandBuilder addArguments(String... args) { |
|
|
38 | 38 | for (var arg : args) |
|
|
39 | 39 | arguments.add(requireNonNull(arg, "arguments element shouldn't be null")); |
|
|
40 | 40 | |
|
|
41 | 41 | return this; |
|
|
42 | 42 | } |
|
|
43 | 43 | |
|
|
44 | 44 | @Override |
|
|
45 | 45 | public CommandSpec build() { |
|
|
46 | 46 | requireNonNull(executable, "Executable must be specified"); |
|
|
47 | 47 | return new CommandSpecRecord(executable, arguments); |
|
|
48 | 48 | } |
|
|
49 | 49 | |
|
|
50 | 50 | } |
|
|
51 | 51 | } |
| @@ -1,17 +1,17 | |||
|
|
1 | package org.implab.gradle.common.exec; | |
|
|
1 | package org.implab.gradle.common.exec.model; | |
|
|
2 | 2 | |
|
|
3 | 3 | import java.io.File; |
|
|
4 | 4 | import java.util.Map; |
|
|
5 | 5 | import java.util.Optional; |
|
|
6 | 6 | |
|
|
7 | 7 | import org.eclipse.jdt.annotation.NonNullByDefault; |
|
|
8 | 8 | |
|
|
9 | 9 | @NonNullByDefault |
|
|
10 | 10 | public interface EnvironmentSpec { |
|
|
11 | 11 | |
|
|
12 | 12 | boolean inheritEnvironment(); |
|
|
13 | 13 | |
|
|
14 | 14 | Map<String,String> environment(); |
|
|
15 | 15 | |
|
|
16 | 16 | Optional<File> workingDirectory(); |
|
|
17 | 17 | } |
| @@ -1,20 +1,23 | |||
|
|
1 | package org.implab.gradle.common.exec; | |
|
|
1 | package org.implab.gradle.common.exec.model; | |
|
|
2 | ||
|
|
3 | import org.implab.gradle.common.exec.runtime.RedirectFrom; | |
|
|
4 | import org.implab.gradle.common.exec.runtime.RedirectTo; | |
|
|
2 | 5 | |
|
|
3 | 6 | import java.util.Optional; |
|
|
4 | 7 | |
|
|
5 | 8 | import org.eclipse.jdt.annotation.NonNullByDefault; |
|
|
6 | 9 | |
|
|
7 | 10 | /** |
|
|
8 | 11 | * The execution shell uses this specification when starting |
|
|
9 | 12 | * a new process. The shell may check for the specified |
|
|
10 | 13 | * redirections and apply them when launching the process. |
|
|
11 | 14 | * The exact moment they are applied is at the shellβs discretion. |
|
|
12 | 15 | */ |
|
|
13 | 16 | @NonNullByDefault |
|
|
14 | 17 | public interface PipeSpec { |
|
|
15 | 18 | Optional<RedirectTo> stdout(); |
|
|
16 | 19 | |
|
|
17 | 20 | Optional<RedirectTo> stderr(); |
|
|
18 | 21 | |
|
|
19 | 22 | Optional<RedirectFrom> stdin(); |
|
|
20 | 23 | } |
| @@ -1,212 +1,214 | |||
|
|
1 | package org.implab.gradle.common.exec; | |
|
|
1 | package org.implab.gradle.common.exec.runtime; | |
|
|
2 | 2 | |
|
|
3 | 3 | import java.util.HashMap; |
|
|
4 | 4 | import java.util.Map; |
|
|
5 | 5 | import java.util.Optional; |
|
|
6 | 6 | import java.util.concurrent.CompletableFuture; |
|
|
7 | 7 | |
|
|
8 | 8 | import org.eclipse.jdt.annotation.NonNullByDefault; |
|
|
9 | 9 | import org.eclipse.jdt.annotation.Nullable; |
|
|
10 | import org.implab.gradle.common.exec.model.EnvironmentSpec; | |
|
|
11 | import org.implab.gradle.common.exec.model.PipeSpec; | |
|
|
10 | 12 | |
|
|
11 | 13 | import java.io.File; |
|
|
12 | 14 | import java.io.IOException; |
|
|
13 | 15 | |
|
|
14 | 16 | import static java.util.Objects.requireNonNull; |
|
|
15 | 17 | |
|
|
16 | 18 | /** Command line builder */ |
|
|
17 | 19 | @NonNullByDefault |
|
|
18 | 20 | public abstract class AbstractExecBuilder<CS> implements ShellExec { |
|
|
19 | 21 | |
|
|
20 | 22 | private boolean inheritEnvironment = true; |
|
|
21 | 23 | |
|
|
22 | 24 | private final Map<String, String> environment = new HashMap<>(); |
|
|
23 | 25 | |
|
|
24 | 26 | private @Nullable File directory; |
|
|
25 | 27 | |
|
|
26 | 28 | private RedirectFrom inputRedirect; |
|
|
27 | 29 | |
|
|
28 | 30 | private RedirectTo outputRedirect; |
|
|
29 | 31 | |
|
|
30 | 32 | private RedirectTo errorRedirect; |
|
|
31 | 33 | |
|
|
32 | 34 | private final CS command; |
|
|
33 | 35 | |
|
|
34 | 36 | protected AbstractExecBuilder(CS command) { |
|
|
35 | 37 | this.command = command; |
|
|
36 | 38 | } |
|
|
37 | 39 | |
|
|
38 | 40 | |
|
|
39 | 41 | /** Sets the working directory */ |
|
|
40 | 42 | @Override |
|
|
41 | 43 | public ShellExec workingDirectory(File directory) { |
|
|
42 | 44 | requireNonNull(directory, "directory parameter can't be null"); |
|
|
43 | 45 | |
|
|
44 | 46 | this.directory = directory; |
|
|
45 | 47 | return this; |
|
|
46 | 48 | } |
|
|
47 | 49 | |
|
|
48 | 50 | @Override |
|
|
49 | 51 | public ShellExec workingDirectory(Optional<? extends File> directory) { |
|
|
50 | 52 | requireNonNull(directory, "directory parameter can't be null"); |
|
|
51 | 53 | |
|
|
52 | 54 | this.directory = directory.orElse(null); |
|
|
53 | 55 | return this; |
|
|
54 | 56 | } |
|
|
55 | 57 | |
|
|
56 | 58 | @Override |
|
|
57 | 59 | public ShellExec inheritEnvironment(boolean inherit) { |
|
|
58 | 60 | this.inheritEnvironment = inherit; |
|
|
59 | 61 | return this; |
|
|
60 | 62 | } |
|
|
61 | 63 | |
|
|
62 | 64 | /** |
|
|
63 | 65 | * Sets the environment value. The value cannot be null. |
|
|
64 | 66 | * |
|
|
65 | 67 | * @param envVar The name of the environment variable |
|
|
66 | 68 | * @param value The value to set, |
|
|
67 | 69 | */ |
|
|
68 | 70 | @Override |
|
|
69 | 71 | public ShellExec putEnvironment(String envVar, String value) { |
|
|
70 | 72 | requireNonNull(value, "Value can't be null"); |
|
|
71 | 73 | requireNonNull(envVar, "envVar parameter can't be null"); |
|
|
72 | 74 | |
|
|
73 | 75 | environment.put(envVar, value); |
|
|
74 | 76 | return this; |
|
|
75 | 77 | } |
|
|
76 | 78 | |
|
|
77 | 79 | @Override |
|
|
78 | 80 | public ShellExec environment(Map<String, ? extends String> env) { |
|
|
79 | 81 | requireNonNull(env, "env parameter can't be null"); |
|
|
80 | 82 | |
|
|
81 | 83 | environment.clear(); |
|
|
82 | 84 | environment.putAll(env); |
|
|
83 | 85 | return this; |
|
|
84 | 86 | } |
|
|
85 | 87 | |
|
|
86 | 88 | /** |
|
|
87 | 89 | * Sets redirection for the stdin, {@link RedirectFrom} will be applied |
|
|
88 | 90 | * every time the process is started. |
|
|
89 | 91 | * |
|
|
90 | 92 | * <p> |
|
|
91 | 93 | * If redirection |
|
|
92 | 94 | */ |
|
|
93 | 95 | @Override |
|
|
94 | 96 | public ShellExec stdin(RedirectFrom from) { |
|
|
95 | 97 | requireNonNull(from, "from parameter can't be null"); |
|
|
96 | 98 | |
|
|
97 | 99 | inputRedirect = from; |
|
|
98 | 100 | return this; |
|
|
99 | 101 | } |
|
|
100 | 102 | |
|
|
101 | 103 | @Override |
|
|
102 | 104 | public ShellExec stdin(Optional<? extends RedirectFrom> from) { |
|
|
103 | 105 | requireNonNull(from, "from parameter can't be null"); |
|
|
104 | 106 | inputRedirect = from.orElse(null); |
|
|
105 | 107 | return this; |
|
|
106 | 108 | } |
|
|
107 | 109 | |
|
|
108 | 110 | /** |
|
|
109 | 111 | * Sets redirection for the stdout, {@link RedirectTo} will be applied |
|
|
110 | 112 | * every time the process is started. |
|
|
111 | 113 | */ |
|
|
112 | 114 | @Override |
|
|
113 | 115 | public ShellExec stdout(RedirectTo out) { |
|
|
114 | 116 | requireNonNull(out, "out parameter can't be null"); |
|
|
115 | 117 | outputRedirect = out; |
|
|
116 | 118 | return this; |
|
|
117 | 119 | } |
|
|
118 | 120 | |
|
|
119 | 121 | @Override |
|
|
120 | 122 | public ShellExec stdout(Optional<? extends RedirectTo> to) { |
|
|
121 | 123 | requireNonNull(to, "from parameter can't be null"); |
|
|
122 | 124 | outputRedirect = to.orElse(null); |
|
|
123 | 125 | return this; |
|
|
124 | 126 | } |
|
|
125 | 127 | |
|
|
126 | 128 | /** |
|
|
127 | 129 | * Sets redirection for the stderr, {@link RedirectTo} will be applied |
|
|
128 | 130 | * every time the process is started. |
|
|
129 | 131 | */ |
|
|
130 | 132 | @Override |
|
|
131 | 133 | public ShellExec stderr(RedirectTo err) { |
|
|
132 | 134 | requireNonNull(err, "err parameter can't be null"); |
|
|
133 | 135 | errorRedirect = err; |
|
|
134 | 136 | return this; |
|
|
135 | 137 | } |
|
|
136 | 138 | |
|
|
137 | 139 | @Override |
|
|
138 | 140 | public ShellExec stderr(Optional<? extends RedirectTo> to) { |
|
|
139 | 141 | requireNonNull(to, "from parameter can't be null"); |
|
|
140 | 142 | errorRedirect = to.orElse(null); |
|
|
141 | 143 | return this; |
|
|
142 | 144 | } |
|
|
143 | 145 | |
|
|
144 | 146 | @Override |
|
|
145 | 147 | public ShellExec from(PipeSpec pipeSpec) { |
|
|
146 | 148 | ShellExec.super.from(pipeSpec); |
|
|
147 | 149 | return this; |
|
|
148 | 150 | } |
|
|
149 | 151 | |
|
|
150 | 152 | @Override |
|
|
151 | 153 | public ShellExec from(EnvironmentSpec environmentSpec) { |
|
|
152 | 154 | ShellExec.super.from(environmentSpec); |
|
|
153 | 155 | return this; |
|
|
154 | 156 | } |
|
|
155 | 157 | |
|
|
156 | 158 | /** Implement this function to */ |
|
|
157 | 159 | protected abstract CompletableFuture<Integer> startInternal( |
|
|
158 | 160 | CS command, |
|
|
159 | 161 | EnvironmentSpec environment, |
|
|
160 | 162 | PipeSpec redirect) throws IOException; |
|
|
161 | 163 | |
|
|
162 | 164 | /** |
|
|
163 | 165 | * Creates and starts new process and returns {@link CompletableFuture}. The |
|
|
164 | 166 | * process may be a remote process, or a shell process or etc. |
|
|
165 | 167 | * |
|
|
166 | 168 | * @return |
|
|
167 | 169 | * @throws IOException |
|
|
168 | 170 | */ |
|
|
169 | 171 | public CompletableFuture<Integer> exec() throws IOException { |
|
|
170 | 172 | return startInternal( |
|
|
171 | 173 | command, |
|
|
172 | 174 | new SelfEnvironmentSpec(), |
|
|
173 | 175 | new SelfPipeSpec()); |
|
|
174 | 176 | } |
|
|
175 | 177 | |
|
|
176 | 178 | private class SelfEnvironmentSpec implements EnvironmentSpec { |
|
|
177 | 179 | @Override |
|
|
178 | 180 | public boolean inheritEnvironment() { |
|
|
179 | 181 | return inheritEnvironment; |
|
|
180 | 182 | } |
|
|
181 | 183 | |
|
|
182 | 184 | @Override |
|
|
183 | 185 | public Map<String, String> environment() { |
|
|
184 | 186 | return environment; |
|
|
185 | 187 | } |
|
|
186 | 188 | |
|
|
187 | 189 | @Override |
|
|
188 | 190 | public Optional<File> workingDirectory() { |
|
|
189 | 191 | return Optional.ofNullable(directory); |
|
|
190 | 192 | } |
|
|
191 | 193 | } |
|
|
192 | 194 | |
|
|
193 | 195 | private class SelfPipeSpec implements PipeSpec { |
|
|
194 | 196 | |
|
|
195 | 197 | @Override |
|
|
196 | 198 | public Optional<RedirectTo> stdout() { |
|
|
197 | 199 | return Optional.ofNullable(outputRedirect); |
|
|
198 | 200 | } |
|
|
199 | 201 | |
|
|
200 | 202 | @Override |
|
|
201 | 203 | public Optional<RedirectTo> stderr() { |
|
|
202 | 204 | return Optional.ofNullable(errorRedirect); |
|
|
203 | 205 | } |
|
|
204 | 206 | |
|
|
205 | 207 | @Override |
|
|
206 | 208 | public Optional<RedirectFrom> stdin() { |
|
|
207 | 209 | return Optional.ofNullable(inputRedirect); |
|
|
208 | 210 | } |
|
|
209 | 211 | |
|
|
210 | 212 | } |
|
|
211 | 213 | |
|
|
212 | 214 | } |
| @@ -1,43 +1,46 | |||
|
|
1 | package org.implab.gradle.common.exec; | |
|
|
1 | package org.implab.gradle.common.exec.runtime; | |
|
|
2 | 2 | |
|
|
3 | 3 | import java.io.ByteArrayInputStream; |
|
|
4 | 4 | import java.io.IOException; |
|
|
5 | 5 | import java.nio.charset.StandardCharsets; |
|
|
6 | 6 | import java.util.concurrent.CompletableFuture; |
|
|
7 | 7 | import java.util.stream.Collectors; |
|
|
8 | 8 | |
|
|
9 | 9 | import org.eclipse.jdt.annotation.NonNullByDefault; |
|
|
10 | import org.implab.gradle.common.exec.model.CommandSpec; | |
|
|
11 | import org.implab.gradle.common.exec.model.EnvironmentSpec; | |
|
|
12 | import org.implab.gradle.common.exec.model.PipeSpec; | |
|
|
10 | 13 | |
|
|
11 | 14 | @NonNullByDefault |
|
|
12 | 15 | class EchoExecBuilder extends AbstractExecBuilder<CommandSpec> { |
|
|
13 | 16 | |
|
|
14 | 17 | private final boolean echoToStderr; |
|
|
15 | 18 | |
|
|
16 | 19 | public EchoExecBuilder(CommandSpec command, boolean echoToStderr) { |
|
|
17 | 20 | super(command); |
|
|
18 | 21 | this.echoToStderr = echoToStderr; |
|
|
19 | 22 | } |
|
|
20 | 23 | |
|
|
21 | 24 | @Override |
|
|
22 | 25 | protected CompletableFuture<Integer> startInternal( |
|
|
23 | 26 | CommandSpec command, |
|
|
24 | 27 | EnvironmentSpec environment, |
|
|
25 | 28 | PipeSpec redirect) throws IOException { |
|
|
26 | 29 | |
|
|
27 | 30 | var outputRedirect = echoToStderr ? redirect.stderr() : redirect.stdout(); |
|
|
28 | 31 | |
|
|
29 | 32 | return outputRedirect |
|
|
30 | 33 | .map(to -> { |
|
|
31 | 34 | var bytes = String |
|
|
32 | 35 | .format( |
|
|
33 | 36 | "exec: %s", |
|
|
34 | 37 | command.commandLine().stream().collect(Collectors.joining(" "))) |
|
|
35 | 38 | .getBytes(StandardCharsets.UTF_8); |
|
|
36 | 39 | |
|
|
37 | 40 | return to.redirect(new ByteArrayInputStream(bytes)) |
|
|
38 | 41 | .thenApply((x) -> 0); |
|
|
39 | 42 | }) |
|
|
40 | 43 | .orElse(CompletableFuture.completedFuture(0)); |
|
|
41 | 44 | } |
|
|
42 | 45 | |
|
|
43 | 46 | } |
| @@ -1,33 +1,34 | |||
|
|
1 | package org.implab.gradle.common.exec; | |
|
|
1 | package org.implab.gradle.common.exec.runtime; | |
|
|
2 | 2 | |
|
|
3 | 3 | import java.io.File; |
|
|
4 | 4 | import java.util.Map; |
|
|
5 | 5 | import java.util.Optional; |
|
|
6 | 6 | |
|
|
7 | 7 | import org.eclipse.jdt.annotation.NonNullByDefault; |
|
|
8 | import org.implab.gradle.common.exec.model.EnvironmentSpec; | |
|
|
8 | 9 | |
|
|
9 | 10 | @NonNullByDefault |
|
|
10 | 11 | public interface EnvironmentBuilder { |
|
|
11 | 12 | /** Sets the specified environment variable */ |
|
|
12 | 13 | EnvironmentBuilder putEnvironment(String envVar, String value); |
|
|
13 | 14 | |
|
|
14 | 15 | /** Replaces environment with the supplied one */ |
|
|
15 | 16 | EnvironmentBuilder environment(Map<String, ? extends String> env); |
|
|
16 | 17 | |
|
|
17 | 18 | /** |
|
|
18 | 19 | * Enables or disables environment inheritance for the child process |
|
|
19 | 20 | */ |
|
|
20 | 21 | EnvironmentBuilder inheritEnvironment(boolean inherit); |
|
|
21 | 22 | |
|
|
22 | 23 | /** Specifies the working directory */ |
|
|
23 | 24 | EnvironmentBuilder workingDirectory(File directory); |
|
|
24 | 25 | |
|
|
25 | 26 | /** Specifies the working directory */ |
|
|
26 | 27 | EnvironmentBuilder workingDirectory(Optional<? extends File> directory); |
|
|
27 | 28 | |
|
|
28 | 29 | /** Copies the supplied environment to this one */ |
|
|
29 | 30 | default EnvironmentBuilder from(EnvironmentSpec environmentSpec) { |
|
|
30 | 31 | return environment(environmentSpec.environment()) |
|
|
31 | 32 | .workingDirectory(environmentSpec.workingDirectory()); |
|
|
32 | 33 | } |
|
|
33 | 34 | } |
| @@ -1,27 +1,28 | |||
|
|
1 | package org.implab.gradle.common.exec; | |
|
|
1 | package org.implab.gradle.common.exec.runtime; | |
|
|
2 | 2 | |
|
|
3 | 3 | import java.util.Optional; |
|
|
4 | 4 | |
|
|
5 | 5 | import org.eclipse.jdt.annotation.NonNullByDefault; |
|
|
6 | import org.implab.gradle.common.exec.model.PipeSpec; | |
|
|
6 | 7 | |
|
|
7 | 8 | @NonNullByDefault |
|
|
8 | 9 | public interface PipeBuilder { |
|
|
9 | 10 | PipeBuilder stdin(RedirectFrom from); |
|
|
10 | 11 | |
|
|
11 | 12 | PipeBuilder stdin(Optional<? extends RedirectFrom> from); |
|
|
12 | 13 | |
|
|
13 | 14 | PipeBuilder stdout(RedirectTo to); |
|
|
14 | 15 | |
|
|
15 | 16 | PipeBuilder stdout(Optional<? extends RedirectTo> to); |
|
|
16 | 17 | |
|
|
17 | 18 | PipeBuilder stderr(RedirectTo to); |
|
|
18 | 19 | |
|
|
19 | 20 | PipeBuilder stderr(Optional<? extends RedirectTo> to); |
|
|
20 | 21 | |
|
|
21 | 22 | default PipeBuilder from(PipeSpec pipeSpec) { |
|
|
22 | 23 | return stdin(pipeSpec.stdin()) |
|
|
23 | 24 | .stdout(pipeSpec.stdout()) |
|
|
24 | 25 | .stderr(pipeSpec.stderr()); |
|
|
25 | 26 | } |
|
|
26 | 27 | |
|
|
27 | 28 | } |
| @@ -1,56 +1,56 | |||
|
|
1 | package org.implab.gradle.common.exec; | |
|
|
1 | package org.implab.gradle.common.exec.runtime; | |
|
|
2 | 2 | |
|
|
3 | 3 | import java.io.File; |
|
|
4 | 4 | import java.io.FileInputStream; |
|
|
5 | 5 | import java.io.InputStream; |
|
|
6 | 6 | import java.io.OutputStream; |
|
|
7 | 7 | import java.util.concurrent.CompletableFuture; |
|
|
8 | 8 | |
|
|
9 | 9 | import org.eclipse.jdt.annotation.NonNullByDefault; |
|
|
10 | 10 | |
|
|
11 | 11 | /** |
|
|
12 | 12 | * Describes how to redirect input streams. This interface is used to configure |
|
|
13 | 13 | * lazy redirection. {@link #redirect(OutputStream)} is called when the process |
|
|
14 | 14 | * is started. Before the process is started the redirection isn't invoked and |
|
|
15 | 15 | * no resources are allocated or used. |
|
|
16 | 16 | */ |
|
|
17 | 17 | @NonNullByDefault |
|
|
18 | 18 | public interface RedirectFrom { |
|
|
19 | 19 | CompletableFuture<Void> redirect(OutputStream to); |
|
|
20 | 20 | |
|
|
21 | 21 | /** |
|
|
22 | 22 | * Read file contents and redirect it to the output stream. |
|
|
23 | 23 | */ |
|
|
24 | 24 | public static RedirectFrom file(final File file) { |
|
|
25 | 25 | return to -> CompletableFuture.runAsync(() -> { |
|
|
26 | 26 | try (var from = new FileInputStream(file); to) { |
|
|
27 | 27 | from.transferTo(to); |
|
|
28 | 28 | } catch (Exception e) { |
|
|
29 | 29 | // silence! |
|
|
30 | 30 | } |
|
|
31 | 31 | }); |
|
|
32 | 32 | } |
|
|
33 | 33 | |
|
|
34 | 34 | public static RedirectFrom stream(final InputStream from) { |
|
|
35 | 35 | return to -> CompletableFuture.runAsync(() -> { |
|
|
36 | 36 | try (from; to) { |
|
|
37 | 37 | from.transferTo(to); |
|
|
38 | 38 | } catch (Exception e) { |
|
|
39 | 39 | // silence! |
|
|
40 | 40 | } |
|
|
41 | 41 | }); |
|
|
42 | 42 | } |
|
|
43 | 43 | |
|
|
44 | 44 | public static RedirectFrom any(final Object output) { |
|
|
45 | 45 | if (output instanceof File f) { |
|
|
46 | 46 | return file(f); |
|
|
47 | 47 | } else if (output instanceof InputStream stm) { |
|
|
48 | 48 | return stream(stm); |
|
|
49 | 49 | } else if (output instanceof RedirectFrom self) { |
|
|
50 | 50 | return self; |
|
|
51 | 51 | } else { |
|
|
52 | 52 | throw new IllegalArgumentException("The specified argument type isn't supported: " + output.getClass()); |
|
|
53 | 53 | } |
|
|
54 | 54 | } |
|
|
55 | 55 | |
|
|
56 | 56 | } |
| @@ -1,111 +1,111 | |||
|
|
1 | package org.implab.gradle.common.exec; | |
|
|
1 | package org.implab.gradle.common.exec.runtime; | |
|
|
2 | 2 | |
|
|
3 | 3 | import java.io.File; |
|
|
4 | 4 | import java.io.FileOutputStream; |
|
|
5 | 5 | import java.io.InputStream; |
|
|
6 | 6 | import java.io.OutputStream; |
|
|
7 | 7 | import java.util.Scanner; |
|
|
8 | 8 | import java.util.concurrent.CompletableFuture; |
|
|
9 | 9 | import java.util.function.Consumer; |
|
|
10 | 10 | import java.util.stream.Collectors; |
|
|
11 | 11 | import java.util.stream.Stream; |
|
|
12 | 12 | |
|
|
13 | 13 | import org.eclipse.jdt.annotation.NonNullByDefault; |
|
|
14 | 14 | |
|
|
15 | 15 | /** |
|
|
16 | 16 | * Redirection specification for the {@link InputStream}. Redirection is invoked |
|
|
17 | 17 | * when the {@link InputStream} becomes available, for example, on process |
|
|
18 | 18 | * start. Before the process is started the redirection isn't invoked and no |
|
|
19 | 19 | * resources are allocated or used. |
|
|
20 | 20 | */ |
|
|
21 | 21 | @NonNullByDefault |
|
|
22 | 22 | public interface RedirectTo { |
|
|
23 | 23 | CompletableFuture<Void> redirect(InputStream from); |
|
|
24 | 24 | |
|
|
25 | 25 | public interface StringConsumer extends Consumer<String> { |
|
|
26 | 26 | default void complete() { |
|
|
27 | 27 | } |
|
|
28 | 28 | } |
|
|
29 | 29 | |
|
|
30 | 30 | /** Creates a redirect to the specified consumer */ |
|
|
31 | 31 | public static RedirectTo eachLine(final Consumer<String> consumer) { |
|
|
32 | 32 | return consumer(new StringConsumer() { |
|
|
33 | 33 | @Override |
|
|
34 | 34 | public void accept(String s) { |
|
|
35 | 35 | consumer.accept(s); |
|
|
36 | 36 | } |
|
|
37 | 37 | }); |
|
|
38 | 38 | } |
|
|
39 | 39 | |
|
|
40 | 40 | public static RedirectTo allText(final Consumer<String> consumer) { |
|
|
41 | 41 | return consumer(new StringConsumer() { |
|
|
42 | 42 | final Stream.Builder<String> builder = Stream.builder(); |
|
|
43 | 43 | |
|
|
44 | 44 | @Override |
|
|
45 | 45 | public void accept(String s) { |
|
|
46 | 46 | builder.accept(s); |
|
|
47 | 47 | } |
|
|
48 | 48 | |
|
|
49 | 49 | @Override |
|
|
50 | 50 | public void complete() { |
|
|
51 | 51 | consumer.accept(builder.build().collect(Collectors.joining("\n"))); |
|
|
52 | 52 | } |
|
|
53 | 53 | }); |
|
|
54 | 54 | } |
|
|
55 | 55 | |
|
|
56 | 56 | /** Creates a redirect to the specified consumer */ |
|
|
57 | 57 | public static RedirectTo consumer(final StringConsumer consumer) { |
|
|
58 | 58 | return (src) -> CompletableFuture.runAsync(() -> { |
|
|
59 | 59 | try (Scanner sc = new Scanner(src)) { |
|
|
60 | 60 | while (sc.hasNextLine()) { |
|
|
61 | 61 | consumer.accept(sc.nextLine()); |
|
|
62 | 62 | } |
|
|
63 | 63 | consumer.complete(); |
|
|
64 | 64 | } |
|
|
65 | 65 | }); |
|
|
66 | 66 | } |
|
|
67 | 67 | |
|
|
68 | 68 | /** |
|
|
69 | 69 | * Creates a redirect to the specified file. There will be opened the output |
|
|
70 | 70 | * stream in this redirection and original stream will be transferred to this |
|
|
71 | 71 | * this output stream. |
|
|
72 | 72 | */ |
|
|
73 | 73 | public static RedirectTo file(final File file) { |
|
|
74 | 74 | return src -> CompletableFuture.runAsync(() -> { |
|
|
75 | 75 | try (src; OutputStream out = new FileOutputStream(file)) { |
|
|
76 | 76 | src.transferTo(out); |
|
|
77 | 77 | } catch (Exception e) { |
|
|
78 | 78 | // silence! |
|
|
79 | 79 | } |
|
|
80 | 80 | }); |
|
|
81 | 81 | } |
|
|
82 | 82 | |
|
|
83 | 83 | /** Creates a redirect to the specified output stream. */ |
|
|
84 | 84 | public static RedirectTo stream(final OutputStream dest) { |
|
|
85 | 85 | return src -> CompletableFuture.runAsync(() -> { |
|
|
86 | 86 | try (dest; src) { |
|
|
87 | 87 | src.transferTo(dest); |
|
|
88 | 88 | } catch (Exception e) { |
|
|
89 | 89 | // silence! |
|
|
90 | 90 | } |
|
|
91 | 91 | }); |
|
|
92 | 92 | } |
|
|
93 | 93 | |
|
|
94 | 94 | /** |
|
|
95 | 95 | * Creates the redirection to the specified destination, actual type of |
|
|
96 | 96 | * redirection will be determined from the type of the output object. |
|
|
97 | 97 | */ |
|
|
98 | 98 | public static RedirectTo any(final Object output) { |
|
|
99 | 99 | if (output instanceof StringConsumer fn) { |
|
|
100 | 100 | return consumer(s -> fn.accept(s)); |
|
|
101 | 101 | } else if (output instanceof File f) { |
|
|
102 | 102 | return file(f); |
|
|
103 | 103 | } else if (output instanceof OutputStream stm) { |
|
|
104 | 104 | return stream(stm); |
|
|
105 | 105 | } else if (output instanceof RedirectTo self) { |
|
|
106 | 106 | return self; |
|
|
107 | 107 | } else { |
|
|
108 | 108 | throw new IllegalArgumentException("The specified argument type isn't supported: " + output.getClass()); |
|
|
109 | 109 | } |
|
|
110 | 110 | } |
|
|
111 | } No newline at end of file | |
|
|
111 | } | |
| @@ -1,29 +1,30 | |||
|
|
1 | package org.implab.gradle.common.exec; | |
|
|
1 | package org.implab.gradle.common.exec.runtime; | |
|
|
2 | 2 | |
|
|
3 | 3 | import org.eclipse.jdt.annotation.NonNullByDefault; |
|
|
4 | import org.implab.gradle.common.exec.model.CommandSpec; | |
|
|
4 | 5 | |
|
|
5 | 6 | @NonNullByDefault |
|
|
6 | 7 | public interface Shell { |
|
|
7 | 8 | |
|
|
8 | 9 | ShellExec create(CommandSpec spec); |
|
|
9 | 10 | |
|
|
10 | 11 | default ShellTool tool(String executable) { |
|
|
11 | 12 | return new ShellTool(executable, this); |
|
|
12 | 13 | } |
|
|
13 | 14 | |
|
|
14 | 15 | /** Creates a new shell to start processes using ProcessBuilder. */ |
|
|
15 | 16 | static Shell system() { |
|
|
16 | 17 | return spec -> new SystemExecBuilder(spec); |
|
|
17 | 18 | } |
|
|
18 | 19 | |
|
|
19 | 20 | /** Creates a stub shell which echoes the specified command line. */ |
|
|
20 | 21 | static Shell echo() { |
|
|
21 | 22 | return spec -> new EchoExecBuilder(spec, false); |
|
|
22 | 23 | } |
|
|
23 | 24 | |
|
|
24 | 25 | /** Creates a stub shell which echoes the specified command line. */ |
|
|
25 | 26 | static Shell echoStderr() { |
|
|
26 | 27 | return spec -> new EchoExecBuilder(spec, false); |
|
|
27 | 28 | } |
|
|
28 | 29 | |
|
|
29 | 30 | } |
| @@ -1,38 +1,38 | |||
|
|
1 | package org.implab.gradle.common.exec; | |
|
|
1 | package org.implab.gradle.common.exec.runtime; | |
|
|
2 | 2 | |
|
|
3 | 3 | import java.io.IOException; |
|
|
4 | 4 | import java.util.ArrayList; |
|
|
5 | 5 | import java.util.List; |
|
|
6 | 6 | import java.util.concurrent.CompletableFuture; |
|
|
7 | 7 | |
|
|
8 | 8 | import org.eclipse.jdt.annotation.NonNullByDefault; |
|
|
9 |
import org.implab.gradle.common. |
|
|
|
9 | import org.implab.gradle.common.core.lang.Exceptions; | |
|
|
10 | 10 | |
|
|
11 | 11 | @NonNullByDefault |
|
|
12 | 12 | public interface ShellExec extends PipeBuilder, EnvironmentBuilder { |
|
|
13 | 13 | |
|
|
14 | 14 | default CompletableFuture<List<String>> readAllLines() throws IOException { |
|
|
15 | 15 | List<String> lines = new ArrayList<>(); |
|
|
16 | 16 | stdout(RedirectTo.consumer(lines::add)); |
|
|
17 | 17 | |
|
|
18 | 18 | return exec() |
|
|
19 | 19 | .thenAccept(Exceptions.unchecked(this::assertExitCode)) |
|
|
20 | 20 | .thenApply(v -> lines); |
|
|
21 | 21 | } |
|
|
22 | 22 | |
|
|
23 | 23 | default CompletableFuture<String> readAllText() throws IOException { |
|
|
24 | 24 | List<String> lines = new ArrayList<>(); |
|
|
25 | 25 | stdout(RedirectTo.consumer(lines::add)); |
|
|
26 | 26 | |
|
|
27 | 27 | return exec() |
|
|
28 | 28 | .thenAccept(Exceptions.unchecked(this::assertExitCode)) |
|
|
29 | 29 | .thenApply(v -> String.join("\n", lines)); |
|
|
30 | 30 | } |
|
|
31 | 31 | |
|
|
32 | 32 | default void assertExitCode(Integer code) throws IOException { |
|
|
33 | 33 | if (code != 0) |
|
|
34 | 34 | throw new IOException(String.format("The process is terminated with code %d", code)); |
|
|
35 | 35 | } |
|
|
36 | 36 | |
|
|
37 | 37 | CompletableFuture<Integer> exec() throws IOException; |
|
|
38 | 38 | } |
| @@ -1,40 +1,42 | |||
|
|
1 | package org.implab.gradle.common.exec; | |
|
|
1 | package org.implab.gradle.common.exec.runtime; | |
|
|
2 | 2 | |
|
|
3 | 3 | import org.eclipse.jdt.annotation.NonNullByDefault; |
|
|
4 | 4 | import org.gradle.api.Action; |
|
|
5 | import org.implab.gradle.common.exec.model.CommandArgumentsBuilder; | |
|
|
6 | import org.implab.gradle.common.exec.model.CommandSpec; | |
|
|
5 | 7 | |
|
|
6 | 8 | @NonNullByDefault |
|
|
7 | 9 | public class ShellTool { |
|
|
8 | 10 | |
|
|
9 | 11 | private final Shell shell; |
|
|
10 | 12 | |
|
|
11 | 13 | private final String executable; |
|
|
12 | 14 | |
|
|
13 | 15 | ShellTool(String executable, Shell shell) { |
|
|
14 | 16 | this.shell = shell; |
|
|
15 | 17 | this.executable = executable; |
|
|
16 | 18 | } |
|
|
17 | 19 | |
|
|
18 | 20 | public ShellExec arguments(String... args) { |
|
|
19 | 21 | return shell.create(CommandSpec.builder() |
|
|
20 | 22 | .executable(executable) |
|
|
21 | 23 | .arguments(args) |
|
|
22 | 24 | .build()); |
|
|
23 | 25 | } |
|
|
24 | 26 | |
|
|
25 | 27 | public ShellExec arguments(Iterable<String> args) { |
|
|
26 | 28 | return shell.create(CommandSpec.builder() |
|
|
27 | 29 | .executable(executable) |
|
|
28 | 30 | .arguments(args) |
|
|
29 | 31 | .build()); |
|
|
30 | 32 | } |
|
|
31 | 33 | |
|
|
32 | 34 | public ShellExec arguments(Action<CommandArgumentsBuilder<?>> args) { |
|
|
33 | 35 | var builder = CommandSpec.builder() |
|
|
34 | 36 | .executable(executable); |
|
|
35 | 37 | args.execute(builder); |
|
|
36 | 38 | |
|
|
37 | 39 | return shell.create(builder.build()); |
|
|
38 | 40 | } |
|
|
39 | 41 | |
|
|
40 | 42 | } |
| @@ -1,52 +1,55 | |||
|
|
1 | package org.implab.gradle.common.exec; | |
|
|
1 | package org.implab.gradle.common.exec.runtime; | |
|
|
2 | 2 | |
|
|
3 | 3 | import java.io.IOException; |
|
|
4 | 4 | import java.lang.ProcessBuilder.Redirect; |
|
|
5 | 5 | import java.util.ArrayList; |
|
|
6 | 6 | import java.util.concurrent.CompletableFuture; |
|
|
7 | 7 | |
|
|
8 | 8 | import org.eclipse.jdt.annotation.NonNullByDefault; |
|
|
9 | import org.implab.gradle.common.exec.model.CommandSpec; | |
|
|
10 | import org.implab.gradle.common.exec.model.EnvironmentSpec; | |
|
|
11 | import org.implab.gradle.common.exec.model.PipeSpec; | |
|
|
9 | 12 | |
|
|
10 | 13 | @NonNullByDefault |
|
|
11 | 14 | class SystemExecBuilder extends AbstractExecBuilder<CommandSpec> { |
|
|
12 | 15 | SystemExecBuilder(CommandSpec command) { |
|
|
13 | 16 | super(command); |
|
|
14 | 17 | } |
|
|
15 | 18 | |
|
|
16 | 19 | @Override |
|
|
17 | 20 | protected CompletableFuture<Integer> startInternal( |
|
|
18 | 21 | CommandSpec command, |
|
|
19 | 22 | EnvironmentSpec environment, |
|
|
20 | 23 | PipeSpec redirect) throws IOException { |
|
|
21 | 24 | |
|
|
22 | 25 | var builder = new ProcessBuilder(command.commandLine()); |
|
|
23 | 26 | |
|
|
24 | 27 | environment.workingDirectory().ifPresent(builder::directory); |
|
|
25 | 28 | |
|
|
26 | 29 | // if the env isn't inherited we need to clear it |
|
|
27 | 30 | if (!environment.inheritEnvironment()) |
|
|
28 | 31 | builder.environment().clear(); |
|
|
29 | 32 | |
|
|
30 | 33 | builder.environment().putAll(environment.environment()); |
|
|
31 | 34 | |
|
|
32 | 35 | var tasks = new ArrayList<CompletableFuture<?>>(); |
|
|
33 | 36 | |
|
|
34 | 37 | if (!redirect.stdout().isPresent()) |
|
|
35 | 38 | builder.redirectOutput(Redirect.DISCARD); |
|
|
36 | 39 | if (!redirect.stderr().isPresent()) |
|
|
37 | 40 | builder.redirectError(Redirect.DISCARD); |
|
|
38 | 41 | |
|
|
39 | 42 | // run process |
|
|
40 | 43 | var proc = builder.start(); |
|
|
41 | 44 | |
|
|
42 | 45 | tasks.add(proc.onExit()); |
|
|
43 | 46 | |
|
|
44 | 47 | redirect.stdin().map(from -> from.redirect(proc.getOutputStream())).ifPresent(tasks::add); |
|
|
45 | 48 | redirect.stdout().map(to -> to.redirect(proc.getInputStream())).ifPresent(tasks::add); |
|
|
46 | 49 | redirect.stderr().map(to -> to.redirect(proc.getErrorStream())).ifPresent(tasks::add); |
|
|
47 | 50 | |
|
|
48 | 51 | return CompletableFuture |
|
|
49 | 52 | .allOf(tasks.toArray(new CompletableFuture<?>[0])) |
|
|
50 | 53 | .thenApply(t -> proc.exitValue()); |
|
|
51 | 54 | } |
|
|
52 | 55 | } |
| @@ -1,108 +1,108 | |||
|
|
1 | package org.implab.gradle.common.tasks; | |
|
|
1 | package org.implab.gradle.common.exec.tasks; | |
|
|
2 | 2 | |
|
|
3 | 3 | import java.io.IOException; |
|
|
4 | 4 | import java.util.Map; |
|
|
5 | 5 | import java.util.Optional; |
|
|
6 | 6 | import java.util.concurrent.ExecutionException; |
|
|
7 | 7 | |
|
|
8 | 8 | import org.gradle.api.DefaultTask; |
|
|
9 | 9 | import org.gradle.api.file.DirectoryProperty; |
|
|
10 | 10 | import org.gradle.api.provider.MapProperty; |
|
|
11 | 11 | import org.gradle.api.provider.Property; |
|
|
12 | 12 | import org.gradle.api.tasks.Internal; |
|
|
13 | 13 | import org.gradle.api.tasks.TaskAction; |
|
|
14 |
import org.implab.gradle.common. |
|
|
|
15 |
import org.implab.gradle.common. |
|
|
|
16 |
import org.implab.gradle.common. |
|
|
|
17 |
import org.implab.gradle.common. |
|
|
|
18 |
import org.implab.gradle.common.exec. |
|
|
|
19 |
import org.implab.gradle.common.exec. |
|
|
|
20 |
import org.implab.gradle.common. |
|
|
|
21 |
import org.implab.gradle.common. |
|
|
|
14 | import org.implab.gradle.common.core.lang.Exceptions; | |
|
|
15 | import org.implab.gradle.common.core.lang.Values; | |
|
|
16 | import org.implab.gradle.common.exec.dsl.RedirectFromSpec; | |
|
|
17 | import org.implab.gradle.common.exec.dsl.RedirectToSpec; | |
|
|
18 | import org.implab.gradle.common.exec.dsl.TaskEnvSpecMixin; | |
|
|
19 | import org.implab.gradle.common.exec.dsl.TaskPipeSpecMixin; | |
|
|
20 | import org.implab.gradle.common.exec.runtime.RedirectTo; | |
|
|
21 | import org.implab.gradle.common.exec.runtime.ShellExec; | |
|
|
22 | 22 | |
|
|
23 | 23 | public abstract class AbstractShellExecTask |
|
|
24 | 24 | extends DefaultTask |
|
|
25 | 25 | implements TaskPipeSpecMixin, TaskEnvSpecMixin { |
|
|
26 | 26 | |
|
|
27 | 27 | private final RedirectToSpec redirectStderr = new RedirectToSpec(); |
|
|
28 | 28 | |
|
|
29 | 29 | private final RedirectToSpec redirectStdout = new RedirectToSpec(); |
|
|
30 | 30 | |
|
|
31 | 31 | private final RedirectFromSpec redirectStdin = new RedirectFromSpec(); |
|
|
32 | 32 | |
|
|
33 | 33 | @Internal |
|
|
34 | 34 | @Override |
|
|
35 | 35 | public abstract Property<Boolean> getInheritEnvironment(); |
|
|
36 | 36 | |
|
|
37 | 37 | @Internal |
|
|
38 | 38 | @Override |
|
|
39 | 39 | public abstract DirectoryProperty getWorkingDirectory(); |
|
|
40 | 40 | |
|
|
41 | 41 | @Internal |
|
|
42 | 42 | @Override |
|
|
43 | 43 | public abstract MapProperty<String, String> getEnvironment(); |
|
|
44 | 44 | |
|
|
45 | 45 | /** |
|
|
46 | 46 | * STDIN redirection, if not specified, no input will be passed to the command |
|
|
47 | 47 | */ |
|
|
48 | 48 | @Internal |
|
|
49 | 49 | @Override |
|
|
50 | 50 | public RedirectFromSpec getStdin() { |
|
|
51 | 51 | return redirectStdin; |
|
|
52 | 52 | } |
|
|
53 | 53 | |
|
|
54 | 54 | /** |
|
|
55 | 55 | * STDOUT redirection, if not specified, redirected to logger::info |
|
|
56 | 56 | */ |
|
|
57 | 57 | @Internal |
|
|
58 | 58 | @Override |
|
|
59 | 59 | public RedirectToSpec getStdout() { |
|
|
60 | 60 | return redirectStdout; |
|
|
61 | 61 | } |
|
|
62 | 62 | |
|
|
63 | 63 | /** |
|
|
64 | 64 | * STDERR redirection, if not specified, redirected to logger::error |
|
|
65 | 65 | */ |
|
|
66 | 66 | @Internal |
|
|
67 | 67 | @Override |
|
|
68 | 68 | public RedirectToSpec getStderr() { |
|
|
69 | 69 | return redirectStderr; |
|
|
70 | 70 | } |
|
|
71 | 71 | |
|
|
72 | 72 | protected abstract ShellExec execBuilder(); |
|
|
73 | 73 | |
|
|
74 | 74 | protected Optional<RedirectTo> conventionalStderr() { |
|
|
75 | 75 | return Optional.of(RedirectTo.eachLine(getLogger()::error)); |
|
|
76 | 76 | } |
|
|
77 | 77 | |
|
|
78 | 78 | protected Optional<RedirectTo> conventionalStdout() { |
|
|
79 | 79 | return Optional.of(RedirectTo.eachLine(getLogger()::info)); |
|
|
80 | 80 | } |
|
|
81 | 81 | |
|
|
82 | 82 | @TaskAction |
|
|
83 | 83 | public final void run() throws IOException, InterruptedException, ExecutionException { |
|
|
84 | 84 | // create new shell process |
|
|
85 | 85 | var execBuilder = execBuilder(); |
|
|
86 | 86 | |
|
|
87 | 87 | // configure environment |
|
|
88 | 88 | execBuilder |
|
|
89 | 89 | .workingDirectory(Values.optional(getWorkingDirectory().getAsFile())) |
|
|
90 | 90 | .environment(getEnvironment().getOrElse(Map.of())); |
|
|
91 | 91 | |
|
|
92 | 92 | // configure redirects |
|
|
93 | 93 | execBuilder |
|
|
94 | 94 | .stdout(getStdout().getRedirection().or(this::conventionalStdout)) |
|
|
95 | 95 | .stderr(getStderr().getRedirection().or(this::conventionalStderr)) |
|
|
96 | 96 | .stdin(getStdin().getRedirection()); |
|
|
97 | 97 | |
|
|
98 | 98 | // execute |
|
|
99 | 99 | execBuilder.exec() |
|
|
100 | 100 | .thenAccept(Exceptions.unchecked(this::assertExitCode)) |
|
|
101 | 101 | .join(); |
|
|
102 | 102 | } |
|
|
103 | 103 | |
|
|
104 | 104 | protected void assertExitCode(Integer code) throws IOException { |
|
|
105 | 105 | if (code != 0) |
|
|
106 | 106 | throw new IOException(String.format("The process is terminated with code %s", code)); |
|
|
107 | 107 | } |
|
|
108 | } No newline at end of file | |
|
|
108 | } | |
| @@ -1,31 +1,31 | |||
|
|
1 | package org.implab.gradle.common.tasks; | |
|
|
1 | package org.implab.gradle.common.exec.tasks; | |
|
|
2 | 2 | |
|
|
3 | 3 | import org.gradle.api.provider.ListProperty; |
|
|
4 | 4 | import org.gradle.api.provider.Property; |
|
|
5 | 5 | import org.gradle.api.tasks.Internal; |
|
|
6 | import org.implab.gradle.common.dsl.TaskCommandSpecMixin; | |
|
|
7 | import org.implab.gradle.common.exec.CommandSpec; | |
|
|
8 | import org.implab.gradle.common.exec.Shell; | |
|
|
9 | import org.implab.gradle.common.exec.ShellExec; | |
|
|
6 | import org.implab.gradle.common.exec.dsl.TaskCommandSpecMixin; | |
|
|
7 | import org.implab.gradle.common.exec.model.CommandSpec; | |
|
|
8 | import org.implab.gradle.common.exec.runtime.Shell; | |
|
|
9 | import org.implab.gradle.common.exec.runtime.ShellExec; | |
|
|
10 | 10 | |
|
|
11 | 11 | public abstract class ShellExecTask |
|
|
12 | 12 | extends AbstractShellExecTask |
|
|
13 | 13 | implements TaskCommandSpecMixin { |
|
|
14 | 14 | |
|
|
15 | 15 | @Internal |
|
|
16 | 16 | public abstract Property<Shell> getShell(); |
|
|
17 | 17 | |
|
|
18 | 18 | @Internal |
|
|
19 | 19 | @Override |
|
|
20 | 20 | public abstract ListProperty<String> getCommandLine(); |
|
|
21 | 21 | |
|
|
22 | 22 | |
|
|
23 | 23 | @Override |
|
|
24 | 24 | protected ShellExec execBuilder() { |
|
|
25 | 25 | return getShell().get() |
|
|
26 | 26 | .create(CommandSpec.builder() |
|
|
27 | 27 | .commandLine(getCommandLine().get()) |
|
|
28 | 28 | .build()); |
|
|
29 | 29 | } |
|
|
30 | 30 | |
|
|
31 | 31 | } |
| @@ -1,142 +1,142 | |||
|
|
1 | package org.implab.gradle.common.json; | |
|
|
1 | package org.implab.gradle.common.json.core; | |
|
|
2 | 2 | |
|
|
3 | 3 | import java.io.File; |
|
|
4 | 4 | import java.io.IOException; |
|
|
5 | 5 | import java.util.Map; |
|
|
6 | 6 | import java.util.function.Consumer; |
|
|
7 | 7 | import java.util.function.Function; |
|
|
8 | 8 | |
|
|
9 |
import org.implab.gradle.common. |
|
|
|
9 | import org.implab.gradle.common.core.lang.LazyValue; | |
|
|
10 | 10 | |
|
|
11 | 11 | import com.fasterxml.jackson.annotation.JsonInclude.Include; |
|
|
12 | 12 | import com.fasterxml.jackson.core.JsonProcessingException; |
|
|
13 | 13 | import com.fasterxml.jackson.core.type.TypeReference; |
|
|
14 | 14 | import com.fasterxml.jackson.core.util.DefaultIndenter; |
|
|
15 | 15 | import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; |
|
|
16 | 16 | import com.fasterxml.jackson.databind.JsonMappingException; |
|
|
17 | 17 | import com.fasterxml.jackson.databind.ObjectMapper; |
|
|
18 | 18 | import com.fasterxml.jackson.databind.SerializationFeature; |
|
|
19 | 19 | import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; |
|
|
20 | 20 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; |
|
|
21 | 21 | |
|
|
22 | 22 | public class Json { |
|
|
23 | 23 | |
|
|
24 | 24 | private final static LazyValue<ObjectMapper> mapper = new LazyValue<>(Json::createMapper); |
|
|
25 | 25 | |
|
|
26 | 26 | private static ObjectMapper createMapper() { |
|
|
27 | 27 | DefaultPrettyPrinter prettyPrinter = new DefaultPrettyPrinter(); |
|
|
28 | 28 | prettyPrinter.indentArraysWith(DefaultIndenter.SYSTEM_LINEFEED_INSTANCE); |
|
|
29 | 29 | |
|
|
30 | 30 | return new ObjectMapper() |
|
|
31 | 31 | .registerModule(new Jdk8Module()) |
|
|
32 | 32 | .registerModule(new JavaTimeModule()) |
|
|
33 | 33 | .setSerializationInclusion(Include.NON_EMPTY) |
|
|
34 | 34 | .enable(SerializationFeature.INDENT_OUTPUT) |
|
|
35 | 35 | .setDefaultPrettyPrinter(prettyPrinter); |
|
|
36 | 36 | } |
|
|
37 | 37 | |
|
|
38 | 38 | public static ObjectMapper jsonMapper() { |
|
|
39 | 39 | return mapper.get(); |
|
|
40 | 40 | } |
|
|
41 | 41 | |
|
|
42 | 42 | public static Consumer<Object> fileWriter(File file) { |
|
|
43 | 43 | return value -> { |
|
|
44 | 44 | try { |
|
|
45 | 45 | jsonMapper().writeValue(file, value); |
|
|
46 | 46 | } catch (IOException e) { |
|
|
47 | 47 | throw new SerializationException(e); |
|
|
48 | 48 | } |
|
|
49 | 49 | }; |
|
|
50 | 50 | } |
|
|
51 | 51 | |
|
|
52 | 52 | public static void write(File file, Object value) { |
|
|
53 | 53 | try { |
|
|
54 | 54 | jsonMapper().writeValue(file, value); |
|
|
55 | 55 | } catch (IOException e) { |
|
|
56 | 56 | throw new SerializationException(e); |
|
|
57 | 57 | } |
|
|
58 | 58 | } |
|
|
59 | 59 | |
|
|
60 | 60 | public static String stringify(Object value) { |
|
|
61 | 61 | try { |
|
|
62 | 62 | return jsonMapper().writeValueAsString(value); |
|
|
63 | 63 | } catch (JsonProcessingException e) { |
|
|
64 | 64 | throw new SerializationException(e); |
|
|
65 | 65 | } |
|
|
66 | 66 | } |
|
|
67 | 67 | |
|
|
68 | 68 | public static <T> T parse(String json, Class<T> clazz) { |
|
|
69 | 69 | try { |
|
|
70 | 70 | return jsonMapper().readValue(json, clazz); |
|
|
71 | 71 | } catch (JsonProcessingException e) { |
|
|
72 | 72 | throw new SerializationException(e); |
|
|
73 | 73 | } |
|
|
74 | 74 | } |
|
|
75 | 75 | |
|
|
76 | 76 | public static <T> T parse(String json, TypeReference<T> type) { |
|
|
77 | 77 | try { |
|
|
78 | 78 | return jsonMapper().readValue(json, type); |
|
|
79 | 79 | } catch (JsonProcessingException e) { |
|
|
80 | 80 | throw new SerializationException(e); |
|
|
81 | 81 | } |
|
|
82 | 82 | } |
|
|
83 | 83 | |
|
|
84 | 84 | public static <V> Map<String, V> parseMap(String json, Class<V> clazz) { |
|
|
85 | 85 | return parse(json, new TypeReference<>() { |
|
|
86 | 86 | }); |
|
|
87 | 87 | } |
|
|
88 | 88 | |
|
|
89 | 89 | public static <V> Function<String, Map<String, V>> mapParser(Class<V> clazz) { |
|
|
90 | 90 | return json -> parseMap(json, clazz); |
|
|
91 | 91 | } |
|
|
92 | 92 | |
|
|
93 | 93 | public static <T> T read(File file, TypeReference<T> type) { |
|
|
94 | 94 | try { |
|
|
95 | 95 | return jsonMapper().readValue(file, type); |
|
|
96 | 96 | } catch (IOException e) { |
|
|
97 | 97 | throw new SerializationException(e); |
|
|
98 | 98 | } |
|
|
99 | 99 | } |
|
|
100 | 100 | |
|
|
101 | 101 | public static <V> Map<String, V> readMap(File file, Class<V> clazz) { |
|
|
102 | 102 | return read(file, new TypeReference<>() { |
|
|
103 | 103 | }); |
|
|
104 | 104 | } |
|
|
105 | 105 | |
|
|
106 | 106 | public static <T> T updateValue(T target, Object source) { |
|
|
107 | 107 | try { |
|
|
108 | 108 | return jsonMapper().updateValue(target, source); |
|
|
109 | 109 | } catch (JsonMappingException e) { |
|
|
110 | 110 | throw new SerializationException(e); |
|
|
111 | 111 | } |
|
|
112 | 112 | } |
|
|
113 | 113 | |
|
|
114 | 114 | public static <T> T convertValue(Object source, Class<T> clazz) { |
|
|
115 | 115 | var mapper = jsonMapper(); |
|
|
116 | 116 | var buf = mapper.getSerializerProvider().bufferForValueConversion(jsonMapper()); |
|
|
117 | 117 | |
|
|
118 | 118 | try { |
|
|
119 | 119 | mapper.writeValue(buf, source); |
|
|
120 | 120 | return mapper.readValue(buf.asParser(), clazz); |
|
|
121 | 121 | } catch (IOException e) { |
|
|
122 | 122 | throw new SerializationException(e); |
|
|
123 | 123 | } |
|
|
124 | 124 | } |
|
|
125 | 125 | |
|
|
126 | 126 | public static class SerializationException extends RuntimeException { |
|
|
127 | 127 | public SerializationException() { |
|
|
128 | 128 | } |
|
|
129 | 129 | |
|
|
130 | 130 | public SerializationException(String message) { |
|
|
131 | 131 | super(message); |
|
|
132 | 132 | } |
|
|
133 | 133 | |
|
|
134 | 134 | public SerializationException(Throwable cause) { |
|
|
135 | 135 | super(cause); |
|
|
136 | 136 | } |
|
|
137 | 137 | |
|
|
138 | 138 | public SerializationException(String message, Throwable cause) { |
|
|
139 | 139 | super(message, cause); |
|
|
140 | 140 | } |
|
|
141 | 141 | } |
|
|
142 | 142 | } |
| @@ -1,66 +1,66 | |||
|
|
1 | package org.implab.gradle.common.json; | |
|
|
1 | package org.implab.gradle.common.json.core; | |
|
|
2 | 2 | |
|
|
3 | 3 | import java.util.Arrays; |
|
|
4 | 4 | import java.util.LinkedHashSet; |
|
|
5 | 5 | import java.util.Set; |
|
|
6 | 6 | import java.util.function.Predicate; |
|
|
7 | 7 | |
|
|
8 | 8 | import org.gradle.api.Action; |
|
|
9 |
import org.implab.gradle.common. |
|
|
|
9 | import org.implab.gradle.common.core.lang.Closures; | |
|
|
10 | 10 | |
|
|
11 | 11 | import groovy.lang.Closure; |
|
|
12 | 12 | |
|
|
13 | 13 | /** |
|
|
14 | 14 | * ΠΡΠΎΡΡΠ°Ρ include/exclude-ΠΌΠ°ΡΠΊΠ° ΠΏΠΎ ΠΊΠ»ΡΡΠ°ΠΌ Π²Π΅ΡΡ Π½Π΅Π³ΠΎ ΡΡΠΎΠ²Π½Ρ. |
|
|
15 | 15 | */ |
|
|
16 | 16 | public class MapImportSpec { |
|
|
17 | 17 | |
|
|
18 | 18 | private final Set<String> includes = new LinkedHashSet<>(); |
|
|
19 | 19 | private final Set<String> excludes = new LinkedHashSet<>(); |
|
|
20 | 20 | |
|
|
21 | 21 | public void include(String... keys) { |
|
|
22 | 22 | includes.addAll(Arrays.asList(keys)); |
|
|
23 | 23 | } |
|
|
24 | 24 | |
|
|
25 | 25 | public void exclude(String... keys) { |
|
|
26 | 26 | excludes.addAll(Arrays.asList(keys)); |
|
|
27 | 27 | } |
|
|
28 | 28 | |
|
|
29 | 29 | public Set<String> getIncludes() { |
|
|
30 | 30 | return includes; |
|
|
31 | 31 | } |
|
|
32 | 32 | |
|
|
33 | 33 | public Set<String> getExcludes() { |
|
|
34 | 34 | return excludes; |
|
|
35 | 35 | } |
|
|
36 | 36 | |
|
|
37 | 37 | public boolean hasIncludes() { |
|
|
38 | 38 | return !includes.isEmpty(); |
|
|
39 | 39 | } |
|
|
40 | 40 | |
|
|
41 | 41 | public boolean isExcluded(String key) { |
|
|
42 | 42 | return excludes.contains(key); |
|
|
43 | 43 | } |
|
|
44 | 44 | |
|
|
45 | 45 | public boolean isIncluded(String key) { |
|
|
46 | 46 | return !hasIncludes() || includes.contains(key); |
|
|
47 | 47 | } |
|
|
48 | 48 | |
|
|
49 | 49 | public boolean matches(String key) { |
|
|
50 | 50 | return isIncluded(key) && !isExcluded(key); |
|
|
51 | 51 | } |
|
|
52 | 52 | |
|
|
53 | 53 | public Predicate<String> toPredicate() { |
|
|
54 | 54 | return this::matches; |
|
|
55 | 55 | } |
|
|
56 | 56 | |
|
|
57 | 57 | public static Predicate<String> buildPredicate(Action<? super MapImportSpec> action) { |
|
|
58 | 58 | var spec = new MapImportSpec(); |
|
|
59 | 59 | action.execute(spec); |
|
|
60 | 60 | return spec.toPredicate(); |
|
|
61 | 61 | } |
|
|
62 | 62 | |
|
|
63 | 63 | public static Predicate<String> buildPredicate(Closure<?> closure) { |
|
|
64 | 64 | return buildPredicate(Closures.action(closure)); |
|
|
65 | 65 | } |
|
|
66 | } No newline at end of file | |
|
|
66 | } | |
| @@ -1,29 +1,29 | |||
|
|
1 | package org.implab.gradle.common.json; | |
|
|
1 | package org.implab.gradle.common.json.dsl; | |
|
|
2 | 2 | |
|
|
3 | 3 | |
|
|
4 | 4 | import org.gradle.api.provider.Provider; |
|
|
5 | 5 | |
|
|
6 | 6 | import java.util.ArrayList; |
|
|
7 | 7 | import java.util.Collections; |
|
|
8 | 8 | import java.util.List; |
|
|
9 | 9 | |
|
|
10 | 10 | |
|
|
11 | 11 | public class DefaultJsonArraySpec implements JsonArraySpec { |
|
|
12 | 12 | |
|
|
13 | 13 | private final List<Object> values = new ArrayList<>(); |
|
|
14 | 14 | |
|
|
15 | 15 | @Override |
|
|
16 | 16 | public void add(Object value) { |
|
|
17 | 17 | if (value instanceof Provider<?>) { |
|
|
18 | 18 | throw new IllegalArgumentException( |
|
|
19 | 19 | "Providers are not allowed inside JSON arrays; " + |
|
|
20 | 20 | "use top-level metadata.set(\"key\", provider) instead." |
|
|
21 | 21 | ); |
|
|
22 | 22 | } |
|
|
23 | 23 | values.add(value); |
|
|
24 | 24 | } |
|
|
25 | 25 | |
|
|
26 | 26 | public List<Object> toList() { |
|
|
27 | 27 | return Collections.unmodifiableList(values); |
|
|
28 | 28 | } |
|
|
29 | } No newline at end of file | |
|
|
29 | } | |
| @@ -1,26 +1,26 | |||
|
|
1 | package org.implab.gradle.common.json; | |
|
|
1 | package org.implab.gradle.common.json.dsl; | |
|
|
2 | 2 | |
|
|
3 | 3 | import org.gradle.api.provider.Provider; |
|
|
4 | 4 | |
|
|
5 | 5 | import java.util.Collections; |
|
|
6 | 6 | import java.util.LinkedHashMap; |
|
|
7 | 7 | import java.util.Map; |
|
|
8 | 8 | |
|
|
9 | 9 | public class DefaultJsonObjectSpec implements GroovyObjectSpec { |
|
|
10 | 10 | |
|
|
11 | 11 | private final Map<String, Object> values = new LinkedHashMap<>(); |
|
|
12 | 12 | |
|
|
13 | 13 | @Override |
|
|
14 | 14 | public void set(String key, Object value) { |
|
|
15 | 15 | if (value instanceof Provider<?>) { |
|
|
16 | 16 | throw new IllegalArgumentException( |
|
|
17 | 17 | "Providers are not allowed inside nested JSON objects; " + |
|
|
18 | 18 | "use top-level metadata.set(\"" + key + "\", provider) instead."); |
|
|
19 | 19 | } |
|
|
20 | 20 | values.put(key, value); |
|
|
21 | 21 | } |
|
|
22 | 22 | |
|
|
23 | 23 | public Map<String, Object> toMap() { |
|
|
24 | 24 | return Collections.unmodifiableMap(values); |
|
|
25 | 25 | } |
|
|
26 | } No newline at end of file | |
|
|
26 | } | |
| @@ -1,38 +1,38 | |||
|
|
1 | package org.implab.gradle.common.json; | |
|
|
1 | package org.implab.gradle.common.json.dsl; | |
|
|
2 | 2 | |
|
|
3 | 3 | import java.util.Arrays; |
|
|
4 | 4 | |
|
|
5 | 5 | import groovy.lang.Closure; |
|
|
6 | 6 | import groovy.lang.MissingMethodException; |
|
|
7 | 7 | |
|
|
8 | 8 | public interface GroovyObjectSpec extends JsonObjectSpec { |
|
|
9 | 9 | |
|
|
10 | 10 | default void propertyMissing(String name, Object value) { |
|
|
11 | 11 | set(name, value); |
|
|
12 | 12 | } |
|
|
13 | 13 | |
|
|
14 | 14 | default Object methodMissing(String name, Object args) { |
|
|
15 | 15 | Object[] arr = (Object[]) args; |
|
|
16 | 16 | |
|
|
17 | 17 | // author { ... }, repository { ... } |
|
|
18 | 18 | if (arr.length == 1 && arr[0] instanceof Closure<?>) { |
|
|
19 | 19 | DefaultJsonObjectSpec spec = new DefaultJsonObjectSpec(); |
|
|
20 | 20 | Closure<?> cl = (Closure<?>) arr[0]; |
|
|
21 | 21 | cl.setDelegate(spec); |
|
|
22 | 22 | cl.setResolveStrategy(Closure.DELEGATE_FIRST); |
|
|
23 | 23 | cl.call(); |
|
|
24 | 24 | set(name, spec.toMap()); |
|
|
25 | 25 | return null; |
|
|
26 | 26 | } else { |
|
|
27 | 27 | boolean hasClosure = Arrays.stream(arr) |
|
|
28 | 28 | .anyMatch(a -> a instanceof Closure<?>); |
|
|
29 | 29 | |
|
|
30 | 30 | if (!hasClosure) { |
|
|
31 | 31 | set(name, Arrays.asList(arr)); |
|
|
32 | 32 | return null; |
|
|
33 | 33 | } |
|
|
34 | 34 | } |
|
|
35 | 35 | |
|
|
36 | 36 | throw new MissingMethodException(name, getClass(), arr); |
|
|
37 | 37 | } |
|
|
38 | 38 | } |
| @@ -1,20 +1,20 | |||
|
|
1 | package org.implab.gradle.common.json; | |
|
|
1 | package org.implab.gradle.common.json.dsl; | |
|
|
2 | 2 | |
|
|
3 | 3 | import org.gradle.api.Action; |
|
|
4 | 4 | |
|
|
5 | 5 | public interface JsonArraySpec { |
|
|
6 | 6 | |
|
|
7 | 7 | void add(Object value); |
|
|
8 | 8 | |
|
|
9 | 9 | default void obj(Action<? super JsonObjectSpec> action) { |
|
|
10 | 10 | DefaultJsonObjectSpec child = new DefaultJsonObjectSpec(); |
|
|
11 | 11 | action.execute(child); |
|
|
12 | 12 | add(child.toMap()); |
|
|
13 | 13 | } |
|
|
14 | 14 | |
|
|
15 | 15 | default void arr(Action<? super JsonArraySpec> action) { |
|
|
16 | 16 | DefaultJsonArraySpec child = new DefaultJsonArraySpec(); |
|
|
17 | 17 | action.execute(child); |
|
|
18 | 18 | add(child.toList()); |
|
|
19 | 19 | } |
|
|
20 | } No newline at end of file | |
|
|
20 | } | |
| @@ -1,23 +1,24 | |||
|
|
1 | package org.implab.gradle.common.json; | |
|
|
1 | package org.implab.gradle.common.json.dsl; | |
|
|
2 | 2 | |
|
|
3 | 3 | import java.io.File; |
|
|
4 | 4 | |
|
|
5 | 5 | import org.gradle.api.Action; |
|
|
6 | 6 | import org.gradle.api.file.RegularFile; |
|
|
7 | 7 | import org.gradle.api.provider.Provider; |
|
|
8 | import org.implab.gradle.common.json.core.MapImportSpec; | |
|
|
8 | 9 | |
|
|
9 | 10 | public interface JsonMapSpec extends GroovyObjectSpec { |
|
|
10 | 11 | void from(File file, Action<? super MapImportSpec> action); |
|
|
11 | 12 | |
|
|
12 | 13 | default void from(File file) { |
|
|
13 | 14 | from(file, spec -> { |
|
|
14 | 15 | }); |
|
|
15 | 16 | } |
|
|
16 | 17 | |
|
|
17 | 18 | void from(Provider<RegularFile> fileProvider, Action<? super MapImportSpec> action); |
|
|
18 | 19 | |
|
|
19 | 20 | default void from(Provider<RegularFile> fileProvider) { |
|
|
20 | 21 | from(fileProvider, spec -> { |
|
|
21 | 22 | }); |
|
|
22 | 23 | } |
|
|
23 | 24 | } |
| @@ -1,20 +1,20 | |||
|
|
1 | package org.implab.gradle.common.json; | |
|
|
1 | package org.implab.gradle.common.json.dsl; | |
|
|
2 | 2 | |
|
|
3 | 3 | import org.gradle.api.Action; |
|
|
4 | 4 | |
|
|
5 | 5 | public interface JsonObjectSpec { |
|
|
6 | 6 | |
|
|
7 | 7 | void set(String key, Object value); |
|
|
8 | 8 | |
|
|
9 | 9 | default void obj(String key, Action<? super JsonObjectSpec> action) { |
|
|
10 | 10 | DefaultJsonObjectSpec child = new DefaultJsonObjectSpec(); |
|
|
11 | 11 | action.execute(child); |
|
|
12 | 12 | set(key, child.toMap()); |
|
|
13 | 13 | } |
|
|
14 | 14 | |
|
|
15 | 15 | default void arr(String key, Action<? super JsonArraySpec> action) { |
|
|
16 | 16 | DefaultJsonArraySpec child = new DefaultJsonArraySpec(); |
|
|
17 | 17 | action.execute(child); |
|
|
18 | 18 | set(key, child.toList()); |
|
|
19 | 19 | } |
|
|
20 | } No newline at end of file | |
|
|
20 | } | |
| @@ -1,181 +1,180 | |||
|
|
1 | package org.implab.gradle.common.tasks; | |
|
|
1 | package org.implab.gradle.common.json.tasks; | |
|
|
2 | 2 | |
|
|
3 | 3 | import java.util.LinkedHashMap; |
|
|
4 | 4 | import java.util.Map; |
|
|
5 | 5 | import java.util.Objects; |
|
|
6 | 6 | import java.util.function.Predicate; |
|
|
7 | 7 | import java.util.stream.Collectors; |
|
|
8 | 8 | |
|
|
9 | 9 | import javax.inject.Inject; |
|
|
10 | 10 | |
|
|
11 | 11 | import org.eclipse.jdt.annotation.NonNullByDefault; |
|
|
12 | 12 | import org.gradle.api.Action; |
|
|
13 | 13 | import org.gradle.api.DefaultTask; |
|
|
14 | 14 | import org.gradle.api.file.RegularFile; |
|
|
15 | 15 | import org.gradle.api.file.RegularFileProperty; |
|
|
16 | 16 | import org.gradle.api.provider.MapProperty; |
|
|
17 | 17 | import org.gradle.api.provider.Provider; |
|
|
18 | 18 | import org.gradle.api.provider.ProviderFactory; |
|
|
19 | 19 | import org.gradle.api.tasks.Input; |
|
|
20 | 20 | import org.gradle.api.tasks.Internal; |
|
|
21 | 21 | import org.gradle.api.tasks.Optional; |
|
|
22 | 22 | import org.gradle.api.tasks.OutputFile; |
|
|
23 | import org.gradle.api.tasks.SkipWhenEmpty; | |
|
|
24 | 23 | import org.gradle.api.tasks.TaskAction; |
|
|
25 |
import org.implab.gradle.common. |
|
|
|
26 |
import org.implab.gradle.common. |
|
|
|
27 |
import org.implab.gradle.common.json.Json |
|
|
|
28 |
import org.implab.gradle.common. |
|
|
|
29 |
import org.implab.gradle.common. |
|
|
|
24 | import org.implab.gradle.common.core.gradle.Properties; | |
|
|
25 | import org.implab.gradle.common.core.lang.Closures; | |
|
|
26 | import org.implab.gradle.common.json.core.Json; | |
|
|
27 | import org.implab.gradle.common.json.core.MapImportSpec; | |
|
|
28 | import org.implab.gradle.common.json.dsl.JsonObjectSpec; | |
|
|
30 | 29 | |
|
|
31 | 30 | import groovy.lang.Closure; |
|
|
32 | 31 | |
|
|
33 | 32 | /** |
|
|
34 | 33 | * A Gradle task that writes JSON content to a file. |
|
|
35 | 34 | * |
|
|
36 | 35 | * This task allows you to build up a JSON object through various methods and |
|
|
37 | 36 | * write it to a file. |
|
|
38 | 37 | * You can add content directly, import from other JSON files, or merge in map |
|
|
39 | 38 | * data with optional filtering. |
|
|
40 | 39 | * |
|
|
41 | 40 | * <h3>Usage Example:</h3> |
|
|
42 | 41 | * |
|
|
43 | 42 | * <pre> |
|
|
44 | 43 | * tasks.register('generateConfig', WriteJson) { |
|
|
45 | 44 | * outputFile = file('build/config.json') |
|
|
46 | 45 | * content { |
|
|
47 | 46 | * version = '1.0.0' |
|
|
48 | 47 | * name = 'myapp' |
|
|
49 | 48 | * } |
|
|
50 | 49 | * from file('src/base-config.json') |
|
|
51 | 50 | * putAll providers.provider { ['debug': true] } |
|
|
52 | 51 | * } |
|
|
53 | 52 | * </pre> |
|
|
54 | 53 | * |
|
|
55 | 54 | * <h3>Properties:</h3> |
|
|
56 | 55 | * <ul> |
|
|
57 | 56 | * <li><strong>content</strong> - A map of string keys to object values that |
|
|
58 | 57 | * will be serialized to JSON</li> |
|
|
59 | 58 | * <li><strong>outputFile</strong> - The file where the JSON will be |
|
|
60 | 59 | * written</li> |
|
|
61 | 60 | * </ul> |
|
|
62 | 61 | * |
|
|
63 | 62 | * <h3>Methods:</h3> |
|
|
64 | 63 | * <ul> |
|
|
65 | 64 | * <li><strong>content()</strong> - Configure content using a closure or |
|
|
66 | 65 | * action</li> |
|
|
67 | 66 | * <li><strong>from()</strong> - Import JSON content from a file with optional |
|
|
68 | 67 | * key filtering</li> |
|
|
69 | 68 | * <li><strong>putAll()</strong> - Merge a map provider with optional key |
|
|
70 | 69 | * filtering</li> |
|
|
71 | 70 | * </ul> |
|
|
72 | 71 | * |
|
|
73 | 72 | * @since 1.0 |
|
|
74 | 73 | */ |
|
|
75 | 74 | @NonNullByDefault |
|
|
76 | 75 | public abstract class WriteJson extends DefaultTask { |
|
|
77 | 76 | |
|
|
78 | 77 | private final ProviderFactory providers; |
|
|
79 | 78 | |
|
|
80 | 79 | private final JsonObjectSpec contentSpec = new JsonObjectSpec() { |
|
|
81 | 80 | @Override |
|
|
82 | 81 | public void set(String key, Object value) { |
|
|
83 | 82 | Properties.putMapEntry(getContent(), key, value); |
|
|
84 | 83 | } |
|
|
85 | 84 | }; |
|
|
86 | 85 | |
|
|
87 | 86 | @Input |
|
|
88 | 87 | @Optional |
|
|
89 | 88 | public abstract MapProperty<String, Object> getContent(); |
|
|
90 | 89 | |
|
|
91 | 90 | @OutputFile |
|
|
92 | 91 | public abstract RegularFileProperty getOutputFile(); |
|
|
93 | 92 | |
|
|
94 | 93 | @Internal |
|
|
95 | 94 | public Provider<String> getJson() { |
|
|
96 | 95 | return getContent().map(Json::stringify); |
|
|
97 | 96 | } |
|
|
98 | 97 | |
|
|
99 | 98 | @Inject |
|
|
100 | 99 | public WriteJson(ProviderFactory providers) { |
|
|
101 | 100 | this.providers = providers; |
|
|
102 | 101 | } |
|
|
103 | 102 | |
|
|
104 | 103 | public void content(Action<? super JsonObjectSpec> configure) { |
|
|
105 | 104 | configure.execute(contentSpec); |
|
|
106 | 105 | } |
|
|
107 | 106 | |
|
|
108 | 107 | public void content(Closure<?> configure) { |
|
|
109 | 108 | Closures.apply(configure, contentSpec); |
|
|
110 | 109 | } |
|
|
111 | 110 | |
|
|
112 | 111 | public void from(RegularFile file, Closure<?> configure) { |
|
|
113 | 112 | importContents(file, MapImportSpec.buildPredicate(configure)); |
|
|
114 | 113 | } |
|
|
115 | 114 | |
|
|
116 | 115 | public void from(RegularFile file, Action<? super MapImportSpec> configure) { |
|
|
117 | 116 | importContents(file, MapImportSpec.buildPredicate(configure)); |
|
|
118 | 117 | } |
|
|
119 | 118 | |
|
|
120 | 119 | public void from(RegularFile file) { |
|
|
121 | 120 | importContents(file, x -> true); |
|
|
122 | 121 | } |
|
|
123 | 122 | |
|
|
124 | 123 | public void from(Provider<RegularFile> file, Closure<?> configure) { |
|
|
125 | 124 | importContents(file, MapImportSpec.buildPredicate(configure)); |
|
|
126 | 125 | } |
|
|
127 | 126 | |
|
|
128 | 127 | public void from(Provider<RegularFile> file, Action<? super MapImportSpec> configure) { |
|
|
129 | 128 | importContents(file, MapImportSpec.buildPredicate(configure)); |
|
|
130 | 129 | } |
|
|
131 | 130 | |
|
|
132 | 131 | public void from(Provider<RegularFile> file) { |
|
|
133 | 132 | importContents(file, x -> true); |
|
|
134 | 133 | } |
|
|
135 | 134 | |
|
|
136 | 135 | public void putAll(Provider<Map<String, Object>> mapProvider, Action<? super MapImportSpec> configure) { |
|
|
137 | 136 | importMap(mapProvider, MapImportSpec.buildPredicate(configure)); |
|
|
138 | 137 | } |
|
|
139 | 138 | |
|
|
140 | 139 | public void putAll(Provider<Map<String, Object>> mapProvider, Closure<?> configure) { |
|
|
141 | 140 | importMap(mapProvider, MapImportSpec.buildPredicate(configure)); |
|
|
142 | 141 | } |
|
|
143 | 142 | |
|
|
144 | 143 | public void putAll(Provider<Map<String, Object>> mapProvider) { |
|
|
145 | 144 | importMap(mapProvider, x -> true); |
|
|
146 | 145 | } |
|
|
147 | 146 | |
|
|
148 | 147 | private void importContents(Provider<RegularFile> file, Predicate<String> keyPredicate) { |
|
|
149 | 148 | var jsonProvider = providers.fileContents(file).getAsText() |
|
|
150 | 149 | .map(Json.mapParser(Object.class)::apply); |
|
|
151 | 150 | |
|
|
152 | 151 | importMap(jsonProvider, keyPredicate); |
|
|
153 | 152 | } |
|
|
154 | 153 | |
|
|
155 | 154 | private void importContents(RegularFile file, Predicate<String> keyPredicate) { |
|
|
156 | 155 | var jsonProvider = providers.fileContents(file).getAsText() |
|
|
157 | 156 | .map(Json.mapParser(Object.class)::apply); |
|
|
158 | 157 | |
|
|
159 | 158 | importMap(jsonProvider, keyPredicate); |
|
|
160 | 159 | } |
|
|
161 | 160 | |
|
|
162 | 161 | private void importMap(Provider<Map<String, Object>> mapProvider, Predicate<? super String> keyPredicate) { |
|
|
163 | 162 | var filteredProvider = mapProvider.map(v -> v.entrySet().stream() |
|
|
164 | 163 | .filter(pair -> keyPredicate.test(pair.getKey()) && Objects.nonNull(pair.getValue())) |
|
|
165 | 164 | .collect(Collectors.toMap( |
|
|
166 | 165 | Map.Entry::getKey, |
|
|
167 | 166 | Map.Entry::getValue, |
|
|
168 | 167 | (a, b) -> b, |
|
|
169 | 168 | LinkedHashMap::new))); |
|
|
170 | 169 | |
|
|
171 | 170 | getContent().putAll(filteredProvider); |
|
|
172 | 171 | } |
|
|
173 | 172 | |
|
|
174 | 173 | @TaskAction |
|
|
175 | 174 | public void run() { |
|
|
176 | 175 | var data = getContent().getOrElse(Map.of()); |
|
|
177 | 176 | var file = getOutputFile().getAsFile().get(); |
|
|
178 | 177 | Json.write(file, data); |
|
|
179 | 178 | } |
|
|
180 | 179 | |
|
|
181 | 180 | } |
| @@ -1,136 +1,208 | |||
|
|
1 |
package org.implab.gradle.common. |
|
|
|
1 | package org.implab.gradle.common.sources; | |
|
|
2 | 2 | |
|
|
3 | 3 | import java.io.File; |
|
|
4 | 4 | import java.nio.file.Paths; |
|
|
5 | 5 | import java.util.HashSet; |
|
|
6 | import java.util.LinkedHashMap; | |
|
|
6 | 7 | import java.util.List; |
|
|
8 | import java.util.Map; | |
|
|
7 | 9 | import java.util.Objects; |
|
|
8 | 10 | import java.util.Set; |
|
|
9 | 11 | import java.util.concurrent.Callable; |
|
|
12 | import java.util.function.Function; | |
|
|
10 | 13 | import java.util.stream.Collectors; |
|
|
11 | 14 | |
|
|
12 | 15 | import javax.inject.Inject; |
|
|
13 | 16 | |
|
|
17 | import org.gradle.api.InvalidUserDataException; | |
|
|
14 | 18 | import org.gradle.api.Named; |
|
|
15 | 19 | import org.gradle.api.NamedDomainObjectContainer; |
|
|
20 | import org.gradle.api.Task; | |
|
|
21 | import org.gradle.api.file.ConfigurableFileCollection; | |
|
|
16 | 22 | import org.gradle.api.file.DirectoryProperty; |
|
|
17 | 23 | import org.gradle.api.file.FileCollection; |
|
|
18 | 24 | import org.gradle.api.file.ProjectLayout; |
|
|
19 | 25 | import org.gradle.api.file.SourceDirectorySet; |
|
|
20 | import org.gradle.api.logging.Logger; | |
|
|
21 | import org.gradle.api.logging.Logging; | |
|
|
22 | 26 | import org.gradle.api.model.ObjectFactory; |
|
|
27 | import org.gradle.api.tasks.TaskProvider; | |
|
|
23 | 28 | import org.gradle.util.Configurable; |
|
|
24 |
import org.implab.gradle.common. |
|
|
|
29 | import org.implab.gradle.common.core.lang.Closures; | |
|
|
25 | 30 | |
|
|
26 | 31 | import groovy.lang.Closure; |
|
|
27 | 32 | |
|
|
33 | /** | |
|
|
34 | * A configurable source set abstraction with named output roles. | |
|
|
35 | * | |
|
|
36 | * <p> | |
|
|
37 | * Each instance aggregates multiple {@link SourceDirectorySet source sets} | |
|
|
38 | * under a shared name and exposes typed outputs that must be declared up front. | |
|
|
39 | * Default locations are {@code src/<name>} for sources and | |
|
|
40 | * {@code build/<name>} for outputs, both of which can be customized via the | |
|
|
41 | * exposed {@link DirectoryProperty} setters. | |
|
|
42 | * </p> | |
|
|
43 | * | |
|
|
44 | * <p> | |
|
|
45 | * Outputs are grouped by roles to make task wiring explicit. A role must be | |
|
|
46 | * declared with {@link #declareRoles(String, String...)} (or the synonym | |
|
|
47 | * {@link #declareOutputs(String, String...)}) before files can be registered | |
|
|
48 | * against it. Attempting to register or retrieve an undeclared role results in | |
|
|
49 | * {@link InvalidUserDataException}. | |
|
|
50 | * </p> | |
|
|
51 | */ | |
|
|
28 | 52 | public abstract class GenericSourceSet |
|
|
29 | 53 | implements Named, Configurable<GenericSourceSet> { |
|
|
30 | private final Logger logger = Logging.getLogger(GenericSourceSet.class); | |
|
|
31 | ||
|
|
32 | 54 | private final String name; |
|
|
33 | 55 | |
|
|
34 | 56 | private final NamedDomainObjectContainer<SourceDirectorySet> sourceDirectorySets; |
|
|
35 | 57 | |
|
|
36 | private final NamedDomainObjectContainer<GenericSourceSetOutput> outputs; | |
|
|
58 | private final Map<String, ConfigurableFileCollection> outputs; | |
|
|
37 | 59 | |
|
|
38 | 60 | private final FileCollection allOutputs; |
|
|
39 | 61 | |
|
|
40 | 62 | private final FileCollection allSourceDirectories; |
|
|
41 | 63 | |
|
|
42 | 64 | private final ObjectFactory objects; |
|
|
43 | 65 | |
|
|
44 |
private final Set<String> declared |
|
|
|
66 | private final Set<String> declaredRoles = new HashSet<>(); | |
|
|
45 | 67 | |
|
|
46 | 68 | @Inject |
|
|
47 | 69 | public GenericSourceSet(String name, ObjectFactory objects, ProjectLayout layout) { |
|
|
48 | 70 | this.name = name; |
|
|
49 | 71 | this.objects = objects; |
|
|
50 | 72 | |
|
|
51 | 73 | sourceDirectorySets = objects.domainObjectContainer( |
|
|
52 | 74 | SourceDirectorySet.class, |
|
|
53 | 75 | this::createSourceDirectorySet); |
|
|
54 | 76 | |
|
|
55 | outputs = objects.domainObjectContainer(GenericSourceSetOutput.class); | |
|
|
77 | outputs = new LinkedHashMap<>(); | |
|
|
56 | 78 | |
|
|
57 | 79 | allSourceDirectories = objects.fileCollection().from(sourceDirectoriesProvider()); |
|
|
58 | 80 | |
|
|
59 | 81 | allOutputs = objects.fileCollection().from(outputsProvider()); |
|
|
60 | 82 | |
|
|
61 | outputs.addRule("Register a declared set on demand", this::registerOutputOnDemand); | |
|
|
62 | ||
|
|
63 | 83 | getSourceSetDir().convention(layout |
|
|
64 | 84 | .getProjectDirectory() |
|
|
65 | 85 | .dir(Paths.get("src", name).toString())); |
|
|
66 | 86 | |
|
|
67 | 87 | getOutputsDir().convention(layout |
|
|
68 | 88 | .getBuildDirectory() |
|
|
69 | 89 | .dir(name)); |
|
|
70 | 90 | } |
|
|
71 | 91 | |
|
|
72 | 92 | @Override |
|
|
73 | 93 | public String getName() { |
|
|
74 | 94 | return name; |
|
|
75 | 95 | } |
|
|
76 | 96 | |
|
|
97 | /** | |
|
|
98 | * Base directory for this source set. Defaults to {@code src/<name>} under | |
|
|
99 | * the project directory. | |
|
|
100 | */ | |
|
|
77 | 101 | public abstract DirectoryProperty getSourceSetDir(); |
|
|
78 | 102 | |
|
|
103 | /** | |
|
|
104 | * Base directory for outputs of this source set. Defaults to | |
|
|
105 | * {@code build/<name>}. | |
|
|
106 | */ | |
|
|
79 | 107 | public abstract DirectoryProperty getOutputsDir(); |
|
|
80 | 108 | |
|
|
109 | /** | |
|
|
110 | * The container of {@link SourceDirectorySet} instances that belong to this | |
|
|
111 | * logical source set. | |
|
|
112 | */ | |
|
|
81 | 113 | public NamedDomainObjectContainer<SourceDirectorySet> getSets() { |
|
|
82 | 114 | return sourceDirectorySets; |
|
|
83 | 115 | } |
|
|
84 | 116 | |
|
|
85 | public NamedDomainObjectContainer<GenericSourceSetOutput> getOutputs() { | |
|
|
86 | return outputs; | |
|
|
87 |
|
|
|
|
88 | ||
|
|
117 | /** | |
|
|
118 | * All registered outputs grouped across roles. | |
|
|
119 | */ | |
|
|
89 | 120 | public FileCollection getAllOutputs() { |
|
|
90 | 121 | return allOutputs; |
|
|
91 | 122 | } |
|
|
92 | 123 | |
|
|
124 | /** | |
|
|
125 | * All source directories from every contained {@link SourceDirectorySet}. | |
|
|
126 | */ | |
|
|
93 | 127 | public FileCollection getAllSourceDirectories() { |
|
|
94 | 128 | return allSourceDirectories; |
|
|
95 | 129 | } |
|
|
96 | 130 | |
|
|
97 | public FileCollection output(String name) { | |
|
|
98 | return getOutputs().getAt(name).getFileCollection(); | |
|
|
131 | /** | |
|
|
132 | * Returns the file collection for the specified output role, creating it | |
|
|
133 | * if necessary. | |
|
|
134 | * | |
|
|
135 | * @throws InvalidUserDataException if the role was not declared | |
|
|
136 | */ | |
|
|
137 | public ConfigurableFileCollection output(String name) { | |
|
|
138 | requireDeclaredRole(name); | |
|
|
139 | return outputs.computeIfAbsent(name, key -> objects.fileCollection()); | |
|
|
99 | 140 | } |
|
|
100 | 141 | |
|
|
101 | public void declareOutputs(String name, String... extra) { | |
|
|
102 | declaredOutputs.add(Objects.requireNonNull(name, "declareOutputs: The output name cannot be null")); | |
|
|
142 | /** | |
|
|
143 | * Declares allowed output roles. Roles must be declared before registering | |
|
|
144 | * files under them. | |
|
|
145 | */ | |
|
|
146 | public void declareRoles(String name, String... extra) { | |
|
|
147 | declaredRoles.add(Objects.requireNonNull(name, "declareRoles: The output name cannot be null")); | |
|
|
103 | 148 | for (var x : extra) |
|
|
104 |
declared |
|
|
|
149 | declaredRoles.add(Objects.requireNonNull(x, "declareRoles: The output name cannot be null")); | |
|
|
105 | 150 | } |
|
|
106 | 151 | |
|
|
107 | @Override | |
|
|
108 | public GenericSourceSet configure(@SuppressWarnings("rawtypes") Closure configure) { | |
|
|
109 | Closures.apply(configure, this); | |
|
|
110 | return this; | |
|
|
152 | /** | |
|
|
153 | * Alias for {@link #declareRoles(String, String...)} kept for DSL clarity. | |
|
|
154 | */ | |
|
|
155 | public void declareOutputs(String name, String... extra) { | |
|
|
156 | declareRoles(name, extra); | |
|
|
157 | } | |
|
|
158 | ||
|
|
159 | /** | |
|
|
160 | * Registers files produced elsewhere under the given role. | |
|
|
161 | */ | |
|
|
162 | public void registerOutput(String name, Object... files) { | |
|
|
163 | output(name).from(files); | |
|
|
111 | 164 | } |
|
|
112 | 165 | |
|
|
113 | private void registerOutputOnDemand(String outputName) { | |
|
|
114 | logger.info("SourceSet '{}': registerOutputOnDemand '{}'", name, outputName); | |
|
|
166 | /** | |
|
|
167 | * Registers output files produced by a task, using a mapper to extract the | |
|
|
168 | * output from the task. The task will be added as a build dependency of this | |
|
|
169 | * output. | |
|
|
170 | */ | |
|
|
171 | public <T extends Task> void registerOutput(String name, TaskProvider<T> task, | |
|
|
172 | Function<? super T, ?> mapper) { | |
|
|
173 | output(name).from(task.map(mapper::apply)) | |
|
|
174 | .builtBy(task); | |
|
|
175 | } | |
|
|
115 | 176 | |
|
|
116 | if (declaredOutputs.contains(outputName)) | |
|
|
117 | outputs.register(outputName); | |
|
|
177 | /** | |
|
|
178 | * Applies a Groovy closure to this source set, enabling DSL-style | |
|
|
179 | * configuration. | |
|
|
180 | */ | |
|
|
181 | @Override | |
|
|
182 | public GenericSourceSet configure(Closure configure) { | |
|
|
183 | Closures.apply(configure, this); | |
|
|
184 | return this; | |
|
|
118 | 185 | } |
|
|
119 | 186 | |
|
|
120 | 187 | private SourceDirectorySet createSourceDirectorySet(String name) { |
|
|
121 | 188 | return objects.sourceDirectorySet(name, name); |
|
|
122 | 189 | } |
|
|
123 | 190 | |
|
|
191 | private void requireDeclaredRole(String roleName) { | |
|
|
192 | if (!declaredRoles.contains(roleName)) { | |
|
|
193 | throw new InvalidUserDataException( | |
|
|
194 | "Output role '" + roleName + "' is not declared for source set '" + name + "'"); | |
|
|
195 | } | |
|
|
196 | } | |
|
|
197 | ||
|
|
124 | 198 | private Callable<List<? extends FileCollection>> outputsProvider() { |
|
|
125 | return () -> outputs.stream() | |
|
|
126 | .map(GenericSourceSetOutput::getFileCollection) | |
|
|
127 | .toList(); | |
|
|
199 | return () -> outputs.values().stream().toList(); | |
|
|
128 | 200 | } |
|
|
129 | 201 | |
|
|
130 | 202 | private Callable<Set<File>> sourceDirectoriesProvider() { |
|
|
131 | 203 | return () -> sourceDirectorySets.stream() |
|
|
132 | 204 | .flatMap(x -> x.getSrcDirs().stream()) |
|
|
133 | 205 | .collect(Collectors.toSet()); |
|
|
134 | 206 | } |
|
|
135 | 207 | |
|
|
136 | 208 | } |
| @@ -1,60 +1,59 | |||
|
|
1 | package org.implab.gradle.common; | |
|
|
1 | package org.implab.gradle.common.sources; | |
|
|
2 | 2 | |
|
|
3 | 3 | import org.gradle.api.Action; |
|
|
4 | 4 | import org.gradle.api.Plugin; |
|
|
5 | 5 | import org.gradle.api.Project; |
|
|
6 | 6 | import org.gradle.api.logging.Logger; |
|
|
7 | 7 | import org.gradle.api.logging.Logging; |
|
|
8 | 8 | import org.gradle.api.tasks.Copy; |
|
|
9 | 9 | import org.gradle.api.tasks.Delete; |
|
|
10 | 10 | import org.gradle.api.tasks.TaskContainer; |
|
|
11 | 11 | import org.gradle.api.tasks.TaskProvider; |
|
|
12 | 12 | import org.gradle.language.base.plugins.LifecycleBasePlugin; |
|
|
13 |
import org.implab.gradle.common. |
|
|
|
14 | import org.implab.gradle.common.utils.Strings; | |
|
|
13 | import org.implab.gradle.common.core.lang.Strings; | |
|
|
15 | 14 | |
|
|
16 | 15 | /** |
|
|
17 | 16 | * This plugin creates a {@code sources} extension which is |
|
|
18 | 17 | * a container for {@link GenericSourceSet}. |
|
|
19 | 18 | * |
|
|
20 | 19 | */ |
|
|
21 | 20 | public abstract class SourcesPlugin implements Plugin<Project> { |
|
|
22 | 21 | private final Logger logger = Logging.getLogger(SourcesPlugin.class); |
|
|
23 | 22 | |
|
|
24 | 23 | private final String POLICY_NAME = "SourcesPolicy"; |
|
|
25 | 24 | |
|
|
26 | 25 | private final String EXTENSION_NAME = "sources"; |
|
|
27 | 26 | |
|
|
28 | 27 | @Override |
|
|
29 | 28 | public void apply(Project target) { |
|
|
30 | 29 | target.getPlugins().apply(LifecycleBasePlugin.class); |
|
|
31 | 30 | |
|
|
32 | 31 | var sources = target.getObjects().domainObjectContainer(GenericSourceSet.class); |
|
|
33 | 32 | var tasks = target.getTasks(); |
|
|
34 | 33 | var clean = tasks.named(LifecycleBasePlugin.CLEAN_TASK_NAME); |
|
|
35 | 34 | |
|
|
36 | 35 | target.getExtensions().add(EXTENSION_NAME, sources); |
|
|
37 | 36 | |
|
|
38 | 37 | sources.configureEach(configureSourceSet(tasks, clean)); |
|
|
39 | 38 | } |
|
|
40 | 39 | |
|
|
41 | 40 | private Action<GenericSourceSet> configureSourceSet(TaskContainer tasks, TaskProvider<?> clean) { |
|
|
42 | 41 | return sourceSet -> { |
|
|
43 | 42 | var name = sourceSet.getName(); |
|
|
44 | 43 | |
|
|
45 | 44 | logger.info("{}: Processing source set '{}'", POLICY_NAME, name); |
|
|
46 | 45 | |
|
|
47 | 46 | var taskName = "clean" + Strings.capitalize(sourceSet.getName()); |
|
|
48 | 47 | |
|
|
49 | 48 | logger.info("{}: Register task '{}' of type [{}]", POLICY_NAME, taskName, Copy.class.getTypeName()); |
|
|
50 | 49 | |
|
|
51 | 50 | var task = tasks.register(taskName, Delete.class, self -> { |
|
|
52 | 51 | self.delete(sourceSet.getOutputsDir()); |
|
|
53 | 52 | }); |
|
|
54 | 53 | |
|
|
55 | 54 | clean.configure(self -> { |
|
|
56 | 55 | self.dependsOn(task); |
|
|
57 | 56 | }); |
|
|
58 | 57 | }; |
|
|
59 | 58 | } |
|
|
60 | 59 | } |
|
|
1 | NO CONTENT: file was removed |
General Comments 0
You need to be logged in to leave comments.
Login now
