1 /**
2  * Utilities for loading Java properties files.
3  *
4  * Authors:
5  *  Mike Bierlee, m.bierlee@lostmoment.com
6  * Copyright: 2022-2025 Mike Bierlee
7  * License:
8  *  This software is licensed under the terms of the MIT license.
9  *  The full terms of the license can be found in the LICENSE file.
10  */
11 
12 module mirage.java;
13 
14 import mirage.config : ConfigDictionary;
15 import mirage.keyvalue : KeyValueConfigFactory, SupportHashtagComments, SupportSemicolonComments,
16     SupportExclamationComments, SupportSections, NormalizeQuotedValues, SupportEqualsSeparator,
17     SupportColonSeparator, SupportKeysWithoutValues, SupportMultilineValues;
18 
19 /** 
20  * Creates configuration dictionaries from Java properties.
21  *
22  * Format specifications: 
23  *   https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Properties.html#load(java.io.Reader)
24  *   https://en.wikipedia.org/wiki/.properties
25  */
26 class JavaPropertiesFactory : KeyValueConfigFactory!(
27     SupportHashtagComments.yes,
28     SupportSemicolonComments.no,
29     SupportExclamationComments.yes,
30     SupportSections.no,
31     NormalizeQuotedValues.no,
32     SupportEqualsSeparator.yes,
33     SupportColonSeparator.yes,
34     SupportKeysWithoutValues.yes,
35     SupportMultilineValues.yes
36 ) {
37 }
38 
39 /** 
40  * Parse Java properties from the given Java properties string.
41 
42  * Params:
43  *   properties = Text contents of the config to be parsed.
44  * Returns: The parsed configuration.
45  */
46 ConfigDictionary parseJavaProperties(const string properties) {
47     return new JavaPropertiesFactory().parseConfig(properties);
48 }
49 
50 /// ditto
51 alias parseJavaConfig = parseJavaProperties;
52 
53 /** 
54  * Load a Java properties file from disk.
55  *
56  * Params:
57  *   filePath = Path to the Java properties file.
58  * Returns: The loaded configuration.
59  */
60 ConfigDictionary loadJavaProperties(const string filePath) {
61     return new JavaPropertiesFactory().loadFile(filePath);
62 }
63 
64 /// ditto
65 alias loadJavaConfig = loadJavaProperties;
66 
67 version (unittest) {
68     import std.exception : assertThrown;
69     import std.process : environment;
70     import mirage.config : ConfigCreationException;
71 
72     @("Parse java properties")
73     unittest {
74         auto config = parseJavaProperties("
75             # I have a comment
76             bla=one
77             di.bla=two
78             meh: very # except when meh=not very
79             much = not much
80             much: much !important!!!!!!!!
81             empty
82             multi = we are \\
83                     two lines
84         ");
85 
86         assert(config.get("bla") == "one");
87         assert(config.get("di.bla") == "two");
88         assert(config.get("meh") == "very");
89         assert(config.get("much") == "much");
90         assert(config.get("empty") == "");
91         assert(config.get("multi") == "we are two lines");
92     }
93 
94     @("Parse java properties file")
95     unittest {
96         auto config = loadJavaProperties("testfiles/java.properties");
97         assert(config.get("bla") == "one");
98         assert(config.get("di.bla") == "two");
99     }
100 
101     @("Substitute env vars")
102     unittest {
103         environment["MIRAGE_TEST_ENVY"] = "Much";
104         auto config = parseJavaProperties("envy=$MIRAGE_TEST_ENVY");
105 
106         assert(config.get("envy") == "Much");
107     }
108 
109     @("Use value from other key")
110     unittest {
111         auto config = parseJavaProperties("
112             one=money
113             two=${one}
114         ");
115 
116         assert(config.get("two") == "money");
117     }
118 
119     @("Values and keys are trimmed")
120     unittest {
121         auto config = parseJavaConfig("
122             one    =       money
123         ");
124 
125         assert(config.get("one") == "money");
126     }
127 
128     @("Quotes in values are preserved")
129     unittest {
130         auto config = parseJavaProperties("
131             one=\"two\"
132             three='four'
133         ");
134 
135         assert(config.get("one") == "\"two\"");
136         assert(config.get("three") == "'four'");
137     }
138 }