1
+ package io .github .townyadvanced ;
2
+
3
+ import java .io .IOException ;
4
+ import java .nio .charset .StandardCharsets ;
5
+ import java .nio .file .Files ;
6
+ import java .nio .file .Path ;
7
+ import java .nio .file .StandardOpenOption ;
8
+ import java .util .ArrayList ;
9
+ import java .util .HashMap ;
10
+ import java .util .List ;
11
+
12
+ import org .bukkit .configuration .InvalidConfigurationException ;
13
+ import org .bukkit .configuration .file .YamlConfiguration ;
14
+ import org .bukkit .configuration .file .YamlConstructor ;
15
+ import org .bukkit .configuration .file .YamlRepresenter ;
16
+ import org .bukkit .plugin .Plugin ;
17
+ import org .yaml .snakeyaml .DumperOptions ;
18
+ import org .yaml .snakeyaml .Yaml ;
19
+ import org .yaml .snakeyaml .representer .Representer ;
20
+
21
+ /**
22
+ * @author dumptruckman
23
+ * @author Articdive
24
+ * @author LlmDl
25
+ */
26
+ public class CommentedConfiguration extends YamlConfiguration {
27
+ private final HashMap <String , String > comments = new HashMap <>();
28
+ private final Path path ;
29
+ private final Plugin plugin ;
30
+
31
+ private final DumperOptions yamlOptions = new DumperOptions ();
32
+ private final Representer yamlRepresenter = new YamlRepresenter ();
33
+ private final Yaml yaml = new Yaml (new YamlConstructor (), yamlRepresenter , yamlOptions );
34
+
35
+ public CommentedConfiguration (Plugin plugin , Path path ) {
36
+ super ();
37
+ this .plugin = plugin ;
38
+ this .path = path ;
39
+
40
+ try {
41
+ // Spigot 1.18.1 added SnakeYaml's ability to use Comments in yaml.
42
+ // They have it enabled by default, we need to stop it happening.
43
+ yamlOptions .setProcessComments (false );
44
+ } catch (NoSuchMethodError ignored ) {}
45
+ }
46
+
47
+ /**
48
+ * Load the yaml configuration file into memory.
49
+ * @return true if file is able to load.
50
+ */
51
+ public boolean load () {
52
+ return loadFile ();
53
+ }
54
+
55
+ private boolean loadFile () {
56
+ try {
57
+ this .load (path .toFile ());
58
+ return true ;
59
+ } catch (InvalidConfigurationException | IOException e ) {
60
+ plugin .getLogger ().warning (String .format ("Loading error: Failed to load file %s (does it pass a yaml parser?)." , path ));
61
+ plugin .getLogger ().warning ("https://jsonformatter.org/yaml-parser" );
62
+ plugin .getLogger ().warning (e .getMessage ());
63
+ return false ;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Save the yaml configuration file from memory to file.
69
+ */
70
+ public void save () {
71
+
72
+ // Save the config just like normal
73
+ boolean saved = saveFile ();
74
+
75
+ // if there's comments to add and it saved fine, we need to add comments
76
+ if (!comments .isEmpty () && saved ) {
77
+
78
+ // String list of each line in the config file
79
+ List <String > yamlContents ;
80
+ try {
81
+ yamlContents = Files .readAllLines (path , StandardCharsets .UTF_8 );
82
+ } catch (IOException e ) {
83
+ plugin .getLogger ().warning (String .format ("Failed to read file %s." , path ));
84
+ plugin .getLogger ().warning (e .getMessage ());
85
+ yamlContents = new ArrayList <>();
86
+ }
87
+
88
+ // This will hold the newly formatted line
89
+ StringBuilder newContents = readConfigToString (yamlContents );
90
+
91
+ // Write to file
92
+ try {
93
+ Files .write (path , newContents .toString ().getBytes (StandardCharsets .UTF_8 ), StandardOpenOption .WRITE );
94
+ } catch (IOException e ) {
95
+ plugin .getLogger ().warning (String .format ("Saving error: Failed to write to file %s." , path ));
96
+ plugin .getLogger ().warning (e .getMessage ());
97
+ }
98
+ }
99
+ }
100
+
101
+ private boolean saveFile () {
102
+ try {
103
+ this .save (path .toFile ());
104
+ return true ;
105
+ } catch (Exception e ) {
106
+ return false ;
107
+ }
108
+ }
109
+
110
+ private StringBuilder readConfigToString (List <String > yamlContents ) {
111
+ // This will hold the newly formatted line
112
+ StringBuilder newContents = new StringBuilder ();
113
+ // This holds the current path the lines are at in the config
114
+ String currentPath = "" ;
115
+ // This flags if the line is a node or unknown text.
116
+ boolean node ;
117
+ // The depth of the path. (number of words separated by periods - 1)
118
+ int depth = 0 ;
119
+
120
+ // Loop through the config lines
121
+ for (String line : yamlContents ) {
122
+ // If the line is a node (and not something like a list value)
123
+ if (line .contains (": " ) || (line .length () > 1 && line .charAt (line .length () - 1 ) == ':' )) {
124
+
125
+ // This is a node so flag it as one
126
+ node = true ;
127
+
128
+ // Grab the index of the end of the node name
129
+ int index ;
130
+ index = line .indexOf (": " );
131
+ if (index < 0 ) {
132
+ index = line .length () - 1 ;
133
+ }
134
+ // If currentPath is empty, store the node name as the currentPath.
135
+ if (currentPath .isEmpty ()) {
136
+ currentPath = line .substring (0 , index );
137
+ } else {
138
+ // Calculate the whitespace preceding the node name
139
+ int whiteSpace = 0 ;
140
+ for (int n = 0 ; n < line .length (); n ++) {
141
+ if (line .charAt (n ) == ' ' ) {
142
+ whiteSpace ++;
143
+ } else {
144
+ break ;
145
+ }
146
+ }
147
+ // Find out if the current depth (whitespace * 2) is greater/lesser/equal to the previous depth
148
+ if (whiteSpace / 2 > depth ) {
149
+ // Path is deeper. Add a . and the node name
150
+ currentPath += "." + line .substring (whiteSpace , index );
151
+ depth ++;
152
+ } else if (whiteSpace / 2 < depth ) {
153
+ // Path is shallower, calculate current depth from whitespace (whitespace / 2) and subtract that many levels from the currentPath
154
+ int newDepth = whiteSpace / 2 ;
155
+ for (int i = 0 ; i < depth - newDepth ; i ++) {
156
+ currentPath = currentPath .replace (currentPath .substring (currentPath .lastIndexOf ("." )), "" );
157
+ }
158
+ // Grab the index of the final period
159
+ int lastIndex = currentPath .lastIndexOf ("." );
160
+ if (lastIndex < 0 ) {
161
+ // if there isn't a final period, set the current path to nothing because we're at root
162
+ currentPath = "" ;
163
+ } else {
164
+ // If there is a final period, replace everything after it with nothing
165
+ currentPath = currentPath .replace (currentPath .substring (currentPath .lastIndexOf ("." )), "" );
166
+ currentPath += "." ;
167
+ }
168
+ // Add the new node name to the path
169
+ currentPath += line .substring (whiteSpace , index );
170
+ // Reset the depth
171
+ depth = newDepth ;
172
+ } else {
173
+ // Path is same depth, replace the last path node name to the current node name
174
+ int lastIndex = currentPath .lastIndexOf ("." );
175
+ if (lastIndex < 0 ) {
176
+ // if there isn't a final period, set the current path to nothing because we're at root
177
+ currentPath = "" ;
178
+ } else {
179
+ // If there is a final period, replace everything after it with nothing
180
+ currentPath = currentPath .replace (currentPath .substring (currentPath .lastIndexOf ("." )), "" );
181
+ currentPath += "." ;
182
+ }
183
+ currentPath += line .substring (whiteSpace , index );
184
+ }
185
+ }
186
+ } else {
187
+ node = false ;
188
+ }
189
+
190
+ if (node ) {
191
+ // If there's a comment for the current path, retrieve it and flag that path as already commented
192
+ String comment = comments .get (currentPath );
193
+
194
+ if (comment != null ) {
195
+ // Add the comment to the beginning of the current line
196
+ line = comment + System .getProperty ("line.separator" ) + line ;
197
+ }
198
+ }
199
+ // Add the line to the total config String
200
+ newContents .append (line ).append (System .getProperty ("line.separator" ));
201
+ }
202
+
203
+ /*
204
+ * Due to a Bukkit Bug with the Configuration we just need to remove any extra
205
+ * comments at the start of a file.
206
+ */
207
+ while (newContents .toString ().startsWith (" " + System .getProperty ("line.separator" )))
208
+ newContents = new StringBuilder (newContents .toString ().replaceFirst (" " + System .getProperty ("line.separator" ), "" ));
209
+
210
+ return newContents ;
211
+ }
212
+
213
+ /**
214
+ * Adds a comment just before the specified path. The comment can be
215
+ * multiple lines. An empty string will indicate a blank line.
216
+ *
217
+ * @param path Configuration path to add comment.
218
+ * @param commentLines Comments to add. One String per line.
219
+ */
220
+ public void addComment (String path , String ... commentLines ) {
221
+
222
+ StringBuilder commentstring = new StringBuilder ();
223
+ StringBuilder leadingSpaces = new StringBuilder ();
224
+ for (int n = 0 ; n < path .length (); n ++) {
225
+ if (path .charAt (n ) == '.' ) {
226
+ leadingSpaces .append (" " );
227
+ }
228
+ }
229
+ for (String line : commentLines ) {
230
+ if (!line .isEmpty ()) {
231
+ line = leadingSpaces + line ;
232
+ } else {
233
+ line = " " ;
234
+ }
235
+ if (commentstring .length () > 0 ) {
236
+ commentstring .append (System .getProperty ("line.separator" ));
237
+ }
238
+ commentstring .append (line );
239
+ }
240
+ comments .put (path , commentstring .toString ());
241
+ }
242
+
243
+ @ Override
244
+ public String saveToString () {
245
+ yamlOptions .setIndent (options ().indent ());
246
+ yamlOptions .setDefaultFlowStyle (DumperOptions .FlowStyle .BLOCK );
247
+ yamlOptions .setWidth (10000 );
248
+ yamlRepresenter .setDefaultFlowStyle (DumperOptions .FlowStyle .BLOCK );
249
+
250
+ String dump = yaml .dump (getValues (false ));
251
+ return dump .equals ("{}\n " ) ? "" : dump ;
252
+ }
253
+ }
0 commit comments