Gridarta Editor
MapsIndexer.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.index;
21 
22 import java.io.File;
23 import java.io.FileInputStream;
24 import java.io.FileNotFoundException;
25 import java.io.FileOutputStream;
26 import java.io.IOException;
27 import java.io.InputStream;
28 import java.io.ObjectInputStream;
29 import java.io.ObjectOutputStream;
30 import java.io.OutputStream;
31 import java.nio.charset.StandardCharsets;
32 import java.util.Arrays;
33 import java.util.concurrent.Semaphore;
46 import net.sf.gridarta.utils.Xtea;
47 import org.apache.log4j.Category;
48 import org.apache.log4j.Logger;
49 import org.jetbrains.annotations.NotNull;
50 import org.jetbrains.annotations.Nullable;
51 
57 public class MapsIndexer<G extends GameObject<G, A, R>, A extends MapArchObject<A>, R extends Archetype<G, A, R>> {
58 
62  @NotNull
63  private static final Category LOG = Logger.getLogger(MapsIndexer.class);
64 
68  @NotNull
69  private final Semaphore mapsIndexSemaphore = new Semaphore(1);
70 
74  @NotNull
75  private final MapsIndex mapsIndex;
76 
80  @NotNull
82 
86  @NotNull
88 
93  @NotNull
94  private final Object syncMapsDirectory = new Object();
95 
99  @Nullable
100  private File mapsDirectory;
101 
106  @Nullable
107  private File newMapsDirectory;
108 
112  @NotNull
113  private final Object syncState = new Object();
114 
118  @NotNull
119  private State state = State.INIT;
120 
124  private enum State {
125 
130 
136 
141 
145  IDLE
146 
147  }
148 
153  @NotNull
155 
156  @Override
157  public void currentMapChanged(@Nullable final MapControl<G, A, R> mapControl) {
158  // ignore
159  }
160 
161  @Override
162  public void mapCreated(@NotNull final MapControl<G, A, R> mapControl, final boolean interactive) {
163  if (!mapControl.isPickmap()) {
164  mapControl.addMapControlListener(mapControlListener);
165  }
166  }
167 
168  @Override
169  public void mapClosing(@NotNull final MapControl<G, A, R> mapControl) {
170  // ignore
171  }
172 
173  @Override
174  public void mapClosed(@NotNull final MapControl<G, A, R> mapControl) {
175  if (!mapControl.isPickmap()) {
176  mapControl.removeMapControlListener(mapControlListener);
177  }
178  }
179 
180  };
181 
185  @NotNull
187 
188  @Override
189  public void saved(@NotNull final DefaultMapControl<G, A, R> mapControl) {
190  final MapFile mapFile = mapControl.getMapModel().getMapFile();
191  if (mapFile != null) {
192  mapsIndex.setPending(mapFile);
193  }
194  }
195 
196  };
197 
202  @NotNull
204 
205  @Override
206  public void mapsDirectoryChanged(@NotNull final File mapsDirectory) {
207  synchronized (syncMapsDirectory) {
209  }
210  }
211 
212  };
213 
218  @NotNull
220 
221  @Override
222  public void valueAdded(@NotNull final MapFile value) {
223  // ignore
224  }
225 
226  @Override
227  public void valueRemoved(@NotNull final MapFile value) {
228  // ignore
229  }
230 
231  @Override
232  public void nameChanged() {
233  // ignore
234  }
235 
236  @Override
237  public void pendingChanged() {
238  mapsIndexSemaphore.release();
239  }
240 
241  @Override
242  public void indexingFinished() {
243  // ignore
244  }
245 
246  };
247 
251  @NotNull
252  private final Runnable runnable = () -> {
253  while (!Thread.currentThread().isInterrupted()) {
256  if (state == State.IDLE) {
257  try {
258  mapsIndexSemaphore.acquire();
259  } catch (final InterruptedException ignored) {
260  Thread.currentThread().interrupt();
261  break;
262  }
263  }
264  }
265  };
266 
270  @NotNull
271  private final Thread thread = new Thread(runnable, "indexer");
272 
280  public MapsIndexer(@NotNull final MapsIndex mapsIndex, @NotNull final MapManager<G, A, R> mapManager, @NotNull final ProjectSettings projectSettings) {
281  this.mapsIndex = mapsIndex;
282  this.mapManager = mapManager;
283  this.projectSettings = projectSettings;
285  }
286 
290  public void start() {
293  synchronized (syncMapsDirectory) {
295  }
297  thread.start();
298  }
299 
305  public void stop() throws InterruptedException {
306  try {
307  thread.interrupt();
308  thread.join();
309  } finally {
312  for (final MapControl<G, A, R> mapControl : mapManager.getOpenedMaps()) {
313  if (!mapControl.isPickmap()) {
314  mapControl.removeMapControlListener(mapControlListener);
315  }
316  }
318  }
319 
320  synchronized (syncMapsDirectory) {
321  saveMapsIndex();
322  }
323  }
324 
330  public void waitForIdle() throws InterruptedException {
331  synchronized (syncState) {
332  while (state != State.IDLE || mapsIndex.hasPending()) {
333  syncState.wait(1000L);
334  }
335  }
336  }
337 
342  private void updateMapsDirectory() {
343  final File tmpMapsDirectory;
344  synchronized (syncMapsDirectory) {
345  if (newMapsDirectory == null || (mapsDirectory != null && mapsDirectory.equals(newMapsDirectory))) {
346  return;
347  }
348 
349  setState(State.SCAN);
350 
351  saveMapsIndex();
352 
353  tmpMapsDirectory = newMapsDirectory;
354  assert tmpMapsDirectory != null;
355  mapsDirectory = tmpMapsDirectory;
356  newMapsDirectory = null;
357 
358  loadMapsIndex();
359  }
361  scanMapsDirectoryInt(new MapFile(tmpMapsDirectory), "");
363  }
364 
368  private void saveMapsIndex() {
369  assert Thread.holdsLock(syncMapsDirectory);
370  if (mapsDirectory == null) {
371  return;
372  }
373 
374  if (!projectSettings.saveIndices()) {
375  return;
376  }
377 
378  if (!mapsIndex.isModified()) {
379  return;
380  }
381 
382  assert mapsDirectory != null;
383  final File cacheFile = getCacheFile(mapsDirectory);
384  try {
385  try (OutputStream outputStream = new FileOutputStream(cacheFile)) {
386  try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream)) {
387  mapsIndex.save(objectOutputStream);
388  }
389  }
390  if (LOG.isInfoEnabled()) {
391  LOG.info(cacheFile + ": saved " + mapsIndex.size() + " entries");
392  }
393  } catch (final IOException ex) {
394  LOG.warn(cacheFile + ": cannot save cache file: " + ex.getMessage());
395  }
396  }
397 
401  private void loadMapsIndex() {
402  assert Thread.holdsLock(syncMapsDirectory);
403  assert mapsDirectory != null;
404  final File cacheFile = getCacheFile(mapsDirectory);
405  try {
406  try (InputStream inputStream = new FileInputStream(cacheFile)) {
407  try (ObjectInputStream objectInputStream = new ObjectInputStream(inputStream)) {
408  mapsIndex.load(objectInputStream);
409  }
410  }
411  if (LOG.isInfoEnabled()) {
412  LOG.info(cacheFile + ": loaded " + mapsIndex.size() + " entries");
413  }
414  } catch (final FileNotFoundException ex) {
415  if (LOG.isDebugEnabled()) {
416  LOG.debug(cacheFile + ": cache file does not exist: " + ex.getMessage());
417  }
418  mapsIndex.clear();
419  } catch (final IOException ex) {
420  LOG.warn(cacheFile + ": cannot load cache file: " + ex.getMessage());
421  mapsIndex.clear();
422  }
423  }
424 
430  private void scanMapsDirectoryInt(@NotNull final MapFile dir, @NotNull final String mapPath) {
431  final File[] files = dir.getFile().listFiles();
432  if (files == null) {
433  return;
434  }
435 
436  for (final File file : files) {
437  final MapFile mapFile = new MapFile(dir, file.getName());
438  if (file.isFile() && !file.getName().endsWith("~")) {
439  mapsIndex.add(mapFile, file.lastModified());
440  } else if (file.isDirectory() && !file.getName().equalsIgnoreCase(".svn") && !file.getName().equalsIgnoreCase(".dedit")) {
441  scanMapsDirectoryInt(mapFile, mapPath + "/" + file.getName());
442  }
443  }
444  }
445 
450  private void indexPendingMaps() {
451  final MapFile mapFile = mapsIndex.removePending();
452  if (mapFile == null) {
453  setState(State.IDLE);
454  return;
455  }
456 
457  setState(State.INDEX);
458  final long timestamp = mapFile.getFile().lastModified();
459  final MapControl<G, A, R> mapControl;
460  try {
461  mapControl = mapManager.openMapFile(mapFile, false);
462  } catch (final IOException ex) {
463  if (LOG.isInfoEnabled()) {
464  LOG.info(mapFile + ": load failed:" + ex.getMessage());
465  }
466  return;
467  }
468  try {
469  final String mapName = mapControl.getMapModel().getMapArchObject().getMapName();
470  if (LOG.isDebugEnabled()) {
471  LOG.debug(mapFile + ": indexing as '" + mapName + "'");
472  }
473  mapsIndex.setName(mapFile, timestamp, mapName);
474  } finally {
475  mapManager.release(mapControl);
476  }
477  }
478 
484  @NotNull
485  private static File getCacheFile(@NotNull final File mapsDirectory) {
486  final byte[] key = new byte[16];
487  final Xtea xtea = new Xtea(key);
488  final byte[] data = mapsDirectory.getAbsoluteFile().toString().getBytes(StandardCharsets.UTF_8);
489  final byte[] hash = new byte[8];
490  final byte[] in = new byte[8];
491  final byte[] out = new byte[8];
492  int i;
493  for (i = 0; i + 8 < data.length; i++) {
494  System.arraycopy(data, i, in, 0, 8);
495  xtea.encrypt(in, out);
496  for (int j = 0; j < 8; j++) {
497  hash[j] ^= out[j];
498  }
499  }
500  final int len = data.length % 8;
501  System.arraycopy(data, i, in, 0, len);
502  in[len] = 1;
503  Arrays.fill(in, len + 1, 8, (byte) 0);
504  xtea.encrypt(in, out);
505  for (int j = 0; j < 8; j++) {
506  hash[j] ^= out[j];
507  }
508 
509  final StringBuilder sb = new StringBuilder("index/maps/");
510  for (int j = 0; j < 8; j++) {
511  sb.append(String.format("%02x", hash[j] & 0xff));
512  }
513  sb.append(mapsDirectory.getName());
514  final File file = ConfigFileUtils.getHomeFile(sb.toString());
515  final File dir = file.getParentFile();
516  if (dir != null && !dir.exists() && !dir.mkdirs()) {
517  LOG.warn("cannot create directory: " + dir);
518  }
519  return file;
520  }
521 
526  private void setState(@NotNull final State state) {
527  synchronized (syncState) {
528  if (this.state == state) {
529  return;
530  }
531 
532  this.state = state;
534  syncState.notifyAll();
535  }
536  }
537 
541  private void reportStateChange() {
542  if (LOG.isDebugEnabled()) {
543  LOG.debug("state=" + state);
544  }
545 
546  if (state == State.IDLE) {
548  }
549  }
550 
551 }
net.sf.gridarta.model.mapmodel.MapFile
Definition: MapFile.java:31
net.sf.gridarta.model.mapmanager
Definition: AbstractMapManager.java:20
net.sf.gridarta.model.mapmanager.MapManager.getOpenedMaps
List< MapControl< G, A, R > > getOpenedMaps()
net.sf.gridarta.model.mapmanager.MapManager.release
void release(@NotNull MapControl< G, A, R > mapControl)
net.sf.gridarta.model.index.AbstractIndex.beginUpdate
void beginUpdate()
Definition: AbstractIndex.java:118
net.sf.gridarta.model.index.MapsIndexer.indexListener
final IndexListener< MapFile > indexListener
Definition: MapsIndexer.java:219
net.sf.gridarta.model.mapcontrol.DefaultMapControl
Definition: DefaultMapControl.java:44
net.sf.gridarta
net.sf.gridarta.model.mapmanager.MapManager.addMapManagerListener
void addMapManagerListener(@NotNull MapManagerListener< G, A, R > listener)
net.sf.gridarta.model.index.MapsIndexer.newMapsDirectory
File newMapsDirectory
Definition: MapsIndexer.java:107
net.sf.gridarta.model.index.MapsIndexer.mapControlListener
final MapControlListener< G, A, R > mapControlListener
Definition: MapsIndexer.java:186
net.sf.gridarta.model.maparchobject.MapArchObject
Definition: MapArchObject.java:40
net.sf.gridarta.model.index.AbstractIndex.isModified
boolean isModified()
Definition: AbstractIndex.java:262
net.sf.gridarta.model.index.MapsIndexer.mapsIndexSemaphore
final Semaphore mapsIndexSemaphore
Definition: MapsIndexer.java:69
net.sf.gridarta.model.index.AbstractIndex.setPending
void setPending(@NotNull final V value)
Definition: AbstractIndex.java:198
net.sf.gridarta.model.index.MapsIndexer.state
State state
Definition: MapsIndexer.java:119
net.sf.gridarta.model.index.AbstractIndex.save
void save(@NotNull final ObjectOutputStream objectOutputStream)
Definition: AbstractIndex.java:279
files
Standard Edition Runtime Environment README Import and export control rules on cryptographic software vary from country to country The Java Cryptography Java provides two different sets of cryptographic policy files
Definition: README.txt:26
net.sf.gridarta.model.index.AbstractIndex.size
int size()
Definition: AbstractIndex.java:98
net.sf.gridarta.model.index.AbstractIndex.addIndexListener
void addIndexListener(@NotNull final IndexListener< V > listener)
Definition: AbstractIndex.java:269
net.sf
net.sf.gridarta.model.index.AbstractIndex.add
void add(@NotNull final V value, final long timestamp)
Definition: AbstractIndex.java:154
net.sf.gridarta.model.mapmodel
Definition: AboveFloorInsertionMode.java:20
net.sf.gridarta.model.index.AbstractIndex.hasPending
boolean hasPending()
Definition: AbstractIndex.java:255
net.sf.gridarta.utils.ConfigFileUtils
Definition: ConfigFileUtils.java:29
net.sf.gridarta.model.index.MapsIndexer.LOG
static final Category LOG
Definition: MapsIndexer.java:63
net.sf.gridarta.model.index.MapsIndexer.projectSettings
final ProjectSettings projectSettings
Definition: MapsIndexer.java:87
net.sf.gridarta.model.index.AbstractIndex.removePending
V removePending()
Definition: AbstractIndex.java:241
net.sf.gridarta.model.archetype
Definition: AbstractArchetype.java:20
net.sf.gridarta.model.mapmanager.MapManager.openMapFile
MapControl< G, A, R > openMapFile(@NotNull MapFile mapFile, boolean interactive)
net.sf.gridarta.model.mapcontrol
Definition: DefaultMapControl.java:20
net.sf.gridarta.model.index.IndexListener
Definition: IndexListener.java:30
net.sf.gridarta.model.index.MapsIndexer.scanMapsDirectoryInt
void scanMapsDirectoryInt(@NotNull final MapFile dir, @NotNull final String mapPath)
Definition: MapsIndexer.java:430
net.sf.gridarta.model.gameobject.GameObject
Definition: GameObject.java:36
net.sf.gridarta.model.index.MapsIndexer.MapsIndexer
MapsIndexer(@NotNull final MapsIndex mapsIndex, @NotNull final MapManager< G, A, R > mapManager, @NotNull final ProjectSettings projectSettings)
Definition: MapsIndexer.java:280
net.sf.gridarta.model.index.MapsIndexer.mapsDirectory
File mapsDirectory
Definition: MapsIndexer.java:100
net.sf.gridarta.model.index.MapsIndexer.State.IDLE
IDLE
Definition: MapsIndexer.java:145
net.sf.gridarta.model.index.AbstractIndex.removeIndexListener
void removeIndexListener(@NotNull final IndexListener< V > listener)
Definition: AbstractIndex.java:274
net.sf.gridarta.model.index.MapsIndexer.loadMapsIndex
void loadMapsIndex()
Definition: MapsIndexer.java:401
net.sf.gridarta.model.index.MapsIndexer.State.INDEX
INDEX
Definition: MapsIndexer.java:140
net.sf.gridarta.model.gameobject
Definition: AbstractGameObject.java:20
net
net.sf.gridarta.model.mapcontrol.MapControlListener
Definition: MapControlListener.java:33
net.sf.gridarta.utils.Xtea.encrypt
void encrypt(final byte @NotNull[] plaintext, final byte @NotNull[] ciphertext)
Definition: Xtea.java:65
net.sf.gridarta.model.settings.ProjectSettings.saveIndices
boolean saveIndices()
net.sf.gridarta.model.index.MapsIndexer
Definition: MapsIndexer.java:57
net.sf.gridarta.model.index.MapsIndexer.mapsIndex
final MapsIndex mapsIndex
Definition: MapsIndexer.java:75
net.sf.gridarta.model.index.MapsIndex
Definition: MapsIndex.java:28
net.sf.gridarta.model.settings.ProjectSettings.removeProjectSettingsListener
void removeProjectSettingsListener(@NotNull ProjectSettingsListener listener)
net.sf.gridarta.model.index.AbstractIndex.endUpdate
void endUpdate()
Definition: AbstractIndex.java:127
net.sf.gridarta.model.settings.ProjectSettings.addProjectSettingsListener
void addProjectSettingsListener(@NotNull ProjectSettingsListener listener)
net.sf.gridarta.model.index.AbstractIndex.indexingFinished
void indexingFinished()
Definition: AbstractIndex.java:327
net.sf.gridarta.model.index.MapsIndexer.updateMapsDirectory
void updateMapsDirectory()
Definition: MapsIndexer.java:342
net.sf.gridarta.utils.ConfigFileUtils.getHomeFile
static File getHomeFile(@NotNull final String filename)
Definition: ConfigFileUtils.java:53
net.sf.gridarta.model.index.MapsIndexer.State.INIT
INIT
Definition: MapsIndexer.java:129
net.sf.gridarta.model.settings.ProjectSettingsListener
Definition: ProjectSettingsListener.java:30
net.sf.gridarta.model.mapmanager.MapManager.removeMapManagerListener
void removeMapManagerListener(@NotNull MapManagerListener< G, A, R > listener)
net.sf.gridarta.model.index.MapsIndexer.indexPendingMaps
void indexPendingMaps()
Definition: MapsIndexer.java:450
net.sf.gridarta.model.mapmodel.MapModel.getMapArchObject
A getMapArchObject()
net.sf.gridarta.model.mapmanager.MapManager
Definition: MapManager.java:37
net.sf.gridarta.model.mapcontrol.MapControl
Definition: MapControl.java:35
net.sf.gridarta.model.index.AbstractIndex.clear
void clear()
Definition: AbstractIndex.java:316
net.sf.gridarta.model.index.MapsIndexer.setState
void setState(@NotNull final State state)
Definition: MapsIndexer.java:526
net.sf.gridarta.model.index.MapsIndexer.stop
void stop()
Definition: MapsIndexer.java:305
net.sf.gridarta.model.mapmanager.MapManagerListener
Definition: MapManagerListener.java:42
net.sf.gridarta.model.settings.ProjectSettings
Definition: ProjectSettings.java:29
net.sf.gridarta.model
net.sf.gridarta.model.index.MapsIndexer.waitForIdle
void waitForIdle()
Definition: MapsIndexer.java:330
net.sf.gridarta.model.index.MapsIndexer.reportStateChange
void reportStateChange()
Definition: MapsIndexer.java:541
net.sf.gridarta.model.index.MapsIndexer.saveMapsIndex
void saveMapsIndex()
Definition: MapsIndexer.java:368
net.sf.gridarta.model.index.MapsIndexer.start
void start()
Definition: MapsIndexer.java:290
net.sf.gridarta.model.index.MapsIndexer.syncMapsDirectory
final Object syncMapsDirectory
Definition: MapsIndexer.java:94
net.sf.gridarta.model.index.AbstractIndex.setName
void setName(@NotNull final V value, final long timestamp, @NotNull final String name)
Definition: AbstractIndex.java:217
net.sf.gridarta.model.settings.ProjectSettings.getMapsDirectory
File getMapsDirectory()
net.sf.gridarta.model.index.MapsIndexer.projectSettingsListener
final ProjectSettingsListener projectSettingsListener
Definition: MapsIndexer.java:203
net.sf.gridarta.model.index.MapsIndexer.mapManager
final MapManager< G, A, R > mapManager
Definition: MapsIndexer.java:81
net.sf.gridarta.model.maparchobject
Definition: AbstractMapArchObject.java:20
net.sf.gridarta.model.mapmodel.MapFile.getFile
File getFile()
Definition: MapFile.java:102
net.sf.gridarta.model.index.AbstractIndex.load
void load(@NotNull final ObjectInputStream objectInputStream)
Definition: AbstractIndex.java:292
net.sf.gridarta.model.index.MapsIndexer.syncState
final Object syncState
Definition: MapsIndexer.java:113
net.sf.gridarta.model.index.MapsIndexer.mapManagerListener
final MapManagerListener< G, A, R > mapManagerListener
Definition: MapsIndexer.java:154
net.sf.gridarta.model.index.MapsIndexer.thread
final Thread thread
Definition: MapsIndexer.java:271
net.sf.gridarta.model.index.MapsIndexer.getCacheFile
static File getCacheFile(@NotNull final File mapsDirectory)
Definition: MapsIndexer.java:485
net.sf.gridarta.model.mapcontrol.MapControl.getMapModel
MapModel< G, A, R > getMapModel()
net.sf.gridarta.utils.Xtea
Definition: Xtea.java:28
net.sf.gridarta.model.index.MapsIndexer.State
Definition: MapsIndexer.java:124
net.sf.gridarta.model.archetype.Archetype
Definition: Archetype.java:41
net.sf.gridarta.utils
Definition: ActionBuilderUtils.java:20
net.sf.gridarta.model.index.MapsIndexer.State.SCAN
SCAN
Definition: MapsIndexer.java:135
net.sf.gridarta.model.settings
Definition: AbstractDefaultProjectSettings.java:20
net.sf.gridarta.model.index.MapsIndexer.runnable
final Runnable runnable
Definition: MapsIndexer.java:252