1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package org.apache.commons.configuration2.plist;
19
20 import java.io.PrintWriter;
21 import java.io.Reader;
22 import java.io.Writer;
23 import java.math.BigDecimal;
24 import java.math.BigInteger;
25 import java.nio.charset.Charset;
26 import java.nio.charset.StandardCharsets;
27 import java.text.DateFormat;
28 import java.text.ParseException;
29 import java.text.SimpleDateFormat;
30 import java.util.ArrayList;
31 import java.util.Arrays;
32 import java.util.Base64;
33 import java.util.Calendar;
34 import java.util.Collection;
35 import java.util.Date;
36 import java.util.HashMap;
37 import java.util.Iterator;
38 import java.util.LinkedList;
39 import java.util.List;
40 import java.util.Map;
41 import java.util.TimeZone;
42
43 import javax.xml.parsers.SAXParserFactory;
44
45 import org.apache.commons.configuration2.BaseHierarchicalConfiguration;
46 import org.apache.commons.configuration2.FileBasedConfiguration;
47 import org.apache.commons.configuration2.HierarchicalConfiguration;
48 import org.apache.commons.configuration2.ImmutableConfiguration;
49 import org.apache.commons.configuration2.MapConfiguration;
50 import org.apache.commons.configuration2.ex.ConfigurationException;
51 import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
52 import org.apache.commons.configuration2.io.FileLocator;
53 import org.apache.commons.configuration2.io.FileLocatorAware;
54 import org.apache.commons.configuration2.tree.ImmutableNode;
55 import org.apache.commons.configuration2.tree.InMemoryNodeModel;
56 import org.apache.commons.lang3.StringUtils;
57 import org.apache.commons.text.StringEscapeUtils;
58 import org.xml.sax.Attributes;
59 import org.xml.sax.EntityResolver;
60 import org.xml.sax.InputSource;
61 import org.xml.sax.SAXException;
62 import org.xml.sax.XMLReader;
63 import org.xml.sax.helpers.DefaultHandler;
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 public class XMLPropertyListConfiguration extends BaseHierarchicalConfiguration implements FileBasedConfiguration, FileLocatorAware {
132
133
134
135
136
137 private static final class ArrayNodeBuilder extends PListNodeBuilder {
138
139
140 private final List<Object> list = new ArrayList<>();
141
142
143
144
145
146
147 @Override
148 public void addValue(final Object value) {
149 list.add(value);
150 }
151
152
153
154
155
156
157 @Override
158 protected Object getNodeValue() {
159 return list;
160 }
161 }
162
163
164
165
166
167 private static class PListNodeBuilder {
168
169
170
171
172
173 private static final DateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
174 static {
175 FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
176 }
177
178
179
180
181
182 private static final DateFormat GNUSTEP_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z");
183
184
185 private final Collection<PListNodeBuilder> childBuilders = new LinkedList<>();
186
187
188 private String name;
189
190
191 private Object value;
192
193
194
195
196
197
198 public void addChild(final PListNodeBuilder child) {
199 childBuilders.add(child);
200 }
201
202
203
204
205
206
207 public void addDataValue(final String value) {
208 addValue(Base64.getMimeDecoder().decode(value.getBytes(DATA_ENCODING)));
209 }
210
211
212
213
214
215
216
217 public void addDateValue(final String value) {
218 try {
219 if (value.indexOf(' ') != -1) {
220
221 synchronized (GNUSTEP_FORMAT) {
222 addValue(GNUSTEP_FORMAT.parse(value));
223 }
224 } else {
225
226 synchronized (FORMAT) {
227 addValue(FORMAT.parse(value));
228 }
229 }
230 } catch (final ParseException e) {
231 throw new IllegalArgumentException(String.format("'%s' cannot be parsed to a date.", value), e);
232 }
233 }
234
235
236
237
238 public void addFalseValue() {
239 addValue(Boolean.FALSE);
240 }
241
242
243
244
245
246
247 public void addIntegerValue(final String value) {
248 addValue(new BigInteger(value));
249 }
250
251
252
253
254
255
256 public void addList(final ArrayNodeBuilder node) {
257 addValue(node.getNodeValue());
258 }
259
260
261
262
263
264
265 public void addRealValue(final String value) {
266 addValue(new BigDecimal(value));
267 }
268
269
270
271
272 public void addTrueValue() {
273 addValue(Boolean.TRUE);
274 }
275
276
277
278
279
280
281
282
283 public void addValue(final Object v) {
284 if (value == null) {
285 value = v;
286 } else if (value instanceof Collection) {
287
288 @SuppressWarnings("unchecked")
289 final Collection<Object> collection = (Collection<Object>) value;
290 collection.add(v);
291 } else {
292 final List<Object> list = new ArrayList<>();
293 list.add(value);
294 list.add(v);
295 value = list;
296 }
297 }
298
299
300
301
302
303
304 public ImmutableNode createNode() {
305 final ImmutableNode.Builder nodeBuilder = new ImmutableNode.Builder(childBuilders.size());
306 childBuilders.forEach(child -> nodeBuilder.addChild(child.createNode()));
307 return nodeBuilder.name(name).value(getNodeValue()).create();
308 }
309
310
311
312
313
314
315
316 protected Object getNodeValue() {
317 return value;
318 }
319
320
321
322
323
324
325 public void setName(final String nodeName) {
326 name = nodeName;
327 }
328 }
329
330
331
332
333 private final class XMLPropertyListHandler extends DefaultHandler {
334
335
336 private final StringBuilder buffer = new StringBuilder();
337
338
339 private final List<PListNodeBuilder> stack = new ArrayList<>();
340
341
342 private final PListNodeBuilder resultBuilder;
343
344 public XMLPropertyListHandler() {
345 resultBuilder = new PListNodeBuilder();
346 push(resultBuilder);
347 }
348
349 @Override
350 public void characters(final char[] ch, final int start, final int length) throws SAXException {
351 buffer.append(ch, start, length);
352 }
353
354 @Override
355 public void endElement(final String uri, final String localName, final String qName) throws SAXException {
356 if ("key".equals(qName)) {
357
358 final PListNodeBuilder node = new PListNodeBuilder();
359 node.setName(buffer.toString());
360 peekNE().addChild(node);
361 push(node);
362 } else if ("dict".equals(qName)) {
363
364 final PListNodeBuilder builder = pop();
365 assert builder != null : "Stack was empty!";
366 if (peek() instanceof ArrayNodeBuilder) {
367
368 final XMLPropertyListConfiguration config = new XMLPropertyListConfiguration(builder.createNode());
369
370
371 final ArrayNodeBuilder node = (ArrayNodeBuilder) peekNE();
372 node.addValue(config);
373 }
374 } else {
375 switch (qName) {
376 case "string":
377 peekNE().addValue(buffer.toString());
378 break;
379 case "integer":
380 peekNE().addIntegerValue(buffer.toString());
381 break;
382 case "real":
383 peekNE().addRealValue(buffer.toString());
384 break;
385 case "true":
386 peekNE().addTrueValue();
387 break;
388 case "false":
389 peekNE().addFalseValue();
390 break;
391 case "data":
392 peekNE().addDataValue(buffer.toString());
393 break;
394 case "date":
395 try {
396 peekNE().addDateValue(buffer.toString());
397 } catch (final IllegalArgumentException iex) {
398 getLogger().warn("Ignoring invalid date property " + buffer);
399 }
400 break;
401 case "array": {
402 final ArrayNodeBuilder array = (ArrayNodeBuilder) pop();
403 peekNE().addList(array);
404 break;
405 }
406 default:
407 break;
408 }
409
410
411
412 if (!(peek() instanceof ArrayNodeBuilder)) {
413 pop();
414 }
415 }
416
417 buffer.setLength(0);
418 }
419
420
421
422
423
424
425 public PListNodeBuilder getResultBuilder() {
426 return resultBuilder;
427 }
428
429
430
431
432 private PListNodeBuilder peek() {
433 if (!stack.isEmpty()) {
434 return stack.get(stack.size() - 1);
435 }
436 return null;
437 }
438
439
440
441
442
443
444
445 private PListNodeBuilder peekNE() {
446 final PListNodeBuilder result = peek();
447 if (result == null) {
448 throw new ConfigurationRuntimeException("Access to empty stack.");
449 }
450 return result;
451 }
452
453
454
455
456 private PListNodeBuilder pop() {
457 if (!stack.isEmpty()) {
458 return stack.remove(stack.size() - 1);
459 }
460 return null;
461 }
462
463
464
465
466 private void push(final PListNodeBuilder node) {
467 stack.add(node);
468 }
469
470 @Override
471 public void startElement(final String uri, final String localName, final String qName, final Attributes attributes) throws SAXException {
472 if ("array".equals(qName)) {
473 push(new ArrayNodeBuilder());
474 } else if ("dict".equals(qName) && peek() instanceof ArrayNodeBuilder) {
475
476 push(new PListNodeBuilder());
477 }
478 }
479 }
480
481
482 private static final int INDENT_SIZE = 4;
483
484
485 private static final Charset DATA_ENCODING = StandardCharsets.UTF_8;
486
487
488
489
490
491
492
493
494 private static Map<String, Object> transformMap(final Map<?, ?> src) {
495 final Map<String, Object> dest = new HashMap<>();
496 for (final Map.Entry<?, ?> e : src.entrySet()) {
497 if (e.getKey() instanceof String) {
498 dest.put((String) e.getKey(), e.getValue());
499 }
500 }
501 return dest;
502 }
503
504
505 private FileLocator locator;
506
507
508
509
510
511 public XMLPropertyListConfiguration() {
512 }
513
514
515
516
517
518
519
520
521 public XMLPropertyListConfiguration(final HierarchicalConfiguration<ImmutableNode> configuration) {
522 super(configuration);
523 }
524
525
526
527
528
529
530 XMLPropertyListConfiguration(final ImmutableNode root) {
531 super(new InMemoryNodeModel(root));
532 }
533
534 @Override
535 protected void addPropertyInternal(final String key, final Object value) {
536 if (value instanceof byte[] || value instanceof List) {
537 addPropertyDirect(key, value);
538 } else if (value instanceof Object[]) {
539 addPropertyDirect(key, Arrays.asList((Object[]) value));
540 } else {
541 super.addPropertyInternal(key, value);
542 }
543 }
544
545
546
547
548
549
550 @Override
551 public void initFileLocator(final FileLocator locator) {
552 this.locator = locator;
553 }
554
555
556
557
558 private void printNode(final PrintWriter out, final int indentLevel, final ImmutableNode node) {
559 final String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
560
561 if (node.getNodeName() != null) {
562 out.println(padding + "<key>" + StringEscapeUtils.escapeXml10(node.getNodeName()) + "</key>");
563 }
564
565 final List<ImmutableNode> children = node.getChildren();
566 if (!children.isEmpty()) {
567 out.println(padding + "<dict>");
568
569 final Iterator<ImmutableNode> it = children.iterator();
570 while (it.hasNext()) {
571 final ImmutableNode child = it.next();
572 printNode(out, indentLevel + 1, child);
573
574 if (it.hasNext()) {
575 out.println();
576 }
577 }
578
579 out.println(padding + "</dict>");
580 } else if (node.getValue() == null) {
581 out.println(padding + "<dict/>");
582 } else {
583 final Object value = node.getValue();
584 printValue(out, indentLevel, value);
585 }
586 }
587
588
589
590
591 private void printValue(final PrintWriter out, final int indentLevel, final Object value) {
592 final String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
593
594 if (value instanceof Date) {
595 synchronized (PListNodeBuilder.FORMAT) {
596 out.println(padding + "<date>" + PListNodeBuilder.FORMAT.format((Date) value) + "</date>");
597 }
598 } else if (value instanceof Calendar) {
599 printValue(out, indentLevel, ((Calendar) value).getTime());
600 } else if (value instanceof Number) {
601 if (value instanceof Double || value instanceof Float || value instanceof BigDecimal) {
602 out.println(padding + "<real>" + value.toString() + "</real>");
603 } else {
604 out.println(padding + "<integer>" + value.toString() + "</integer>");
605 }
606 } else if (value instanceof Boolean) {
607 if (((Boolean) value).booleanValue()) {
608 out.println(padding + "<true/>");
609 } else {
610 out.println(padding + "<false/>");
611 }
612 } else if (value instanceof List) {
613 out.println(padding + "<array>");
614 ((List<?>) value).forEach(o -> printValue(out, indentLevel + 1, o));
615 out.println(padding + "</array>");
616 } else if (value instanceof HierarchicalConfiguration) {
617
618 @SuppressWarnings("unchecked")
619 final HierarchicalConfiguration<ImmutableNode> config = (HierarchicalConfiguration<ImmutableNode>) value;
620 printNode(out, indentLevel, config.getNodeModel().getNodeHandler().getRootNode());
621 } else if (value instanceof ImmutableConfiguration) {
622
623 out.println(padding + "<dict>");
624 final ImmutableConfiguration config = (ImmutableConfiguration) value;
625 config.forEach((k, v) -> {
626
627 final ImmutableNode node = new ImmutableNode.Builder().name(k).value(v).create();
628
629 printNode(out, indentLevel + 1, node);
630 out.println();
631 });
632 out.println(padding + "</dict>");
633 } else if (value instanceof Map) {
634
635 final Map<String, Object> map = transformMap((Map<?, ?>) value);
636 printValue(out, indentLevel, new MapConfiguration(map));
637 } else if (value instanceof byte[]) {
638 final String base64 = new String(Base64.getMimeEncoder().encode((byte[]) value), DATA_ENCODING);
639 out.println(padding + "<data>" + StringEscapeUtils.escapeXml10(base64) + "</data>");
640 } else if (value != null) {
641 out.println(padding + "<string>" + StringEscapeUtils.escapeXml10(String.valueOf(value)) + "</string>");
642 } else {
643 out.println(padding + "<string/>");
644 }
645 }
646
647 @Override
648 public void read(final Reader in) throws ConfigurationException {
649
650 final EntityResolver resolver = (publicId, systemId) -> new InputSource(getClass().getClassLoader().getResourceAsStream("PropertyList-1.0.dtd"));
651
652 final XMLPropertyListHandler handler = new XMLPropertyListHandler();
653 try {
654 final SAXParserFactory factory = SAXParserFactory.newInstance();
655 factory.setValidating(true);
656 final XMLReader xmlReader = factory.newSAXParser().getXMLReader();
657 xmlReader.setEntityResolver(resolver);
658 xmlReader.setContentHandler(handler);
659 xmlReader.parse(new InputSource(in));
660 getNodeModel().mergeRoot(handler.getResultBuilder().createNode(), null, null, null, this);
661 } catch (final Exception e) {
662 throw new ConfigurationException("Unable to parse the configuration file", e);
663 }
664 }
665
666 private void setPropertyDirect(final String key, final Object value) {
667 setDetailEvents(false);
668 try {
669 clearProperty(key);
670 addPropertyDirect(key, value);
671 } finally {
672 setDetailEvents(true);
673 }
674 }
675
676 @Override
677 protected void setPropertyInternal(final String key, final Object value) {
678
679 if (value instanceof byte[] || value instanceof List) {
680 setPropertyDirect(key, value);
681 } else if (value instanceof Object[]) {
682 setPropertyDirect(key, Arrays.asList((Object[]) value));
683 } else {
684 super.setPropertyInternal(key, value);
685 }
686 }
687
688 @Override
689 public void write(final Writer out) throws ConfigurationException {
690 if (locator == null) {
691 throw new ConfigurationException(
692 "Save operation not properly initialized! Do not call write(Writer) directly, but use a FileHandler to save a configuration.");
693 }
694 final PrintWriter writer = new PrintWriter(out);
695 if (locator.getEncoding() != null) {
696 writer.println("<?xml version=\"1.0\" encoding=\"" + locator.getEncoding() + "\"?>");
697 } else {
698 writer.println("<?xml version=\"1.0\"?>");
699 }
700 writer.println("<!DOCTYPE plist SYSTEM \"file://localhost/System/Library/DTDs/PropertyList.dtd\">");
701 writer.println("<plist version=\"1.0\">");
702 printNode(writer, 1, getNodeModel().getNodeHandler().getRootNode());
703 writer.println("</plist>");
704 writer.flush();
705 }
706 }