1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.syncope.core.provisioning.java.job;
20
21 import java.time.format.DateTimeParseException;
22 import java.util.ArrayList;
23 import java.util.HashMap;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.Optional;
27 import java.util.Set;
28 import java.util.concurrent.ConcurrentHashMap;
29 import java.util.concurrent.ExecutionException;
30 import java.util.concurrent.Future;
31 import java.util.concurrent.atomic.AtomicReference;
32 import java.util.stream.Collectors;
33 import javax.annotation.Resource;
34 import javax.validation.ConstraintViolation;
35 import javax.validation.ValidationException;
36 import javax.validation.Validator;
37 import org.apache.commons.jexl3.JexlContext;
38 import org.apache.commons.jexl3.MapContext;
39 import org.apache.commons.lang3.BooleanUtils;
40 import org.apache.commons.lang3.StringUtils;
41 import org.apache.commons.lang3.math.NumberUtils;
42 import org.apache.commons.lang3.tuple.Pair;
43 import org.apache.syncope.common.lib.command.CommandArgs;
44 import org.apache.syncope.common.lib.form.FormProperty;
45 import org.apache.syncope.common.lib.form.SyncopeForm;
46 import org.apache.syncope.core.persistence.api.dao.ImplementationDAO;
47 import org.apache.syncope.core.persistence.api.entity.task.FormPropertyDef;
48 import org.apache.syncope.core.persistence.api.entity.task.MacroTask;
49 import org.apache.syncope.core.persistence.api.entity.task.MacroTaskCommand;
50 import org.apache.syncope.core.persistence.api.entity.task.TaskExec;
51 import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
52 import org.apache.syncope.core.provisioning.api.macro.Command;
53 import org.apache.syncope.core.provisioning.api.macro.MacroActions;
54 import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
55 import org.apache.syncope.core.provisioning.api.utils.FormatUtils;
56 import org.apache.syncope.core.spring.implementation.ImplementationManager;
57 import org.quartz.JobExecutionContext;
58 import org.quartz.JobExecutionException;
59 import org.springframework.aop.support.AopUtils;
60 import org.springframework.beans.factory.annotation.Autowired;
61 import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
62 import org.springframework.security.concurrent.DelegatingSecurityContextCallable;
63 import org.springframework.util.ReflectionUtils;
64
65 public class MacroJobDelegate extends AbstractSchedTaskJobDelegate<MacroTask> {
66
67 public static final String MACRO_TASK_FORM_JOBDETAIL_KEY = "macroTaskForm";
68
69 @Autowired
70 protected ImplementationDAO implementationDAO;
71
72 @Autowired
73 protected Validator validator;
74
75 @Resource(name = "batchExecutor")
76 protected ThreadPoolTaskExecutor executor;
77
78 protected final Map<String, MacroActions> perContextActions = new ConcurrentHashMap<>();
79
80 protected final Map<String, Command<?>> perContextCommands = new ConcurrentHashMap<>();
81
82 protected Optional<JexlContext> check(
83 final SyncopeForm macroTaskForm,
84 final Optional<MacroActions> actions,
85 final StringBuilder output) throws JobExecutionException {
86
87 if (macroTaskForm == null) {
88 return Optional.empty();
89 }
90
91
92 Set<String> missingFormProperties = task.getFormPropertyDefs().stream().
93 filter(FormPropertyDef::isRequired).
94 map(fpd -> Pair.of(
95 fpd.getKey(),
96 macroTaskForm.getProperty(fpd.getKey()).map(p -> p.getValue() != null))).
97 filter(pair -> pair.getRight().isEmpty()).
98 map(Pair::getLeft).
99 collect(Collectors.toSet());
100 if (!missingFormProperties.isEmpty()) {
101 throw new JobExecutionException("Required form properties missing: " + missingFormProperties);
102 }
103
104
105 Map<String, Object> vars = new HashMap<>();
106 for (FormPropertyDef fpd : task.getFormPropertyDefs()) {
107 String value = macroTaskForm.getProperty(fpd.getKey()).map(FormProperty::getValue).orElse(null);
108 if (value == null) {
109 continue;
110 }
111
112 switch (fpd.getType()) {
113 case String:
114 if (Optional.ofNullable(fpd.getStringRegEx()).
115 map(pattern -> !pattern.matcher(value).matches()).
116 orElse(false)) {
117
118 throw new JobExecutionException("RegEx not matching for " + fpd.getKey() + ": " + value);
119 }
120
121 vars.put(fpd.getKey(), value);
122 break;
123
124 case Password:
125 vars.put(fpd.getKey(), value);
126 break;
127
128 case Boolean:
129 vars.put(fpd.getKey(), BooleanUtils.toBoolean(value));
130 break;
131
132 case Date:
133 try {
134 vars.put(fpd.getKey(), StringUtils.isBlank(fpd.getDatePattern())
135 ? FormatUtils.parseDate(value)
136 : FormatUtils.parseDate(value, fpd.getDatePattern()));
137 } catch (DateTimeParseException e) {
138 throw new JobExecutionException("Unparseable date " + fpd.getKey() + ": " + value, e);
139 }
140 break;
141
142 case Long:
143 vars.put(fpd.getKey(), NumberUtils.toLong(value));
144 break;
145
146 case Enum:
147 if (!fpd.getEnumValues().containsKey(value)) {
148 throw new JobExecutionException("Not allowed for " + fpd.getKey() + ": " + value);
149 }
150
151 vars.put(fpd.getKey(), value);
152 break;
153
154 case Dropdown:
155 if (!fpd.isDropdownFreeForm()) {
156 List<String> values = fpd.isDropdownSingleSelection()
157 ? List.of(value)
158 : List.of(value.split(";"));
159
160 if (!actions.map(a -> a.getDropdownValues(fpd.getKey()).keySet()).
161 orElse(Set.of()).containsAll(values)) {
162
163 throw new JobExecutionException("Not allowed for " + fpd.getKey() + ": " + values);
164 }
165 }
166
167 vars.put(fpd.getKey(), value);
168 break;
169
170 default:
171 }
172 }
173
174
175 try {
176 actions.ifPresent(a -> a.validate(macroTaskForm, vars));
177 } catch (ValidationException e) {
178 throw new JobExecutionException("Invalid form submitted for task " + task.getKey(), e);
179 }
180
181 output.append("Form parameter values: ").append(vars).append("\n\n");
182
183 return vars.isEmpty() ? Optional.empty() : Optional.of(new MapContext(vars));
184 }
185
186 protected String run(
187 final List<Pair<Command<CommandArgs>, CommandArgs>> commands,
188 final Optional<MacroActions> actions,
189 final StringBuilder output,
190 final boolean dryRun)
191 throws JobExecutionException {
192
193 Future<AtomicReference<Pair<String, Throwable>>> future = executor.submit(
194 new DelegatingSecurityContextCallable<>(() -> {
195
196 AtomicReference<Pair<String, Throwable>> error = new AtomicReference<>();
197
198 for (int i = 0; i < commands.size() && error.get() == null; i++) {
199 Pair<Command<CommandArgs>, CommandArgs> command = commands.get(i);
200
201 try {
202 String args = POJOHelper.serialize(command.getRight());
203 output.append("Command[").append(command.getLeft().getClass().getName()).append("]: ").
204 append(args).append("\n");
205
206 if (!dryRun) {
207 actions.ifPresent(a -> a.beforeCommand(command.getLeft(), command.getRight()));
208
209 String cmdOut = command.getLeft().run(command.getRight());
210
211 actions.ifPresent(a -> a.afterCommand(command.getLeft(), command.getRight(), cmdOut));
212
213 output.append(cmdOut);
214 }
215 } catch (Throwable t) {
216 if (task.isContinueOnError()) {
217 output.append("Continuing on error: <").append(t.getMessage()).append('>');
218
219 LOG.error("While running {} with args {}, continuing on error",
220 command.getLeft().getClass().getName(), command.getRight(), t);
221 } else {
222 error.set(Pair.of(AopUtils.getTargetClass(command.getLeft()).getName(), t));
223 }
224 }
225 output.append("\n\n");
226 }
227
228 return error;
229 }));
230
231 try {
232 AtomicReference<Pair<String, Throwable>> error = future.get();
233 if (error.get() != null) {
234 throw new JobExecutionException("While running " + error.get().getLeft(), error.get().getRight());
235 }
236 } catch (ExecutionException | InterruptedException e) {
237 throw new JobExecutionException("While waiting for macro commands completion", e);
238 }
239
240 output.append("COMPLETED");
241
242 return actions.filter(a -> !dryRun).map(a -> a.afterAll(output)).orElse(output).toString();
243 }
244
245 @SuppressWarnings("unchecked")
246 @Override
247 protected String doExecute(final boolean dryRun, final String executor, final JobExecutionContext context)
248 throws JobExecutionException {
249
250 Optional<MacroActions> actions;
251 if (task.getMacroActions() == null) {
252 actions = Optional.empty();
253 } else {
254 try {
255 actions = Optional.of(ImplementationManager.build(
256 task.getMacroActions(),
257 () -> perContextActions.get(task.getMacroActions().getKey()),
258 instance -> perContextActions.put(task.getMacroActions().getKey(), instance)));
259 } catch (Exception e) {
260 throw new JobExecutionException("Could not build " + task.getMacroActions().getKey(), e);
261 }
262 }
263
264 StringBuilder output = new StringBuilder();
265
266 SyncopeForm macroTaskForm = (SyncopeForm) context.getMergedJobDataMap().get(MACRO_TASK_FORM_JOBDETAIL_KEY);
267 Optional<JexlContext> jexlContext = check(macroTaskForm, actions, output);
268
269 if (!dryRun) {
270 actions.ifPresent(MacroActions::beforeAll);
271 }
272
273 List<Pair<Command<CommandArgs>, CommandArgs>> commands = new ArrayList<>();
274 for (MacroTaskCommand command : task.getCommands()) {
275 Command<CommandArgs> runnable;
276 try {
277 runnable = (Command<CommandArgs>) ImplementationManager.build(
278 command.getCommand(),
279 () -> perContextCommands.get(command.getCommand().getKey()),
280 instance -> perContextCommands.put(command.getCommand().getKey(), instance));
281 } catch (Exception e) {
282 throw new JobExecutionException("Could not build " + command.getCommand().getKey(), e);
283 }
284
285 CommandArgs args;
286 if (command.getArgs() == null) {
287 try {
288 args = ImplementationManager.emptyArgs(command.getCommand());
289 } catch (Exception e) {
290 throw new JobExecutionException("While getting empty args from " + command.getKey(), e);
291 }
292 } else {
293 args = command.getArgs();
294
295 jexlContext.ifPresent(ctx -> ReflectionUtils.doWithFields(
296 args.getClass(),
297 field -> {
298 if (String.class.equals(field.getType())) {
299 field.setAccessible(true);
300 Object value = field.get(args);
301 if (value instanceof String) {
302 field.set(args, JexlUtils.evaluateTemplate((String) value, ctx));
303 }
304 }
305 },
306 field -> !field.isSynthetic()));
307
308 Set<ConstraintViolation<Object>> violations = validator.validate(args);
309 if (!violations.isEmpty()) {
310 LOG.error("While validating {}: {}", args, violations);
311
312 throw new JobExecutionException(
313 "While running " + command.getKey(),
314 new IllegalArgumentException(args.getClass().getName()));
315 }
316 }
317
318 commands.add(Pair.of(runnable, args));
319 }
320
321 return run(commands, actions, output, dryRun);
322 }
323
324 @Override
325 protected boolean hasToBeRegistered(final TaskExec<?> execution) {
326 return task.isSaveExecs();
327 }
328 }