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.Calendar;
33 import java.util.Collection;
34 import java.util.Date;
35 import java.util.HashMap;
36 import java.util.Iterator;
37 import java.util.LinkedList;
38 import java.util.List;
39 import java.util.Map;
40 import java.util.TimeZone;
41
42 import javax.xml.parsers.SAXParser;
43 import javax.xml.parsers.SAXParserFactory;
44
45 import org.apache.commons.codec.binary.Base64;
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.decodeBase64(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 if ("string".equals(qName)) {
372 peekNE().addValue(buffer.toString());
373 } else if ("integer".equals(qName)) {
374 peekNE().addIntegerValue(buffer.toString());
375 } else if ("real".equals(qName)) {
376 peekNE().addRealValue(buffer.toString());
377 } else if ("true".equals(qName)) {
378 peekNE().addTrueValue();
379 } else if ("false".equals(qName)) {
380 peekNE().addFalseValue();
381 } else if ("data".equals(qName)) {
382 peekNE().addDataValue(buffer.toString());
383 } else if ("date".equals(qName)) {
384 try {
385 peekNE().addDateValue(buffer.toString());
386 } catch (final IllegalArgumentException iex) {
387 getLogger().warn("Ignoring invalid date property " + buffer);
388 }
389 } else if ("array".equals(qName)) {
390 final ArrayNodeBuilder array = (ArrayNodeBuilder) pop();
391 peekNE().addList(array);
392 }
393
394
395
396 if (!(peek() instanceof ArrayNodeBuilder)) {
397 pop();
398 }
399 }
400
401 buffer.setLength(0);
402 }
403
404
405
406
407
408
409 public PListNodeBuilder getResultBuilder() {
410 return resultBuilder;
411 }
412
413
414
415
416 private PListNodeBuilder peek() {
417 if (!stack.isEmpty()) {
418 return stack.get(stack.size() - 1);
419 }
420 return null;
421 }
422
423
424
425
426
427
428
429 private PListNodeBuilder peekNE() {
430 final PListNodeBuilder result = peek();
431 if (result == null) {
432 throw new ConfigurationRuntimeException("Access to empty stack!");
433 }
434 return result;
435 }
436
437
438
439
440 private PListNodeBuilder pop() {
441 if (!stack.isEmpty()) {
442 return stack.remove(stack.size() - 1);
443 }
444 return null;
445 }
446
447
448
449
450 private void push(final PListNodeBuilder node) {
451 stack.add(node);
452 }
453
454 @Override
455 public void startElement(final String uri, final String localName, final String qName, final Attributes attributes) throws SAXException {
456 if ("array".equals(qName)) {
457 push(new ArrayNodeBuilder());
458 } else if ("dict".equals(qName) && peek() instanceof ArrayNodeBuilder) {
459
460 push(new PListNodeBuilder());
461 }
462 }
463 }
464
465
466 private static final int INDENT_SIZE = 4;
467
468
469 private static final Charset DATA_ENCODING = StandardCharsets.UTF_8;
470
471
472
473
474
475
476
477
478 private static Map<String, Object> transformMap(final Map<?, ?> src) {
479 final Map<String, Object> dest = new HashMap<>();
480 for (final Map.Entry<?, ?> e : src.entrySet()) {
481 if (e.getKey() instanceof String) {
482 dest.put((String) e.getKey(), e.getValue());
483 }
484 }
485 return dest;
486 }
487
488
489 private FileLocator locator;
490
491
492
493
494
495 public XMLPropertyListConfiguration() {
496 }
497
498
499
500
501
502
503
504
505 public XMLPropertyListConfiguration(final HierarchicalConfiguration<ImmutableNode> configuration) {
506 super(configuration);
507 }
508
509
510
511
512
513
514 XMLPropertyListConfiguration(final ImmutableNode root) {
515 super(new InMemoryNodeModel(root));
516 }
517
518 @Override
519 protected void addPropertyInternal(final String key, final Object value) {
520 if (value instanceof byte[] || value instanceof List) {
521 addPropertyDirect(key, value);
522 } else if (value instanceof Object[]) {
523 addPropertyDirect(key, Arrays.asList((Object[]) value));
524 } else {
525 super.addPropertyInternal(key, value);
526 }
527 }
528
529
530
531
532
533
534 @Override
535 public void initFileLocator(final FileLocator locator) {
536 this.locator = locator;
537 }
538
539
540
541
542 private void printNode(final PrintWriter out, final int indentLevel, final ImmutableNode node) {
543 final String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
544
545 if (node.getNodeName() != null) {
546 out.println(padding + "<key>" + StringEscapeUtils.escapeXml10(node.getNodeName()) + "</key>");
547 }
548
549 final List<ImmutableNode> children = node.getChildren();
550 if (!children.isEmpty()) {
551 out.println(padding + "<dict>");
552
553 final Iterator<ImmutableNode> it = children.iterator();
554 while (it.hasNext()) {
555 final ImmutableNode child = it.next();
556 printNode(out, indentLevel + 1, child);
557
558 if (it.hasNext()) {
559 out.println();
560 }
561 }
562
563 out.println(padding + "</dict>");
564 } else if (node.getValue() == null) {
565 out.println(padding + "<dict/>");
566 } else {
567 final Object value = node.getValue();
568 printValue(out, indentLevel, value);
569 }
570 }
571
572
573
574
575 private void printValue(final PrintWriter out, final int indentLevel, final Object value) {
576 final String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
577
578 if (value instanceof Date) {
579 synchronized (PListNodeBuilder.FORMAT) {
580 out.println(padding + "<date>" + PListNodeBuilder.FORMAT.format((Date) value) + "</date>");
581 }
582 } else if (value instanceof Calendar) {
583 printValue(out, indentLevel, ((Calendar) value).getTime());
584 } else if (value instanceof Number) {
585 if (value instanceof Double || value instanceof Float || value instanceof BigDecimal) {
586 out.println(padding + "<real>" + value.toString() + "</real>");
587 } else {
588 out.println(padding + "<integer>" + value.toString() + "</integer>");
589 }
590 } else if (value instanceof Boolean) {
591 if (((Boolean) value).booleanValue()) {
592 out.println(padding + "<true/>");
593 } else {
594 out.println(padding + "<false/>");
595 }
596 } else if (value instanceof List) {
597 out.println(padding + "<array>");
598 ((List<?>) value).forEach(o -> printValue(out, indentLevel + 1, o));
599 out.println(padding + "</array>");
600 } else if (value instanceof HierarchicalConfiguration) {
601
602 @SuppressWarnings("unchecked")
603 final HierarchicalConfiguration<ImmutableNode> config = (HierarchicalConfiguration<ImmutableNode>) value;
604 printNode(out, indentLevel, config.getNodeModel().getNodeHandler().getRootNode());
605 } else if (value instanceof ImmutableConfiguration) {
606
607 out.println(padding + "<dict>");
608
609 final ImmutableConfiguration config = (ImmutableConfiguration) value;
610 final Iterator<String> it = config.getKeys();
611 while (it.hasNext()) {
612
613 final String key = it.next();
614 final ImmutableNode node = new ImmutableNode.Builder().name(key).value(config.getProperty(key)).create();
615
616
617 printNode(out, indentLevel + 1, node);
618
619 if (it.hasNext()) {
620 out.println();
621 }
622 }
623 out.println(padding + "</dict>");
624 } else if (value instanceof Map) {
625
626 final Map<String, Object> map = transformMap((Map<?, ?>) value);
627 printValue(out, indentLevel, new MapConfiguration(map));
628 } else if (value instanceof byte[]) {
629 final String base64 = new String(Base64.encodeBase64((byte[]) value), DATA_ENCODING);
630 out.println(padding + "<data>" + StringEscapeUtils.escapeXml10(base64) + "</data>");
631 } else if (value != null) {
632 out.println(padding + "<string>" + StringEscapeUtils.escapeXml10(String.valueOf(value)) + "</string>");
633 } else {
634 out.println(padding + "<string/>");
635 }
636 }
637
638 @Override
639 public void read(final Reader in) throws ConfigurationException {
640
641 final EntityResolver resolver = (publicId, systemId) -> new InputSource(getClass().getClassLoader().getResourceAsStream("PropertyList-1.0.dtd"));
642
643
644 final XMLPropertyListHandler handler = new XMLPropertyListHandler();
645 try {
646 final SAXParserFactory factory = SAXParserFactory.newInstance();
647 factory.setValidating(true);
648
649 final SAXParser parser = factory.newSAXParser();
650 parser.getXMLReader().setEntityResolver(resolver);
651 parser.getXMLReader().setContentHandler(handler);
652 parser.getXMLReader().parse(new InputSource(in));
653
654 getNodeModel().mergeRoot(handler.getResultBuilder().createNode(), null, null, null, this);
655 } catch (final Exception e) {
656 throw new ConfigurationException("Unable to parse the configuration file", e);
657 }
658 }
659
660 private void setPropertyDirect(final String key, final Object value) {
661 setDetailEvents(false);
662 try {
663 clearProperty(key);
664 addPropertyDirect(key, value);
665 } finally {
666 setDetailEvents(true);
667 }
668 }
669
670 @Override
671 protected void setPropertyInternal(final String key, final Object value) {
672
673 if (value instanceof byte[] || value instanceof List) {
674 setPropertyDirect(key, value);
675 } else if (value instanceof Object[]) {
676 setPropertyDirect(key, Arrays.asList((Object[]) value));
677 } else {
678 super.setPropertyInternal(key, value);
679 }
680 }
681
682 @Override
683 public void write(final Writer out) throws ConfigurationException {
684 if (locator == null) {
685 throw new ConfigurationException(
686 "Save operation not properly " + "initialized! Do not call write(Writer) directly," + " but use a FileHandler to save a configuration.");
687 }
688 final PrintWriter writer = new PrintWriter(out);
689
690 if (locator.getEncoding() != null) {
691 writer.println("<?xml version=\"1.0\" encoding=\"" + locator.getEncoding() + "\"?>");
692 } else {
693 writer.println("<?xml version=\"1.0\"?>");
694 }
695
696 writer.println("<!DOCTYPE plist SYSTEM \"file://localhost/System/Library/DTDs/PropertyList.dtd\">");
697 writer.println("<plist version=\"1.0\">");
698
699 printNode(writer, 1, getNodeModel().getNodeHandler().getRootNode());
700
701 writer.println("</plist>");
702 writer.flush();
703 }
704 }