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-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.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) {
208  newMapsDirectory = projectSettings.getMapsDirectory();
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 = new Runnable() {
253 
254  @Override
255  public void run() {
256  while (!Thread.currentThread().isInterrupted()) {
259  if (state == State.IDLE) {
260  try {
261  mapsIndexSemaphore.acquire();
262  } catch (final InterruptedException ignored) {
263  Thread.currentThread().interrupt();
264  break;
265  }
266  }
267  }
268  }
269 
270  };
271 
275  @NotNull
276  private final Thread thread = new Thread(runnable, "indexer");
277 
285  public MapsIndexer(@NotNull final MapsIndex mapsIndex, @NotNull final MapManager<G, A, R> mapManager, @NotNull final ProjectSettings projectSettings) {
286  this.mapsIndex = mapsIndex;
287  this.mapManager = mapManager;
288  this.projectSettings = projectSettings;
290  }
291 
295  public void start() {
296  projectSettings.addProjectSettingsListener(projectSettingsListener);
297  mapManager.addMapManagerListener(mapManagerListener);
298  synchronized (syncMapsDirectory) {
299  newMapsDirectory = projectSettings.getMapsDirectory();
300  }
301  mapsIndex.addIndexListener(indexListener);
302  thread.start();
303  }
304 
310  public void stop() throws InterruptedException {
311  try {
312  thread.interrupt();
313  thread.join();
314  } finally {
315  projectSettings.removeProjectSettingsListener(projectSettingsListener);
316  mapManager.removeMapManagerListener(mapManagerListener);
317  for (final MapControl<G, A, R> mapControl : mapManager.getOpenedMaps()) {
318  if (!mapControl.isPickmap()) {
319  mapControl.removeMapControlListener(mapControlListener);
320  }
321  }
322  mapsIndex.removeIndexListener(indexListener);
323  }
324 
325  synchronized (syncMapsDirectory) {
326  saveMapsIndex();
327  }
328  }
329 
335  public void waitForIdle() throws InterruptedException {
336  synchronized (syncState) {
337  while (state != State.IDLE || mapsIndex.hasPending()) {
338  syncState.wait(1000L);
339  }
340  }
341  }
342 
347  private void updateMapsDirectory() {
348  final File tmpMapsDirectory;
349  synchronized (syncMapsDirectory) {
350  if (newMapsDirectory == null || (mapsDirectory != null && mapsDirectory.equals(newMapsDirectory))) {
351  return;
352  }
353 
354  setState(State.SCAN);
355 
356  saveMapsIndex();
357 
358  tmpMapsDirectory = newMapsDirectory;
359  assert tmpMapsDirectory != null;
360  mapsDirectory = tmpMapsDirectory;
361  newMapsDirectory = null;
362 
363  loadMapsIndex();
364  }
365  mapsIndex.beginUpdate();
366  scanMapsDirectoryInt(new MapFile(tmpMapsDirectory), "");
367  mapsIndex.endUpdate();
368  }
369 
373  private void saveMapsIndex() {
374  assert Thread.holdsLock(syncMapsDirectory);
375  if (mapsDirectory == null) {
376  return;
377  }
378 
379  if (!projectSettings.saveIndices()) {
380  return;
381  }
382 
383  if (!mapsIndex.isModified()) {
384  return;
385  }
386 
387  assert mapsDirectory != null;
388  final File cacheFile = getCacheFile(mapsDirectory);
389  try {
390  try (OutputStream outputStream = new FileOutputStream(cacheFile)) {
391  try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream)) {
392  mapsIndex.save(objectOutputStream);
393  }
394  }
395  if (LOG.isInfoEnabled()) {
396  LOG.info(cacheFile + ": saved " + mapsIndex.size() + " entries");
397  }
398  } catch (final IOException ex) {
399  LOG.warn(cacheFile + ": cannot save cache file: " + ex.getMessage());
400  }
401  }
402 
406  private void loadMapsIndex() {
407  assert Thread.holdsLock(syncMapsDirectory);
408  assert mapsDirectory != null;
409  final File cacheFile = getCacheFile(mapsDirectory);
410  try {
411  try (InputStream inputStream = new FileInputStream(cacheFile)) {
412  try (ObjectInputStream objectInputStream = new ObjectInputStream(inputStream)) {
413  mapsIndex.load(objectInputStream);
414  }
415  }
416  if (LOG.isInfoEnabled()) {
417  LOG.info(cacheFile + ": loaded " + mapsIndex.size() + " entries");
418  }
419  } catch (final FileNotFoundException ex) {
420  if (LOG.isDebugEnabled()) {
421  LOG.debug(cacheFile + ": cache file does not exist: " + ex.getMessage());
422  }
423  mapsIndex.clear();
424  } catch (final IOException ex) {
425  LOG.warn(cacheFile + ": cannot load cache file: " + ex.getMessage());
426  mapsIndex.clear();
427  }
428  }
429 
435  private void scanMapsDirectoryInt(@NotNull final MapFile dir, @NotNull final String mapPath) {
436  final File[] files = dir.getFile().listFiles();
437  if (files == null) {
438  return;
439  }
440 
441  for (final File file : files) {
442  final MapFile mapFile = new MapFile(dir, file.getName());
443  if (file.isFile() && !file.getName().endsWith("~")) {
444  mapsIndex.add(mapFile, file.lastModified());
445  } else if (file.isDirectory() && !file.getName().equalsIgnoreCase(".svn") && !file.getName().equalsIgnoreCase(".dedit")) {
446  scanMapsDirectoryInt(mapFile, mapPath + "/" + file.getName());
447  }
448  }
449  }
450 
455  private void indexPendingMaps() {
456  final MapFile mapFile = mapsIndex.removePending();
457  if (mapFile == null) {
458  setState(State.IDLE);
459  return;
460  }
461 
462  setState(State.INDEX);
463  final long timestamp = mapFile.getFile().lastModified();
464  final MapControl<G, A, R> mapControl;
465  try {
466  mapControl = mapManager.openMapFile(mapFile, false);
467  } catch (final IOException ex) {
468  if (LOG.isInfoEnabled()) {
469  LOG.info(mapFile + ": load failed:" + ex.getMessage());
470  }
471  return;
472  }
473  try {
474  final String mapName = mapControl.getMapModel().getMapArchObject().getMapName();
475  if (LOG.isDebugEnabled()) {
476  LOG.debug(mapFile + ": indexing as '" + mapName + "'");
477  }
478  mapsIndex.setName(mapFile, timestamp, mapName);
479  } finally {
480  mapManager.release(mapControl);
481  }
482  }
483 
489  @NotNull
490  private static File getCacheFile(@NotNull final File mapsDirectory) {
491  final byte[] key = new byte[16];
492  final Xtea xtea = new Xtea(key);
493  final byte[] data;
494  data = mapsDirectory.getAbsoluteFile().toString().getBytes(StandardCharsets.UTF_8);
495  final byte[] hash = new byte[8];
496  final byte[] in = new byte[8];
497  final byte[] out = new byte[8];
498  int i;
499  for (i = 0; i + 8 < data.length; i++) {
500  System.arraycopy(data, i, in, 0, 8);
501  xtea.encrypt(in, out);
502  for (int j = 0; j < 8; j++) {
503  hash[j] ^= out[j];
504  }
505  }
506  final int len = data.length % 8;
507  System.arraycopy(data, i, in, 0, len);
508  in[len] = (byte) 1;
509  Arrays.fill(in, len + 1, 8, (byte) 0);
510  xtea.encrypt(in, out);
511  for (int j = 0; j < 8; j++) {
512  hash[j] ^= out[j];
513  }
514 
515  final StringBuilder sb = new StringBuilder("index/maps/");
516  for (int j = 0; j < 8; j++) {
517  sb.append(String.format("%02x", hash[j] & 0xff));
518  }
519  sb.append(mapsDirectory.getName());
520  final File file = ConfigFileUtils.getHomeFile(sb.toString());
521  final File dir = file.getParentFile();
522  if (dir != null && !dir.exists() && !dir.mkdirs()) {
523  LOG.warn("cannot create directory: " + dir);
524  }
525  return file;
526  }
527 
532  private void setState(@NotNull final State state) {
533  synchronized (syncState) {
534  if (this.state == state) {
535  return;
536  }
537 
538  this.state = state;
540  syncState.notifyAll();
541  }
542  }
543 
547  private void reportStateChange() {
548  if (LOG.isDebugEnabled()) {
549  LOG.debug("state=" + state);
550  }
551 
552  if (state == State.IDLE) {
553  mapsIndex.indexingFinished();
554  }
555  }
556 
557 }
Indexes maps by map name.
Definition: MapsIndex.java:29
void removeMapManagerListener(@NotNull MapManagerListener< G, A, R > listener)
Removes a MapManagerListener to be notified.
void encrypt(@NotNull final byte[] plaintext, @NotNull final byte[] ciphertext)
Encrypts a data block.
Definition: Xtea.java:66
void removeIndexListener(@NotNull final IndexListener< V > listener)
Removes an IndexListener to be notified of changes.
final ProjectSettingsListener projectSettingsListener
The ProjectSettingsListener attached to projectSettings for tracking changed maps directories...
A MapManager manages all opened maps.
Definition: MapManager.java:37
void release(@NotNull MapControl< G, A, R > mapControl)
Releases a MapControl instance.
final Object syncMapsDirectory
The object for synchronizing access to mapsDirectory and newMapsDirectory.
void setPending(@NotNull final V value)
Marks a value as pending.
boolean hasPending()
Returns whether at least one pending value exists.
final Semaphore mapsIndexSemaphore
A pending map may exist in mapsIndex if a permit is available.
Settings that apply to a project.
Maintains a MapsIndex for the maps directory.
Interface for listeners interested in Index related events.
void updateMapsDirectory()
Checks whether newMapsDirectory has been set and updates mapsDirectory accordingly.
static File getCacheFile(@NotNull final File mapsDirectory)
Returns the cache file for a given maps directory.
Implements the XTEA algorithm.
Definition: Xtea.java:28
File newMapsDirectory
The maps directory to index.
static File getHomeFile(@NotNull final String filename)
Return the filename to use when dealing with this application&#39;s and current users&#39; home directory...
INIT
Indexer has been created but is not yet running.
void load(@NotNull final ObjectInputStream objectInputStream)
unchecked
INDEX
Indexer is indexing maps differing from the current index.
V removePending()
Returns one pending value.
final Runnable runnable
The Runnable scanning the maps directory and updating the index.
final MapControlListener< G, A, R > mapControlListener
The MapControlListener attached to all opened maps.
void setName(@NotNull final V value, final long timestamp, @NotNull final String name)
Associates a value with a name.
MapModel< G, A, R > getMapModel()
Returns the map model.
final Thread thread
The Thread executing runnable.
void addIndexListener(@NotNull final IndexListener< V > listener)
Adds an IndexListener to be notified of changes.
Base package of all Gridarta classes.
Reflects a game object (object on a map).
Definition: GameObject.java:36
static final Category LOG
The Logger for printing log messages.
Loader for loading resources the user&#39;s home directory.
Interface for listeners listening to MapManager changes.
final MapsIndex mapsIndex
The MapsIndex being updated.
Interface for listeners listening on changes in MapControl instances.
void indexingFinished()
Should be called after indexing has finished.
GameObjects are the objects based on Archetypes found on maps.
int size()
Returns the number of values in this cache.
void waitForIdle()
Blocks the calling thread until all pending maps have been indexed.
void save(@NotNull final ObjectOutputStream objectOutputStream)
Saves the state to an ObjectOutputStream.
MapsIndexer(@NotNull final MapsIndex mapsIndex, @NotNull final MapManager< G, A, R > mapManager, @NotNull final ProjectSettings projectSettings)
Creates a new instance.
File mapsDirectory
The currently indexed maps directory.
void addProjectSettingsListener(@NotNull ProjectSettingsListener listener)
Adds a ProjectSettingsListener to be notified of changes.
final ProjectSettings projectSettings
The ProjectSettings instance.
void scanMapsDirectoryInt(@NotNull final MapFile dir, @NotNull final String mapPath)
Scans a directory for files.
void removeProjectSettingsListener(@NotNull ProjectSettingsListener listener)
Removes a ProjectSettingsListener to be notified of changes.
boolean saveIndices()
Returns whether indices should be saved to disk.
A getMapArchObject()
Returns the Map Arch Object with the meta information about the map.
void addMapManagerListener(@NotNull MapManagerListener< G, A, R > listener)
Adds a MapManagerListener to be notified.
Currently nothing more than a marker interface for unification.
Definition: MapControl.java:35
File getFile()
Returns a File for this map file.
Definition: MapFile.java:102
void saveMapsIndex()
Saves mapsIndex to its cache file if modified.
void add(@NotNull final V value, final long timestamp)
Adds a value to the cache.
File getMapsDirectory()
Returns the default maps directory.
Interface for listeners interested in ProjectSettings events.
void setState(@NotNull final State state)
Sets a new value to state.
final IndexListener< MapFile > indexListener
The IndexListener attached to mapsIndex to detect newly added pending maps.
final MapManager< G, A, R > mapManager
The MapManager for loading map files.
void clear()
Clears all values from the index.
boolean isModified()
Returns whether the state was modified since last save.
void loadMapsIndex()
Loads mapsIndex from its cache file.
MapControl< G, A, R > openMapFile(@NotNull MapFile mapFile, boolean interactive)
Loads a map file.
void reportStateChange()
Logs a changed value of state.
final Object syncState
The object for synchronizing access to state.
void indexPendingMaps()
Indexes one pending map from mapsIndex.
SCAN
Indexer is scanning the maps directory searching for files differing to the current index...
final MapManagerListener< G, A, R > mapManagerListener
The MapManagerListener attached to mapManager to detect current map changes.
List< MapControl< G, A, R > > getOpenedMaps()
Returns all opened maps.
The location of a map file with a map directory.
Definition: MapFile.java:31