forked from Mwexim/skript-parser
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathVariableStorage.java
More file actions
374 lines (340 loc) · 14.4 KB
/
VariableStorage.java
File metadata and controls
374 lines (340 loc) · 14.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
package io.github.syst3ms.skriptparser.variables;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import io.github.syst3ms.skriptparser.config.Config.ConfigSection;
import io.github.syst3ms.skriptparser.file.FileElement;
import io.github.syst3ms.skriptparser.file.FileSection;
import io.github.syst3ms.skriptparser.lang.entries.OptionLoader;
import io.github.syst3ms.skriptparser.log.ErrorType;
import io.github.syst3ms.skriptparser.log.SkriptLogger;
import io.github.syst3ms.skriptparser.types.Type;
import io.github.syst3ms.skriptparser.types.TypeManager;
import io.github.syst3ms.skriptparser.types.changers.TypeSerializer;
import io.github.syst3ms.skriptparser.variables.SerializedVariable.Value;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
/**
* A variable storage is holds the means and methods of storing variables.
* <p>
* This is usually some sort of database, and could be as simply as a text file.
* <p>
* Must contain a constructor of just SkriptLogger.
*/
public abstract class VariableStorage implements Closeable {
/**
* The name of the database used in the configurations.
*/
protected final String name;
protected final Gson gson;
protected final LinkedBlockingQueue<SerializedVariable> changesQueue = new LinkedBlockingQueue<>(1000);
private final SkriptLogger logger;
private final long errorInterval = 10;
/**
* Whether this variable storage has been {@link #close() closed}.
*/
protected volatile boolean closed;
/**
* The pattern of the variable name this storage accepts.
* {@code null} for '{@code .*}' or '{@code .*}'.
*/
@Nullable
private Pattern variableNamePattern;
private File file;
private long lastError = Long.MIN_VALUE;
/**
* Creates a new variable storage with the given name.
* Gson will be handled.
*
* @param logger the logger to print logs to.
* @param name the name.
*/
protected VariableStorage(SkriptLogger logger, @NotNull String name) {
this(logger, new GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE)
.serializeNulls()
.create(),
name);
}
/**
* Creates a new variable storage with the given names.
*
* @param logger the logger to print logs to.
* @param gson the gson that controls the serialization of the json elements.
* @param name the name.
*/
protected VariableStorage(SkriptLogger logger, @NotNull Gson gson, @NotNull String name) {
this.logger = logger;
this.name = name;
this.gson = gson;
// TODO allow this runnable to be interupted.
CompletableFuture.runAsync(() -> {
while (!closed) {
try {
SerializedVariable variable = changesQueue.take();
SerializedVariable.Value value = variable.value();
if (value != null) {
save(variable.name(), value.type(), value.data());
} else {
save(variable.name(), null, null);
}
} catch (InterruptedException ignored) {
}
}
});
}
/**
* Gets the string value at the given key of the given section node.
*
* @param section the file section.
* @param key the key node.
* @return the value, or {@code null} if the value was invalid,
* or not found.
*/
@Nullable
protected String getConfigurationValue(FileSection section, String key) {
return getConfigurationValue(section, key, String.class);
}
/**
* Gets the value at the given key of the given section node,
* parsed with the given class type.
*
* @param section the file section.
* @param key the key node.
* @param classType the class type.
* @param <T> the class type generic.
* @return the parsed value, or {@code null} if the value was invalid,
* or not found.
*/
@Nullable
@SuppressWarnings("unchecked")
protected <T> T getConfigurationValue(FileSection section, String key, Class<T> classType) {
Optional<FileElement> value = section.get(key);
if (!value.isPresent()) {
logger.error("The configuration is missing the entry for '" + key + "' for the database '" + name + "'", ErrorType.SEMANTIC_ERROR);
return null;
}
String[] split = value.get().getLineContent().split(OptionLoader.OPTION_SPLIT_PATTERN);
if (split.length < 2) {
logger.error("The configuration entry '" + key + "' is not a option entry (key: value) for the database '" + name + "'", ErrorType.SEMANTIC_ERROR);
return null;
}
String content = split[1];
if (classType.equals(String.class))
return (T) content;
Optional<? extends Type<T>> type = TypeManager.getByClassExact(classType);
if (!type.isPresent()) {
logger.error("The class type '" + classType.getName() + "' is not registered. Cannot parse node '" + key + "' for database '" + name + "'", ErrorType.SEMANTIC_ERROR);
return null;
}
Optional<Function<String, ? extends T>> parser = type.get().getLiteralParser();
if (!parser.isPresent()) {
logger.error("Type " + type.get().withIndefiniteArticle(true) + " cannot be parsed as a literal.", ErrorType.SEMANTIC_ERROR);
return null;
}
T parsedValue = parser.get().apply(content);
if (parsedValue == null) {
logger.error("The entry for '" + key + "' in the database '" + name + "' must be " +
type.get().withIndefiniteArticle(true), ErrorType.SEMANTIC_ERROR);
return null;
}
return parsedValue;
}
/**
* Loads the configuration for this variable storage
* from the given section node.
*
* @param section the section node.
* @return whether the loading succeeded.
*/
public final boolean loadConfiguration(ConfigSection section) {
String pattern = section.getString("pattern");
if (pattern == null) {
this.logger.error("The database '" + this.name + "' is missing the pattern entry!", ErrorType.SEMANTIC_ERROR);
return false;
}
try {
// Set variable name pattern, see field javadoc for explanation of null value
variableNamePattern = pattern.equals(".*") || pattern.equals(".+") ? null : Pattern.compile(pattern);
} catch (PatternSyntaxException e) {
logger.error("Invalid pattern '" + pattern + "': " + e.getLocalizedMessage(), ErrorType.SEMANTIC_ERROR);
return false;
}
if (requiresFile()) {
String fileName = section.getString("file");
if (fileName == null)
return false;
this.file = getFile(fileName).getAbsoluteFile();
if (file.exists() && !file.isFile()) {
logger.error("The database file '" + file.getName() + "' does not exist or is a directory.", ErrorType.SEMANTIC_ERROR);
return false;
}
try {
file.createNewFile();
} catch (IOException e) {
logger.error("Cannot create the database file '" + file.getName() + "': " + e.getLocalizedMessage(), ErrorType.SEMANTIC_ERROR);
return false;
}
// Check for read & write permissions to the file
if (!file.canWrite()) {
logger.error("Cannot write to the database file '" + file.getName() + "'!", ErrorType.SEMANTIC_ERROR);
return false;
}
if (!file.canRead()) {
logger.error("Cannot read from the database file '" + file.getName() + "'!", ErrorType.SEMANTIC_ERROR);
return false;
}
}
// Get the subclass to load it's part of the configuration section.
return load(section);
}
/**
* Loads configurations and should start loading variables too.
*
* @return Whether the database could be loaded successfully,
* i.e. whether the configuration is correct and all variables could be loaded.
*/
protected abstract boolean load(ConfigSection section);
protected void loadVariable(String name, SerializedVariable variable) {
Value value = variable.value();
if (value == null) {
Variables.queueVariableChange(name, null);
return;
}
loadVariable(name, value.type(), value.data());
}
/**
* Loads a variable into Skript ram. Call this inside {@link #load(ConfigSection)}
*
* @param name the name of the variable.
* @param type the type of the variable.
* @param value the serialized value of the variable.
*/
protected void loadVariable(String name, @NotNull String type, @NotNull JsonElement value) {
if (value == null || type == null)
throw new IllegalArgumentException("value and/or typeName cannot be null");
Variables.queueVariableChange(name, deserialize(type, value));
}
/**
* Called after all storages have been loaded, and variables
* have been redistributed if settings have changed.
* This should commit the first transaction.
*/
protected abstract void allLoaded();
/**
* Checks if this storage requires a file for storing data, like SQLite.
*
* @return if this storage needs a file.
*/
protected abstract boolean requiresFile();
/**
* Gets the file needed for this variable storage from the given file name.
* <p>
* Will only be called if {@link #requiresFile()} is {@code true}.
*
* @param fileName the given file name.
* @return the {@link File} object.
*/
@Nullable
protected abstract File getFile(String fileName);
/**
* Checks if this variable storage accepts the given variable name.
*
* @param variableName the variable name.
* @return if this storage accepts the variable name.
* @see #variableNamePattern
*/
boolean accept(@Nullable String variableName) {
if (variableName == null)
return false;
return variableNamePattern == null || variableNamePattern.matcher(variableName).matches();
}
/**
* Creates a {@link SerializedVariable} from the given variable name and value.
* Can be overriden to add custom encryption in your implemented VariableStorage.
* Call super.
*
* @param name the variable name.
* @param value the variable value.
* @return the serialized variable.
*/
@SuppressWarnings("unchecked")
public <T> SerializedVariable serialize(String name, @Nullable T value) {
if (value == null)
return new SerializedVariable(name, null);
Type<T> type = (Type<T>) TypeManager.getByClass(value.getClass()).orElse(null);
if (type == null) return null;
//throw new UnsupportedOperationException("Class '" + value.getClass().getName() + "' cannot be serialized. No type registered.");
TypeSerializer<T> serializer = type.getSerializer().orElse(null);
if (serializer == null) return null;
//throw new UnsupportedOperationException("Class '" + value.getClass().getName() + "' cannot be serialized. No type serializer.");
JsonElement element = serializer.serialize(gson, value);
return new SerializedVariable(name, new Value(type.getBaseName(), element));
}
/**
* Used by {@link #loadVariable(String, String, JsonElement)}.
* You don't need to use this method, but if you need to read the Object, this method allows for deserialization.
*
* @param typeName The name of the type.
* @param value The value that represents a object.
* @return The Object after deserialization, not present if not possible to deserialize due to missing serializer on Type.
*/
protected Object deserialize(@NotNull String typeName, @NotNull JsonElement value) {
if (value == null || typeName == null)
throw new IllegalArgumentException("value and/or typeName cannot be null");
Type<?> type = TypeManager.getByExactName(typeName).orElse(null);
if (type == null)
throw new UnsupportedOperationException("Class '" + value.getClass().getName() + "' cannot be deserialized. No type registered.");
TypeSerializer<?> serializer = type.getSerializer().orElse(null);
if (serializer == null)
throw new UnsupportedOperationException("Class '" + value.getClass().getName() + "' cannot be deserialized. No type serializer.");
return serializer.deserialize(gson, value);
}
/**
* Saves the given serialized variable.
*
* @param variable the serialized variable.
*/
final void save(SerializedVariable variable) {
if (!changesQueue.offer(variable)) {
if (lastError < System.currentTimeMillis() - errorInterval * 1000) {
// Inform console about overload of variable changes
System.out.println("Skript cannot save any variables to the database '" + name + "'. " +
"The thread will hang to avoid losing variable.");
lastError = System.currentTimeMillis();
}
while (true) {
try {
changesQueue.put(variable);
break;
} catch (InterruptedException ignored) {
}
}
}
}
protected void clearChangesQueue() {
changesQueue.clear();
}
/**
* Saves a variable.
* <p>
* {@code type} and {@code value} are <i>both</i> {@code null}
* if this call is to delete the variable.
*
* @param name the name of the variable.
* @param type the type of the variable.
* @param value the serialized value of the variable.
* @return Whether the variable was saved.
*/
protected abstract boolean save(String name, @Nullable String type, @Nullable JsonElement value);
}