summaryrefslogtreecommitdiff
path: root/support/arara/source/src/main/kotlin/org/islandoftex/arara/model/Interpreter.kt
blob: 49cf40f1a2f18b8f16a0492d984899eb8c308122 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
// SPDX-License-Identifier: BSD-3-Clause
package org.islandoftex.arara.model

import java.io.File
import kotlin.time.ExperimentalTime
import org.islandoftex.arara.Arara
import org.islandoftex.arara.configuration.AraraSpec
import org.islandoftex.arara.localization.LanguageController
import org.islandoftex.arara.localization.Messages
import org.islandoftex.arara.ruleset.Argument
import org.islandoftex.arara.ruleset.Command
import org.islandoftex.arara.ruleset.Conditional
import org.islandoftex.arara.ruleset.Directive
import org.islandoftex.arara.ruleset.Rule
import org.islandoftex.arara.ruleset.RuleCommand
import org.islandoftex.arara.ruleset.RuleUtils
import org.islandoftex.arara.utils.CommonUtils
import org.islandoftex.arara.utils.DisplayUtils
import org.islandoftex.arara.utils.InterpreterUtils
import org.islandoftex.arara.utils.Methods
import org.mvel2.templates.TemplateRuntime
import org.slf4j.LoggerFactory

/**
 * Interprets the list of directives.
 *
 * @author Island of TeX
 * @version 5.0
 * @since 4.0
 */
class Interpreter(
    /**
     * The list of directives to be interpreted and evaluated.
     */
    val directives: List<Directive>
) {
    /**
     * Exception class to represent that the interpreter should stop for some
     * reason
     */
    private class HaltExpectedException(msg: String) : Exception(msg)

    /**
     * Gets the rule according to the provided directive.
     *
     * @param directive The provided directive.
     * @return The absolute canonical path of the rule, given the provided
     * directive.
     * @throws AraraException Something wrong happened, to be caught in the
     * higher levels.
     */
    @Throws(AraraException::class)
    private fun getRule(directive: Directive): File {
        return InterpreterUtils.buildRulePath(directive.identifier)
                ?: throw AraraException(
                        messages.getMessage(
                                Messages.ERROR_INTERPRETER_RULE_NOT_FOUND,
                                directive.identifier,
                                "(" + CommonUtils.allRulePaths
                                        .joinToString("; ") + ")"
                        )
                )
    }

    // TODO: in the following, extract the printing into the higher level
    // function
    /**
     * "Run" a boolean return value
     * @param value The boolean.
     * @param conditional The conditional to print in dry-run mode.
     * @param authors The authors of the rule.
     * @return Returns [value]
     */
    private fun runBoolean(
        value: Boolean,
        conditional: Conditional,
        authors: List<String>
    ): Boolean {
        logger.info(messages.getMessage(Messages.LOG_INFO_BOOLEAN_MODE,
                value.toString()))

        if (Arara.config[AraraSpec.Execution.dryrun]) {
            DisplayUtils.printAuthors(authors)
            DisplayUtils.wrapText(messages.getMessage(Messages
                    .INFO_INTERPRETER_DRYRUN_MODE_BOOLEAN_MODE,
                    value))
            DisplayUtils.printConditional(conditional)
        }

        return value
    }

    /**
     * Run a command
     * @param command The command to run.
     * @param conditional The conditional applied to the run (only for printing).
     * @param authors The rule authors (only for printing).
     * @param ruleCommandExitValue The exit value of the rule command.
     * @return Success of the execution.
     * @throws AraraException Execution failed.
     */
    @ExperimentalTime
    @Throws(AraraException::class)
    @Suppress("TooGenericExceptionCaught")
    private fun runCommand(
        command: Command,
        conditional: Conditional,
        authors: List<String>,
        ruleCommandExitValue: String?
    ): Boolean {
        logger.info(messages.getMessage(Messages.LOG_INFO_SYSTEM_COMMAND,
                command))
        var success = true

        if (!Arara.config[AraraSpec.Execution.dryrun]) {
            val code = InterpreterUtils.run(command)
            val check: Any = try {
                val context = mapOf<String, Any>("value" to code)
                TemplateRuntime.eval(
                        "@{ " + (ruleCommandExitValue ?: "value == 0") + " }",
                        context)
            } catch (exception: RuntimeException) {
                throw AraraException(CommonUtils.ruleErrorHeader +
                        messages.getMessage(Messages
                                .ERROR_INTERPRETER_EXIT_RUNTIME_ERROR),
                        exception)
            }

            success = if (check is Boolean) {
                check
            } else {
                throw AraraException(
                        CommonUtils.ruleErrorHeader + messages.getMessage(
                                Messages.ERROR_INTERPRETER_WRONG_EXIT_CLOSURE_RETURN
                        )
                )
            }
        } else {
            DisplayUtils.printAuthors(authors)
            DisplayUtils.wrapText(messages.getMessage(
                    Messages.INFO_INTERPRETER_DRYRUN_MODE_SYSTEM_COMMAND,
                    command))
            DisplayUtils.printConditional(conditional)
        }

        return success
    }

    /**
     * Converts the command evaluation result to a flat list.
     * @param result The result
     * @return A flat list.
     */
    private fun resultToList(result: Any) = if (result is List<*>) {
        CommonUtils.flatten(result)
    } else {
        listOf(result)
    }

    /**
     * Execute a command.
     * @param command The command to evaluate.
     * @param conditional Under which condition to execute.
     * @param rule The rule (only passed for output purposes).
     * @param parameters The parameters for evaluation
     * @throws AraraException Running the command failed.
     */
    @ExperimentalTime
    @Throws(AraraException::class)
    @Suppress("TooGenericExceptionCaught", "ThrowsCount")
    private fun executeCommand(
        command: RuleCommand,
        conditional: Conditional,
        rule: Rule,
        parameters: Map<String, Any>
    ) {
        val result: Any = try {
            TemplateRuntime.eval(command.command!!, parameters)
        } catch (exception: RuntimeException) {
            throw AraraException(CommonUtils.ruleErrorHeader +
                    messages.getMessage(Messages
                            .ERROR_INTERPRETER_COMMAND_RUNTIME_ERROR),
                    exception)
        }

        // TODO: check nullability
        resultToList(result).filter { it.toString().isNotBlank() }
                .forEach { current ->
                    DisplayUtils.printEntry(rule.name, command.name
                            ?: messages.getMessage(Messages
                                    .INFO_LABEL_UNNAMED_TASK))

                    val success = when (current) {
                        is Boolean -> runBoolean(current, conditional,
                                rule.authors)
                        is Command -> runCommand(current, conditional,
                                rule.authors, command.exit)
                        else -> TODO("error: this should not happen" +
                                "we are only supporting boolean + command")
                    }

                    DisplayUtils.printEntryResult(success)

                    if (Arara.config[AraraSpec.Execution.haltOnErrors] && !success)
                    // TODO: localize
                        throw HaltExpectedException("Command failed")

                    // TODO: document this key
                    val haltKey = "arara:${Arara.config[AraraSpec
                            .Execution.reference].name}:halt"
                    if (Session.contains(haltKey)) {
                        Arara.config[AraraSpec.Execution.status] =
                                Session[haltKey].toString().toInt()
                        // TODO: localize
                        throw HaltExpectedException("User requested halt")
                    }
                }
    }

    /**
     * Executes each directive, throwing an exception if something bad has
     * happened.
     *
     * @throws AraraException Something wrong happened, to be caught in the
     * higher levels.
     */
    @ExperimentalTime
    @Throws(AraraException::class)
    @Suppress("NestedBlockDepth")
    fun execute() {
        for (directive in directives) {
            logger.info(messages.getMessage(Messages.LOG_INFO_INTERPRET_RULE,
                    directive.identifier))

            Arara.config[AraraSpec.Execution.file] =
                    directive.parameters.getValue("reference") as File
            val file = getRule(directive)

            logger.info(messages.getMessage(Messages.LOG_INFO_RULE_LOCATION,
                    file.parent))

            Arara.config[AraraSpec.Execution.InfoSpec.ruleId] =
                    directive.identifier
            Arara.config[AraraSpec.Execution.InfoSpec.rulePath] =
                    file.parent
            Arara.config[AraraSpec.Execution.DirectiveSpec.lines] =
                    directive.lineNumbers

            // parse the rule identified by the directive
            // (may throw an exception)
            val rule = RuleUtils.parseRule(file, directive.identifier)
            val parameters = parseArguments(rule, directive)
                    .plus(Methods.getRuleMethods())

            val evaluator = Evaluator()

            var available = true
            if (InterpreterUtils.runPriorEvaluation(directive.conditional)) {
                available = evaluator.evaluate(directive.conditional)
            }

            // if this directive is conditionally disabled, skip
            if (!available) continue
            // if not execute the commands associated with the directive
            do {
                rule.commands.forEach { command ->
                    try {
                        executeCommand(command, directive.conditional, rule, parameters)
                    } catch (_: HaltExpectedException) {
                        // if the user uses the halt rule to trigger
                        // a halt, this will be raised
                        return
                    }
                }
            } while (evaluator.evaluate(directive.conditional))
        }
    }

    /**
     * Parses the rule arguments against the provided directive.
     *
     * @param rule The rule object.
     * @param directive The directive.
     * @return A map containing all arguments resolved according to the
     * directive parameters.
     * @throws AraraException Something wrong happened, to be caught in the
     * higher levels.
     */
    @Throws(AraraException::class)
    private fun parseArguments(rule: Rule, directive: Directive):
            Map<String, Any> {
        val arguments = rule.arguments
        val unknown = CommonUtils.getUnknownKeys(directive.parameters,
                arguments).minus("reference")
        if (unknown.isNotEmpty())
            throw AraraException(CommonUtils.ruleErrorHeader +
                    messages.getMessage(
                            Messages.ERROR_INTERPRETER_UNKNOWN_KEYS,
                            "(" + unknown.joinToString(", ") + ")"))

        val resolvedArguments = mutableMapOf<String, Any>()
        resolvedArguments["reference"] = directive.parameters
                .getValue("reference")

        val context = mapOf(
                "parameters" to directive.parameters,
                "reference" to directive.parameters.getValue("reference")
        ).plus(Methods.getRuleMethods())

        arguments.forEach { argument ->
            resolvedArguments[argument.identifier!!] = processArgument(argument,
                    directive.parameters.containsKey(argument.identifier!!),
                    context)
        }

        return resolvedArguments
    }

    /**
     * Process a single argument and return the evaluated result.
     * @param argument The argument to process.
     * @param idInDirectiveParams Whether the argument's identifier is
     *   contained in the directive's parameters field.
     * @param context The context for the evaluation.
     * @return The result of the evaluation.
     * @throws AraraException The argument could not be processed.
     */
    @Throws(AraraException::class)
    @Suppress("TooGenericExceptionCaught", "ThrowsCount")
    private fun processArgument(
        argument: Argument,
        idInDirectiveParams: Boolean,
        context: Map<String, Any>
    ): Any {
        if (argument.isRequired && !idInDirectiveParams)
            throw AraraException(CommonUtils.ruleErrorHeader +
                    messages.getMessage(
                            Messages.ERROR_INTERPRETER_ARGUMENT_IS_REQUIRED,
                            argument.identifier!!))

        var ret = argument.default?.let {
            try {
                TemplateRuntime.eval(it, context)
            } catch (exception: RuntimeException) {
                throw AraraException(CommonUtils.ruleErrorHeader +
                        messages.getMessage(Messages
                                .ERROR_INTERPRETER_DEFAULT_VALUE_RUNTIME_ERROR),
                        exception)
            }
        } ?: ""

        if (argument.flag != null && idInDirectiveParams) {
            ret = try {
                TemplateRuntime.eval(argument.flag!!, context)
            } catch (exception: RuntimeException) {
                throw AraraException(CommonUtils.ruleErrorHeader + messages
                        .getMessage(Messages
                                .ERROR_INTERPRETER_FLAG_RUNTIME_EXCEPTION),
                        exception)
            }
        }

        return ret
    }

    companion object {
        // the application messages obtained from the
        // language controller
        private val messages = LanguageController

        // the class logger obtained from
        // the logger factory
        private val logger = LoggerFactory.getLogger(Interpreter::class.java)
    }
}