10
10
import java .util .AbstractMap ;
11
11
import java .util .AbstractSet ;
12
12
import java .util .ArrayList ;
13
+ import java .util .Arrays ;
14
+ import java .util .Collection ;
15
+ import java .util .Collections ;
13
16
import java .util .Iterator ;
14
17
import java .util .LinkedHashMap ;
18
+ import java .util .LinkedHashSet ;
15
19
import java .util .List ;
20
+ import java .util .NoSuchElementException ;
16
21
import java .util .Set ;
22
+ import java .util .function .Function ;
23
+ import java .util .regex .Matcher ;
24
+ import java .util .regex .Pattern ;
17
25
import java .util .stream .Collectors ;
26
+ import java .util .stream .IntStream ;
27
+
28
+ import static org .codejive .properties .PropertiesParser .unescape ;
18
29
19
30
public class Properties extends AbstractMap <String , String > {
20
31
private final LinkedHashMap <String , String > values = new LinkedHashMap <>();
@@ -26,7 +37,7 @@ public Set<Entry<String, String>> entrySet() {
26
37
@ Override
27
38
public Iterator <Entry <String , String >> iterator () {
28
39
return new Iterator <Entry <String , String >>() {
29
- Iterator <Entry <String , String >> iter = values .entrySet ().iterator ();
40
+ final Iterator <Entry <String , String >> iter = values .entrySet ().iterator ();
30
41
31
42
@ Override
32
43
public boolean hasNext () {
@@ -53,18 +64,263 @@ public int size() {
53
64
};
54
65
}
55
66
67
+ public Set <String > rawKeySet () {
68
+ return tokens .stream ()
69
+ .filter (t -> t .type == PropertiesParser .Type .KEY )
70
+ .map (PropertiesParser .Token ::getRaw )
71
+ .collect (Collectors .toCollection (LinkedHashSet ::new ));
72
+ }
73
+
74
+ public Collection <String > rawValues () {
75
+ return IntStream .range (0 , tokens .size ())
76
+ .filter (idx -> tokens .get (idx ).type == PropertiesParser .Type .KEY )
77
+ .mapToObj (idx -> tokens .get (idx + 2 ).getRaw ())
78
+ .collect (Collectors .toList ());
79
+ }
80
+
81
+ @ Override
82
+ public String get (Object key ) {
83
+ return values .get (key );
84
+ }
85
+
86
+ public String getRaw (String rawKey ) {
87
+ int idx = indexOf (unescape (rawKey ));
88
+ if (idx >=0 ) {
89
+ return tokens .get (idx + 2 ).getRaw ();
90
+ } else {
91
+ return null ;
92
+ }
93
+ }
94
+
56
95
@ Override
57
96
public String put (String key , String value ) {
58
- // TODO handle adds and replaces
97
+ String rawKey = escape (key , true );
98
+ String rawValue = escape (value , false );
99
+ if (values .containsKey (key )) {
100
+ int idx = indexOf (key );
101
+ addNew (idx , rawKey , key , rawValue , value );
102
+ } else {
103
+ addNew (-1 , rawKey , key , rawValue , value );
104
+ }
105
+ return values .put (key , value );
106
+ }
107
+
108
+ /**
109
+ * Works like `put()` but uses raw values for keys and values.
110
+ * This means these keys and values will not be escaped before being serialized.
111
+ * @param rawKey key with which the specified value is to be associated
112
+ * @param rawValue value to be associated with the specified key
113
+ * @return the previous value associated with key, or null if there was no mapping for key.
114
+ */
115
+ public String putRaw (String rawKey , String rawValue ) {
116
+ String key = unescape (rawKey );
117
+ String value = unescape (rawValue );
118
+ if (values .containsKey (key )) {
119
+ int idx = indexOf (key );
120
+ addNew (idx , rawKey , key , rawValue , value );
121
+ } else {
122
+ addNew (-1 , rawKey , key , rawValue , value );
123
+ }
59
124
return values .put (key , value );
60
125
}
61
126
127
+ // Add new tokens to the end of the list of tokens
128
+ private void addNew (int index , String rawKey , String key , String rawValue , String value ) {
129
+ // Add a newline whitespace token if necessary
130
+ int idx = index >= 0 ? index : tokens .size ();
131
+ if (idx > 0 ) {
132
+ PropertiesParser .Token token = tokens .get (idx - 1 );
133
+ if (token .getType () != PropertiesParser .Type .WHITESPACE ) {
134
+ addToken (index , new PropertiesParser .Token (PropertiesParser .Type .WHITESPACE , "\n " ));
135
+ }
136
+ }
137
+ // Add tokens for key, separator and value
138
+ addToken (index , new PropertiesParser .Token (PropertiesParser .Type .KEY , rawKey , key ));
139
+ addToken (index , new PropertiesParser .Token (PropertiesParser .Type .SEPARATOR , "=" ));
140
+ addToken (index , new PropertiesParser .Token (PropertiesParser .Type .VALUE , rawValue , value ));
141
+ }
142
+
143
+ private void addToken (int index , PropertiesParser .Token token ) {
144
+ if (index >= 0 ) {
145
+ tokens .add (index , token );
146
+ } else {
147
+ tokens .add (token );
148
+ }
149
+ }
150
+
62
151
@ Override
63
152
public String remove (Object key ) {
64
153
// TODO handle remove
65
154
return values .remove (key );
66
155
}
67
156
157
+ /**
158
+ * Gather all the comments directly before the given key
159
+ * and return them as a list. The list will only contain
160
+ * those lines that immediately follow one another, once
161
+ * a non-comment line is encountered gathering will stop.
162
+ * @param key The key to look for
163
+ * @return A list of comment strings or an empty list if
164
+ * no comments lines were found or the key doesn't exist.
165
+ */
166
+ public List <String > getComment (String key ) {
167
+ return getComment (findCommentLines (key ));
168
+ }
169
+
170
+ private List <String > getComment (List <Integer > indices ) {
171
+ return Collections .unmodifiableList (indices .stream ().map (idx -> tokens .get (idx ).getText ()).collect (Collectors .toList ()));
172
+ }
173
+
174
+ public List <String > setComment (String key , String ... comments ) {
175
+ return setComment (key , Arrays .asList (comments ));
176
+ }
177
+
178
+ public List <String > setComment (String key , List <String > comments ) {
179
+ int idx = indexOf (key );
180
+ if (idx < 0 ) {
181
+ throw new NoSuchElementException ("Key not found: " + key );
182
+ }
183
+ List <Integer > indices = findCommentLines (idx );
184
+ List <String > oldcs = getComment (indices );
185
+ String prefix = oldcs .isEmpty () ? "# " : getPrefix (oldcs .get (0 ));
186
+ List <String > newcs = normalizeComments (comments , prefix );
187
+
188
+ // Replace existing comments with new ones
189
+ // (doing it like this respects existing whitespace)
190
+ int i ;
191
+ for (i = 0 ; i < indices .size () && i < newcs .size (); i ++) {
192
+ int n = indices .get (i );
193
+ tokens .set (n , new PropertiesParser .Token (PropertiesParser .Type .COMMENT , newcs .get (i )));
194
+ }
195
+
196
+ // Remove any excess lines (when there are fewer new lines than old ones)
197
+ if (i < indices .size ()) {
198
+ int del = indices .get (i );
199
+ int delcnt = idx - del ;
200
+ for (int j = 0 ; j < delcnt ; j ++) {
201
+ tokens .remove (del );
202
+ }
203
+ }
204
+
205
+ // Add any additional lines (when there are more new lines than old ones)
206
+ int ins = idx ;
207
+ for (int j = i ; j < newcs .size (); j ++) {
208
+ tokens .add (ins ++, new PropertiesParser .Token (PropertiesParser .Type .COMMENT , newcs .get (j )));
209
+ tokens .add (ins ++, new PropertiesParser .Token (PropertiesParser .Type .WHITESPACE , "\n " ));
210
+ }
211
+
212
+ return oldcs ;
213
+ }
214
+
215
+ /**
216
+ * Takes a list of comments and makes sure each of them starts with
217
+ * a valid comment character (either '#' or '!'). If only some lines
218
+ * have missing comment prefixes it will use the ones that were used
219
+ * on previous lines, if not the default will be the value passed as
220
+ * `preferredPrefix`.
221
+ * @param comments list of comment lines
222
+ * @param preferredPrefix the preferred prefix to use
223
+ * @return list of comment lines
224
+ */
225
+ private List <String > normalizeComments (List <String > comments , String preferredPrefix ) {
226
+ ArrayList <String > res = new ArrayList <>(comments .size ());
227
+ for (String c : comments ) {
228
+ if (getPrefix (c ).isEmpty ()) {
229
+ c = preferredPrefix + c ;
230
+ } else {
231
+ preferredPrefix = getPrefix (c );
232
+ }
233
+ res .add (c );
234
+ }
235
+ return res ;
236
+ }
237
+
238
+ private String getPrefix (String comment ) {
239
+ if (comment .startsWith ("# " )) {
240
+ return "# " ;
241
+ } else if (comment .startsWith ("#" )) {
242
+ return "#" ;
243
+ } else if (comment .startsWith ("! " )) {
244
+ return "! " ;
245
+ } else if (comment .startsWith ("!" )) {
246
+ return "!" ;
247
+ } else {
248
+ return "" ;
249
+ }
250
+ }
251
+
252
+ private List <Integer > findCommentLines (String key ) {
253
+ int idx = indexOf (key );
254
+ return findCommentLines (idx );
255
+ }
256
+
257
+ /**
258
+ * Returns a list of token indices pointing to all the comment lines
259
+ * in a comment block. A list of comments is considered a block when
260
+ * they are consecutive lines, without any empty lines in between,
261
+ * using the same comment symbol (so they are either all `!` comments
262
+ * or all `#` ones).
263
+ */
264
+ private List <Integer > findCommentLines (int idx ) {
265
+ List <Integer > result = new ArrayList <>();
266
+ // Skip any preceding whitespace
267
+ idx --;
268
+ while (idx >= 0 && tokens .get (idx ).getType () == PropertiesParser .Type .WHITESPACE ) {
269
+ idx --;
270
+ }
271
+ // Now find the first line of the comment block
272
+ int commentSym = -1 ;
273
+ PropertiesParser .Token token ;
274
+ while (idx >= 0 && (token = tokens .get (idx )).getType () == PropertiesParser .Type .COMMENT ) {
275
+ if (commentSym != -1 && commentSym != token .raw .charAt (0 )) {
276
+ // Comment doesn't start with the same comment symbol, so the block ends here
277
+ break ;
278
+ } else {
279
+ commentSym = token .raw .charAt (0 );
280
+ }
281
+ result .add (0 , idx );
282
+ // Skip any preceding whitespace making sure to stop at EOL
283
+ while (--idx >= 0 && !tokens .get (idx ).isEol ()) {}
284
+ idx --;
285
+ }
286
+ return Collections .unmodifiableList (result );
287
+ }
288
+
289
+ private int indexOf (String key ) {
290
+ return tokens .indexOf (new PropertiesParser .Token (PropertiesParser .Type .KEY , escape (key , true ), key ));
291
+ }
292
+
293
+ private String escape (String raw , boolean forKey ) {
294
+ raw = raw .replace ("\n " , "\\ n" );
295
+ raw = raw .replace ("\r " , "\\ r" );
296
+ raw = raw .replace ("\t " , "\\ t" );
297
+ raw = raw .replace ("\f " , "\\ f" );
298
+ if (forKey ) {
299
+ raw = raw .replace (" " , "\\ " );
300
+ } else {
301
+ if (raw .charAt (raw .length () - 1 ) == ' ' ) {
302
+ raw = raw .substring (0 , raw .length () - 1 ) + "\\ " ;
303
+ }
304
+ }
305
+ raw = replace (raw , "[^\\ x{0000}-\\ x{00FF}]" , m -> "\\ \\ u" + Integer .toString (m .group (0 ).charAt (0 ), 16 ));
306
+ return raw ;
307
+ }
308
+
309
+ private static String replace (String input , String regex , Function <Matcher , String > callback ) {
310
+ return replace (input , Pattern .compile (regex ), callback );
311
+ }
312
+
313
+ private static String replace (String input , Pattern regex , Function <Matcher , String > callback ) {
314
+ StringBuffer resultString = new StringBuffer ();
315
+ Matcher regexMatcher = regex .matcher (input );
316
+ while (regexMatcher .find ()) {
317
+ regexMatcher .appendReplacement (resultString , callback .apply (regexMatcher ));
318
+ }
319
+ regexMatcher .appendTail (resultString );
320
+
321
+ return resultString .toString ();
322
+ }
323
+
68
324
public void load (Path file ) throws IOException {
69
325
try (Reader br = Files .newBufferedReader (file )) {
70
326
load (br );
0 commit comments