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-2023 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 @NotNull [] 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 @NotNull [] 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 = () -> {
288  try {
289  try {
290  stdout.join();
291  stderr.join();
292  tmp[0] = process.waitFor();
293  } catch (final InterruptedException ignored) {
294  Thread.currentThread().interrupt();
295  }
296  } finally {
297  sem.release();
298  }
299  };
300  final Thread thread = new Thread(runnable);
301  thread.start();
302  try {
303  if (!sem.tryAcquire(MAX_EXEC_TIME, TimeUnit.MILLISECONDS)) {
304  errorCollector.collect(new MapCheckerScriptFailureError<>(mapModel, command, "timeout waiting for script to terminate"));
305  return false;
306  }
307  } catch (final InterruptedException ignored) {
308  errorCollector.collect(new MapCheckerScriptFailureError<>(mapModel, command, "interrupted waiting for script to terminate"));
309  return false;
310  }
311  thread.interrupt();
312  if (tmp[0] != 0) {
313  errorCollector.collect(new MapCheckerScriptFailureError<>(mapModel, command, "command exited with status " + tmp[0]));
314  return false;
315  }
316  final String stderrFailure = stderr.getFailure();
317  if (stderrFailure != null) {
318  errorCollector.collect(new MapCheckerScriptFailureError<>(mapModel, command, stderrFailure));
319  return false;
320  }
321  final String stderrOutput = stderr.getOutput();
322  if (!stderrOutput.isEmpty()) {
323  errorCollector.collect(new MapCheckerScriptFailureError<>(mapModel, command, StringUtils.PATTERN_NEWLINE.matcher(stderrOutput).replaceAll("<br>")));
324  return false;
325  }
326  return true;
327  }
328 
336  @NotNull
337  private String @Nullable [] getCommand(@NotNull final MapModel<G, A, R> mapModel, @NotNull final ErrorCollector<G, A, R> errorCollector) {
338  if (tmpFile == null) {
339  try {
340  tmpFile = File.createTempFile("gridarta", null);
341  } catch (final IOException ex) {
342  errorCollector.collect(new MapCheckerScriptIOError<>(mapModel, "create temporary file", ex.getMessage()));
343  return null;
344  }
345  assert tmpFile != null;
346  tmpFile.deleteOnExit();
347  }
348  assert tmpFile != null;
349  final String mapPath = tmpFile.getPath();
350  final String realMapPath = mapModel.getMapFile() == null ? "" : mapModel.getMapFile().toString();
351  try {
352  try (OutputStream outputStream = new FileOutputStream(tmpFile)) {
353  try (Writer writer = new OutputStreamWriter(outputStream, IOUtils.MAP_ENCODING)) {
354  mapWriter.encodeMapFile(mapModel, writer);
355  }
356  }
357  } catch (final IOException ex) {
358  errorCollector.collect(new MapCheckerScriptIOError<>(mapModel, mapPath, ex.getMessage()));
359  return null;
360  }
361 
362  final boolean isWindows = System.getProperty("os.name").contains("Windows");
363  final String[] result;
364  int index = 0;
365  try {
366  if (isWindows) {
367  if (args[0].toLowerCase().endsWith(".py")) {
368  result = new String[args.length + 1];
369  String command;
370  try {
371  command = commandFinder2.getCommand("python.exe");
372  } catch (final IOException ex) {
373  command = "C:" + File.separator + "python34" + File.separator + "python.exe";
374  if (!new File(command).exists()) {
375  throw ex;
376  }
377  }
378  result[index++] = "\"" + command + "\"";
379  } else {
380  result = new String[args.length];
381  }
382  result[index++] = "\"" + commandFinder1.getCommand(args[0]) + "\"";
383  } else {
384  result = new String[args.length];
385  result[index++] = commandFinder1.getCommand(args[0]);
386  }
387  } catch (final IOException ex) {
388  errorCollector.collect(new MapCheckerScriptMissingError<>(mapModel, args[0], ex.getMessage()));
389  return null;
390  }
391  final String mapPathQuoted = Matcher.quoteReplacement(mapPath);
392  final String realMapPathQuoted = Matcher.quoteReplacement(realMapPath);
393  for (int i = 1; i < args.length; i++) {
394  final String tmp = args[i].replaceAll(QUOTED_MAP_PLACEHOLDER, mapPathQuoted).replaceAll(QUOTED_REAL_MAP_PLACEHOLDER, realMapPathQuoted);
395  result[index++] = isWindows ? "\"" + tmp + "\"" : tmp;
396  }
397  return result;
398  }
399 
404  private static class CommandFinder {
405 
410  @Nullable
411  private File cachedCommand;
412 
420  @NotNull
421  private String getCommand(@NotNull final String commandName) throws IOException {
422  final File existingCommand = cachedCommand;
423  if (existingCommand != null && existingCommand.exists()) {
424  return existingCommand.getPath();
425  }
426 
427  final File command = IOUtils.findPathFile(commandName);
428  cachedCommand = command;
429  return command.getPath();
430  }
431 
432  }
433 
434 }
net.sf.gridarta.model.mapmodel.MapModel
A MapModel reflects the data of a map.
Definition: MapModel.java:75
net.sf.gridarta.model.validation.checks.MapCheckerScriptChecker.CommandFinder
Searches for commands in the PATH environment variable.
Definition: MapCheckerScriptChecker.java:404
net.sf.gridarta.model.validation.checks.MapCheckerScriptChecker.MAX_EXEC_TIME
static final int MAX_EXEC_TIME
The maximum execution time of the checker script in milliseconds.
Definition: MapCheckerScriptChecker.java:61
net.sf.gridarta.model.validation.checks.MapCheckerScriptChecker.parseOutput
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.
Definition: MapCheckerScriptChecker.java:190
net.sf.gridarta.model.validation.checks.MapCheckerScriptChecker.REAL_MAP_PLACEHOLDER
static final String REAL_MAP_PLACEHOLDER
The placeholder in the command's arguments for the real map path.
Definition: MapCheckerScriptChecker.java:79
net.sf.gridarta.model.validation.checks.MapCheckerScriptChecker.tmpFile
File tmpFile
The temp file for saving maps to be checked.
Definition: MapCheckerScriptChecker.java:122
net.sf.gridarta
Base package of all Gridarta classes.
net.sf.gridarta.model.mapmodel.MapSquare
A single Map Square.
Definition: MapSquare.java:45
net.sf.gridarta.model.validation.checks.MapCheckerScriptChecker.MAP_PLACEHOLDER
static final String MAP_PLACEHOLDER
The placeholder in the command's arguments for the map to check.
Definition: MapCheckerScriptChecker.java:67
net.sf.gridarta.utils.IOUtils.findPathFile
static File findPathFile(@NotNull final String name)
Searches for.
Definition: IOUtils.java:163
net.sf.gridarta.model.validation.checks.MapCheckerScriptChecker.QUOTED_REAL_MAP_PLACEHOLDER
static final String QUOTED_REAL_MAP_PLACEHOLDER
The quoted placeholder in the command's arguments for the real map path.
Definition: MapCheckerScriptChecker.java:85
net.sf
net.sf.gridarta.model.validation.checks.MapCheckerScriptChecker.args
final String[] args
The command to execute and the arguments to pass.
Definition: MapCheckerScriptChecker.java:115
net.sf.gridarta.model.mapmodel
Definition: AboveFloorInsertionMode.java:20
net.sf.gridarta.model.archetype
Definition: AbstractArchetype.java:20
net.sf.gridarta.model.gameobject.GameObject
Reflects a game object (object on a map).
Definition: GameObject.java:36
net.sf.gridarta.model.validation.checks.MapCheckerScriptChecker.runProcess
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.
Definition: MapCheckerScriptChecker.java:230
net.sf.gridarta.model.validation.checks.MapCheckerScriptMissingError
A MapValidationError that indicates that the map checker script could not be found.
Definition: MapCheckerScriptMissingError.java:35
net.sf.gridarta.model.validation.checks.MapCheckerScriptFailureError
A MapValidationError that indicates that the map validator script could not be executed.
Definition: MapCheckerScriptFailureError.java:35
net.sf.gridarta.model.validation.AbstractValidator.validatorPreferences
final ValidatorPreferences validatorPreferences
The ValidatorPreferences to use.
Definition: AbstractValidator.java:55
net.sf.gridarta.model.validation.MapValidator
Interface for Map Validators.
Definition: MapValidator.java:33
net.sf.gridarta.model.validation.ValidatorPreferences
Configuration parameters for Validators.
Definition: ValidatorPreferences.java:28
net.sf.gridarta.model.gameobject
GameObjects are the objects based on Archetypes found on maps.
Definition: AbstractGameObject.java:20
net
net.sf.gridarta.utils.StringUtils.PATTERN_END_OF_LINE
static final Pattern PATTERN_END_OF_LINE
The pattern to match end of line characters separating lines.
Definition: StringUtils.java:61
net.sf.gridarta.model.validation.checks.MapCheckerScriptChecker.mapWriter
final MapWriter< G, A, R > mapWriter
The MapWriter for saving temporary map files.
Definition: MapCheckerScriptChecker.java:97
net.sf.gridarta.model.io.MapWriter
Interface for classes that write map files.
Definition: MapWriter.java:34
net.sf.gridarta.utils.CopyReader.start
void start()
Starts reading.
Definition: CopyReader.java:110
net.sf.gridarta.model.validation.checks.MapCheckerScriptChecker.CommandFinder.getCommand
String getCommand(@NotNull final String commandName)
Returns the command to execute.
Definition: MapCheckerScriptChecker.java:421
net.sf.gridarta.utils.CopyReader.getOutput
String getOutput()
Returns the reader's output.
Definition: CopyReader.java:153
net.sf.gridarta.model.validation.checks.MapCheckerScriptMapError
A MapValidationError that represents a per map error of the {}.
Definition: MapCheckerScriptMapError.java:35
net.sf.gridarta.model.maparchobject.MapArchObject
Interface for MapArchObjects.
Definition: MapArchObject.java:40
net.sf.gridarta.model.validation.checks.MapCheckerScriptChecker.MapCheckerScriptChecker
MapCheckerScriptChecker(@NotNull final ValidatorPreferences validatorPreferences, @NotNull final MapWriter< G, A, R > mapWriter, @NotNull final String @NotNull[] args)
Creates a new instance.
Definition: MapCheckerScriptChecker.java:130
net.sf.gridarta.model.validation.checks.MapCheckerScriptChecker
Executes a script to check a map.
Definition: MapCheckerScriptChecker.java:55
net.sf.gridarta.model.validation.ErrorCollector
An interface for classes that collect errors.
Definition: ErrorCollector.java:33
net.sf.gridarta.model.validation.AbstractValidator
This is the base class for validators.
Definition: AbstractValidator.java:37
net.sf.gridarta.model.validation.checks.MapCheckerScriptChecker.validateMap
void validateMap(@NotNull final MapModel< G, A, R > mapModel, @NotNull final ErrorCollector< G, A, R > errorCollector)
Validate a map.
Definition: MapCheckerScriptChecker.java:152
net.sf.gridarta.model.validation
This package contains the framework for validating maps.
Definition: AbstractValidator.java:20
net.sf.gridarta.utils.StringUtils
Utility class for string manipulation.
Definition: StringUtils.java:31
net.sf.gridarta.model.io
Reading and writing of maps, handling of paths.
Definition: AbstractAnimationObjectsReader.java:20
net.sf.gridarta.utils.CopyReader.stop
void stop()
Stops reading.
Definition: CopyReader.java:117
net.sf.gridarta.model.validation.checks.MapCheckerScriptChecker.QUOTED_MAP_PLACEHOLDER
static final String QUOTED_MAP_PLACEHOLDER
The quoted.
Definition: MapCheckerScriptChecker.java:73
net.sf.gridarta.model.validation.checks.MapCheckerScriptChecker.commandFinder2
final CommandFinder commandFinder2
The CommandFinder for the script interpreter.
Definition: MapCheckerScriptChecker.java:109
net.sf.gridarta.model
net.sf.gridarta.model.archetype.Archetype
Reflects an Archetype.
Definition: Archetype.java:41
net.sf.gridarta.utils.StringUtils.PATTERN_NEWLINE
static final Pattern PATTERN_NEWLINE
The pattern that matches a single newline ("\n").
Definition: StringUtils.java:109
net.sf.gridarta.model.io.MapWriter.encodeMapFile
void encodeMapFile(@NotNull MapModel< G, A, R > mapModel, @NotNull Writer writer)
Write the whole map-data into a file.
net.sf.gridarta.model.validation.checks.MapCheckerScriptChecker.waitForTermination
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.
Definition: MapCheckerScriptChecker.java:284
net.sf.gridarta.model.validation.checks.MapCheckerScriptChecker.commandFinder1
final CommandFinder commandFinder1
The CommandFinder for the script to execute.
Definition: MapCheckerScriptChecker.java:103
net.sf.gridarta.model.validation.checks.MapCheckerScriptIOError
A ValidationError that reports I/O errors related to {}.
Definition: MapCheckerScriptIOError.java:35
net.sf.gridarta.utils.IOUtils
Utility-class for Gridarta's I/O.
Definition: IOUtils.java:40
net.sf.gridarta.model.validation.checks.MapCheckerScriptChecker.CommandFinder.cachedCommand
File cachedCommand
The command to execute.
Definition: MapCheckerScriptChecker.java:411
net.sf.gridarta.model.validation.checks.MapCheckerScriptChecker.PATTERN_MAP_SQUARE_MESSAGE
static final Pattern PATTERN_MAP_SQUARE_MESSAGE
The Pattern for matching per map square messages.
Definition: MapCheckerScriptChecker.java:91
net.sf.gridarta.model.maparchobject
Definition: AbstractMapArchObject.java:20
net.sf.gridarta.utils.CopyReader
Copies a Reader into a String.
Definition: CopyReader.java:32
net.sf.gridarta.model.validation.checks.MapCheckerScriptChecker.getCommand
String[] getCommand(@NotNull final MapModel< G, A, R > mapModel, @NotNull final ErrorCollector< G, A, R > errorCollector)
Returns the command to execute.
Definition: MapCheckerScriptChecker.java:337
net.sf.gridarta.model.validation.checks.MapCheckerScriptMapSquareError
A SquareValidationError that represents a per map square error of the MapCheckerScriptChecker.
Definition: MapCheckerScriptMapSquareError.java:35
net.sf.gridarta.utils.CopyReader.getFailure
String getFailure()
Returns the failure reason.
Definition: CopyReader.java:140
net.sf.gridarta.model.mapmodel.MapSquare.getMapSquare
MapSquare< G, A, R > getMapSquare()
Returns the MapSquare this game object is part of.
Definition: MapSquare.java:150
net.sf.gridarta.utils
Definition: ActionBuilderUtils.java:20
net.sf.gridarta.utils.IOUtils.MAP_ENCODING
static final String MAP_ENCODING
Encoding to use for maps and other data.
Definition: IOUtils.java:52