Gridarta Editor
MapCheckerScriptChecker.java
Go to the documentation of this file.
1 /*
2  * Gridarta MMORPG map editor for Crossfire, Daimonin and similar games.
3  * Copyright (C) 2000-2015 The Gridarta Developers.
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License along
16  * with this program; if not, write to the Free Software Foundation, Inc.,
17  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18  */
19 
20 package net.sf.gridarta.model.validation.checks;
21 
22 import java.awt.Point;
23 import java.io.File;
24 import java.io.FileOutputStream;
25 import java.io.IOException;
26 import java.io.InputStreamReader;
27 import java.io.OutputStream;
28 import java.io.OutputStreamWriter;
29 import java.io.Writer;
30 import java.util.concurrent.Semaphore;
31 import java.util.concurrent.TimeUnit;
32 import java.util.regex.Matcher;
33 import java.util.regex.Pattern;
45 import net.sf.gridarta.utils.IOUtils;
47 import org.jetbrains.annotations.NotNull;
48 import org.jetbrains.annotations.Nullable;
49 
55 public class MapCheckerScriptChecker<G extends GameObject<G, A, R>, A extends MapArchObject<A>, R extends Archetype<G, A, R>> extends AbstractValidator<G, A, R> implements MapValidator<G, A, R> {
56 
61  private static final int MAX_EXEC_TIME = 30000;
62 
66  @NotNull
67  public static final String MAP_PLACEHOLDER = "${MAP}";
68 
72  @NotNull
73  private static final String QUOTED_MAP_PLACEHOLDER = Pattern.quote(MAP_PLACEHOLDER);
74 
78  @NotNull
79  public static final String REAL_MAP_PLACEHOLDER = "${REAL_MAP}";
80 
84  @NotNull
85  private static final String QUOTED_REAL_MAP_PLACEHOLDER = Pattern.quote(REAL_MAP_PLACEHOLDER);
86 
90  @NotNull
91  private static final Pattern PATTERN_MAP_SQUARE_MESSAGE = Pattern.compile("(\\d+)\\s+(\\d+)\\s+(.*)");
92 
96  @NotNull
98 
102  @NotNull
104 
108  @NotNull
110 
114  @NotNull
115  private final String[] args;
116 
121  @Nullable
122  private File tmpFile;
123 
130  public MapCheckerScriptChecker(@NotNull final ValidatorPreferences validatorPreferences, @NotNull final MapWriter<G, A, R> mapWriter, @NotNull final String[] args) {
131  super(validatorPreferences);
132  this.mapWriter = mapWriter;
133  this.args = args.clone();
134 
135  if (this.args.length < 1) {
136  throw new IllegalArgumentException("no script to execute");
137  }
138 
139  boolean found = false;
140  for (int i = 1; i < this.args.length; i++) {
141  if (this.args[i].contains(MAP_PLACEHOLDER)) {
142  found = true;
143  break;
144  }
145  }
146  if (!found) {
147  throw new IllegalArgumentException("the script to execute doesn't receive the map to check (" + MAP_PLACEHOLDER + ")");
148  }
149  }
150 
151  @Override
152  public void validateMap(@NotNull final MapModel<G, A, R> mapModel, @NotNull final ErrorCollector<G, A, R> errorCollector) {
153  final String[] command = getCommand(mapModel, errorCollector);
154  if (command == null) {
155  return;
156  }
157  final Process process;
158  try {
159  process = Runtime.getRuntime().exec(command);
160  } catch (final IOException ex) {
161  errorCollector.collect(new MapCheckerScriptMissingError<>(mapModel, command[0], ex.getMessage()));
162  return;
163  }
164  @Nullable final String output;
165  try {
166  try {
167  process.getOutputStream().close();
168  } catch (final IOException ex) {
169  errorCollector.collect(new MapCheckerScriptFailureError<>(mapModel, command[0], ex.getMessage() + " (closing stdin)"));
170  return;
171  }
172 
173  output = runProcess(process, errorCollector, mapModel, command[0]);
174  } finally {
175  process.destroy();
176  }
177  if (output != null) {
178  parseOutput(output, errorCollector, mapModel, command[0]);
179  }
180  }
181 
190  private void parseOutput(@NotNull final CharSequence output, @NotNull final ErrorCollector<G, A, R> errorCollector, @NotNull final MapModel<G, A, R> mapModel, @NotNull final String command) {
191  final String[] lines = StringUtils.PATTERN_END_OF_LINE.split(output);
192  for (final String line : lines) {
193  if (!line.isEmpty()) {
194  final Matcher matcher = PATTERN_MAP_SQUARE_MESSAGE.matcher(line);
195  if (matcher.matches()) {
196  final int x;
197  final int y;
198  try {
199  x = Integer.parseInt(matcher.group(1));
200  y = Integer.parseInt(matcher.group(2));
201  } catch (final NumberFormatException ignored) {
202  errorCollector.collect(new MapCheckerScriptFailureError<>(mapModel, command, "syntax error in '" + line + "'"));
203  continue;
204  }
205  final String message = matcher.group(3);
206  final MapSquare<G, A, R> mapSquare;
207  try {
208  mapSquare = mapModel.getMapSquare(new Point(x, y));
209  } catch (final IndexOutOfBoundsException ignored) {
210  errorCollector.collect(new MapCheckerScriptFailureError<>(mapModel, command, "invalid map square in '" + line + "'"));
211  continue;
212  }
213  errorCollector.collect(new MapCheckerScriptMapSquareError<>(mapSquare, message));
214  } else {
215  errorCollector.collect(new MapCheckerScriptMapError<>(mapModel, line));
216  }
217  }
218  }
219  }
220 
229  @Nullable
230  private String runProcess(@NotNull final Process process, @NotNull final ErrorCollector<G, A, R> errorCollector, @NotNull final MapModel<G, A, R> mapModel, @NotNull final String command) {
231  @Nullable String output;
232  final InputStreamReader stdoutReader = new InputStreamReader(process.getInputStream());
233  try {
234  final CopyReader stdout = new CopyReader(stdoutReader);
235  final InputStreamReader stderrReader = new InputStreamReader(process.getErrorStream());
236  try {
237  final CopyReader stderr = new CopyReader(stderrReader);
238  stdout.start();
239  try {
240  stderr.start();
241  try {
242  if (!waitForTermination(process, errorCollector, mapModel, command, stdout, stderr)) {
243  return null;
244  }
245  } finally {
246  stderr.stop();
247  }
248  } finally {
249  stdout.stop();
250  }
251  } finally {
252  try {
253  stderrReader.close();
254  } catch (final IOException ex) {
255  errorCollector.collect(new MapCheckerScriptFailureError<>(mapModel, command, ex.getMessage() + " (closing stderr)"));
256  }
257  }
258  final String stdoutFailure = stdout.getFailure();
259  if (stdoutFailure != null) {
260  errorCollector.collect(new MapCheckerScriptFailureError<>(mapModel, command, stdoutFailure));
261  return null;
262  }
263  output = stdout.getOutput();
264  } finally {
265  try {
266  stdoutReader.close();
267  } catch (final IOException ex) {
268  errorCollector.collect(new MapCheckerScriptFailureError<>(mapModel, command, ex.getMessage() + " (closing stdout)"));
269  }
270  }
271  return output;
272  }
273 
284  private boolean waitForTermination(@NotNull final Process process, @NotNull final ErrorCollector<G, A, R> errorCollector, @NotNull final MapModel<G, A, R> mapModel, @NotNull final String command, @NotNull final CopyReader stdout, @NotNull final CopyReader stderr) {
285  final Semaphore sem = new Semaphore(0);
286  final int[] tmp = { -1, };
287  final Runnable runnable = new Runnable() {
288 
289  @Override
290  public void run() {
291  try {
292  try {
293  stdout.join();
294  stderr.join();
295  tmp[0] = process.waitFor();
296  } catch (final InterruptedException ignored) {
297  Thread.currentThread().interrupt();
298  }
299  } finally {
300  sem.release();
301  }
302  }
303 
304  };
305  final Thread thread = new Thread(runnable);
306  thread.start();
307  try {
308  if (!sem.tryAcquire(MAX_EXEC_TIME, TimeUnit.MILLISECONDS)) {
309  errorCollector.collect(new MapCheckerScriptFailureError<>(mapModel, command, "timeout waiting for script to terminate"));
310  return false;
311  }
312  } catch (final InterruptedException ignored) {
313  errorCollector.collect(new MapCheckerScriptFailureError<>(mapModel, command, "interrupted waiting for script to terminate"));
314  return false;
315  }
316  thread.interrupt();
317  if (tmp[0] != 0) {
318  errorCollector.collect(new MapCheckerScriptFailureError<>(mapModel, command, "command exited with status " + tmp[0]));
319  return false;
320  }
321  final String stderrFailure = stderr.getFailure();
322  if (stderrFailure != null) {
323  errorCollector.collect(new MapCheckerScriptFailureError<>(mapModel, command, stderrFailure));
324  return false;
325  }
326  final String stderrOutput = stderr.getOutput();
327  if (!stderrOutput.isEmpty()) {
328  errorCollector.collect(new MapCheckerScriptFailureError<>(mapModel, command, StringUtils.PATTERN_NEWLINE.matcher(stderrOutput).replaceAll("<br>")));
329  return false;
330  }
331  return true;
332  }
333 
341  @Nullable
342  private String[] getCommand(@NotNull final MapModel<G, A, R> mapModel, @NotNull final ErrorCollector<G, A, R> errorCollector) {
343  if (tmpFile == null) {
344  try {
345  tmpFile = File.createTempFile("gridarta", null);
346  } catch (final IOException ex) {
347  errorCollector.collect(new MapCheckerScriptIOError<>(mapModel, "create temporary file", ex.getMessage()));
348  return null;
349  }
350  assert tmpFile != null;
351  tmpFile.deleteOnExit();
352  }
353  assert tmpFile != null;
354  final String mapPath = tmpFile.getPath();
355  final String realMapPath = mapModel.getMapFile() != null ? mapModel.getMapFile().toString() : "";
356  try {
357  try (OutputStream outputStream = new FileOutputStream(tmpFile)) {
358  try (Writer writer = new OutputStreamWriter(outputStream, IOUtils.MAP_ENCODING)) {
359  mapWriter.encodeMapFile(mapModel, writer);
360  }
361  }
362  } catch (final IOException ex) {
363  errorCollector.collect(new MapCheckerScriptIOError<>(mapModel, mapPath, ex.getMessage()));
364  return null;
365  }
366 
367  final boolean isWindows = System.getProperty("os.name").contains("Windows");
368  final String[] result;
369  int index = 0;
370  try {
371  if (isWindows) {
372  if (args[0].toLowerCase().endsWith(".py")) {
373  result = new String[args.length + 1];
374  String command;
375  try {
376  command = commandFinder2.getCommand("python.exe");
377  } catch (final IOException ex) {
378  command = "C:" + File.separator + "python34" + File.separator + "python.exe";
379  if (!new File(command).exists()) {
380  throw ex;
381  }
382  }
383  result[index++] = "\"" + command + "\"";
384  } else {
385  result = new String[args.length];
386  }
387  result[index++] = "\"" + commandFinder1.getCommand(args[0]) + "\"";
388  } else {
389  result = new String[args.length];
390  result[index++] = commandFinder1.getCommand(args[0]);
391  }
392  } catch (final IOException ex) {
393  errorCollector.collect(new MapCheckerScriptMissingError<>(mapModel, args[0], ex.getMessage()));
394  return null;
395  }
396  final String mapPathQuoted = Matcher.quoteReplacement(mapPath);
397  final String realMapPathQuoted = Matcher.quoteReplacement(realMapPath);
398  for (int i = 1; i < args.length; i++) {
399  final String tmp = args[i].replaceAll(QUOTED_MAP_PLACEHOLDER, mapPathQuoted).replaceAll(QUOTED_REAL_MAP_PLACEHOLDER, realMapPathQuoted);
400  result[index++] = isWindows ? "\"" + tmp + "\"" : tmp;
401  }
402  return result;
403  }
404 
409  private static class CommandFinder {
410 
415  @Nullable
416  private File cachedCommand;
417 
425  @NotNull
426  private String getCommand(@NotNull final String commandName) throws IOException {
427  final File existingCommand = cachedCommand;
428  if (existingCommand != null && existingCommand.exists()) {
429  return existingCommand.getPath();
430  }
431 
432  final File command = IOUtils.findPathFile(commandName);
433  cachedCommand = command;
434  return command.getPath();
435  }
436 
437  }
438 
439 }
static File findPathFile(@NotNull final String name)
Searches for.
Definition: IOUtils.java:162
static final Pattern PATTERN_MAP_SQUARE_MESSAGE
The Pattern for matching per map square messages.
Utility class for string manipulation.
final MapWriter< G, A, R > mapWriter
The MapWriter for saving temporary map files.
String getCommand(@NotNull final String commandName)
Returns the command to execute.
A MapModel reflects the data of a map.
Definition: MapModel.java:75
This is the base class for validators.
Reading and writing of maps, handling of paths.
A MapValidationError that indicates that the map checker script could not be found.
Interface for classes that write map files.
Definition: MapWriter.java:34
This package contains the framework for validating maps.
static final String MAP_ENCODING
Encoding to use for maps and other data.
Definition: IOUtils.java:51
A SquareValidationError that represents a per map square error of the MapCheckerScriptChecker.
static final Pattern PATTERN_NEWLINE
The pattern that matches a single newline ("\n").
File tmpFile
The temp file for saving maps to be checked.
static final String MAP_PLACEHOLDER
The placeholder in the command&#39;s arguments for the map to check.
Base package of all Gridarta classes.
void start()
Starts reading.
static final String QUOTED_REAL_MAP_PLACEHOLDER
The quoted placeholder in the command&#39;s arguments for the real map path.
static final int MAX_EXEC_TIME
The maximum execution time of the checker script in milliseconds.
Reflects a game object (object on a map).
Definition: GameObject.java:36
Copies a Reader into a String.
Definition: CopyReader.java:32
Utility-class for Gridarta&#39;s I/O.
Definition: IOUtils.java:40
String [] getCommand(@NotNull final MapModel< G, A, R > mapModel, @NotNull final ErrorCollector< G, A, R > errorCollector)
Returns the command to execute.
A MapValidationError that indicates that the map validator script could not be executed.
String getOutput()
Returns the reader&#39;s output.
GameObjects are the objects based on Archetypes found on maps.
final CommandFinder commandFinder1
The CommandFinder for the script to execute.
MapCheckerScriptChecker(@NotNull final ValidatorPreferences validatorPreferences, @NotNull final MapWriter< G, A, R > mapWriter, @NotNull final String[] args)
Creates a new instance.
A ValidationError that reports I/O errors related to MapCheckerScriptChecker.
static final String REAL_MAP_PLACEHOLDER
The placeholder in the command&#39;s arguments for the real map path.
final ValidatorPreferences validatorPreferences
The ValidatorPreferences to use.
boolean waitForTermination(@NotNull final Process process, @NotNull final ErrorCollector< G, A, R > errorCollector, @NotNull final MapModel< G, A, R > mapModel, @NotNull final String command, @NotNull final CopyReader stdout, @NotNull final CopyReader stderr)
Waits for a Process to terminate.
static final Pattern PATTERN_END_OF_LINE
The pattern to match end of line characters separating lines.
void validateMap(@NotNull final MapModel< G, A, R > mapModel, @NotNull final ErrorCollector< G, A, R > errorCollector)
Validate a map.
An interface for classes that collect errors.
void stop()
Stops reading.
void encodeMapFile(@NotNull MapModel< G, A, R > mapModel, @NotNull Writer writer)
Write the whole map-data into a file.
void parseOutput(@NotNull final CharSequence output, @NotNull final ErrorCollector< G, A, R > errorCollector, @NotNull final MapModel< G, A, R > mapModel, @NotNull final String command)
Parses output of the executed script.
String getFailure()
Returns the failure reason.
final CommandFinder commandFinder2
The CommandFinder for the script interpreter.
A MapValidationError that represents a per map error of the MapCheckerScriptChecker.
String runProcess(@NotNull final Process process, @NotNull final ErrorCollector< G, A, R > errorCollector, @NotNull final MapModel< G, A, R > mapModel, @NotNull final String command)
Waits for a Process to terminate and returns its output.
final String [] args
The command to execute and the arguments to pass.