Gridarta Editor
Storage.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.preferences;
21 
22 import java.io.BufferedWriter;
23 import java.io.File;
24 import java.io.FileInputStream;
25 import java.io.FileNotFoundException;
26 import java.io.FileOutputStream;
27 import java.io.IOException;
28 import java.io.InputStreamReader;
29 import java.io.LineNumberReader;
30 import java.io.OutputStreamWriter;
31 import java.nio.charset.StandardCharsets;
32 import java.util.Map;
33 import java.util.Map.Entry;
34 import java.util.Set;
35 import java.util.TreeMap;
36 import java.util.TreeSet;
37 import java.util.prefs.BackingStoreException;
38 import java.util.regex.Pattern;
39 import net.sf.japi.swing.action.ActionBuilder;
40 import net.sf.japi.swing.action.ActionBuilderFactory;
41 import org.apache.log4j.Category;
42 import org.apache.log4j.Logger;
43 import org.jetbrains.annotations.NotNull;
44 import org.jetbrains.annotations.Nullable;
45 
50 public class Storage {
51 
55  @NotNull
56  private static final Pattern PATTERN_EQUAL = Pattern.compile("=");
57 
61  @NotNull
62  private static final Category LOG = Logger.getLogger(Storage.class);
63 
67  @NotNull
68  private final String defaultPath;
69 
73  @Nullable
74  private final File file;
75 
79  private boolean noSave = true;
80 
84  @NotNull
85  private static final Pattern PATTERN_IGNORE = Pattern.compile("[\\[].*");
86 
90  @NotNull
91  private final Map<String, Map<String, String>> values = new TreeMap<>();
92 
99  public Storage(@NotNull final String defaultPath, @Nullable final File file) {
100  if (LOG.isDebugEnabled()) {
101  LOG.debug("new");
102  }
103 
104  this.defaultPath = defaultPath;
105  this.file = file;
106 
107  loadValues();
108  noSave = false;
109  }
110 
115  public void newNode(@NotNull final String path) {
116  if (LOG.isDebugEnabled()) {
117  LOG.debug("newNode(" + path + ")");
118  }
119 
120  if (!values.containsKey(path)) {
121  values.put(path, new TreeMap<>());
122  }
123  }
124 
131  @NotNull
132  public String @NotNull [] childrenNames(@NotNull final String path) {
133  if (LOG.isDebugEnabled()) {
134  LOG.debug("childrenNames(" + path + ")");
135  }
136 
137  final String prefix = path + "/";
138  final Set<String> result = new TreeSet<>();
139  for (final String key : values.keySet()) {
140  if (key.startsWith(prefix)) {
141  result.add(key.substring(prefix.length()));
142  }
143  }
144  return result.toArray(new String[0]);
145  }
146 
154  @Nullable
155  public String getValue(@NotNull final String path, @NotNull final String key) {
156  if (LOG.isDebugEnabled()) {
157  LOG.debug("getValue(" + path + ", " + key + ")");
158  }
159 
160  final Map<String, String> map = values.get(path);
161  assert map != null; // AbstractPreferences.getSpi() ensures this
162  return map.get(key);
163  }
164 
171  @NotNull
172  public String @NotNull [] getKeys(@NotNull final String path) {
173  if (LOG.isDebugEnabled()) {
174  LOG.debug("getKeys(" + path + ")");
175  }
176 
177  final Map<String, String> map = values.get(path);
178  assert map != null; // AbstractPreferences.keysSpi() ensures this
179  final Set<String> keys = map.keySet();
180  return keys.toArray(new String[0]);
181  }
182 
189  public void putValue(@NotNull final String path, @NotNull final String key, @NotNull final String value) {
190  if (LOG.isDebugEnabled()) {
191  LOG.debug("putValue(" + path + ", " + key + ", " + value + ")");
192  }
193 
194  final Map<String, String> map = values.get(path);
195  assert map != null; // AbstractPreferences.putSpi() ensures this
196  final String oldValue = map.put(key, value);
197  if (oldValue == null || !oldValue.equals(value)) {
198  setChanged();
199  }
200  }
201 
206  public void removeNode(@NotNull final String path) {
207  if (LOG.isDebugEnabled()) {
208  LOG.debug("removeNode(" + path + ")");
209  }
210 
211  if (values.remove(path) != null) {
212  setChanged();
213  }
214  }
215 
221  public void removeValue(@NotNull final String path, @NotNull final String key) {
222  if (LOG.isDebugEnabled()) {
223  LOG.debug("removeValue(" + path + ", " + key + ")");
224  }
225 
226  final Map<String, String> map = values.get(path);
227  assert map != null; // AbstractPreferences.removeSpi() ensures this
228  if (map.remove(key) != null) {
229  setChanged();
230  }
231  }
232 
239  public void sync(final boolean sync) throws BackingStoreException {
240  if (LOG.isDebugEnabled()) {
241  LOG.debug("sync(" + sync + ")");
242  }
243 
244  try {
245  saveValues();
246  } catch (final IOException ex) {
247  throw new BackingStoreException(ex);
248  }
249  }
250 
255  private void setChanged() {
256  if (noSave) {
257  return;
258  }
259 
260  try {
261  saveValues();
262  } catch (final IOException ex) {
263  LOG.warn(file + ": " + ex.getMessage());
264  }
265  }
266 
270  private void loadValues() {
271  if (file == null) {
272  return;
273  }
274 
275  if (LOG.isDebugEnabled()) {
276  LOG.debug("loadValues: " + file);
277  }
278 
279  values.clear();
280 
281  try {
282  try (FileInputStream fis = new FileInputStream(file)) {
283  try (InputStreamReader isr = new InputStreamReader(fis, StandardCharsets.UTF_8)) {
284  try (LineNumberReader lnr = new LineNumberReader(isr)) {
285  loadValues(lnr);
286  }
287  }
288  }
289  } catch (final FileNotFoundException ignored) {
290  // ignore
291  } catch (final IOException ex) {
292  LOG.warn(file + ": " + ex.getMessage());
293  }
294  }
295 
301  private void loadValues(@NotNull final LineNumberReader lnr) throws IOException {
302  String path = defaultPath;
303  while (true) {
304  final String line2 = lnr.readLine();
305  if (line2 == null) {
306  break;
307  }
308  final String line = Codec.decode(line2.trim());
309  if (line.startsWith("#") || line.isEmpty()) {
310  continue;
311  }
312 
313  if (line.startsWith("[") && line.endsWith("]")) {
314  path = line.substring(1, line.length() - 1);
315  continue;
316  }
317 
318  final String[] tmp = PATTERN_EQUAL.split(line, 2);
319  if (tmp.length != 2) {
320  LOG.warn(file + ":" + lnr.getLineNumber() + ": syntax error");
321  continue;
322  }
323  final String key = tmp[0];
324  final String value = tmp[1];
325 
326  newNode(path);
327  putValue(path, key, value);
328  }
329  }
330 
335  private void saveValues() throws IOException {
336  if (file == null) {
337  return;
338  }
339 
340  if (LOG.isDebugEnabled()) {
341  LOG.debug("saveValues: " + file);
342  }
343 
344  final File tmpFile = new File(file.getPath() + ".tmp");
345  try (FileOutputStream fos = new FileOutputStream(tmpFile)) {
346  try (OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8)) {
347  try (BufferedWriter bw = new BufferedWriter(osw)) {
348  final Map<String, String> defaultNode = values.get(defaultPath);
349  if (defaultNode != null) {
350  saveNode(bw, null, defaultNode);
351  }
352 
353  for (final Entry<String, Map<String, String>> e : values.entrySet()) {
354  if (!e.getKey().equals(defaultPath)) {
355  saveNode(bw, e.getKey(), e.getValue());
356  }
357  }
358  }
359  }
360  }
361 
362  //noinspection ResultOfMethodCallIgnored
363  file.delete(); // Windows cannot overwrite destination file on rename
364  if (!tmpFile.renameTo(file)) {
365  throw new IOException("cannot rename " + tmpFile + " to " + file);
366  }
367  }
368 
376  private static void saveNode(@NotNull final BufferedWriter writer, @Nullable final String path, @NotNull final Map<String, String> node) throws IOException {
377  if (node.isEmpty()) {
378  return;
379  }
380 
381  final ActionBuilder actionBuilder = ActionBuilderFactory.getInstance().getActionBuilder("net.sf.gridarta");
382 
383  if (path != null) {
384  writer.newLine();
385  writer.write("[");
386  writer.write(Codec.encode(path));
387  writer.write("]");
388  writer.newLine();
389  }
390 
391  for (final Entry<String, String> entry : node.entrySet()) {
392  writer.newLine();
393 
394  final String comment = actionBuilder.getString("prefs." + PATTERN_IGNORE.matcher(entry.getKey()).replaceAll(""));
395  if (comment != null) {
396  writer.write("# ");
397  writer.write(comment);
398  writer.newLine();
399  }
400  writer.write(Codec.encode(entry.getKey()));
401  writer.write("=");
402  writer.write(Codec.encode(entry.getValue()));
403  writer.newLine();
404  }
405  }
406 
407 }
net.sf.gridarta.preferences.Codec.encode
static String encode(@NotNull final String str)
Encode a string to make it fit into one line.
Definition: Codec.java:73
net.sf.gridarta.preferences.Storage.PATTERN_EQUAL
static final Pattern PATTERN_EQUAL
The pattern that matches a single equal sign ("=").
Definition: Storage.java:56
net.sf.gridarta.preferences.Storage.removeValue
void removeValue(@NotNull final String path, @NotNull final String key)
Removes the association (if any) for the specified key at a node.
Definition: Storage.java:221
net.sf
net.sf.gridarta.preferences.Storage.newNode
void newNode(@NotNull final String path)
Makes sure a node exists.
Definition: Storage.java:115
net.sf.gridarta.preferences.Codec
Utility class to encode arbitrary Strings to fit in a single text line.
Definition: Codec.java:32
net.sf.gridarta.preferences.Storage.getValue
String getValue(@NotNull final String path, @NotNull final String key)
Returns the value associated with the specified key at a node, or.
Definition: Storage.java:155
net.sf.gridarta.preferences.Storage.defaultPath
final String defaultPath
The default key name for loading/saving values.
Definition: Storage.java:68
net.sf.gridarta.preferences.Storage.putValue
void putValue(@NotNull final String path, @NotNull final String key, @NotNull final String value)
Puts the given key-value association into a node.
Definition: Storage.java:189
net
net.sf.gridarta.preferences.Storage.Storage
Storage(@NotNull final String defaultPath, @Nullable final File file)
Creates a new instance.
Definition: Storage.java:99
net.sf.gridarta.preferences.Storage.PATTERN_IGNORE
static final Pattern PATTERN_IGNORE
Pattern to ignore in path names.
Definition: Storage.java:85
net.sf.gridarta.preferences.Storage.noSave
boolean noSave
If set, do not save changes into file.
Definition: Storage.java:79
net.sf.gridarta.preferences.Storage.LOG
static final Category LOG
The Logger for printing log messages.
Definition: Storage.java:62
net.sf.gridarta.preferences.Storage.childrenNames
String[] childrenNames(@NotNull final String path)
Returns the names of the children of a node.
Definition: Storage.java:132
net.sf.gridarta.preferences.Storage.sync
void sync(final boolean sync)
Saves changes to the underlying file.
Definition: Storage.java:239
net.sf.gridarta.preferences.Storage.saveNode
static void saveNode(@NotNull final BufferedWriter writer, @Nullable final String path, @NotNull final Map< String, String > node)
Saves one node.
Definition: Storage.java:376
net.sf.gridarta.preferences.Storage.removeNode
void removeNode(@NotNull final String path)
Removes a preference node including all preferences that it contains.
Definition: Storage.java:206
net.sf.gridarta.preferences.Storage.loadValues
void loadValues(@NotNull final LineNumberReader lnr)
Loads the values from a LineNumberReader.
Definition: Storage.java:301
net.sf.gridarta.preferences.Storage.setChanged
void setChanged()
This function is called whenever the contents of values has changed.
Definition: Storage.java:255
net.sf.gridarta.preferences.Storage.values
final Map< String, Map< String, String > > values
The stored values.
Definition: Storage.java:91
net.sf.gridarta.preferences.Storage.file
final File file
The file for loading/saving values.
Definition: Storage.java:74
net.sf.gridarta.preferences.Storage
Maintains a set of preference values.
Definition: Storage.java:50
net.sf.gridarta.preferences.Codec.decode
static String decode(@NotNull final String str)
Decode a string which was encoded by encode(String).
Definition: Codec.java:105
net.sf.gridarta.preferences.Storage.saveValues
void saveValues()
Saves the values to the backing file.
Definition: Storage.java:335
net.sf.gridarta.preferences.Storage.getKeys
String[] getKeys(@NotNull final String path)
Returns all of the keys that have an associated value in a node.
Definition: Storage.java:172
net.sf.gridarta.preferences.Storage.loadValues
void loadValues()
Loads the values from the backing file.
Definition: Storage.java:270