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