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