Project

General

Profile

Download (12.3 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

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

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

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

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

    
48
    private URI siteURI = null;
49

    
50
    private File drupalRoot = null;
51

    
52
    private String sshUser = null;
53

    
54
    private String sshHost = null;
55

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

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

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

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

    
78
    /**
79
     * Executes {@code drush vget --exact <variable-key>}
80
     * <p>
81
     * The execution of this command via
82
     * <code>DrushExecuter.execute({@linkplain DrushCommand#variableSet})</code> results in
83
     * a {@code List<String>} return variable with the following elements:
84
     *
85
     * <ol>
86
     * <li>value</li>
87
     * <li>value</li>
88
     * <li>value</li>
89
     * <ol>
90
     */
91
    public static DrushCommand variableGet = new DrushCommand(Arrays.asList("vget", "--exact", "--format=json", "%s"));
92
    /**
93
     * Executes {@code drush vset --exact <variable-key> <variable-value>}
94
     * <p>
95
     * The execution of this command via
96
     * <code>DrushExecuter.execute({@linkplain DrushCommand#variableSet})</code> results in
97
     * a {@code List<String>} return variable with the following elements:
98
     *
99
     * <ol>
100
     * <li>value</li>
101
     * <li>status</li>
102
     * <ol>
103
     */
104
    public static DrushCommand variableSet = new DrushCommand(Arrays.asList("--yes", "vset", "%s", "%s"), null,
105
            "[^\\\"]*\\\"(.*)\\\".*\\[(\\w+)\\]"
106
            );
107

    
108
    /**
109
     * @throws IOException
110
     *             if an I/O error occurs in the ProcessBuilder
111
     * @throws InterruptedException
112
     *             if the Process was interrupted
113
     */
114
    private void findDrushCommand() throws IOException, InterruptedException {
115

    
116
        if (SystemUtils.IS_OS_WINDOWS) {
117
            throw new RuntimeException("not yet implmented for Windows");
118
        }
119

    
120
        List<Object> matches = execute(version);
121
        assert !((String) matches.get(0)).isEmpty() : "No suitable drush command found in the system";
122
        String majorVersion = (String) matches.get(0);
123
        if (Integer.valueOf(majorVersion) < 8) {
124
            throw new RuntimeException("drush version >= 8 required");
125
        }
126

    
127
    }
128

    
129
    /**
130
     * @throws IOException
131
     *             if an I/O error occurs in the ProcessBuilder
132
     * @throws InterruptedException
133
     *             if the Process was interrupted
134
     */
135
    public List<Object> execute(DrushCommand cmd, String... value) throws IOException, InterruptedException {
136

    
137
        List<String> executableWithArgs = new ArrayList<>();
138

    
139
        if (sshHost != null) {
140
            executableWithArgs.add("ssh");
141
            String userHostArg = sshHost;
142
            if (sshUser != null) {
143
                userHostArg = sshUser + "@" + sshHost;
144
            }
145
            executableWithArgs.add(userHostArg);
146
        }
147

    
148
        executableWithArgs.add("drush");
149

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

    
166
        List<Object> matches = new ArrayList<>();
167

    
168
        ProcessBuilder pb = new ProcessBuilder(executableWithArgs);
169
        logger.debug("Command: " + pb.command().toString());
170
        Process process = pb.start();
171
        int exitCode = process.waitFor();
172

    
173
        if (exitCode == 0) {
174
            String out, error;
175
            if(cmd.jsonResult) {
176
                out = readExecutionResponse(matches, process.getInputStream());
177
                error = readExecutionResponse(matches, process.getErrorStream());
178
            } else {
179
                out = readExecutionResponse(matches, process.getInputStream(), cmd.outRegex);
180
                error = readExecutionResponse(matches, process.getErrorStream(), cmd.errRegex);
181
            }
182
            if (out != null && !out.isEmpty()) {
183
                logger.error(error);
184
            }
185
            if (error != null && !error.isEmpty()) {
186
                logger.error(error);
187
            }
188
        } else {
189
            throw new RuntimeException(IOUtils.toString(process.getErrorStream()));
190
        }
191
        return matches;
192
    }
193

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

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

    
262
        }
263
        return out;
264
    }
265

    
266
    public static class DrushCommand {
267

    
268
        Pattern outRegex;
269
        Pattern errRegex;
270
        boolean jsonResult = false;
271
        List<String> args = new ArrayList<>();
272

    
273
        public DrushCommand(List<String> args, String outRegex, String errRegex) {
274
            this.args = args;
275
            if (outRegex != null) {
276
                this.outRegex = Pattern.compile(outRegex, Pattern.MULTILINE);
277
            }
278
            if (errRegex != null) {
279
                this.errRegex = Pattern.compile(errRegex, Pattern.MULTILINE);
280
            }
281
        }
282

    
283
        /**
284
         * For drush commands suopporting the {@code --format=json} option.
285
         * @param args
286
         */
287
        public DrushCommand(List<String> args) {
288
            this.args = args;
289
            this.jsonResult = true;
290
        }
291
    }
292

    
293
    /**
294
     * These tests have not been implemented as jUnit tests since the execution
295
     * it too much dependent from the local environment. Once the
296
     * <code>DrushExecuter</code> is being used in the selenium test suite will
297
     * be tested implicitly anyway.
298
     */
299
    public static void main(String[] args) throws URISyntaxException {
300
        DrushExecuter.logger.setLevel(Level.DEBUG);
301
        try {
302
            DrushExecuter dex = new DrushExecuter();
303
            List<Object> results;
304
            dex.setDrupalRoot(new File("/home/andreas/workspaces/www/drupal-7"));
305
            dex.setSiteURI(new URI("http://edit.test/d7/caryophyllales/"));
306
//            dex.execute(coreStatus);
307
//            dex.execute(help);
308
            results = dex.execute(variableSet, "cdm_webservice_url",
309
                    "http://api.cybertaxonomy.org/cyprus/");
310
            if (!results.get(0).equals("http://api.cybertaxonomy.org/cyprus/")) {
311
                throw new RuntimeException("unexpected result item 0: " + results.get(0));
312
            }
313
            if (!results.get(1).equals("success")) {
314
                throw new RuntimeException("unexpected result item 1: " + results.get(0));
315
            }
316
            results = dex.execute(variableGet, "cdm_webservice_url");
317
            if (!results.get(0).equals("http://api.cybertaxonomy.org/cyprus/")) {
318
                throw new RuntimeException("unexpected result item 0: " + results.get(0));
319
            }
320
            // testing remote execution via ssh
321
            dex.sshHost = "edit-int";
322
            dex.setDrupalRoot(new File("/var/www/drupal-7"));
323
            dex.setSiteURI(new URI("http://int.e-taxonomy.eu/dataportal/integration/cyprus"));
324
            results = dex.execute(variableGet, "cdm_webservice_url");
325
            if (!results.get(0).equals("http://int.e-taxonomy.eu/cdmserver/integration_cyprus/")) {
326
                throw new RuntimeException("unexpected result item 0: " + results.get(0));
327
            }
328

    
329
        } catch (IOException | InterruptedException | AssertionError e) {
330
            e.printStackTrace();
331
        }
332
    }
333

    
334
    public URI getSiteURI() {
335
        return siteURI;
336
    }
337

    
338
    public void setSiteURI(URI siteURI) {
339
        this.siteURI = siteURI;
340
    }
341

    
342
    public File getDrupalRoot() {
343
        return drupalRoot;
344
    }
345

    
346
    public void setDrupalRoot(File drupalRoot) {
347
        this.drupalRoot = drupalRoot;
348
    }
349

    
350
    public String getSshUser() {
351
        return sshUser;
352
    }
353

    
354
    public void setSshUser(String sshUser) {
355
        this.sshUser = sshUser;
356
    }
357

    
358
    public String getSshHost() {
359
        return sshHost;
360
    }
361

    
362
    public void setSshHost(String sshHost) {
363
        this.sshHost = sshHost;
364
    }
365
}
    (1-1/1)