Project

General

Profile

Download (14.5 KB) Statistics
| Branch: | Tag: | Revision:
1
/**
2
* Copyright (C) 2020 EDIT
3
* European Distributed Institute of Taxonomy
4
* http://www.e-taxonomy.eu
5
*
6
* The contents of this file are subject to the Mozilla Public License Version 1.1
7
* See LICENSE.TXT at the top of this package for the full license terms.
8
*/
9
package eu.etaxonomy.drush;
10

    
11
import java.io.File;
12
import java.io.IOException;
13
import java.io.InputStream;
14
import java.net.URI;
15
import java.net.URISyntaxException;
16
import java.util.ArrayList;
17
import java.util.Arrays;
18
import java.util.List;
19
import java.util.Scanner;
20
import java.util.regex.Matcher;
21
import java.util.regex.Pattern;
22
import java.util.stream.Collectors;
23

    
24
import org.apache.commons.io.IOUtils;
25
import org.apache.commons.lang3.SystemUtils;
26
import org.apache.log4j.Level;
27
import org.apache.log4j.Logger;
28

    
29
import com.fasterxml.jackson.core.type.TypeReference;
30
import com.fasterxml.jackson.databind.ObjectMapper;
31

    
32
/**
33
 * Java executor for drush (https://www.drush.org/).
34
 *
35
 * <h3>Local usage:</h3> Set the {@link #setDrupalRoot(File) drupal root folder}
36
 * and the {@link #setSiteURI(URI) site uri}.
37
 *
38
 * <h3>Remote usage:</h3> In addition to the above properties you can also
39
 * define an {@link #setSshHost(String) ssh host} and optionally the according
40
 * {@link #setSshUser(String) ssh user} to execute drush on a remote machine.
41
 *
42
 * @author a.kohlbecker
43
 * @since Jul 31, 2020
44
 */
45
public class DrushExecuter {
46

    
47
    public static final Logger logger = Logger.getLogger(DrushExecuter.class);
48

    
49
    private URI siteURI = null;
50

    
51
    private File drupalRoot = null;
52

    
53
    private String sshUser = null;
54

    
55
    private String sshHost = null;
56

    
57
    public DrushExecuter() throws IOException, InterruptedException, DrushExecutionFailure {
58
        findDrushCommand();
59
    }
60

    
61
    /**
62
     * The execution of this command via
63
     * <code>DrushExecuter.execute({@linkplain #version})</code> results in
64
     * a {@code List<String>} return variable with the following elements:
65
     *
66
     * <ol>
67
     * <li>major</li>
68
     * <li>minor</li>
69
     * <li>patch</li>
70
     * <ol>
71
     */
72
    public static DrushCommand version = new DrushCommand(Arrays.asList("--version"),
73
            "Drush Version\\s+:\\s+(?<major>\\d+)\\.(?<minor>\\d+)\\.(?<patch>\\d+)", null);
74

    
75
    public static DrushCommand help = new DrushCommand(Arrays.asList("help"), null, null);
76

    
77
    public static DrushCommand coreStatus = new DrushCommand(Arrays.asList("core-status"), null, null);
78

    
79
    /**
80
     * Executes {@code drush vget --exact <variable-key>}
81
     * <p>
82
     * The execution of this command via
83
     * <code>DrushExecuter.execute({@linkplain DrushExecuter#variableSet})</code> results in
84
     * a {@code List<String>} return variable with the following elements:
85
     *
86
     * <ol>
87
     * <li>value</li>
88
     * <li>value</li>
89
     * <li>value</li>
90
     * <ol>
91
     */
92
    public static DrushCommand variableGet = new DrushCommand(Arrays.asList("vget", "--exact", "--format=json", "%s"), true);
93
    /**
94
     * Executes {@code drush vset --exact <variable-key> <variable-value>}
95
     * <p>
96
     * The execution of this command via
97
     * <code>DrushExecuter.execute({@linkplain DrushExecuter#variableSet})</code> will not return any values.
98
     * The command will fail with an {@link DrushExecutionFailure} if setting the variable was not successful.
99
     */
100
    public static DrushCommand variableSet = new DrushCommand(Arrays.asList("--yes", "vset", "%s", "%s"), false);
101

    
102
    /**
103
     * @throws IOException
104
     *             if an I/O error occurs in the ProcessBuilder
105
     * @throws InterruptedException
106
     *             if the Process was interrupted
107
     * @throws DrushExecutionFailure
108
     *              if the drush command execution fails with an error code
109
     */
110
    private void findDrushCommand() throws IOException, InterruptedException, DrushExecutionFailure {
111

    
112
        if (SystemUtils.IS_OS_WINDOWS) {
113
            throw new RuntimeException("not yet implmented for Windows");
114
        }
115
        if(DrushCommand.majorVersion == null) {
116
            List<Object> matches = execute(version);
117
            DrushCommand.majorVersion = (String) matches.get(0);
118
            DrushCommand.minorVersion = (String) matches.get(1);
119
            DrushCommand.patchLevel = (String) matches.get(2);
120
        }
121
        if(DrushCommand.majorVersion.isEmpty()) {
122
            throw new RuntimeException("No suitable drush command found in the system");
123
        }
124
        if (Integer.valueOf(DrushCommand.majorVersion) < 8) {
125
            throw new RuntimeException("drush version >= 8 required");
126
        }
127
    }
128

    
129
    public String drushVersion() {
130
        return DrushCommand.majorVersion + "." + DrushCommand.minorVersion + "." + DrushCommand.patchLevel;
131
    }
132

    
133
    /**
134
     * @throws IOException
135
     *             if an I/O error occurs in the ProcessBuilder
136
     * @throws InterruptedException
137
     *             if the Process was interrupted
138
     * @throws DrushExecutionFailure
139
     *              if the drush command execution fails with an error code
140
     */
141
    public List<Object> execute(DrushCommand cmd, String... value) throws IOException, InterruptedException, DrushExecutionFailure {
142

    
143
        List<String> executableWithArgs = new ArrayList<>();
144

    
145
        if (sshHost != null) {
146
            executableWithArgs.add("ssh");
147
            String userHostArg = sshHost;
148
            if (sshUser != null) {
149
                userHostArg = sshUser + "@" + sshHost;
150
            }
151
            executableWithArgs.add(userHostArg);
152
        }
153

    
154
        executableWithArgs.add("drush");
155

    
156
        if (drupalRoot != null) {
157
            executableWithArgs.add("--root=" + drupalRoot.toString());
158
        }
159
        if (siteURI != null) {
160
            executableWithArgs.add("--uri=" + siteURI.toString());
161
        }
162
        int commandSubstitutions = 0;
163
        for (String arg : cmd.args) {
164
            if (arg.contains("%s")) {
165
                executableWithArgs.add(String.format(arg, value[commandSubstitutions]));
166
                commandSubstitutions++;
167
            } else {
168
                executableWithArgs.add(arg);
169
            }
170
        }
171

    
172
        List<Object> matches = new ArrayList<>();
173

    
174
        ProcessBuilder pb = new ProcessBuilder(executableWithArgs);
175
        logger.debug("Command: " + pb.command().toString());
176
        Process process = pb.start();
177
        int exitCode = process.waitFor();
178

    
179
        if (exitCode == 0) {
180
            String out, error;
181
            if(cmd.jsonResult) {
182
                out = readExecutionResponse(matches, process.getInputStream());
183
                error = readExecutionResponse(matches, process.getErrorStream());
184
            } else {
185
                out = readExecutionResponse(matches, process.getInputStream(), cmd.outRegex);
186
                error = readExecutionResponse(matches, process.getErrorStream(), cmd.errRegex);
187
            }
188
            if (out != null && !out.isEmpty()) {
189
                logger.error(error);
190
            }
191
            if (error != null && !error.isEmpty()) {
192
                logger.error(error);
193
            }
194
        } else {
195
            throw new DrushExecutionFailure(
196
                    executableWithArgs,
197
                    IOUtils.toString(process.getInputStream()),
198
                    IOUtils.toString(process.getErrorStream())
199
                    );
200
        }
201
        return matches;
202
    }
203

    
204
    protected String readExecutionResponse(List<Object> matches, InputStream stream, Pattern regex) throws IOException {
205
        String out;
206
        if (regex != null) {
207
            Scanner scanner = new Scanner(stream);
208
            while (true) {
209
                out = scanner.findWithinHorizon(regex, 0);
210
                if (out == null) {
211
                    break;
212
                }
213
                Matcher m = regex.matcher(out);
214
                int patternMatchCount = 0;
215
                while (m.find()) {
216
                    patternMatchCount++;
217
                    if (m.groupCount() > 0) {
218
                        for (int g = 1; g <= m.groupCount(); g++) {
219
                            matches.add(m.group(g));
220
                            logger.debug("match[" + patternMatchCount + "." + g + "]: " + m.group(g));
221
                        }
222
                    } else {
223
                        matches.add(m.group(0));
224
                        logger.debug("entire pattern match[" + patternMatchCount + ".0]: " + m.group(0));
225
                    }
226
                }
227
            }
228
            scanner.close();
229
            return null;
230
        } else {
231
            out = IOUtils.toString(stream);
232
            logger.debug(out);
233
            return out;
234
        }
235
    }
236

    
237
    /**
238
     * @return depending on the drupal variable type different return types are possible:
239
     *  <ul>
240
     *  <li>Object</li>
241
     *  <li>List</li>
242
     *  <li>List</li>
243
     *  <li>String</li>
244
     *  <li>Double</li>
245
     *  <li>Integer</li>
246
     *  </ul>
247
     */
248
    protected String readExecutionResponse(List<Object> matches, InputStream stream) throws IOException {
249
        String out = IOUtils.toString(stream);
250
        if(out != null) {
251
            out = out.trim();
252
            if(!out.isEmpty()) {
253
                ObjectMapper mapper = new ObjectMapper();
254
                if(out.startsWith("[")) {
255
                   matches.add(mapper.readValue(out, new TypeReference<List<Object>>(){}));
256
                } else  {
257
                   matches.add(mapper.readValue(out, Object.class));
258
                }
259
                if(matches.isEmpty()) {
260
                    logger.debug("no result");
261
                } else {
262
                    logger.debug("result object: " + matches.get(0));
263
                }
264
            }
265

    
266
        }
267
        return out;
268
    }
269

    
270
    public static class DrushCommand {
271

    
272
        private static String majorVersion;
273
        private static String minorVersion;
274
        private static String patchLevel;
275
        Pattern outRegex;
276
        Pattern errRegex;
277
        boolean jsonResult = false;
278
        boolean failOnError = false;
279
        List<String> args = new ArrayList<>();
280

    
281
        /**
282
         * For drush commands not supporting output formatting.
283
         *
284
         * @param args
285
         *            the command arguments
286
         * @param outRegex
287
         *            Regular expression to parse the error stream, capture
288
         *            groups will be put into the <code>List</code> of strings
289
         *            returned by
290
         *            {@link DrushExecuter#execute(DrushCommand, String...)}
291
         * @param errRegex
292
         *            Regular expression to parse the error stream, capture
293
         *            groups will be put into the <code>List</code> of strings
294
         *            returned by
295
         *            {@link DrushExecuter#execute(DrushCommand, String...)}
296
         */
297
        public DrushCommand(List<String> args, String outRegex, String errRegex) {
298
            this.args = args;
299
            if (outRegex != null) {
300
                this.outRegex = Pattern.compile(outRegex, Pattern.MULTILINE);
301
            }
302
            if (errRegex != null) {
303
                this.errRegex = Pattern.compile(errRegex, Pattern.MULTILINE);
304
            }
305
        }
306

    
307
        /**
308
         * For drush commands which don't require return value parsing by regex or
309
         * which support the {@code --format=json} option to return structured data.
310
         *
311
         * @param args
312
         *            the command arguments
313
         */
314
        public DrushCommand(List<String> args, boolean jsonResult) {
315
            this.args = args;
316
            this.jsonResult = jsonResult;
317
        }
318

    
319
        public String commandLineString() {
320
            return args.stream().collect(Collectors.joining(" "));
321
        }
322

    
323
    }
324

    
325
    /**
326
     * These tests have not been implemented as jUnit tests since the execution
327
     * it too much dependent from the local environment. Once the
328
     * <code>DrushExecuter</code> is being used in the selenium test suite will
329
     * be tested implicitly anyway.
330
     *
331
     * @throws DrushExecutionFailure
332
     *              if the drush command execution fails with an error code
333
     */
334
    public static void main(String[] args) throws URISyntaxException, DrushExecutionFailure {
335
        DrushExecuter.logger.setLevel(Level.DEBUG);
336
        try {
337
            DrushExecuter dex = new DrushExecuter();
338
            List<Object> results;
339
            dex.setDrupalRoot(new File("/home/andreas/workspaces/www/drupal-7"));
340
            dex.setSiteURI(new URI("http://edit.test/d7/caryophyllales/"));
341
//            dex.execute(coreStatus);
342
//            dex.execute(help);
343
            results = dex.execute(variableSet, "cdm_webservice_url",
344
                    "http://api.cybertaxonomy.org/cyprus/");
345
            results = dex.execute(variableGet, "cdm_webservice_url");
346
            if (!results.get(0).equals("http://api.cybertaxonomy.org/cyprus/")) {
347
                throw new RuntimeException("unexpected result item 0: " + results.get(0));
348
            }
349
            // test for command failure:
350
            DrushExecutionFailure expectedFailure = null;
351
            try {
352
                dex.setDrupalRoot(new File("/home/andreas/workspaces/www/invalid-folder"));
353
                results = dex.execute(variableGet, "cdm_webservice_url");
354
            } catch(DrushExecutionFailure e) {
355
                expectedFailure = e;
356
            }
357
            if(expectedFailure == null) {
358
                throw new AssertionError("DrushExecutionFailure expected due to command failure");
359
            } else {
360
                logger.debug("invalid command has failed as expected");
361
            }
362
            // testing remote execution via ssh
363
            dex.sshHost = "edit-int";
364
            dex.setDrupalRoot(new File("/var/www/drupal-7-cdm-dataportal/web"));
365
            dex.setSiteURI(new URI("http://int.e-taxonomy.eu/dataportal/integration/cyprus"));
366
            results = dex.execute(variableGet, "cdm_webservice_url");
367
            if (!results.get(0).equals("http://int.e-taxonomy.eu/cdmserver/integration_cyprus/")) {
368
                throw new RuntimeException("unexpected result item 0: " + results.get(0));
369
            }
370

    
371
        } catch (IOException | InterruptedException | AssertionError e) {
372
            e.printStackTrace();
373
        }
374
    }
375

    
376
    public URI getSiteURI() {
377
        return siteURI;
378
    }
379

    
380
    public void setSiteURI(URI siteURI) {
381
        this.siteURI = siteURI;
382
    }
383

    
384
    public File getDrupalRoot() {
385
        return drupalRoot;
386
    }
387

    
388
    public void setDrupalRoot(File drupalRoot) {
389
        this.drupalRoot = drupalRoot;
390
    }
391

    
392
    public String getSshUser() {
393
        return sshUser;
394
    }
395

    
396
    public void setSshUser(String sshUser) {
397
        this.sshUser = sshUser;
398
    }
399

    
400
    public String getSshHost() {
401
        return sshHost;
402
    }
403

    
404
    public void setSshHost(String sshHost) {
405
        this.sshHost = sshHost;
406
    }
407
}
(1-1/2)