├── .github └── workflows │ └── maven.yml ├── .gitignore ├── CONTRIBUTING.adoc ├── LICENSE.txt ├── README.adoc ├── TODO ├── apron-wide.svg ├── changelog ├── pom.xml └── src ├── main └── java │ ├── de │ └── poiu │ │ └── apron │ │ ├── ApronOptions.java │ │ ├── MissingKeyAction.java │ │ ├── PropertyFile.java │ │ ├── UnicodeHandling.java │ │ ├── entry │ │ ├── BasicEntry.java │ │ ├── Entry.java │ │ └── PropertyEntry.java │ │ ├── escaping │ │ ├── EscapeUtils.java │ │ └── InvalidUnicodeCharacterException.java │ │ ├── io │ │ ├── PropertyFileReader.java │ │ └── PropertyFileWriter.java │ │ ├── java │ │ └── util │ │ │ ├── Helper.java │ │ │ └── Properties.java │ │ └── reformatting │ │ ├── AttachCommentsTo.java │ │ ├── InvalidFormatException.java │ │ ├── OrderableEntry.java │ │ ├── OrderableEntryList.java │ │ ├── PropertyFormat.java │ │ ├── ReformatOptions.java │ │ └── Reformatter.java │ └── module-info.java └── test ├── java └── de │ └── poiu │ └── apron │ ├── PropertyFileTest.java │ ├── escaping │ └── EscapeUtilsTest.java │ ├── io │ ├── PropertyFileReaderTest.java │ └── PropertyFileWriterTest.java │ ├── java │ └── util │ │ └── PropertiesTest.java │ └── reformatting │ ├── ReformatterPatternTest.java │ └── ReformatterTest.java └── resources └── log4j2.xml /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | name: Java CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | java: [17, 21, 24, 25-ea] 13 | 14 | steps: 15 | - uses: ts-graphviz/setup-graphviz@v2 16 | - uses: actions/checkout@v4 17 | - name: Set up JDK ${{ matrix.java }} 18 | uses: actions/setup-java@v4 19 | with: 20 | distribution: 'zulu' 21 | java-version: ${{ matrix.java }} 22 | - name: Build with Maven 23 | run: mvn -B -DperformRelease=true package --file pom.xml 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | nbproject/ 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.adoc: -------------------------------------------------------------------------------- 1 | = Contributing to Apron 2 | 3 | Thanks for your interest in contributing to Apron. 4 | 5 | == How to contribute 6 | 7 | If you intent to do larger changes it is advisable to open an 8 | https://github.com/poiu-de/apron/issues[Issue] issue first, describing the 9 | problem and the intentions. This helps avoiding duplicate work or your 10 | changes being refused for some reason. 11 | 12 | Of course you are free to just fork the project, do the intented changes 13 | and create a pull request. 14 | 15 | == What to contribute 16 | 17 | === Finding bugs 18 | 19 | Every software has bugs. If you find a bug in Apron please file an 20 | https://github.com/poiu-de/apron/issues[Issue] so they can be resolved. 21 | 22 | Preparing pull requests to solve issues is also highly welcome, of course. 23 | In that case, please provide a commit with a unit test demonstrating the 24 | problem and another commit with the actual fix. 25 | 26 | === Writing code 27 | 28 | When writing some code please follow our 29 | https://hupfdule.github.io/styleguide/javaguide.html[code conventions]. 30 | 31 | Please structure your commits logically and respect the original 32 | formatting. Do not change formattings unrelated to any functional changes, 33 | even if the existing formatting violates the styleguide. To do such 34 | formatting changes, please prepare a separate pull request. 35 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | Apron 2 | ===== 3 | Marco Herrn 4 | 2018-05-14 5 | :compat-mode!: 6 | :toc: 7 | :homepage: https://github.com/poiu-de/apron 8 | :download-page: https://github.com/poiu-de/apron/releases 9 | :javadoc-url: https://javadoc.io/doc/de.poiu.apron/apron/ 10 | :license-link: https://github.com/poiu-de/apron/blob/master/LICENSE.txt 11 | :kilt-homepage: https://poiu-de.github.io/kilt 12 | :log4j2-jul-bridge: https://logging.apache.org/log4j/2.x/log4j-jul/index.html 13 | :slf4j-jul-bridge: https://www.slf4j.org/legacy.html#jul-to-slf4j 14 | :source-highlighter: prettify 15 | :apron-version: 2.1.1 16 | 17 | [.float-group] 18 | -- 19 | image:apron-wide.svg[Apron,role="right", width="75"] 20 | 21 | Apron - Advanced Properties 22 | 23 | Read and write Java .properties files in a more sane manner. 24 | -- 25 | 26 | 27 | What is Apron 28 | ------------- 29 | 30 | Apron is a small library for reading and writing Java .properties files. 31 | The main goal of this library is to be compatible with the 32 | `java.util.Properties` class. Not API-wise (the API is quite different), 33 | but being able to read every Java .properties file and getting exactly the 34 | same key-value pairs as `java.util.Properties` does. 35 | 36 | However Apron maintains the order of the entries in the properties files 37 | and also the comments, blank lines and whitespace before keys and around 38 | separators. 39 | 40 | This allows writing .properties files back that do not differ from the 41 | original ones. 42 | 43 | Since version 2.0.0 Apron provides the ability to reformat and reorder 44 | the content of .properties files according to different constraints. 45 | Refer to <> for a more detailled description. 46 | 47 | Apron was mainly written to be used in the {kilt-homepage}[Kilt toolset], 48 | but was intended from the start to be a general purpose library. 49 | 50 | 51 | What can Apron be used for 52 | -------------------------- 53 | 54 | Some examples for usage scenarios of Apron are: 55 | 56 | - Using .properties files as config files for an application that may be 57 | manually edited by a user as well as modified by the application itself 58 | (e.g. via a configuration dialog). The manual modifications (like the 59 | order of entries, as well as comments, empty lines and even the 60 | formatting of entries) will remain. 61 | 62 | - Exporting and importing Java i18n resource bundles for translation (like 63 | {kilt-homepage}[Kilt] does). 64 | 65 | - Reordering multiple .properties files to contain their entries in the 66 | same order. 67 | 68 | - Reformatting .properties files to conform to a specific format. 69 | 70 | 71 | Prerequisites 72 | ------------- 73 | 74 | Apron has no runtime dependencies on other libraries. 75 | 76 | Apron can be used with Java 8 or higher. 77 | 78 | 79 | Installation 80 | ------------ 81 | 82 | To use Apron in a maven based project use the following maven coordinates: 83 | 84 | [source,xml,subs="verbatim,attributes"] 85 | ---- 86 | 87 | de.poiu.apron 88 | apron 89 | {apron-version} 90 | 91 | ---- 92 | 93 | Otherwise download the jar-file of Apron from the {download-page}[Download 94 | page] and put it into the classpath of your application. 95 | 96 | 97 | Usage 98 | ----- 99 | 100 | The main important class in Apron is `de.poiu.apron.PropertyFile`. 101 | It provides methods to create a new instance by reading a .properties file 102 | from File or InputStream as well as methods for populating an instance 103 | programmatically. 104 | 105 | The main difference to the usual `java.util.Properties` is that this class 106 | does not implement the `java.util.Map` interface and provides access to the 107 | content of the PropertyFile in two different ways: 108 | 109 | - as key-value pairs 110 | - as Entries 111 | 112 | The key-value pairs are the actual interesting content of the .properties 113 | files and are the same as when read via `java.util.Properties`. However, 114 | since PropertyFile is able to retain all comments, blank lines and even the 115 | formatting of the file it stores all this information in objects of type 116 | `Entry`. There are two implementations of `Entry`: 117 | 118 | BasicEntry:: 119 | A non-key-value pair like a comment or an empty line 120 | PropertyEntry:: 121 | An actual key-value pair 122 | 123 | The Entry objects store their content in _escaped_ form. That means all 124 | whitespaces, linebreaks, escape characters, etc. are contained in exactly 125 | the same form as in the written .properties file. 126 | 127 | The key-value pairs instead contain the content in _unescaped_ form (as you 128 | would expect from `java.util.Properties`). 129 | 130 | To minimize confusion the _escaped_ values are stored as CharSequence 131 | whereas the _unescaped_ values are stored as String. 132 | 133 | A PropertyFile instance also allows writing its content back to disk. It 134 | provides 3 methods (each in two variants) for doing so: 135 | 136 | overwrite:: 137 | Writes the contents of the PropertyFile to a new file or overwrite an 138 | existing file. 139 | update:: 140 | Update an existing .properties file with the values in the written 141 | PropertyFile. 142 | save:: 143 | Use either the above mentioned overwrite method if the given file does 144 | not exist or the update method if the file already exists. 145 | 146 | The most interesting method is the `update` method, since this 147 | differentiates PropertyFile from `java.util.Properties`. It actually only 148 | updates the values of the key-value pairs without touching any other 149 | formatting. Blank lines, comments, whitespaces and even escaping and 150 | special formatting of the keys are not altered at all. Also the order of 151 | the key-value pairs remains the same. 152 | 153 | The behaviour when writing a PropertyFile can be altered by providing it an 154 | optional `ApronOptions` object. 155 | 156 | This is an example for a typical usage of PropertyFile as a replacement for 157 | `java.util.Properties`: 158 | 159 | [source,java] 160 | ---- 161 | // Read the file "application.properties" into a PropertyFile 162 | final PropertyFile propertyFile= PropertyFile.from( 163 | new File("application.properties")); 164 | 165 | // Read the value of the key "someKey" 166 | final String someValue= propertyFile.get("someKey"); 167 | 168 | // Set the value of "someKey" to a new value 169 | propertyFile.set("someKey", "aNewValue"); 170 | 171 | // Write the PropertyFile back to file by only updating the modified values 172 | propertyFile.update(new File("application.properties")); 173 | ---- 174 | 175 | This is an example for a more advanced usage of PropertyFile that allows 176 | acessing comment lines and explicitly formatted (escaped) entries: 177 | 178 | [source,java] 179 | ---- 180 | // Read all Entries (that means BasicEntries as well as PropertyEntries) 181 | final List entries= propertyFile.getAllEntries(); 182 | 183 | // Add a comment line to this PropertyFile 184 | propertyFile.appendEntry(new BasicEntry("# A new key-value pair follows")); 185 | 186 | // Add a new key-value pair to this PropertyFile 187 | // Be aware that by using appendEntry() it could be possible to insert 188 | // duplicate keys into this PropertyFile. The behaviour is then undefined. 189 | // It is the responsibility of the user of PropertyFile to avoid this. 190 | // PropertyEntries contain their content in _escaped_ form. Therefore the 191 | // Backslashes and newline character are not really part of the key and value 192 | propertyFile.appendEntry(new PropertyEntry("a new \\\nkey", "a new \\\nvalue")); 193 | 194 | // key-value pairs are _unescaped_. Therefore the following method call 195 | // will return the string "a new value" 196 | final String myNewValue= propertyFile.get("a new key"); 197 | 198 | // Specify an ApronOptions object that writes with ISO-8859-1 encoding 199 | // instead of the default UTF-8. 200 | final ApronOptions apronOptions= ApronOptions.create() 201 | .with(java.nio.charset.StandardCharsets.ISO_8859_1); 202 | 203 | // Write the PropertyFile back to file by only updating the modified values 204 | propertyFile.update(new File("application.properties"), apronOptions); 205 | ---- 206 | 207 | See the {javadoc-url}[Javadoc API] for more details. 208 | 209 | 210 | Reformatting and Reordering 211 | --------------------------- 212 | 213 | Since version 2.0.0 Apron provides a `de.poiu.apron.reformatting.Reformatter` 214 | class that allows reformatting and reordering the content of .properties 215 | files. 216 | 217 | The specific behaviour when reformatting and reordering can be specified 218 | via a `de.poiu.apron.reformatting.ReformatOptions` object. 219 | 220 | For convenience the `de.poiu.apron.PropertyFile` class provides some methods 221 | to reformat or reorder the entries in that PropertyFile. 222 | 223 | 224 | === Reformatting 225 | 226 | When reformatting a format string can be given to specify how to format 227 | leading whitespace, separators and line endings. The default format string 228 | is ` = \n` for 229 | 230 | - no leading whitespace 231 | - an equals sign surrounded by a single whitespace on each side as separator 232 | - a `\n` (line feed) character as new line character 233 | 234 | By default the keys and values of the reformatted files are _not_ modified. 235 | That means any special formatting (like insignificant whitespace, newlines 236 | and escape characters) remain after reformatting. 237 | 238 | This can be changed via the `reformatKeyAndValue` option in which case 239 | these will be modified as well. 240 | 241 | This is an example for reformatting a PropertyFile: 242 | 243 | [source,java] 244 | ---- 245 | // Create the ReformatOptions to use to read and write with UTF-8 (which is the default anyway), 246 | // reformat via a custom format string and also reformat the keys and values. 247 | final ReformatOptions reformatOptions= ReformatOptions.create() 248 | .with(UTF_8) 249 | .withFormat(": \r\n") 250 | .withReformatKeyAndValue(true) 251 | ; 252 | 253 | // Create a Reformatter with the specified ReformatOptions 254 | final Reformatter reformatter= new Reformatter(reformatOptions); 255 | 256 | // Reformat a single .properties file according to the specified ReformatOptions 257 | reformatter.reformat(new File("myproperties.properties")); 258 | ---- 259 | 260 | === Reordering 261 | 262 | Reordering the content of .properties files can be done either by 263 | alphabetically sorting the keys of the key-value pairs or by referring to a 264 | template file in which case the keys are ordered in the same order as in 265 | the template file. 266 | 267 | Apron allows specifying how to handle non-property lines (comments and empty lines) 268 | when reordering. It is possible to move them along with the key-value pair 269 | that _follows_ them or the key-value pair that _precedes_ them or be just left at 270 | the same position as they are. 271 | 272 | This is an example for reordering a PropertyFile: 273 | 274 | [source,java] 275 | ---- 276 | // Create the ReformatOptions to use that does not reorder empty lines and comments 277 | final ReformatOptions reorderOptions= ReformatOptions.create() 278 | .with(AttachCommentsTo.ORIG_LINE) 279 | ; 280 | 281 | // Create a Reformatter with the specified ReformatOptions 282 | final Reformatter reformatter= new Reformatter(reorderOptions); 283 | 284 | // Reorder a single .properties file alphabetically according to the specified ReformatOptions 285 | reformatter.reorderByKey(new File("myproperties.properties")); 286 | 287 | // Reorder a single .properties file according to the order in another .properties file. 288 | // This time we want to reorder comments and empty lines along with the key-value pair that 289 | // follows them. This is possible by specifying a ReformatOptions object when calling the 290 | // corresponding reorder method. 291 | reformatter.reorderByTemplate( 292 | new File("template.properties"), 293 | new File("someOther.properties"), 294 | reorderOptions.with(AttachCommentsTo.NEXT_PROPERTY) 295 | ); 296 | ---- 297 | 298 | 299 | `java.util.Properties` wrapper 300 | ------------------------------ 301 | 302 | Since version 2.1.0 Apron provides a `de.poiu.apron.java.util.Properties` 303 | class as a wrapper to be used as a drop-in replacement where a 304 | `java.util.Properties` object is required. 305 | 306 | This wrapper derives from `java.util.Properties`, but uses an Apron 307 | `PropertyFile` as the actual implementation. 308 | 309 | 310 | === Example 311 | 312 | To use it create it either via 313 | 314 | [source,java] 315 | ---- 316 | de.poiu.apron.PropertyFile propertyFile= … 317 | de.poiu.apron.java.util.Properties properties= 318 | new de.poiu.apron.java.util.Properties(propertyFile); 319 | ---- 320 | 321 | or via 322 | 323 | [source,java] 324 | ---- 325 | de.poiu.apron.PropertyFile propertyFile= … 326 | de.poiu.apron.java.util.Properties properties= propertyFile.asProperties(); 327 | ---- 328 | 329 | All access via the `properties` object will then access to the 330 | `propertyFile` object. Both objects can be used interchangebly to access 331 | the actual contents. 332 | 333 | 334 | === Differences to `java.util.Properties` 335 | 336 | The wrapper tries to fulfil the `java.util.Properties` API as good as 337 | possible. However there are a few differences: 338 | 339 | - `java.util.Properties` is derived from Hashtable and therefore non-String 340 | keys and values can be stored in it (although that is highly 341 | discouraged). As Aprons `PropertyFile` is not derived from Hashtable it 342 | doesn't share this flaw. Therefore trying to use any other objects than 343 | Strings as keys or values will fail. 344 | 345 | - Aprons `PropertyFile` only supports key-value-based `.properties` files. 346 | As `java.util.Properties` also provides methods to read and write to XML 347 | files and those formats are not supported by Apron, the corresponding 348 | methods will always throw an UnsupportedOperationException. 349 | 350 | - `java.util.Properties` being derived from Hashtable is thread-safe. 351 | However Aprons `PropertyFile` is not thread-safe and therefore this 352 | wrapper is also not thread-safe. 353 | 354 | 355 | Logging 356 | ------- 357 | 358 | There are a few cases this library issues some logging statements (when 359 | closing a writer didn't succeed and if an invalid unicode sequence was 360 | found that will be left as is). 361 | Those few logging statements don't justify a dependency on a logging 362 | framework. Therefore we just use java.util.logging for that purpose. 363 | 364 | When using Apron in an application that uses another logging framework 365 | please use those logging frameworks ability to bridge java.util.logging to 366 | their actual implementation. 367 | 368 | For log4j2 this can be done by including the `log4j2-jul` and `log4j2-api` jar 369 | (and some implemention, e.g. `log4j2-core`) and setting the system property 370 | `java.util.logging.manager` to `org.apache.logging.log4j.jul.LogManager`. 371 | See {log4j2-jul-bridge} for more information. 372 | 373 | For slf4j this can be done by including the `jul-to-slf4j` jar (and some 374 | implementation, e.g. `logback`) and programmatically calling 375 | 376 | [source,java] 377 | ---- 378 | SLF4JBridgeHandler.removeHandlersForRootLogger(); 379 | SLF4JBridgeHandler.install(); 380 | ---- 381 | 382 | or setting the handler in the `logging.properties`: 383 | 384 | [source,xml] 385 | ---- 386 | handlers = org.slf4j.bridge.SLF4JBridgeHandler 387 | ---- 388 | 389 | See {slf4j-jul-bridge} for more information. 390 | 391 | 392 | // There are no known bugs at the moment 393 | //Known Bugs 394 | //---------- 395 | 396 | 397 | 398 | License 399 | ------- 400 | 401 | Apron is licensed under the terms of the link:{license-link}[Apache license 402 | 2.0]. 403 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | Release x.x.x 2 | ------------- 3 | - Doch das File Object beim Lesen mit abspeichern? 4 | Dann können wir eine update/overwrite Methode anbieten, die das direkt 5 | benutzt. Diese wirft dann eine Exception, wenn nicht aus einem File 6 | gelesen wurde. 7 | - modularisieren? (Jigsaw) (+Multimodule) 8 | - Options auch fürs Lesen verwenden? Zumindest das Encoding greift ja auch 9 | dort 10 | - nicht unbedingt, da ja _nur_ das Encoding dort relevant ist. 11 | - Javadoc in Asciidoc (muss mit UMLdoclet zusammenspielen) 12 | - package-info.adoc 13 | - Javadoc mit Java 11 generieren. Das würde multi-module build erfordern 14 | - java.util.Properties ableiten (wichtig?) 15 | PropertyFile.asProperties():Properties (Wrapper) 16 | - PropertyFile#appendEntry(...) public machen? Ist ja auch in der README 17 | beschrieben 18 | - PropertyFile#appendEntry(String) als Alternative zu 19 | PropertyFile#appendEntry(BasicEntry)? 20 | -------------------------------------------------------------------------------- /apron-wide.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 39 | 40 | 53 | 54 | 55 | 61 | 64 | 67 | 70 | 73 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /changelog: -------------------------------------------------------------------------------- 1 | Apron changelog 2 | =============== 3 | 4 | Version 2.1.1 - 2021-05-31 5 | -------------------------- 6 | 7 | - Fix handling of escaped CR LF line endings (#11) 8 | Thanks to Vladimir Kroupa and Jan Novotný 9 | 10 | 11 | Version 2.1.0 - 2019-12-20 12 | -------------------------- 13 | 14 | - Provide JPMS module-info to be used in JPMS modularized applications 15 | The module-info will only be used by Java 9+. All other classes are 16 | still compatible with Java 8. 17 | - Provide java.util.Properties wrapper around PropertyFile 18 | - Make `appendEntry` methods public (#7) 19 | - Replace `setValue` method with `set`. `setValue` still exists, but is 20 | deprecated from now on. (#8) 21 | 22 | 23 | Version 2.0.1 - 2018-11-21 24 | -------------------------- 25 | 26 | - Escape newlines to literal newlines (#3) 27 | - When checking for value changes compare unescaped values (#4) 28 | - Escape backslashes when escaping (#5) 29 | 30 | 31 | Version 2.0.0 - 2018-11-09 32 | -------------------------- 33 | 34 | - Added functionality to reformat and reorder the entries in a PropertyFile 35 | - Renamed de.poiu.apron.Options to de.poiu.apron.ApronOptions 36 | 37 | 38 | Version 1.0.0 - 2018-09-26 39 | -------------------------- 40 | 41 | - Initial release of Apron 42 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | de.poiu.apron 5 | apron 6 | 2.1.2-SNAPSHOT 7 | apron 8 | Advanced Properties — Read and write Java .properties files in a more sane manner. 9 | https://github.com/poiu-de/apron 10 | 11 | 12 | 13 | Apache License, Version 2.0 14 | https://www.apache.org/licenses/LICENSE-2.0.txt 15 | 16 | 17 | 18 | 19 | https://github.com/poiu-de/apron 20 | scm:git:git://github.com/poiu-de/apron.git 21 | scm:git:git@github.com:poiu-de/apron.git 22 | HEAD 23 | 24 | 25 | 26 | https://github.com/poiu-de/apron/issues 27 | GitHub Issues 28 | 29 | 30 | 31 | 32 | hupfdule 33 | Marco Herrn 34 | marco@mherrn.de 35 | 36 | 37 | 38 | 39 | 40 | UTF-8 41 | 8 42 | 43 | 44 | 45 | 46 | junit 47 | junit 48 | 4.13.2 49 | test 50 | 51 | 52 | org.assertj 53 | assertj-core 54 | 3.27.3 55 | test 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | org.apache.maven.plugins 64 | maven-clean-plugin 65 | 3.4.1 66 | 67 | 68 | org.apache.maven.plugins 69 | maven-enforcer-plugin 70 | 3.5.0 71 | 72 | 73 | org.apache.maven.plugins 74 | maven-source-plugin 75 | 3.3.1 76 | 77 | 78 | org.apache.maven.plugins 79 | maven-resources-plugin 80 | 3.3.1 81 | 82 | 83 | org.apache.maven.plugins 84 | maven-surefire-plugin 85 | 3.5.3 86 | 87 | 88 | org.apache.maven.plugins 89 | maven-install-plugin 90 | 3.1.4 91 | 92 | 93 | org.apache.maven.plugins 94 | maven-deploy-plugin 95 | 3.1.4 96 | 97 | 98 | org.apache.maven.plugins 99 | maven-jar-plugin 100 | 3.4.2 101 | 102 | 103 | org.apache.maven.plugins 104 | maven-javadoc-plugin 105 | 3.11.2 106 | 107 | 108 | org.apache.maven.plugins 109 | maven-release-plugin 110 | 3.1.1 111 | 112 | 113 | org.apache.maven.plugins 114 | maven-site-plugin 115 | 3.21.0 116 | 117 | 118 | org.apache.maven.plugins 119 | maven-project-info-reports-plugin 120 | 3.9.0 121 | 122 | 123 | org.apache.maven.plugins 124 | maven-compiler-plugin 125 | 3.14.0 126 | 127 | 128 | org.apache.maven.plugins 129 | maven-gpg-plugin 130 | 3.2.7 131 | 132 | 133 | org.sonatype.plugins 134 | nexus-staging-maven-plugin 135 | 1.7.0 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | org.apache.maven.plugins 144 | maven-compiler-plugin 145 | 146 | 147 | default-compile 148 | 149 | 150 | 9 151 | -Xlint:unchecked 152 | 153 | 154 | 155 | base-compile 156 | 157 | compile 158 | 159 | 160 | 161 | 162 | module-info.java 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | [9,) 172 | 173 | 8 174 | 175 | 176 | 177 | 178 | org.apache.maven.plugins 179 | maven-enforcer-plugin 180 | 181 | 182 | enforce-maven 183 | 184 | enforce 185 | 186 | 187 | 188 | 189 | 3.6.3 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | org.apache.maven.plugins 198 | maven-jar-plugin 199 | 200 | 201 | 202 | true 203 | true 204 | 205 | 206 | de.poiu.apron 207 | 208 | 209 | 210 | 211 | 212 | 213 | org.apache.maven.plugins 214 | maven-javadoc-plugin 215 | 216 | 217 | ${java.home}/bin/javadoc 218 | 219 | 220 | nl.talsmasoftware.umldoclet.UMLDoclet 221 | 222 | nl.talsmasoftware 223 | umldoclet 224 | 2.1.2 225 | 226 | 227 | 228 | 229 | 230 | org.apache.maven.plugins 231 | maven-release-plugin 232 | 233 | true 234 | false 235 | release 236 | deploy 237 | 238 | 239 | 240 | 241 | org.sonatype.plugins 242 | nexus-staging-maven-plugin 243 | true 244 | 245 | ossrh 246 | https://oss.sonatype.org 247 | false 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | ossrh 256 | Sonatype Nexus Snapshots 257 | https://oss.sonatype.org/content/repositories/snapshots 258 | 259 | 260 | ossrh 261 | Nexus Release Repository 262 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 263 | 264 | 265 | 266 | 267 | 268 | release 269 | 270 | 271 | 272 | org.apache.maven.plugins 273 | maven-source-plugin 274 | 275 | 276 | attach-sources 277 | 278 | jar-no-fork 279 | 280 | 281 | 282 | 283 | 284 | org.apache.maven.plugins 285 | maven-javadoc-plugin 286 | 287 | 288 | attach-javadocs 289 | 290 | jar 291 | 292 | 293 | 294 | 295 | 296 | org.apache.maven.plugins 297 | maven-gpg-plugin 298 | 299 | 300 | sign-artifacts 301 | verify 302 | 303 | sign 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | -------------------------------------------------------------------------------- /src/main/java/de/poiu/apron/ApronOptions.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Marco Herrn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package de.poiu.apron; 17 | 18 | import java.nio.charset.Charset; 19 | import java.util.Objects; 20 | 21 | import static java.nio.charset.StandardCharsets.UTF_8; 22 | 23 | 24 | /** 25 | * Holder object to encapsulate optional parameters when writing {@link PropertyFile PropertyFiles}. 26 | *

27 | * Be aware that not all combinations of options make sense in all cases. For example a 28 | * MissingKeyAction is not useful when {@link de.poiu.apron.PropertyFile#overwrite(java.io.File, de.poiu.apron.ApronOptions) overwriting} 29 | * a file. In these cases those options are ignored. 30 | *

31 | * By default this class provides the following values: 32 | *

    33 | *
  • UTF-8 encoding to read and write .properties files with UTF-8 encoding
  • 34 | *
  • MissingKeyAction.NOTHING to leave removed key-value-pairs intact when updating .properties files
  • 35 | *
  • UnicodeHandling.DO_NOTHING to not change the original unicode value (unless writing in a 36 | * non-UTF charset in which case Unicode characters are always written as Unicode escape sequences)
  • 37 | *
38 | *

39 | * This class is immutable and therefore thread safe. All modification methods actually return a new object. 40 | * 41 | * @author mherrn 42 | */ 43 | public class ApronOptions { 44 | 45 | ///////////////////////////////////////////////////////////////////////////// 46 | // 47 | // Attributes 48 | 49 | /** The Charset to use for writing a PropertyFile. */ 50 | private final Charset charset; 51 | 52 | /** 53 | * The MissingKeyAction to apply when the updated target .properties file 54 | * contains key-value pairs that do not exist in the written PropertyFile. 55 | */ 56 | private final MissingKeyAction missingKeyAction; 57 | 58 | 59 | /** 60 | * How to handle Unicode values when writing. This only applies when writing with 61 | * a supported Unicode charset, since in all other cases Unicode values are always written 62 | * as Unicode escape sequences. 63 | */ 64 | private final UnicodeHandling unicodeHandling; 65 | 66 | ///////////////////////////////////////////////////////////////////////////// 67 | // 68 | // Constructors 69 | 70 | /** 71 | * Creates a new ApronOptions object with the default values. 72 | *

73 | * This is exactly the as if calling the static {@link #create()} method. 74 | */ 75 | public ApronOptions() { 76 | this(UTF_8, MissingKeyAction.NOTHING, UnicodeHandling.DO_NOTHING); 77 | } 78 | 79 | 80 | /** 81 | * Creates a new ApronOptions object with the given values. 82 | *

83 | * While this constructor is public and is absolutely safe to use, in most cases it is 84 | * more convenient to use the provided fluent interface, e.g. 85 | * 86 | *

 87 |    * final Options options= Options.create()
 88 |    *                               .with(StandardCharsets.ISO_8859_1)
 89 |    *                               .with(MissingKeyAction.DELETE);
 90 |    * 
91 | * 92 | * @param charset the Charset to use for writing a PropertyFile 93 | * @param missingKeyAction the MissingKeyAction to apply when the updated target .properties file 94 | * contains key-value pairs that do not exist in the written PropertyFile 95 | * @param unicodeHandling how to handle Unicode values when writing. 96 | */ 97 | public ApronOptions(final Charset charset, final MissingKeyAction missingKeyAction, final UnicodeHandling unicodeHandling) { 98 | Objects.requireNonNull(charset); 99 | Objects.requireNonNull(missingKeyAction); 100 | Objects.requireNonNull(unicodeHandling); 101 | this.charset= charset; 102 | this.missingKeyAction= missingKeyAction; 103 | this.unicodeHandling= unicodeHandling; 104 | } 105 | 106 | 107 | ///////////////////////////////////////////////////////////////////////////// 108 | // 109 | // Methods 110 | 111 | /** 112 | * Creates a new Options object with the default values. 113 | * 114 | * @return the newly created Options object 115 | */ 116 | public static ApronOptions create() { 117 | return new ApronOptions(); 118 | } 119 | 120 | 121 | /** 122 | * Returns a copy of this Options object, but with the given charset. 123 | * 124 | * @param charset the Charset to use when writing the PropertyFile. 125 | * @return this Options object 126 | */ 127 | public ApronOptions with(final Charset charset) { 128 | return new ApronOptions(charset, this.missingKeyAction, this.unicodeHandling); 129 | } 130 | 131 | 132 | /** 133 | * Returns a copy of this Options object, but with the given MissingKeyAction. 134 | *

135 | * This is only meaningful on updating a File. When writing to an output stream or overwriting a 136 | * file or creating a new file, this options does nothing. 137 | * 138 | * @param missingKeyAction how to handle key-value-pairs that exist in in the written PropertyFile, but not in the updated one 139 | * @return this Options object 140 | */ 141 | public ApronOptions with(final MissingKeyAction missingKeyAction) { 142 | return new ApronOptions(this.charset, missingKeyAction, this.unicodeHandling); 143 | } 144 | 145 | 146 | /** 147 | * Returns a copy of this Options object, but with the given UnicodeHandling 148 | * 149 | * @param unicodeHandling how to handle Unicode values when writing a PropertyFile. 150 | * @return this Options object 151 | */ 152 | public ApronOptions with(final UnicodeHandling unicodeHandling) { 153 | return new ApronOptions(this.charset, this.missingKeyAction, unicodeHandling); 154 | } 155 | 156 | 157 | /** 158 | * Returns the Charset with which to write a PropertyFile. 159 | * 160 | * @return the Charset with which to write a PropertyFile 161 | */ 162 | public Charset getCharset() { 163 | return charset; 164 | } 165 | 166 | 167 | /** 168 | * Returns the MissingKeyAction to use when updating a .properties file. 169 | *

170 | * This is only meaningful on updating a File. When writing to an output stream or overwriting a 171 | * file or creating a new file, this options does nothing. 172 | * 173 | * @return the MissingKeyAction to use when updating a .properties file 174 | */ 175 | public MissingKeyAction getMissingKeyAction() { 176 | return missingKeyAction; 177 | } 178 | 179 | 180 | /** 181 | * Returns the UnicodeHandling to use when writing a .properties file. 182 | * 183 | * @return the UnicodeHandling to use when writing a .properties file 184 | */ 185 | public UnicodeHandling getUnicodeHandling() { 186 | return unicodeHandling; 187 | } 188 | 189 | 190 | @Override 191 | public boolean equals(final Object o) { 192 | if (o == this) { 193 | return true; 194 | } 195 | if (o instanceof ApronOptions) { 196 | final ApronOptions that = (ApronOptions) o; 197 | return this.charset.equals(that.getCharset()) 198 | && this.missingKeyAction.equals(that.getMissingKeyAction()) 199 | && this.unicodeHandling.equals(that.getUnicodeHandling()); 200 | } 201 | return false; 202 | } 203 | 204 | 205 | @Override 206 | public int hashCode() { 207 | int h$ = 1; 208 | h$ *= 1000003; 209 | h$ ^= charset.hashCode(); 210 | h$ *= 1000003; 211 | h$ ^= missingKeyAction.hashCode(); 212 | h$ *= 1000003; 213 | h$ ^= unicodeHandling.hashCode(); 214 | return h$; 215 | } 216 | 217 | 218 | @Override 219 | public String toString() { 220 | return "Options{" + "charset=" + charset + ", missingKeyAction=" + missingKeyAction + ", unicodeHandling=" + unicodeHandling + '}'; 221 | } 222 | 223 | } 224 | -------------------------------------------------------------------------------- /src/main/java/de/poiu/apron/MissingKeyAction.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Marco Herrn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package de.poiu.apron; 17 | 18 | 19 | /** 20 | * A enum indicating what to do if a key-value-pair was found in the file a {@link de.poiu.apron.PropertyFile} 21 | * is written to, but not in the written PropertyFile. 22 | *

23 | * This is only relevant when updating existing files. 24 | * 25 | * @author mherrn 26 | */ 27 | public enum MissingKeyAction { 28 | /** Do nothing and leave the existing key-value-pair as it is (the default). */ 29 | NOTHING, 30 | /** Delete the key-value-pair from the file. */ 31 | DELETE, 32 | /** Comment the lines of the key-value-pair out. */ 33 | COMMENT, 34 | ; 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/de/poiu/apron/UnicodeHandling.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Marco Herrn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package de.poiu.apron; 17 | 18 | /** 19 | * Specifies how Unicode characters are written when writing a {@link de.poiu.apron.PropertyFile} to 20 | * a file or OutputStream. 21 | *

22 | * This is actually only relevant when writing in a supported UTF charset, since in other cases 23 | * Unicode characters are always written as Unicode escape sequences. 24 | * 25 | * @author mherrn 26 | */ 27 | public enum UnicodeHandling { 28 | /** 29 | * Leave existing strings as they are; write new strings according to the given charset (the default). 30 | * This does only work with UTF encodings. 31 | */ 32 | DO_NOTHING, 33 | /** 34 | * Escape all Unicode characters with \\uxxxx escape sequences. 35 | */ 36 | ESCAPE, 37 | /** 38 | * Write all Unicode characters as their real unicode value. 39 | * This does only work with UTF encodings. 40 | */ 41 | UNICODE, 42 | /** 43 | * Writes Unicode characters as their real unicode value when using UTF encoding, otherwise escape them. 44 | * This does only work with UTF encodings. 45 | */ 46 | BY_CHARSET, 47 | ; 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/de/poiu/apron/entry/BasicEntry.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Marco Herrn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package de.poiu.apron.entry; 17 | 18 | import java.util.Objects; 19 | 20 | 21 | /** 22 | * A Basic entry of a PropertyFile with no special meaning. 23 | * Ususally these are lines containg only whitespace, empty lines and comment lines. 24 | *

25 | * No un-/escaping will be done for such entries. They are handled as simple strings. 26 | * 27 | * @author mherrn 28 | */ 29 | public class BasicEntry implements Entry { 30 | 31 | ///////////////////////////////////////////////////////////////////////////// 32 | // 33 | // Attributes 34 | 35 | /** The actual content of this BasicEntry */ 36 | private final CharSequence content; 37 | 38 | 39 | ///////////////////////////////////////////////////////////////////////////// 40 | // 41 | // Constructors 42 | 43 | /** 44 | * Creates a new BasicEntry with the given content. 45 | * 46 | * @param content the actual content of the new BasicEntry 47 | */ 48 | public BasicEntry(final CharSequence content) { 49 | this.content= content; 50 | } 51 | 52 | 53 | ///////////////////////////////////////////////////////////////////////////// 54 | // 55 | // Methods 56 | 57 | @Override 58 | public CharSequence toCharSequence() { 59 | return content; 60 | } 61 | 62 | 63 | @Override 64 | public int hashCode() { 65 | int hash = 5; 66 | hash = 97 * hash + Objects.hashCode(this.content); 67 | return hash; 68 | } 69 | 70 | 71 | @Override 72 | public boolean equals(Object obj) { 73 | if (this == obj) { 74 | return true; 75 | } 76 | if (obj == null) { 77 | return false; 78 | } 79 | if (getClass() != obj.getClass()) { 80 | return false; 81 | } 82 | final BasicEntry other = (BasicEntry) obj; 83 | if (!Objects.equals(this.content, other.content)) { 84 | return false; 85 | } 86 | return true; 87 | } 88 | 89 | 90 | @Override 91 | public String toString() { 92 | return "BasicEntry{" + "content=" + content + '}'; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/de/poiu/apron/entry/Entry.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Marco Herrn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package de.poiu.apron.entry; 17 | 18 | 19 | /** 20 | * An Entry in a PropertyFile. 21 | * An entry corresponds to a logical line in a .properties file. 22 | *

23 | * These usually are empty lines, comments and key-value-pairs. 24 | * 25 | * @author mherrn 26 | */ 27 | public interface Entry { 28 | /** 29 | * Returns the actual content of this entry. 30 | *

31 | * If the Entry needs escaping the returned CharSequence already contains this escaping. 32 | * This allows the returned CharSequence to written directly to a .properties file 33 | * where certain characters (like whitespace or newlines) need to be escaped. 34 | *

35 | * The returned CharSequence also contains the trailing line ending character. 36 | * 37 | * @return the escaped content of this entry 38 | */ 39 | public CharSequence toCharSequence(); 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/de/poiu/apron/entry/PropertyEntry.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Marco Herrn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package de.poiu.apron.entry; 17 | 18 | import java.util.Objects; 19 | 20 | 21 | /** 22 | * An entry of a PropertyFile containing a key-value-pair. 23 | *

24 | * Additionally to the actual key and value this class stores 25 | *

    26 | *
  • the leading whitespace
  • 27 | *
  • the separator (with optional surrounding whitespace)
  • 28 | *
  • and the line ending character
  • 29 | *
30 | *

31 | * of the entry. This allows writing this entry back to file in exactly the same form as it was 32 | * written, retaining all the formattings. 33 | * 34 | * @author mherrn 35 | */ 36 | public class PropertyEntry implements Entry { 37 | 38 | ///////////////////////////////////////////////////////////////////////////// 39 | // 40 | // Attributes 41 | 42 | /** The leading whitespace before the key */ 43 | private CharSequence leadingWhitespace= ""; 44 | /** The actual key */ 45 | private final CharSequence key; 46 | /** The separator with surrounding whitespace */ 47 | private CharSequence separator= " = "; 48 | /** The actual value */ 49 | private CharSequence value; 50 | /** The line ending */ 51 | private CharSequence lineEnding= "\n"; 52 | 53 | 54 | ///////////////////////////////////////////////////////////////////////////// 55 | // 56 | // Constructors 57 | 58 | /** 59 | * Creates a new PropertyEntry with the given escaped key and value. 60 | *

61 | * No leading whitespace is added. 62 | *

63 | * The separator will be '=' surrounded by a single space on both sides. 64 | *

65 | * The line ending will be a '\n'. 66 | * 67 | * @param key the escaped key 68 | * @param value the esacepd value 69 | */ 70 | public PropertyEntry(final CharSequence key, final CharSequence value) { 71 | Objects.requireNonNull(key); 72 | Objects.requireNonNull(value); 73 | 74 | this.key = key; 75 | this.value = value; 76 | } 77 | 78 | 79 | /** 80 | * Creates a new PropertyEntry with the given escaped key and value. 81 | * Additionally the leading whitespace, separator (whith optional surrounding whitespace) and the 82 | * line ending character(s) need to be given. 83 | * 84 | * @param leadingWhitespace the leading whitespace before the key 85 | * @param key the escaped key 86 | * @param separator the separator with optional surrounding whitespace 87 | * @param value the esacepd value 88 | * @param lineEnding the line ending 89 | */ 90 | public PropertyEntry(final CharSequence leadingWhitespace, final CharSequence key, final CharSequence separator, final CharSequence value, final CharSequence lineEnding) { 91 | Objects.requireNonNull(leadingWhitespace); 92 | Objects.requireNonNull(key); 93 | Objects.requireNonNull(separator); 94 | Objects.requireNonNull(value); 95 | Objects.requireNonNull(lineEnding); 96 | 97 | this.leadingWhitespace= leadingWhitespace; 98 | this.key= key; 99 | this.separator= separator; 100 | this.value= value; 101 | this.lineEnding= lineEnding; 102 | } 103 | 104 | 105 | ///////////////////////////////////////////////////////////////////////////// 106 | // 107 | // Methods 108 | 109 | @Override 110 | public CharSequence toCharSequence() { 111 | return new StringBuilder() 112 | .append(leadingWhitespace) 113 | .append(key) 114 | .append(separator) 115 | .append(value) 116 | .append(lineEnding); 117 | } 118 | 119 | 120 | /** 121 | * Returns the escaped leading whitespace of this PropertyEntry. 122 | * 123 | * @return the escaped leading whitespace of this PropertyEntry 124 | * @since 2.0.0 125 | */ 126 | public CharSequence getLeadingWhitespace() { 127 | return leadingWhitespace; 128 | } 129 | 130 | 131 | /** 132 | * Returns the escaped key of this PropertyEntry. 133 | * 134 | * @return the escaped key of this PropertyEntry 135 | */ 136 | public CharSequence getKey() { 137 | return this.key; 138 | } 139 | 140 | 141 | /** 142 | * Returns the escaped separator with optional surrounding whitespace of this PropertyEntry. 143 | * 144 | * @return the escaped separator with optional surrounding whitespace of this PropertyEntry 145 | * @since 2.0.0 146 | */ 147 | public CharSequence getSeparator() { 148 | return separator; 149 | } 150 | 151 | 152 | /** 153 | * Returns the escaped value of this PropertyEntry. 154 | * 155 | * @return the escaped value of this PropertyEntry 156 | */ 157 | public CharSequence getValue() { 158 | return this.value; 159 | } 160 | 161 | 162 | /** 163 | * Sets the new escaped value for this PropertyEntry. 164 | * 165 | * @param value the new escaped value for this PropertyEntry 166 | */ 167 | public void setValue(final CharSequence value) { 168 | this.value= value; 169 | } 170 | 171 | 172 | /** 173 | * Returns the escaped line ending of this PropertyEntry. 174 | * 175 | * @return the escaped line ending of this PropertyEntry 176 | * @since 2.0.0 177 | */ 178 | public CharSequence getLineEnding() { 179 | return lineEnding; 180 | } 181 | 182 | 183 | @Override 184 | public int hashCode() { 185 | int hash = 3; 186 | hash = 97 * hash + Objects.hashCode(this.leadingWhitespace); 187 | hash = 97 * hash + Objects.hashCode(this.key); 188 | hash = 97 * hash + Objects.hashCode(this.separator); 189 | hash = 97 * hash + Objects.hashCode(this.value); 190 | hash = 97 * hash + Objects.hashCode(this.lineEnding); 191 | return hash; 192 | } 193 | 194 | 195 | @Override 196 | public boolean equals(Object obj) { 197 | if (this == obj) { 198 | return true; 199 | } 200 | if (obj == null) { 201 | return false; 202 | } 203 | if (getClass() != obj.getClass()) { 204 | return false; 205 | } 206 | final PropertyEntry other = (PropertyEntry) obj; 207 | if (!Objects.equals(this.leadingWhitespace, other.leadingWhitespace)) { 208 | return false; 209 | } 210 | if (!Objects.equals(this.key, other.key)) { 211 | return false; 212 | } 213 | if (!Objects.equals(this.separator, other.separator)) { 214 | return false; 215 | } 216 | if (!Objects.equals(this.value, other.value)) { 217 | return false; 218 | } 219 | if (!Objects.equals(this.lineEnding, other.lineEnding)) { 220 | return false; 221 | } 222 | return true; 223 | } 224 | 225 | 226 | @Override 227 | public String toString() { 228 | return "PropertyEntry{" + "leadingWhitespace=" + leadingWhitespace + ", key=" + key + ", separator=" + separator + ", value=" + value + ", lineEnding=" + lineEnding + '}'; 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/main/java/de/poiu/apron/escaping/EscapeUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Marco Herrn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package de.poiu.apron.escaping; 17 | 18 | import java.util.logging.Level; 19 | import java.util.logging.Logger; 20 | 21 | 22 | /** 23 | * Helper class for escaping and unescaping of entries in .properties files. 24 | * 25 | * @author mherrn 26 | */ 27 | public class EscapeUtils { 28 | 29 | private static final Logger LOGGER= Logger.getLogger(EscapeUtils.class.getName()); 30 | 31 | /** The valid hex digits. Used for un-/escaping of unicode characters */ 32 | private static final char[] HEX_DIGITS= {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9' 33 | , 'a', 'b', 'c', 'd', 'e', 'f' 34 | , 'A', 'B', 'C', 'D', 'E', 'F' }; 35 | 36 | 37 | ///////////////////////////////////////////////////////////////////////////// 38 | // 39 | // Attributes 40 | 41 | ///////////////////////////////////////////////////////////////////////////// 42 | // 43 | // Constructors 44 | 45 | ///////////////////////////////////////////////////////////////////////////// 46 | // 47 | // Methods 48 | 49 | /** 50 | * Translates escaped Unicode values of the form \\u\d\d\d\d (where \d is a valid hex 51 | * digig) back to Unicode. 52 | * 53 | * @param input CharSequence that is being translated 54 | * @return the result of the conversion 55 | * @throws InvalidUnicodeCharacterException if the given excape sequence cannot be conerted to unicode 56 | */ 57 | static char translateUnicode(final CharSequence input) throws InvalidUnicodeCharacterException { 58 | // Get 4 hex digits 59 | final CharSequence unicode = input.subSequence(2, 6); 60 | try { 61 | final int value = Integer.parseInt(unicode.toString(), 16); 62 | return (char) value; 63 | } catch (final NumberFormatException nfe) { 64 | throw new InvalidUnicodeCharacterException("Unable to parse unicode value: " + unicode, nfe); 65 | } 66 | } 67 | 68 | 69 | /** 70 | * Returns a copy of the given CharSequence where leading whitespace from each line 71 | * is removed. 72 | * 73 | * @param s the CharSeuqence to operate on 74 | * @return the given CharSequences with leading whitespace stripped from all lines 75 | */ 76 | public static CharSequence removeLeadingWhitespace(final CharSequence s) { 77 | final StringBuilder sb= new StringBuilder(); 78 | 79 | boolean nonWhitespaceFound= false; 80 | for (int i=0; i < s.length(); i++) { 81 | final char c= s.charAt(i); 82 | 83 | if (c == '\n') { 84 | nonWhitespaceFound= false; 85 | sb.append(c); 86 | } else if (c == '\r') { 87 | if (i < s.length() && (char) s.charAt(i) == '\n') { 88 | i++; 89 | } 90 | nonWhitespaceFound= false; 91 | sb.append(c); 92 | } else if (!nonWhitespaceFound && (c == ' ' || c == '\t' || c == '\f')) { 93 | // this is whitespace at the beginning of a line, we ignore it 94 | } else { 95 | nonWhitespaceFound= true; 96 | sb.append(c); 97 | } 98 | } 99 | 100 | return sb; 101 | } 102 | 103 | 104 | /** 105 | * Unescapes a given char sequence. The following unescaping will be done by this method: 106 | *

    107 | *
  • all \\uXXXX unicode escape sequences are replaced by the actual unicode value
  • 108 | *
  • all backslashes that escape characters that need no escaping are removed
  • 109 | *
  • backslashes as the last character are removed
  • 110 | *
  • literal newlines are replaced by real newlines
  • 111 | *
  • whitespace after a newline is removed
  • 112 | *
113 | *

114 | * This method does not throw an exception. If an invalid unicode escape sequence is found, an 115 | * error will be logged and the invalid escape sequence will be left unmodified. 116 | *

117 | * This method may be used for property keys as well as property values. There is no difference 118 | * in the unescaping. 119 | * 120 | * @param s the char sequence to unescape 121 | * @return the unescaped char sequence 122 | */ 123 | public static CharSequence unescape(final CharSequence s) { 124 | final StringBuilder sb= new StringBuilder(); 125 | 126 | boolean nonWhitespaceFound= false; 127 | for (int i=0; i < s.length(); i++) { 128 | final char c= s.charAt(i); 129 | 130 | if (c == '\\') { 131 | // if this is the last character, just remove it 132 | if (i == s.length() -1) { 133 | continue; 134 | } 135 | 136 | // two backslashes are reduced to one 137 | if (i + 1 < s.length() && (char) s.charAt(i + 1) == '\\') { 138 | sb.append(c); 139 | i++; 140 | continue; 141 | } 142 | 143 | // process unicode escape sequence 144 | if (i + 1 < s.length() && (char) s.charAt(i + 1) == 'u') { 145 | if (i + 5 < s.length()) { 146 | // only try to unescape a unicode value if there are enough characters left… 147 | try { 148 | // translate the escape sequence to unicode and add that unicode character 149 | sb.append(translateUnicode(s.subSequence(i, i + 6))); 150 | i= i+5; 151 | } catch (InvalidUnicodeCharacterException e) { 152 | // if the character cannot be translated, leave the escape sequence as it is, 153 | // but log an error. 154 | sb.append(c); 155 | LOGGER.log(Level.SEVERE, "Found invalid unicode escape sequence {0}. No conversion will be done. Be aware that this file cannot be read by java.util.Properties! The escape sequence should be fixed!", s.subSequence(i, i + 6)); 156 | } 157 | continue; 158 | } else { 159 | //…otherwise just leave it as it is 160 | sb.append(c); 161 | LOGGER.log(Level.SEVERE, "Found invalid unicode escape sequence {0}. No conversion will be done. Be aware that this file cannot be read by java.util.Properties! The escape sequence should be fixed!", s.subSequence(i, i + s.length() - i)); 162 | } 163 | } 164 | 165 | //replace literal newline with real newline 166 | if (i + 1 < s.length() && (char) s.charAt(i + 1) == 'n') { 167 | sb.append('\n'); 168 | i++; 169 | } else if (i + 1 < s.length() && (char) s.charAt(i + 1) == 'r') { 170 | sb.append('\r'); 171 | i++; 172 | if (i + 2 < s.length() && (char) s.charAt(i + 2) == '\\' && (char) s.charAt(i + 3) == 'n') { 173 | sb.append('\n'); 174 | i+=2; 175 | } 176 | } 177 | 178 | // in all other cases the backslash is silently dropped 179 | 180 | } else if (c == '\n') { 181 | // remove all newline characters 182 | nonWhitespaceFound= false; 183 | } else if (c == '\r') { 184 | // remove all newline characters 185 | if (i + 1 < s.length() && (char) s.charAt(i + 1) == '\n') { 186 | i++; 187 | } 188 | nonWhitespaceFound= false; 189 | } else if (!nonWhitespaceFound && (c == ' ' || c == '\t' || c == '\f')) { 190 | // this is whitespace at the beginning of a line, we ignore it 191 | } else { 192 | // all other characters go to the output 193 | nonWhitespaceFound= true; 194 | sb.append(c); 195 | } 196 | } 197 | 198 | return sb; 199 | } 200 | 201 | 202 | /** 203 | * Returns a copy of the given CharSequence where all Unicode escaped sequences are 204 | * replaced by their actual unicode value. 205 | * 206 | * @param s the CharSequence to operate on 207 | * @return the given CharSequence with all unicode escape sequences replaced by their actual unicode value 208 | */ 209 | //This method is very much "inspired" by org.apache.commons.text.translate.UnicodeEscaper 210 | public static CharSequence unescapeUnicode(final CharSequence s) { 211 | final StringBuilder sb= new StringBuilder(); 212 | 213 | for (int i=0; i < s.length(); i++) { 214 | final char c= s.charAt(i); 215 | 216 | if (c == '\\') { 217 | // process unicode escape sequence 218 | if (i + 1 < s.length() && (char) s.charAt(i + 1) == 'u') { 219 | if (i + 5 < s.length()) { 220 | // only try to unescape a unicode value if there are enough characters left… 221 | try { 222 | // translate the escape sequence to unicode and add that unicode character 223 | sb.append(translateUnicode(s.subSequence(i, i + 6))); 224 | i= i+5; 225 | } catch (InvalidUnicodeCharacterException e) { 226 | // if the character cannot be translated, leave the escape sequence as it is, 227 | // but log an error. 228 | sb.append(c); 229 | LOGGER.log(Level.SEVERE, "Found invalid unicode escape sequence {0}. No conversion will be done. Be aware that this file cannot be read by java.util.Properties! The escape sequence should be fixed!", s.subSequence(i, i + 6)); 230 | } 231 | } else { 232 | //…otherwise just leave it as it is 233 | sb.append(c); 234 | LOGGER.log(Level.SEVERE, "Found invalid unicode escape sequence {0}. No conversion will be done. Be aware that this file cannot be read by java.util.Properties! The escape sequence should be fixed!", s.subSequence(i, i + s.length() - i)); 235 | } 236 | } else { 237 | // if this backslash is not part of unicode escape, just leave it as it is 238 | sb.append(c); 239 | } 240 | } else { 241 | // all other characters go unmodified to the output 242 | sb.append(c); 243 | } 244 | } 245 | 246 | return sb; 247 | } 248 | 249 | 250 | /** 251 | * Returns a copy of the given CharSequence where all characters that need to be 252 | * escaped to be used as a value in a .properties file are escaped. 253 | *

254 | * The following conversions are done when escaping property values: 255 | *

    256 | *
  • newline characters are translated to literal newlines.
  • 257 | *
  • backslashes are escaped by a leading backslash
  • 258 | *
259 | *

260 | * Unicode values remain in their Unicode form and are not replaced by \\uXXXX unicode escape sequences. 261 | * This will be done when writing (if necessary) 262 | * 263 | * @param s the CharSequence to operate on 264 | * @return the given CharSequences escaped to be used as a value in a .properties file 265 | */ 266 | public static CharSequence escapePropertyValue(final CharSequence s) { 267 | final StringBuilder sb= new StringBuilder(); 268 | 269 | for (int i=0; i < s.length(); i++) { 270 | final char c= s.charAt(i); 271 | 272 | //FIXME: Should this be changed to a switch-statement now? Could be more readable. 273 | // Maybe this should be done when other characters need to be prependend by a backslash. 274 | // In that case we can use the same 'case' 275 | if (c == '\n') { 276 | sb.append('\\'); 277 | sb.append('n'); 278 | } else if (c == '\r') { 279 | sb.append('\\'); 280 | sb.append('r'); 281 | } else if (c == '\\') { 282 | sb.append('\\'); 283 | sb.append(c); 284 | } else { 285 | sb.append(c); 286 | } 287 | } 288 | 289 | return sb; 290 | } 291 | 292 | 293 | /** 294 | * Returns a copy of the given CharSequence where all characters that need to be 295 | * escaped to be used as a key in a .properties file are escaped by a backslash. 296 | * These are actually 297 | *

    298 | *
  • newline characters
  • 299 | *
  • whitespace characters
  • 300 | *
  • the comment characters '#' and '!'
  • 301 | *
  • the assignment characters '=' and ':'
  • 302 | *
  • backslash characters
  • 303 | *
304 | *

305 | * Unicode values remain in their Unicode form and are not replaced by \\uXXXX unicode escape sequences. 306 | * This will be done when writing (if necessary) 307 | * 308 | * @param s the CharSequence to operate on 309 | * @return the given CharSequences escaped to be used as a key in a .properties file 310 | */ 311 | public static CharSequence escapePropertyKey(final CharSequence s) { 312 | final StringBuilder sb= new StringBuilder(); 313 | 314 | for (int i=0; i < s.length(); i++) { 315 | final char c= s.charAt(i); 316 | 317 | if (c == ' ' || c == '\t' || c == '\f' 318 | || c == '=' || c == ':' 319 | || c == '\n' || c =='\r' 320 | || c == '#' || c == '!' 321 | || c == '\\' 322 | ) { 323 | sb.append('\\'); 324 | } 325 | 326 | sb.append(c); 327 | 328 | // do not escape the \n in a \r\n sequence 329 | if (c == '\r' && i + 1 < s.length()) { 330 | final char nextChar= s.charAt(i + 1); 331 | if (nextChar == '\n') { 332 | sb.append(nextChar); 333 | i++; 334 | } 335 | } 336 | } 337 | 338 | return sb; 339 | } 340 | 341 | 342 | /** 343 | * Escapes a unicode character to a unicode escape sequence of the form \\uXXXX. 344 | * This method does not check whether a character needs such escaping. If a valid ASCII or 345 | * ISO-8859-1 character is given, this we be escaped as well. 346 | * 347 | * @param c the unicode character to escape 348 | * @return the escaped unicode character 349 | */ 350 | //This method is very much "inspired" by org.apache.commons.text.translate.UnicodeEscaper 351 | public static CharSequence escapeUnicode(final char c) { 352 | final StringBuilder sb= new StringBuilder(); 353 | 354 | final int codepoint= (int) c; 355 | if (codepoint > 0xffff) { 356 | sb.append("\\u").append(Integer.toHexString(codepoint)); 357 | } else { 358 | sb.append("\\u"); 359 | sb.append(HEX_DIGITS[(codepoint >> 12) & 15]); 360 | sb.append(HEX_DIGITS[(codepoint >> 8) & 15]); 361 | sb.append(HEX_DIGITS[(codepoint >> 4) & 15]); 362 | sb.append(HEX_DIGITS[(codepoint) & 15]); 363 | } 364 | 365 | return sb; 366 | } 367 | 368 | 369 | /** 370 | * Escapes all unicode characters in a CharSequence to a unicode escape sequence of 371 | * the form \\uXXXX. 372 | * This escapes all characters with codepoints > 0x7f. 373 | * 374 | * @param charSequence the CharSequence to escape 375 | * @return the CharSequence with unicode characters escaped 376 | */ 377 | //This method is very much "inspired" by org.apache.commons.text.translate.UnicodeEscaper 378 | public static CharSequence escapeUnicode(final CharSequence charSequence) { 379 | final StringBuilder sb= new StringBuilder(); 380 | 381 | for (int i=0; i < charSequence.length(); i++) { 382 | final int codepoint= (int) charSequence.charAt(i); 383 | 384 | if (codepoint <= 0x7f) { 385 | // all characters up to 0x7f are written as is 386 | sb.append(charSequence.charAt(i)); 387 | } else { 388 | // all characters above 0x7f are written as unicode escape sequences 389 | if (codepoint > 0xffff) { 390 | sb.append("\\u").append(Integer.toHexString(codepoint)); 391 | } else { 392 | sb.append("\\u"); 393 | sb.append(HEX_DIGITS[(codepoint >> 12) & 15]); 394 | sb.append(HEX_DIGITS[(codepoint >> 8) & 15]); 395 | sb.append(HEX_DIGITS[(codepoint >> 4) & 15]); 396 | sb.append(HEX_DIGITS[(codepoint) & 15]); 397 | } 398 | } 399 | } 400 | 401 | return sb; 402 | } 403 | 404 | 405 | /** 406 | * Checks whether the given char c is one of the chars in 407 | * possibleValues. 408 | * 409 | * @param c the char to check for 410 | * @param possibleValues the reference chars 411 | * @return whether the given char one of the given possible values 412 | */ 413 | private static boolean isOf(final char c, final char[] possibleValues) { 414 | for (char p : possibleValues) { 415 | if (p == c) { 416 | return true; 417 | } 418 | } 419 | return false; 420 | } 421 | 422 | 423 | /** 424 | * Comments a CharSequence. This is done by prepending it with a '#' character. 425 | *

426 | * Since PropertyEntries can span multiple lines, this method also prepends each consecutive line 427 | * with a '#' character. However there is not check whether the given CharSequence is a PropertyEntry. 428 | * This method just comments out all lines in the given CharSequence. 429 | * 430 | * @param charSequence the CharSequence to comment out 431 | * @return the commented out CharSequence 432 | */ 433 | //FIXME: This is not real escaping. Should this method be moved to another (probably new) class? 434 | public static CharSequence comment(final CharSequence charSequence) { 435 | final StringBuilder sb= new StringBuilder(); 436 | 437 | //start with a comment character 438 | sb.append("#"); 439 | 440 | for (int i=0; i < charSequence.length(); i++) { 441 | final char c= charSequence.charAt(i); 442 | 443 | sb.append(c); 444 | 445 | // add a comment character after each newline 446 | if (c == '\n' || c == '\r' ) { 447 | if (c == '\r' && i + 1 < charSequence.length() && charSequence.charAt(i + 1) == '\n') { 448 | sb.append('\n'); 449 | i++; 450 | } 451 | 452 | //only append a comment char if there really are more characters after the newline 453 | if (i + 1 < charSequence.length()) { 454 | sb.append("#"); 455 | } 456 | } 457 | } 458 | 459 | return sb; 460 | } 461 | } 462 | -------------------------------------------------------------------------------- /src/main/java/de/poiu/apron/escaping/InvalidUnicodeCharacterException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Marco Herrn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package de.poiu.apron.escaping; 17 | 18 | 19 | /** 20 | * An exception indicating that an invalid unicode escape sequence was given. 21 | * 22 | * @author mherrn 23 | */ 24 | class InvalidUnicodeCharacterException extends Exception { 25 | 26 | /** 27 | * @see Exception#Exception() 28 | */ 29 | public InvalidUnicodeCharacterException() { 30 | } 31 | 32 | 33 | /** 34 | * @see Exception#Exception(java.lang.String) 35 | */ 36 | public InvalidUnicodeCharacterException(String msg) { 37 | super(msg); 38 | } 39 | 40 | 41 | /** 42 | * @see Exception#Exception(java.lang.String, java.lang.Throwable) 43 | */ 44 | public InvalidUnicodeCharacterException(String message, Throwable cause) { 45 | super(message, cause); 46 | } 47 | 48 | 49 | /** 50 | * @see Exception#Exception(java.lang.Throwable) 51 | */ 52 | public InvalidUnicodeCharacterException(Throwable cause) { 53 | super(cause); 54 | } 55 | 56 | 57 | /** 58 | * @see Exception#Exception(java.lang.String, java.lang.Throwable, boolean, boolean) 59 | */ 60 | public InvalidUnicodeCharacterException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { 61 | super(message, cause, enableSuppression, writableStackTrace); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/de/poiu/apron/io/PropertyFileReader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Marco Herrn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package de.poiu.apron.io; 17 | 18 | import de.poiu.apron.entry.BasicEntry; 19 | import de.poiu.apron.entry.Entry; 20 | import de.poiu.apron.entry.PropertyEntry; 21 | import java.io.BufferedReader; 22 | import java.io.Closeable; 23 | import java.io.File; 24 | import java.io.FileInputStream; 25 | import java.io.FileNotFoundException; 26 | import java.io.IOException; 27 | import java.io.InputStream; 28 | import java.io.InputStreamReader; 29 | import java.io.Reader; 30 | import java.nio.charset.Charset; 31 | import java.util.Optional; 32 | import java.util.logging.Level; 33 | import java.util.logging.Logger; 34 | 35 | 36 | /** 37 | * A reader to read a PropertyFile from different sources. 38 | *

39 | * Be aware that this class is not thread safe! 40 | * 41 | * @author mherrn 42 | */ 43 | public class PropertyFileReader implements Closeable { 44 | 45 | private static final Logger LOGGER= Logger.getLogger(PropertyFileReader.class.getName()); 46 | 47 | 48 | ///////////////////////////////////////////////////////////////////////////// 49 | // 50 | // Attributes 51 | 52 | /** The actual reader to read. */ 53 | private final BufferedReader reader; 54 | 55 | 56 | ///////////////////////////////////////////////////////////////////////////// 57 | // 58 | // Constructors 59 | 60 | /** 61 | * Creates a new PropertyFileReader to read from the given file. 62 | *

63 | * The file is assumed to be UTF-8 encoded. 64 | * 65 | * @param propertyFile the file to read 66 | * @throws java.io.FileNotFoundException if the given file does not exist 67 | */ 68 | public PropertyFileReader(final File propertyFile) throws FileNotFoundException { 69 | this(propertyFile, Charset.forName("UTF-8")); 70 | } 71 | 72 | 73 | /** 74 | * Creates a new PropertyFileReader to read from the given file. 75 | *

76 | * The file is assumed to be in the given encoding. 77 | * 78 | * @param propertyFile the file to read 79 | * @param charset the encoding of the file 80 | * @throws java.io.FileNotFoundException if the given file does not exist 81 | */ 82 | public PropertyFileReader(final File propertyFile, final Charset charset) throws FileNotFoundException { 83 | this.reader= new BufferedReader(new InputStreamReader(new FileInputStream(propertyFile), charset)); 84 | } 85 | 86 | 87 | /** 88 | * Creates a new PropertyFileReader to read from the given reader. 89 | * 90 | * @param reader the reader to read from 91 | */ 92 | public PropertyFileReader(final Reader reader) { 93 | this.reader= new BufferedReader(reader); 94 | } 95 | 96 | 97 | /** 98 | * Creates a new PropertyFileReader to read from the given InputStream. 99 | *

100 | * The stream is assumed to be UTF-8 encoded. 101 | * 102 | * @param inputStream the InputStream to read from 103 | */ 104 | public PropertyFileReader(final InputStream inputStream) { 105 | this(inputStream, Charset.forName("UTF-8")); 106 | } 107 | 108 | 109 | /** 110 | * Creates a new PropertyFileReader to read from the given InputStream. 111 | *

112 | * The stream is assumed to be in the given encoding. 113 | * 114 | * @param inputStream the InputStream to read from 115 | * @param charset the encoding of the stream 116 | */ 117 | public PropertyFileReader(final InputStream inputStream, final Charset charset) { 118 | this.reader= new BufferedReader(new InputStreamReader(inputStream, charset)); 119 | } 120 | 121 | 122 | ///////////////////////////////////////////////////////////////////////////// 123 | // 124 | // Methods 125 | 126 | /** 127 | * Reads a single entry from this reader. 128 | *

129 | * If no the source does not provide any more entries, null is returned. 130 | * 131 | * @return the next Entry or null if there are no more entries 132 | * @throws java.io.IOException if reading the next entry failed 133 | */ 134 | public Entry readEntry() throws IOException { 135 | final CharSequence logicalLine= readLogicalLine(); 136 | 137 | if (logicalLine != null) { 138 | // process this logical line 139 | final Entry entry= this.parseLogicalLine(logicalLine); 140 | return entry; 141 | } else { 142 | // or return null if there are no more entries 143 | return null; 144 | } 145 | } 146 | 147 | 148 | /** 149 | * Reads a logical line from the reader. 150 | * This actually reads until the next unescaped line break 151 | * (either '\n', '\r' or '\r' immediately followed by '\n'). 152 | * The line break will be included in the string. 153 | *

154 | * Be aware that this method always respects the escaping of line breaks, though the 155 | * {@link java.util.Properties#load(java.io.InputStream) } API states that this is not valid 156 | * for comment lines. Therefore the returned logical line may be split if it contains a comment 157 | * line with a backslash as the last character. 158 | * 159 | * @return null, if there is nothing more to read or a string containing the 160 | * logical lines content including the newline characters 161 | * @throws IOException 162 | */ 163 | private CharSequence readLogicalLine() throws IOException { 164 | // if there is nothing more to read, return null to indicate the EOS 165 | if (!reader.ready()) { 166 | return null; 167 | } 168 | 169 | // otherwise read the next logical line into a StringBuilder 170 | final StringBuilder sb= new StringBuilder(); 171 | 172 | int cInt; 173 | boolean escaped= false; 174 | boolean isCommentLine= false; 175 | boolean isEmptyLine= true; 176 | while ((cInt= reader.read()) != -1) { 177 | final char c= (char) cInt; 178 | sb.append(c); 179 | 180 | //if the first non-whitespace character is a comment char, this is a comment 181 | if (isEmptyLine && (c == '#' || c == '!')) { 182 | isCommentLine= true; 183 | isEmptyLine= false; 184 | } 185 | 186 | //if any non-whitespace character is found, this line is not empty 187 | if (isEmptyLine && c != ' ' && c != '\t' && c != '\f' && !escaped) { 188 | // still consider this line as empty if the current char is a backslash and the next a newline 189 | reader.mark(1); 190 | final int nextCInt= reader.read(); 191 | reader.reset(); 192 | if (c != '\\' || (nextCInt != -1 && (nextCInt != '\n' && nextCInt != '\r'))) { 193 | isEmptyLine= false; 194 | } 195 | } 196 | 197 | // Stop at the first unescaped newline (or each newline for comment and empty lines) 198 | if (c == '\n' && (!escaped || isCommentLine || isEmptyLine)) { 199 | break; 200 | } else if (c == '\r' && !escaped) { 201 | // If the next character is \n consume that as well 202 | reader.mark(1); 203 | final int nextCInt= reader.read(); 204 | if (nextCInt != -1 && nextCInt == '\n') { 205 | sb.append((char) nextCInt); 206 | } else { 207 | reader.reset(); 208 | } 209 | break; 210 | } 211 | 212 | if (c == '\r' && escaped) { 213 | // check for \r\n sequence - in that case, both should be escaped, keep escaped flag on 214 | reader.mark(1); 215 | final int nextCInt= reader.read(); 216 | // not the \r\n case 217 | if (nextCInt != '\n') { 218 | escaped = false; 219 | } 220 | reader.reset(); 221 | } else { 222 | escaped = c == '\\' && !escaped; 223 | } 224 | } 225 | 226 | return sb; 227 | } 228 | 229 | 230 | /** 231 | * Parses a logical line into an Entry. 232 | *

233 | * If the logical line is empty or a comment, a BasicEntry will be returned. Otherwise a 234 | * PropertyEntry will be returned. 235 | * 236 | * @param logicalLine the line to process 237 | * @return the Entry for the given line 238 | */ 239 | private Entry parseLogicalLine(CharSequence logicalLine) { 240 | if (this.isComment(logicalLine) || this.isEmpty(logicalLine)) { 241 | return new BasicEntry(logicalLine.toString()); 242 | } 243 | 244 | // if the line was no comment and not empty it is a valid key-value-pair 245 | final CharSequence leadingWhitespace= this.parseLeadingWhitespace(logicalLine); 246 | final CharSequence key= this.parseKey(logicalLine, leadingWhitespace.length()); 247 | final CharSequence separator= this.parseSeparator(logicalLine, leadingWhitespace.length() + key.length()); 248 | final CharSequence valueWithLineEnding= this.parseValue(logicalLine, leadingWhitespace.length() + key.length() + separator.length()); 249 | final CharSequence[] valueAndLineEnding= this.splitValueAndLineEnding(valueWithLineEnding); 250 | final CharSequence value= valueAndLineEnding[0]; 251 | final CharSequence lineEnding= valueAndLineEnding[1].length() > 0 ? valueAndLineEnding[1] : "\n"; 252 | 253 | return new PropertyEntry(leadingWhitespace.toString(), key.toString(), separator.toString(), value.toString(), lineEnding.toString()); 254 | } 255 | 256 | 257 | /** 258 | * Checks whether the given logical line is a comment line. 259 | *

260 | * A line is considered a comment line if the first non-whitespace character is either a '#' 261 | * or a '!'. 262 | * 263 | * @param logicalLine the logical line to check 264 | * @return whether the given line is a comment line 265 | */ 266 | private boolean isComment(final CharSequence logicalLine) { 267 | for (int i= 0; i < logicalLine.length(); i++) { 268 | final char c= logicalLine.charAt(i); 269 | 270 | // skip all whitespace 271 | if (c == ' ' || c == '\t' || c == '\f' || c == '\n' || c == '\r') { 272 | continue; 273 | } 274 | 275 | // if the first read non-whitespace character is a comment indicator, then this is a comment 276 | if (c == '!' || c == '#') { 277 | return true; 278 | } else { 279 | return false; 280 | } 281 | } 282 | 283 | //return false if the input string was empty 284 | return false; 285 | } 286 | 287 | 288 | /** 289 | * Checks whether the given logical line is an empty line. 290 | *

291 | * A line is considered empty if it contains only non-escaped whitespace characters. 292 | * 293 | * @param logicalLine the logical line to check 294 | * @return whether the given line is empty 295 | */ 296 | private boolean isEmpty(final CharSequence logicalLine) { 297 | boolean escaped= false; 298 | for (int i= 0; i < logicalLine.length(); i++) { 299 | final char c= logicalLine.charAt(i); 300 | 301 | //a backslash as the last character of the line is ignored 302 | if (c == '\\') { 303 | if (i + 1 < logicalLine.length() && !escaped) { 304 | final char nextChar= logicalLine.charAt(i + 1); 305 | if (nextChar == '\n' || nextChar == '\r') { 306 | return true; 307 | } 308 | } else { 309 | escaped= !escaped; 310 | } 311 | } 312 | 313 | // if any non-whitespace character was found, this line is not whitespace 314 | if (c != ' ' && c != '\t' && c != '\f' && c != '\n' && c != '\r') { 315 | return false; 316 | } 317 | } 318 | 319 | //return true if the input string was empty or we have not found any non-whitespace character 320 | return true; 321 | } 322 | 323 | 324 | /** 325 | * Returns the leading whitespace from the given logical line. 326 | * 327 | * @param logicalLine the line to process 328 | * @return the leading whitespace from the given logical line 329 | */ 330 | private CharSequence parseLeadingWhitespace(final CharSequence logicalLine) { 331 | for (int i= 0; i < logicalLine.length(); i++) { 332 | final char c= logicalLine.charAt(i); 333 | 334 | // if there is no key, the whitespace is assigned to the separator and no leading whitespace remains 335 | if (c == '=' || c == ':') { 336 | return ""; 337 | } 338 | 339 | if (c != ' ' && c != '\t' && c != '\f' && c != '\n' && c != '\r') { 340 | return logicalLine.subSequence(0, i); 341 | } 342 | } 343 | 344 | return logicalLine.toString(); 345 | } 346 | 347 | 348 | /** 349 | * Returns the key from the given logical line. 350 | *

351 | * Special characters in the key (e.g. whitespace) will still be escaped in the returned key. 352 | *

353 | * This method requires that leading whitespace was already read and the therefore the starting 354 | * index of the key is known. 355 | * 356 | * @param logicalLine the logical line to process 357 | * @param startAt the starting position of the key 358 | * @return the key from the given logical line 359 | */ 360 | private CharSequence parseKey(final CharSequence logicalLine, final int startAt) { 361 | // Must be set to true at the start of each line and to false when the first non-whitespace 362 | // character was read 363 | boolean ignoreWhitespace= false; 364 | // Remembers the start of the following whitespace that may or may not be part of a key 365 | int startOfWhitespace= -1; 366 | 367 | for (int i= startAt; i < logicalLine.length(); i++) { 368 | final char c= logicalLine.charAt(i); 369 | 370 | // skip whitespace at the start of each line 371 | if (ignoreWhitespace && (c == ' ' || c == '\t' || c == '\f')) { 372 | continue; 373 | } 374 | 375 | // set ignoreWhitespace to true if we reached the end of the line 376 | // and add the read content to the result 377 | if (c == '\n' || c == '\r') { 378 | //consume following \n if present 379 | if (i + 1 < logicalLine.length()) { 380 | final char nextChar= logicalLine.charAt(i + 1); 381 | if (c == '\r' && nextChar == '\n') { 382 | i++; 383 | } 384 | } 385 | 386 | ignoreWhitespace= true; 387 | } 388 | 389 | // if this character is a backslash treat the next one as part of the key 390 | if (c == '\\' && i + 1 < logicalLine.length()) { 391 | // if the next character is a newline, then we need to ignore whitespace again 392 | final char nextChar= (char) logicalLine.charAt(i + 1); 393 | if (nextChar == '\n' || nextChar == '\r') { 394 | ignoreWhitespace= true; 395 | } 396 | 397 | i++; 398 | startOfWhitespace= i + 1; 399 | } else { 400 | // a non-escaped whitespace, equals-sign or colon means that we have reached the 401 | // separator and therefore can stop there 402 | if (c == ' ' || c == '\t' || c == '\f' || c == '\n' || c == '\r' || c == '=' || c == ':') { 403 | if (startOfWhitespace != -1) { 404 | return logicalLine.subSequence(startAt, startOfWhitespace); 405 | } else { 406 | return logicalLine.subSequence(startAt, i); 407 | } 408 | } else { 409 | ignoreWhitespace= false; 410 | startOfWhitespace= -1; 411 | } 412 | } 413 | } 414 | 415 | // if we didn't find an end of the key, the whole remaining characters are the key 416 | return logicalLine.subSequence(startAt, logicalLine.length()); 417 | } 418 | 419 | 420 | /** 421 | * Returns the separator with surrounding whitespace from the given logical line. 422 | *

423 | * This method requires that leading whitespace and key were already read and the therefore 424 | * the starting index of the separator (or its surrounding whitespace) is known. 425 | * 426 | * @param logicalLine the logical line to process 427 | * @param startAt the starting position of the separator 428 | * @return the separator with its surrounding whitespace from the given logical line 429 | */ 430 | private CharSequence parseSeparator(final CharSequence logicalLine, final int startAt) { 431 | // the equals-sign and the colon may occur only once 432 | boolean separatorCharConsumed= false; 433 | 434 | for (int i= startAt; i < logicalLine.length(); i++) { 435 | final char c= logicalLine.charAt(i); 436 | 437 | if (c == '=' || c == ':') { 438 | if (separatorCharConsumed) { 439 | // if we found another occurrence of a non-whitespace separator char this will be part of 440 | // the value 441 | return logicalLine.subSequence(startAt, i); 442 | } else { 443 | // otherwise we mark that we have found a non-whitespace separator char 444 | separatorCharConsumed= true; 445 | } 446 | } 447 | 448 | // if any non-separator char was found, we can stop here 449 | if (c != ' ' && c != '\t' && c != '\f' && c != '=' && c != ':') { 450 | return logicalLine.subSequence(startAt, i); 451 | } 452 | } 453 | 454 | // if we didn't find an end of the separator, the whole remaining characters are the separator 455 | return logicalLine.subSequence(startAt, logicalLine.length()); 456 | } 457 | 458 | 459 | /** 460 | * Returns the value from the given logical line. 461 | *

462 | * Special characters in the value (e.g. newline characters) will still be escaped in the returned key. 463 | *

464 | * This method requires that leading whitespace, key and separator were already read and the 465 | * therefore the starting index of the value is known. 466 | *

467 | * 468 | * 469 | * @param logicalLine the logical line to process 470 | * @param startAt the starting position of the value 471 | * @return the value from the given logical line 472 | */ 473 | private CharSequence parseValue(final CharSequence logicalLine, final int startAt) { 474 | // Must be set to true at the start of each line and to false when the first non-whitespace 475 | // character was read 476 | boolean ignoreWhitespace= true; 477 | 478 | Optional firstNonWhitespaceCharPos= Optional.empty(); 479 | 480 | for (int i= startAt; i < logicalLine.length(); i++) { 481 | final char c= logicalLine.charAt(i); 482 | 483 | // skip whitespace at the start of each line 484 | if (ignoreWhitespace && (c == ' ' || c == '\t' || c == '\f')) { 485 | continue; 486 | } 487 | 488 | // if we found the first nonWhitespace char, remmber it 489 | if (!firstNonWhitespaceCharPos.isPresent()) { 490 | firstNonWhitespaceCharPos= Optional.of(i); 491 | } 492 | 493 | // set ignoreWhitespace to true if we reached the end of the line 494 | // and add the read content to the result 495 | if (c == '\n' || c == '\r') { 496 | //consume following \n if present 497 | if (i + 1 < logicalLine.length()) { 498 | final char nextChar= logicalLine.charAt(i + 1); 499 | if (c == '\r' && nextChar == '\n') { 500 | i++; 501 | } 502 | } 503 | 504 | ignoreWhitespace= true; 505 | } 506 | } 507 | 508 | return logicalLine.subSequence(firstNonWhitespaceCharPos.orElse(startAt), logicalLine.length()); 509 | } 510 | 511 | 512 | /** 513 | * Splits a CharSequence known to be a value and its (optional) trailing newline 514 | * character(s) into a CharSequence array with exactly two parts. 515 | *

516 | * The first part will be the actual value without the trailing newline character(s), 517 | * the second one will be the trailing newline characters. 518 | *

519 | * Both CharSequences are guaranteed to be non-null. If any of them does not exist an empty 520 | * CharSequence will be returned for the part. 521 | * 522 | * @param valueWithLineEnding the CharSequence to process 523 | * @return an array containing the value at the first position and the trailing newline character(s) as the second position 524 | */ 525 | private CharSequence[] splitValueAndLineEnding(CharSequence valueWithLineEnding) { 526 | final CharSequence[] result= new CharSequence[2]; 527 | 528 | for (int i= valueWithLineEnding.length() - 1; i > -1; i--) { 529 | final char c= valueWithLineEnding.charAt(i); 530 | 531 | if (c != '\r' && c != '\n') { 532 | result[0]= valueWithLineEnding.subSequence(0, i + 1); 533 | result[1]= valueWithLineEnding.subSequence(i + 1, valueWithLineEnding.length()); 534 | return result; 535 | } 536 | } 537 | 538 | // if we only found newline characters, the value will be empty 539 | result[0]= ""; 540 | result[1]= valueWithLineEnding; 541 | return result; 542 | } 543 | 544 | 545 | @Override 546 | public void close() throws IOException { 547 | if (this.reader != null) { 548 | try { 549 | this.reader.close(); 550 | } catch (IOException e) { 551 | LOGGER.log(Level.WARNING, "Error closing reader.", e); 552 | } 553 | } 554 | } 555 | } 556 | -------------------------------------------------------------------------------- /src/main/java/de/poiu/apron/io/PropertyFileWriter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Marco Herrn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package de.poiu.apron.io; 17 | 18 | import de.poiu.apron.ApronOptions; 19 | import de.poiu.apron.UnicodeHandling; 20 | import de.poiu.apron.entry.Entry; 21 | import de.poiu.apron.escaping.EscapeUtils; 22 | import java.io.BufferedWriter; 23 | import java.io.Closeable; 24 | import java.io.File; 25 | import java.io.FileNotFoundException; 26 | import java.io.FileOutputStream; 27 | import java.io.IOException; 28 | import java.io.OutputStream; 29 | import java.io.OutputStreamWriter; 30 | import java.io.Writer; 31 | import java.util.logging.Level; 32 | import java.util.logging.Logger; 33 | 34 | import static java.nio.charset.StandardCharsets.UTF_16; 35 | import static java.nio.charset.StandardCharsets.UTF_16BE; 36 | import static java.nio.charset.StandardCharsets.UTF_16LE; 37 | import static java.nio.charset.StandardCharsets.UTF_8; 38 | 39 | 40 | /** 41 | * A writer to write a PropertyFile to different targets. 42 | *

43 | * Be aware that this class is not thread safe! 44 | * 45 | * @author mherrn 46 | */ 47 | public class PropertyFileWriter implements Closeable { 48 | 49 | private static final Logger LOGGER= Logger.getLogger(PropertyFileWriter.class.getName()); 50 | 51 | 52 | ///////////////////////////////////////////////////////////////////////////// 53 | // 54 | // Attributes 55 | 56 | /** The actual Writer doing the writing */ 57 | private final BufferedWriter writer; 58 | 59 | /** The options to use for writing */ 60 | private final ApronOptions options; 61 | 62 | 63 | ///////////////////////////////////////////////////////////////////////////// 64 | // 65 | // Constructors 66 | 67 | /** 68 | * Creates a new PropertyFileWrite to write to the given file. 69 | *

70 | * The file will be written in UTF-8 encoding. 71 | * 72 | * @param propertyFile the file to write to 73 | * @throws java.io.FileNotFoundException if the file cannot be written 74 | */ 75 | public PropertyFileWriter(final File propertyFile) throws FileNotFoundException { 76 | this(propertyFile, ApronOptions.create().with(UTF_8)); 77 | } 78 | 79 | 80 | /** 81 | * Creates a new PropertyFileWrite to write to the given file. 82 | * 83 | * @param propertyFile the file to write to 84 | * @param options the options to use for writing 85 | * @throws java.io.FileNotFoundException if the file cannot be written 86 | */ 87 | public PropertyFileWriter(final File propertyFile, final ApronOptions options) throws FileNotFoundException { 88 | this.writer= new BufferedWriter(new OutputStreamWriter(new FileOutputStream(propertyFile), options.getCharset())); 89 | this.options= options; 90 | } 91 | 92 | 93 | /** 94 | * 95 | * @param writer 96 | * @deprecated Deprecated, since we would not be able to find out the Encoding of that writer. 97 | * But we need to know the encoding to decide whether we escape unicode sequences or not via \\uxxxx. 98 | */ 99 | @Deprecated 100 | private PropertyFileWriter(final Writer writer) { 101 | this.writer= new BufferedWriter(writer); 102 | this.options= null; 103 | } 104 | 105 | 106 | /** 107 | * Creates a new PropertyFileWrite to write to the given OutputStream. 108 | *

109 | * UTF-8 encoding will be used to write to the OutputStream. 110 | * 111 | * @param outputStream the OutputStream to write to 112 | */ 113 | public PropertyFileWriter(final OutputStream outputStream) { 114 | this(outputStream, ApronOptions.create().with(UTF_8)); 115 | } 116 | 117 | 118 | /** 119 | * Creates a new PropertyFileWrite to write to the given OutputStream. 120 | * 121 | * @param outputStream the OutputStream to write to 122 | * @param options the options to use for writing 123 | */ 124 | public PropertyFileWriter(final OutputStream outputStream, final ApronOptions options) { 125 | this.writer= new BufferedWriter(new OutputStreamWriter(outputStream, options.getCharset())); 126 | this.options= options; 127 | } 128 | 129 | 130 | ///////////////////////////////////////////////////////////////////////////// 131 | // 132 | // Methods 133 | 134 | /** 135 | * Writes an Entry to this Writer. 136 | * 137 | * @param entry the entry to write 138 | * @throws java.io.IOException if the writing failed 139 | */ 140 | public void writeEntry(final Entry entry) throws IOException { 141 | if (options.getUnicodeHandling() == UnicodeHandling.ESCAPE 142 | || !useUTF()) { 143 | // if the encoding is not one of the supported Unicode encodings 144 | // escape all unicode values with \\uxxxx 145 | writer.append(EscapeUtils.escapeUnicode(entry.toCharSequence())); 146 | } else if (options.getUnicodeHandling() == UnicodeHandling.UNICODE) { 147 | writer.append(EscapeUtils.unescapeUnicode(entry.toCharSequence())); 148 | } else if (options.getUnicodeHandling() == UnicodeHandling.BY_CHARSET 149 | && useUTF()) { 150 | writer.append(EscapeUtils.unescapeUnicode(entry.toCharSequence())); 151 | } else { 152 | // …otherwise write the content as is 153 | writer.append(entry.toCharSequence()); 154 | } 155 | } 156 | 157 | 158 | @Override 159 | public void close() throws IOException { 160 | if (this.writer != null) { 161 | try { 162 | this.writer.close(); 163 | } catch (IOException e) { 164 | LOGGER.log(Level.WARNING, "Error closing writer.", e); 165 | } 166 | } 167 | } 168 | 169 | 170 | /** 171 | * Checks if the requested charset is one of the supported Unicode encodings. 172 | * 173 | * @return whether the requested charset is one of the supported Unicode encodings 174 | */ 175 | private boolean useUTF() { 176 | //FIXME: UTF-32 is not in the required Charsets. Should we still support it? 177 | return options.getCharset() == UTF_8 178 | || options.getCharset() == UTF_16 179 | || options.getCharset() == UTF_16LE 180 | || options.getCharset() == UTF_16BE; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/main/java/de/poiu/apron/java/util/Helper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Marco Herrn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package de.poiu.apron.java.util; 17 | 18 | 19 | /** 20 | * Small helper methods that are used in multiple places. 21 | * 22 | * @author mherrn 23 | */ 24 | class Helper { 25 | 26 | /** 27 | * Checks whether we currently run with a Java version of at least 9. 28 | * 29 | * @return true if the currently running Java VM is at least version 9, 30 | * otherwise false 31 | */ 32 | static boolean isJava9OrHigher() { 33 | final String javaVersion = System.getProperty("java.version"); 34 | 35 | if (javaVersion.contains(".")) { 36 | final int majorVersion = Integer.parseInt(javaVersion.split("\\.")[0]); 37 | return majorVersion >= 9; 38 | } else { 39 | // no dot in the version ususally means an early access version like "14-ea" 40 | return true; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/de/poiu/apron/reformatting/AttachCommentsTo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Marco Herrn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package de.poiu.apron.reformatting; 17 | 18 | import de.poiu.apron.entry.Entry; 19 | import de.poiu.apron.entry.PropertyEntry; 20 | import java.util.ArrayList; 21 | import java.util.Arrays; 22 | import java.util.Collections; 23 | import java.util.List; 24 | import java.util.ListIterator; 25 | import java.util.stream.Collectors; 26 | 27 | 28 | /** 29 | * Specifies how to handle comment lines and empty lines when reordering the properties in .properties files. 30 | * 31 | * @author mherrn 32 | * @since 2.0.0 33 | */ 34 | public enum AttachCommentsTo { 35 | /** 36 | * Comments and empty lines are attached to the key-value pair after them. 37 | */ 38 | NEXT_PROPERTY { 39 | /** 40 | * Creates a list of OrderableEntries where BasicEntries are combined with the PropertyEntry directly 41 | * following them. BasicEntries at the end of the given list form their own OrderableEntry. 42 | * 43 | * @param entries the entries to convert 44 | * @return the result of the conversion 45 | */ 46 | @Override 47 | OrderableEntryList toOrderableEntries(final List entries) { 48 | final List orderableEntries= new ArrayList<>(); 49 | 50 | final List buffer= new ArrayList<>(); 51 | for (final Entry entry : entries) { 52 | buffer.add(entry); 53 | if (entry instanceof PropertyEntry) { 54 | orderableEntries.add(new OrderableEntry(buffer)); 55 | buffer.clear(); 56 | } 57 | } 58 | 59 | if (!buffer.isEmpty()) { 60 | orderableEntries.add(new OrderableEntry(buffer)); 61 | } 62 | 63 | return new OrderableEntryList(orderableEntries); 64 | } 65 | 66 | /** 67 | * Sorts a list of OrderableEntries. 68 | * All BasicEntries that are not connected to a PropertyEntry are put to the end of the list. 69 | * 70 | * @param orderableEntries the OrderableEntries to sort 71 | */ 72 | @Override 73 | void sort(final List orderableEntries) { 74 | Collections.sort(orderableEntries, (final OrderableEntry o1, final OrderableEntry o2) -> { 75 | if (!o1.propertyEntry.isPresent() && !o2.propertyEntry.isPresent()) { 76 | return 0; 77 | } 78 | 79 | if (!o1.propertyEntry.isPresent()) { 80 | return 1; 81 | } 82 | 83 | if (!o2.propertyEntry.isPresent()) { 84 | return -1; 85 | } 86 | 87 | return o1.propertyEntry.get().getKey().toString().compareTo( 88 | o2.propertyEntry.get().getKey().toString()); 89 | }); 90 | } 91 | }, 92 | 93 | 94 | /** 95 | * Comments and empty lines are attached to the key-value pair before them. 96 | */ 97 | PREV_PROPERTY { 98 | /** 99 | * Creates a list of OrderableEntries where BasicEntries are combined with the PropertyEntry directly 100 | * preceding them. BasicEntries at the start of the given list form their own OrderableEntry. 101 | * 102 | * @param entries the entries to convert 103 | * @return the result of the conversion 104 | */ 105 | @Override 106 | OrderableEntryList toOrderableEntries(final List entries) { 107 | final List orderableEntries= new ArrayList<>(); 108 | 109 | final List buffer= new ArrayList<>(); 110 | for (final Entry entry : entries) { 111 | if (entry instanceof PropertyEntry) { 112 | orderableEntries.add(new OrderableEntry(buffer)); 113 | buffer.clear(); 114 | } 115 | buffer.add(entry); 116 | } 117 | 118 | if (!buffer.isEmpty()) { 119 | orderableEntries.add(new OrderableEntry(buffer)); 120 | } 121 | 122 | return new OrderableEntryList(orderableEntries); 123 | } 124 | 125 | /** 126 | * Sorts a list of OrderableEntries. 127 | * All BasicEntries that are not connected to a PropertyEntry are put to the beginning of the list. 128 | * 129 | * @param orderableEntries the OrderableEntries to sort 130 | */ 131 | @Override 132 | void sort(final List orderableEntries) { 133 | Collections.sort(orderableEntries, (final OrderableEntry o1, final OrderableEntry o2) -> { 134 | if (!o1.propertyEntry.isPresent()&& !o2.propertyEntry.isPresent()) { 135 | return 0; 136 | } 137 | 138 | if (!o1.propertyEntry.isPresent()) { 139 | return -1; 140 | } 141 | 142 | if (!o2.propertyEntry.isPresent()) { 143 | return 1; 144 | } 145 | 146 | return o1.propertyEntry.get().getKey().toString().compareTo( 147 | o2.propertyEntry.get().getKey().toString()); 148 | }); 149 | } 150 | }, 151 | 152 | 153 | /** 154 | * Comments and empty lines remain at their current position. 155 | * For example 156 | * 157 | *

158 |    * # Comment 1
159 |    * key F = F
160 |    * key L = L
161 |    *
162 |    * # Comment 2
163 |    * key B = B
164 |    * # Comment 3
165 |    * key A = A
166 |    * 
167 | * 168 | * would be changed (by sorting alphabetically) to 169 | * 170 | *
171 |    * # Comment 1
172 |    * key A = A
173 |    * key B = B
174 |    *
175 |    * # Comment 2
176 |    * key F = F
177 |    * # Comment 3
178 |    * key L = L
179 |    * 
180 | */ 181 | ORIG_LINE { 182 | /** 183 | * Converts each entry to a single OrderableEntry. BasicEntries and PropertyEntries are not 184 | * combined. 185 | * 186 | * @param entries the entries to convert 187 | * @return the result of the conversion 188 | */ 189 | @Override 190 | OrderableEntryList toOrderableEntries(final List entries) { 191 | final List orderableEntries= entries.stream() 192 | .map(e -> { return new OrderableEntry(Arrays.asList(e)); }) 193 | .collect(Collectors.toList()); 194 | return new OrderableEntryList(orderableEntries); 195 | } 196 | 197 | /** 198 | * Sorts a list of OrderableEntries by only switching the positions of PropertyEntries. 199 | * All BasicEntries are left at their position. 200 | * 201 | * @param orderableEntries the OrderableEntries to sort 202 | */ 203 | @Override 204 | void sort(final List orderableEntries) { 205 | // only sort the PropertyEntries 206 | final List sortedPropertyEntries= orderableEntries.stream() 207 | .filter(_oe -> { return _oe.propertyEntry.isPresent(); }) 208 | .collect(Collectors.toList()); 209 | 210 | // sort the PropertyEntries 211 | Collections.sort(sortedPropertyEntries, (final OrderableEntry o1, final OrderableEntry o2) -> { 212 | if (!o1.propertyEntry.isPresent() 213 | || !o2.propertyEntry.isPresent()) { 214 | throw new IllegalArgumentException("The given OrderableEntries _must_ contain a PropertyEntry."); 215 | } 216 | 217 | return o1.propertyEntry.get().getKey().toString().compareTo( 218 | o2.propertyEntry.get().getKey().toString()); 219 | }); 220 | 221 | // now replace the existing PropertyEntries with the sorted ones (in the new order) 222 | for (final ListIterator it= orderableEntries.listIterator(); it.hasNext(); ) { 223 | final OrderableEntry next= it.next(); 224 | if (next.propertyEntry.isPresent()) { 225 | it.set(sortedPropertyEntries.remove(0)); 226 | } 227 | } 228 | 229 | if (!sortedPropertyEntries.isEmpty()) { 230 | throw new IllegalStateException("sortedPropertyEntries is not empty! This should never happen!"); 231 | } 232 | } 233 | }, 234 | ; 235 | 236 | 237 | /** 238 | * Returns an OrderableEntryList for the given list of entries. 239 | *

240 | * The OrderableEntryList combines PropertyEntries and BasicEntries according to this enums 241 | * strategy to attach comments. 242 | * 243 | * @param entries the entries for which to create an OrderableEntryList 244 | * @return the OrderableEntryList for the given list of entries 245 | */ 246 | abstract OrderableEntryList toOrderableEntries(final List entries); 247 | 248 | 249 | /** 250 | * Sorts a list of OrderableEntries according to this enums strategy to attach comments. 251 | * 252 | * @param orderableEntries the OrderableEntries to sort 253 | */ 254 | abstract void sort(final List orderableEntries); 255 | } 256 | -------------------------------------------------------------------------------- /src/main/java/de/poiu/apron/reformatting/InvalidFormatException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Marco Herrn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package de.poiu.apron.reformatting; 17 | 18 | 19 | /** 20 | * Indicates that the given PropertyFormat has an invalid format. 21 | * 22 | * @author mherrn 23 | * @since 2.0.0 24 | */ 25 | public class InvalidFormatException extends RuntimeException { 26 | 27 | /** 28 | * Constructs a new InvalidFormatException with {@code null} as its 29 | * detail message. The cause is not initialized, and may subsequently be 30 | * initialized by a call to {@link #initCause}. 31 | * @see java.lang.RuntimeException#RuntimeException() 32 | */ 33 | public InvalidFormatException() { 34 | } 35 | 36 | 37 | /** 38 | * Constructs a new InvalidFormatException with the specified detail message. 39 | * The cause is not initialized, and may subsequently be initialized by a 40 | * call to {@link #initCause}. 41 | * 42 | * @param message the detail message. The detail message is saved for 43 | * later retrieval by the {@link #getMessage()} method. 44 | * @see java.lang.RuntimeException#RuntimeException(java.lang.String) 45 | */ 46 | public InvalidFormatException(String message) { 47 | super(message); 48 | } 49 | 50 | 51 | /** 52 | * Constructs a new InvalidFormatException with the specified detail message and 53 | * cause.

Note that the detail message associated with 54 | * {@code cause} is not automatically incorporated in 55 | * this runtime exception's detail message. 56 | * 57 | * @param message the detail message (which is saved for later retrieval 58 | * by the {@link #getMessage()} method). 59 | * @param cause the cause (which is saved for later retrieval by the 60 | * {@link #getCause()} method). (A {@code null} value is 61 | * permitted, and indicates that the cause is nonexistent or 62 | * unknown.) 63 | * @see java.lang.RuntimeException#RuntimeException(java.lang.String, java.lang.Throwable) 64 | */ 65 | public InvalidFormatException(String message, Throwable cause) { 66 | super(message, cause); 67 | } 68 | 69 | 70 | /** 71 | * Constructs a new InvalidFormatException with the specified cause and a 72 | * detail message of {@code (cause==null ? null : cause.toString())} 73 | * (which typically contains the class and detail message of 74 | * {@code cause}). This constructor is useful for runtime exceptions 75 | * that are little more than wrappers for other throwables. 76 | * 77 | * @param cause the cause (which is saved for later retrieval by the 78 | * {@link #getCause()} method). (A {@code null} value is 79 | * permitted, and indicates that the cause is nonexistent or 80 | * unknown.) 81 | * @see java.lang.RuntimeException#RuntimeException(java.lang.Throwable) 82 | */ 83 | public InvalidFormatException(Throwable cause) { 84 | super(cause); 85 | } 86 | 87 | 88 | /** 89 | * Constructs a new InvalidFormatException with the specified detail 90 | * message, cause, suppression enabled or disabled, and writable 91 | * stack trace enabled or disabled. 92 | * 93 | * @param message the detail message. 94 | * @param cause the cause. (A {@code null} value is permitted, 95 | * and indicates that the cause is nonexistent or unknown.) 96 | * @param enableSuppression whether or not suppression is enabled 97 | * or disabled 98 | * @param writableStackTrace whether or not the stack trace should 99 | * be writable 100 | * @see java.lang.RuntimeException#RuntimeException(java.lang.String, java.lang.Throwable, boolean, boolean) 101 | */ 102 | public InvalidFormatException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { 103 | super(message, cause, enableSuppression, writableStackTrace); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/de/poiu/apron/reformatting/OrderableEntry.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Marco Herrn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package de.poiu.apron.reformatting; 17 | 18 | import de.poiu.apron.entry.Entry; 19 | import de.poiu.apron.entry.PropertyEntry; 20 | import java.util.ArrayList; 21 | import java.util.List; 22 | import java.util.Objects; 23 | import java.util.Optional; 24 | 25 | 26 | /** 27 | * Encapsulates an (optional) PropertyEntry and a list of BasicEntries that are attached to that 28 | * PropertyEntry or stand on their own. 29 | * 30 | * @author mherrn 31 | */ 32 | class OrderableEntry { 33 | 34 | /** 35 | * The PropertyEntry of this OrderableEntry. In the case of BasicEntries that do not belong 36 | * to a PropertyEntry this will be empty. 37 | */ 38 | final Optional propertyEntry; 39 | 40 | /** 41 | * The list of entries that comprise this OrderableEntry. At max 1 PropertyEntry may be contained 42 | * in this list. 43 | */ 44 | final List entries; 45 | 46 | 47 | /** 48 | * Creates a new OrderableEntry with the given list of Entries. 49 | *

50 | * This list may contain at max 1 PropertyEntry, but an arbitrary number of BasicEntries. 51 | * However an empty list is not accepted. 52 | * 53 | * @param entries the Entries that comprise this OrderableEntry 54 | */ 55 | public OrderableEntry(final List entries) { 56 | Objects.requireNonNull(entries); 57 | if (entries.isEmpty()) { 58 | throw new IllegalArgumentException("The given list of entries may not be empty."); 59 | } 60 | 61 | PropertyEntry pe= null; 62 | for (final Entry e : entries) { 63 | if (e instanceof PropertyEntry) { 64 | if (pe != null) { 65 | throw new RuntimeException("At max one PropertyEntry is allowed in the list of entries."); 66 | } 67 | 68 | pe= (PropertyEntry) e; 69 | } 70 | } 71 | 72 | this.propertyEntry= Optional.ofNullable(pe); 73 | this.entries= new ArrayList<>(entries); 74 | } 75 | 76 | 77 | @Override 78 | public String toString() { 79 | return "OrderableEntry{" + "propertyEntry=" + propertyEntry + ", entries=" + entries + '}'; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/de/poiu/apron/reformatting/OrderableEntryList.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Marco Herrn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package de.poiu.apron.reformatting; 17 | 18 | import de.poiu.apron.entry.Entry; 19 | import java.util.ArrayList; 20 | import java.util.Collections; 21 | import java.util.Iterator; 22 | import java.util.List; 23 | import java.util.Optional; 24 | import java.util.stream.Collectors; 25 | 26 | 27 | /** 28 | * A list of {@link OrderableEntry OrderableEntries}. 29 | *

30 | * Be aware that this class does not implement the {@link java.util.List} interface, but instead 31 | * only provides the methods that are meaningful for this class. 32 | * 33 | * @author mherrn 34 | */ 35 | class OrderableEntryList { 36 | 37 | 38 | ///////////////////////////////////////////////////////////////////////////// 39 | // 40 | // Attributes 41 | 42 | /** 43 | * The actual List of OrderableEntries that comprise this OrderableEntryList. 44 | */ 45 | private final List orderableEntries= new ArrayList<>(); 46 | 47 | 48 | ///////////////////////////////////////////////////////////////////////////// 49 | // 50 | // Constructors 51 | 52 | /** 53 | * Creates a new OrderableEntryList from the given List of OrderableEntries. 54 | * 55 | * @param orderableEntries the OrderableEntries that comprise this OrderableEntryList 56 | */ 57 | public OrderableEntryList(final List orderableEntries) { 58 | this.orderableEntries.addAll(orderableEntries); 59 | } 60 | 61 | 62 | ///////////////////////////////////////////////////////////////////////////// 63 | // 64 | // Methods 65 | 66 | /** 67 | * Returns the first (and hopefully only) OrderableEntry that contains a PropertyEntry with the 68 | * given key. 69 | *

70 | * If no such OrderableEntry is found the returned Optional is empty. 71 | * 72 | * @param propertyKey the property-key to search for 73 | * @return the first OrderableEntry containing a PropertyEntry with the given key 74 | */ 75 | //FIXME: This is a very naïve approach and not very performant 76 | // To make it more perfomant we need to manage our own mapping 77 | public Optional get(final CharSequence propertyKey) { 78 | return orderableEntries.stream() 79 | .filter(e -> { return e.propertyEntry.isPresent(); }) 80 | .findFirst(); 81 | } 82 | 83 | 84 | /** 85 | * Returns and removes the first (and hopefully only) OrderableEntry that contains a PropertyEntry 86 | * with the given key. 87 | *

88 | * If no such OrderableEntry is found the returned Optional is empty and nothing is removed from 89 | * this OrderableEntryList. 90 | *

91 | * 92 | * @param propertyKey the property-key to search for 93 | * @return the first OrderableEntry containing a PropertyEntry with the given key 94 | */ 95 | public Optional pop(final CharSequence propertyKey) { 96 | for (final Iterator it= this.orderableEntries.iterator(); it.hasNext(); ) { 97 | final OrderableEntry e= it.next(); 98 | 99 | if (e.propertyEntry.isPresent() && e.propertyEntry.get().getKey().equals(propertyKey)) { 100 | it.remove(); 101 | return Optional.of(e); 102 | } 103 | } 104 | 105 | return Optional.empty(); 106 | } 107 | 108 | 109 | /** 110 | * Returns the OrderableEntries in this OrderableEntryList. 111 | *

112 | * The returned list ist the actual list and changes to this list will be reflected. 113 | * 114 | * @return the OrderableEntries in this OrderableEntryList 115 | */ 116 | //FIXME: Should we return a mutable copy instead? 117 | public List getAll() { 118 | return this.orderableEntries; 119 | } 120 | 121 | 122 | /** 123 | * Returns the Entries contained in this OrderableEntryLists OrderableEntries. 124 | *

125 | * This is actually a flattened view on this OrderableEntryLists OrderableEntries. 126 | * Changes to this list will not be reflected in this OrderableEntryList. 127 | * 128 | * @return the Entries contained in this OrderableEntryLists OrderableEntries 129 | */ 130 | public List getAsEntries() { 131 | return Collections.unmodifiableList( 132 | this.orderableEntries.stream() 133 | .flatMap(_oe -> { return _oe.entries.stream(); }) 134 | .collect(Collectors.toList()) 135 | ); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/main/java/de/poiu/apron/reformatting/PropertyFormat.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Marco Herrn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package de.poiu.apron.reformatting; 17 | 18 | 19 | /** 20 | * Specifies the target format of a key-value pair for reformatting. 21 | *

22 | * This format specifies the leading whitespace, the separator (with optional surrounding 23 | * whitespace) and the line ending character(s). 24 | * 25 | * @author mherrn 26 | */ 27 | class PropertyFormat { 28 | 29 | /** The leading whitespace in front of the key. */ 30 | public final CharSequence leadingWhitespace; 31 | 32 | /** The separator between key and value (with optional surrounding whitespace). */ 33 | public final CharSequence separator; 34 | 35 | /** The line ending character(s). */ 36 | public final CharSequence lineEnding; 37 | 38 | 39 | /** 40 | * Creates a new PropertyFormat with the given values. 41 | * 42 | * @param leadingWhitespace the leading whitespace in front of the key 43 | * @param separator the separatorbetween key and value (with optional surrounding whitespace) 44 | * @param lineEnding the line ending character(s) 45 | */ 46 | public PropertyFormat(final CharSequence leadingWhitespace, final CharSequence separator, final CharSequence lineEnding) { 47 | this.leadingWhitespace = leadingWhitespace; 48 | this.separator = separator; 49 | this.lineEnding = lineEnding; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/de/poiu/apron/reformatting/ReformatOptions.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Marco Herrn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package de.poiu.apron.reformatting; 17 | 18 | import de.poiu.apron.UnicodeHandling; 19 | import java.nio.charset.Charset; 20 | import java.util.Objects; 21 | 22 | import static java.nio.charset.StandardCharsets.UTF_8; 23 | 24 | 25 | /** 26 | * Holder object to encapsulate optional parameters when reformatting {@link de.poiu.apron.PropertyFile PropertyFiles} 27 | * via {@link de.poiu.apron.reformatting.Reformatter}. 28 | *

29 | * Be aware that not all combinations of options make sense in all cases. For example a 30 | * {@link de.poiu.apron.reformatting.AttachCommentsTo} is not useful when {@link de.poiu.apron.reformatting.Reformatter#reformat(java.io.File) reformatting key-value pairs} 31 | * in a PropertyFile. In these cases those options are ignored. 32 | *

33 | * By default this class provides the following values: 34 | *

    35 | *
  • UTF-8 encoding to read and write .properties files with UTF-8 encoding
  • 36 | *
  • UnicodeHandling.DO_NOTHING to not change the original unicode value (unless writing in a 37 | * non-UTF charset in which case Unicode characters are always written as Unicode escape sequences)
  • 38 | *
  • the format string <key> = <value>\\n to use for formatting key-value pairs
  • 39 | *
  • false for reformatKeyAndValue to not touch the actual key and value on reformatting
  • 40 | *
  • AttachKeyAndValue.NEXT_PROPERTY to attach comments and empty lines the key-value pair after them 41 | * on reordering
  • 42 | *
43 | *

44 | * This class is immutable and therefore thread safe. All modification methods actually return a new object. 45 | * 46 | * @author mherrn 47 | * @since 2.0.0 48 | */ 49 | public class ReformatOptions { 50 | 51 | ///////////////////////////////////////////////////////////////////////////// 52 | // 53 | // Attributes 54 | 55 | // Common Options 56 | 57 | /** The Charset to use for reading and writing a PropertyFile. */ 58 | private final Charset charset; 59 | 60 | /** 61 | * How to handle Unicode values when writing. This only applies when writing with 62 | * a supported Unicode charset, since in all other cases Unicode values are always written 63 | * as Unicode escape sequences. 64 | */ 65 | private final UnicodeHandling unicodeHandling; 66 | 67 | // Reformat options 68 | 69 | /** The format to use when reformatting key-value pairs. */ 70 | private final String format; 71 | 72 | /** Whether to reformat the keys and values themselves when reformatting key-value pairs. */ 73 | private final boolean reformatKeyAndValue; 74 | 75 | // Reorder options 76 | 77 | /** How to handle comments and empty lines when reordering key-value pairs. */ 78 | private final AttachCommentsTo attachCommentsTo; 79 | 80 | 81 | ///////////////////////////////////////////////////////////////////////////// 82 | // 83 | // Constructors 84 | 85 | /** 86 | * Creates a new Options object with the default values. 87 | *

88 | * This is exactly the as if calling the static {@link #create()} method. 89 | */ 90 | public ReformatOptions() { 91 | this(UTF_8, UnicodeHandling.DO_NOTHING, " = \\n", false, AttachCommentsTo.NEXT_PROPERTY); 92 | } 93 | 94 | 95 | /** 96 | * Creates a new ReformatOptions object with the given values. 97 | *

98 | * While this constructor is public and is absolutely safe to use, in most cases it is 99 | * more convenient to use the provided fluent interface, e.g. 100 | * 101 | *

102 |    * final ReformatOptions reformatOptions= ReformatOptions.create()
103 |    *                               .with(StandardCharsets.ISO_8859_1)
104 |    *                               .withFormat("<key> :\\n\t<value>\\n");
105 |    * 
106 | * 107 | * @param charset the Charset to use for reading and writing a PropertyFile 108 | * @param unicodeHandling how to handle Unicode values when writing. 109 | * @param format the format to use on reformatting key-value pairs 110 | * @param reformatKeyAndValue whether to reformat the key and value on reformatting 111 | * @param attachCommentsTo how to handle comments and empty lines on reordering 112 | */ 113 | public ReformatOptions(final Charset charset, 114 | final UnicodeHandling unicodeHandling, 115 | final String format, 116 | final boolean reformatKeyAndValue, 117 | final AttachCommentsTo attachCommentsTo) { 118 | Objects.requireNonNull(charset); 119 | Objects.requireNonNull(unicodeHandling); 120 | Objects.requireNonNull(format); 121 | Objects.requireNonNull(attachCommentsTo); 122 | this.charset= charset; 123 | this.unicodeHandling= unicodeHandling; 124 | this.format= format; 125 | this.reformatKeyAndValue= reformatKeyAndValue; 126 | this.attachCommentsTo= attachCommentsTo; 127 | } 128 | 129 | 130 | ///////////////////////////////////////////////////////////////////////////// 131 | // 132 | // Methods 133 | 134 | /** 135 | * Creates a new ReformatOptions object with the default values. 136 | * 137 | * @return the newly created ReformatOptions object 138 | */ 139 | public static ReformatOptions create() { 140 | return new ReformatOptions(); 141 | } 142 | 143 | 144 | /** 145 | * Returns a copy of this ReformatOptions object, but with the given charset. 146 | * 147 | * @param charset the Charset to use when writing the PropertyFile. 148 | * @return this ReformatOptions object 149 | */ 150 | public ReformatOptions with(final Charset charset) { 151 | return new ReformatOptions(charset, this.unicodeHandling, this.format, this.reformatKeyAndValue, this.attachCommentsTo); 152 | } 153 | 154 | 155 | /** 156 | * Returns a copy of this ReformatOptions object, but with the given UnicodeHandling value. 157 | * 158 | * @param unicodeHandling how to handle unicode characters on writing the PropertyFile 159 | * @return this ReformatOptions object 160 | */ 161 | public ReformatOptions with(final UnicodeHandling unicodeHandling) { 162 | return new ReformatOptions(this.charset, unicodeHandling, this.format, this.reformatKeyAndValue, this.attachCommentsTo); 163 | } 164 | 165 | 166 | /** 167 | * Returns a copy of this ReformatOptions object, but with the given format string. 168 | *

169 | * The given format string must conform to the following specification: 170 | *

    171 | *
  • It may contain some leading whitespace before the key.
  • 172 | *
  • It must contain the string <key> to indicate the position of the 173 | * properties key (case doesn't matter)
  • 174 | *
  • It must contain a separator char (either a colon or an equals sign) which may 175 | * be surrounded by some whitespace characters.
  • 176 | *
  • It must contain the string <value> to indicate the position of the 177 | * properties value (case doesn't matter)
  • 178 | *
  • It must contain the line ending char(s) (either \n or \r 179 | * or \r\n)
  • 180 | *
181 | * The allowed whitespace characters are the space character, the tab character and the linefeed character. 182 | *

183 | * Therefore a typical format string is 184 | *

185 |    * <key> = <value>\n
186 |    * 
187 | * for 188 | *
    189 | *
  • no leading whitespace
  • 190 | *
  • an equals sign as separator surrounded by a single whitespace character on each side
  • 191 | *
  • \n as the line ending char.
  • 192 | *
193 | * But it may as well be 194 | *
195 |    * \t \f<key>\t: <value>\r\n
196 |    * 
197 | * for a rather strange format with 198 | *
    199 | *
  • a tab, a whitespace and a linefeed char as leading whitespace
  • 200 | *
  • a colon as separator char preceded by a tab and followed a single space character
  • 201 | *
  • \r\n as the line ending chars 202 | *
203 | *

204 | * If the format string is omitted the default value of <key> = <value>\n 205 | * will be used. 206 | * 207 | * @param format the format string to use when reformatting a PropertyFile 208 | * @return this ReformatOptions object 209 | */ 210 | public ReformatOptions withFormat(final String format) { 211 | return new ReformatOptions(this.charset, this.unicodeHandling, format, this.reformatKeyAndValue, this.attachCommentsTo); 212 | } 213 | 214 | 215 | /** 216 | * Returns a copy of this ReformatOptions object, but with the given value to reformat keys and values. 217 | *

218 | * This value specifies whether the keys and values of the reformatted entries are also reformatted 219 | * by removing insignificant whitespace, newlines and escape characters. 220 | * 221 | * @param reformatKeyAndValue whether to reformat the key and value when reformatting a PropertyFile 222 | * @return this ReformatOptions object 223 | */ 224 | public ReformatOptions withReformatKeyAndValue(final boolean reformatKeyAndValue) { 225 | return new ReformatOptions(this.charset, this.unicodeHandling, format, reformatKeyAndValue, this.attachCommentsTo); 226 | } 227 | 228 | 229 | /** 230 | * Returns a copy of this ReformatOptions object, but with the given AttachCommentsTo value. 231 | * 232 | * @param attachCommentsTo how to handle comments and empty lines when reordering a PropertyFile 233 | * @return this ReformatOptions object 234 | */ 235 | public ReformatOptions with(final AttachCommentsTo attachCommentsTo) { 236 | return new ReformatOptions(this.charset, this.unicodeHandling, this.format, this.reformatKeyAndValue, attachCommentsTo); 237 | } 238 | 239 | 240 | /** 241 | * Returns the Charset with which to write a PropertyFile. 242 | * 243 | * @return the Charset with which to write a PropertyFile 244 | */ 245 | public Charset getCharset() { 246 | return charset; 247 | } 248 | 249 | 250 | /** 251 | * Returns the UnicodeHandling to use when writing a PropertyFile. 252 | * 253 | * @return the UnicodeHandling to use when writing a PropertyFile 254 | */ 255 | public UnicodeHandling getUnicodeHandling() { 256 | return unicodeHandling; 257 | } 258 | 259 | 260 | /** 261 | * Returns the format string to use when reformatting a PropertyFile. 262 | * 263 | * @return the format string to use when reformatting a PropertyFile 264 | */ 265 | public String getFormat() { 266 | return format; 267 | } 268 | 269 | 270 | /** 271 | * Returns whether the key and value should be reformatted when reformatting a PropertyFile. 272 | * 273 | * @return whether the key and value should be reformatted when reformatting a PropertyFile 274 | */ 275 | public boolean getReformatKeyAndValue() { 276 | return reformatKeyAndValue; 277 | } 278 | 279 | 280 | /** 281 | * Returns how to handle comments and empty lines when reordering a PropertyFile. 282 | * 283 | * @return how to handle comments and empty lines when reordering a PropertyFile 284 | */ 285 | public AttachCommentsTo getAttachCommentsTo() { 286 | return attachCommentsTo; 287 | } 288 | 289 | 290 | @Override 291 | public boolean equals(final Object o) { 292 | if (o == this) { 293 | return true; 294 | } 295 | if (o instanceof ReformatOptions) { 296 | final ReformatOptions that = (ReformatOptions) o; 297 | return this.charset.equals(that.getCharset()) 298 | && this.unicodeHandling.equals(that.getUnicodeHandling()) 299 | && this.format.equals(that.format) 300 | && this.reformatKeyAndValue == that.reformatKeyAndValue 301 | && this.attachCommentsTo.equals(that.attachCommentsTo) 302 | ; 303 | } 304 | return false; 305 | } 306 | 307 | 308 | @Override 309 | public int hashCode() { 310 | int h$ = 1; 311 | h$ *= 1000003; 312 | h$ ^= charset.hashCode(); 313 | h$ *= 1000003; 314 | h$ ^= unicodeHandling.hashCode(); 315 | h$ *= 1000003; 316 | h$ ^= format.hashCode(); 317 | h$ *= 1000003; 318 | h$ ^= reformatKeyAndValue ? 1 : 0; 319 | h$ *= 1000003; 320 | h$ ^= attachCommentsTo.hashCode(); 321 | return h$; 322 | } 323 | 324 | 325 | @Override 326 | public String toString() { 327 | return "ReformatOptions{" + "charset=" + charset + ", unicodeHandling=" + unicodeHandling + ", format=" + format + ", reformatKeyAndValue=" + reformatKeyAndValue + ", attachCommentsTo=" + attachCommentsTo + '}'; 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /src/main/java/de/poiu/apron/reformatting/Reformatter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Marco Herrn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package de.poiu.apron.reformatting; 17 | 18 | import de.poiu.apron.ApronOptions; 19 | import de.poiu.apron.PropertyFile; 20 | import de.poiu.apron.entry.BasicEntry; 21 | import de.poiu.apron.entry.Entry; 22 | import de.poiu.apron.entry.PropertyEntry; 23 | import de.poiu.apron.escaping.EscapeUtils; 24 | import java.io.File; 25 | import java.nio.charset.Charset; 26 | import java.util.ArrayList; 27 | import java.util.List; 28 | import java.util.Objects; 29 | import java.util.logging.Logger; 30 | import java.util.regex.Matcher; 31 | import java.util.regex.Pattern; 32 | 33 | 34 | 35 | /** 36 | * Reformat .properties files. 37 | * 38 | * @author mherrn 39 | * @since 2.0.0 40 | */ 41 | public class Reformatter { 42 | 43 | private static final Logger LOGGER= Logger.getLogger(Reformatter.class.getName()); 44 | 45 | /** 46 | * The placeholder for the property key to be used as a snipped in a regex. 47 | */ 48 | private static final String PATTERN_SNIPPET_PLACEHOLDER_KEY= ""; 49 | 50 | /** 51 | * The placeholder for the value key to be used as a snipped in a regex. 52 | */ 53 | private static final String PATTERN_SNIPPET_PLACEHOLDER_VALUE= ""; 54 | 55 | /** 56 | * The allowed whitespace characters to be used as a snipped in a regex. 57 | */ 58 | private static final String PATTERN_SNIPPET_WHITESPACE= "( |\\\\t|\\\\f)"; 59 | 60 | /** 61 | * The allowed separator characters along with the allowed surrounding whitespace characters 62 | * to be used as a snipped in a regex. 63 | */ 64 | private static final String PATTERN_SNIPPET_SEPARATOR= "( |\\\\t|\\\\f|=|:)"; 65 | 66 | /** 67 | * The allowed line ending characters to be used as a snipped in a regex. 68 | */ 69 | private static final String PATTERN_SNIPPET_LINE_ENDING= "(\\\\n|\\\\r|\\\\r\\\\n)"; 70 | 71 | 72 | /** 73 | * A pattern specifying a valid format string for reformatting key-value pairs in .properties files. 74 | *

75 | * This pattern defines the following capture groups to refer to parts of the parsed format string: 76 | *

    77 | *
  • LEADINGWHITESPACE
  • 78 | *
  • SEPARATOR
  • 79 | *
  • LINEENDING
  • 80 | *
81 | * The pattern matches case insensitive. Therefore the placeholders <key> and <value> 82 | * may be given in any case. 83 | */ 84 | protected static final Pattern PATTERN_PROPERTY_FORMAT= Pattern.compile("" 85 | + "^" 86 | + "(?i)" // match case insensitive 87 | + "(?"+PATTERN_SNIPPET_WHITESPACE+"*)" // optional leading whitespace 88 | + PATTERN_SNIPPET_PLACEHOLDER_KEY 89 | + "(?"+PATTERN_SNIPPET_WHITESPACE+"*"+PATTERN_SNIPPET_SEPARATOR+""+PATTERN_SNIPPET_WHITESPACE+"*)" // separator char with optional surrounding whitespace 90 | + PATTERN_SNIPPET_PLACEHOLDER_VALUE 91 | + "(?"+PATTERN_SNIPPET_LINE_ENDING+")" // line ending 92 | + "$" 93 | ); 94 | 95 | 96 | ///////////////////////////////////////////////////////////////////////////// 97 | // 98 | // Attributes 99 | 100 | private final ReformatOptions reformatOptions; 101 | 102 | 103 | ///////////////////////////////////////////////////////////////////////////// 104 | // 105 | // Constructors 106 | 107 | /** 108 | * Creates a new Reformatter with the default ReformatOptions. 109 | */ 110 | public Reformatter() { 111 | this.reformatOptions= new ReformatOptions(); 112 | } 113 | 114 | 115 | /** 116 | * Creates a new Reformatter with the given ReformatOptions. 117 | * 118 | * @param reformatOptions the ReformatOptiosn to use with this Reformatter 119 | */ 120 | public Reformatter(final ReformatOptions reformatOptions) { 121 | this.reformatOptions= reformatOptions; 122 | } 123 | 124 | 125 | ///////////////////////////////////////////////////////////////////////////// 126 | // 127 | // Methods 128 | 129 | /** 130 | * Reformats the key-value pairs in the given .properties file according to the default 131 | * {@link de.poiu.apron.reformatting.ReformatOptions}. 132 | *

133 | * This method actually changes the file on disk. 134 | * 135 | * @param propertyFile the .properties file whose key-value pairs to reformat 136 | * @throws de.poiu.apron.reformatting.InvalidFormatException if the given format string is invalid 137 | */ 138 | public void reformat(final File propertyFile) { 139 | this.reformat(propertyFile, this.reformatOptions); 140 | } 141 | 142 | 143 | /** 144 | * Reformats the key-value pairs in the given .properties file according to the given 145 | * ReformatOptions. 146 | *

147 | * This method actually changes the files on disk. 148 | * 149 | * @param file the .properties file whose key-value pairs to reformat 150 | * @param reformatOptions the ReformatOptions to use when reformatting the .properties file 151 | * @throws de.poiu.apron.reformatting.InvalidFormatException if the given format string is invalid 152 | */ 153 | public void reformat(final File file, final ReformatOptions reformatOptions) { 154 | Objects.requireNonNull(file); 155 | Objects.requireNonNull(reformatOptions); 156 | 157 | final PropertyFile pf= PropertyFile.from(file, reformatOptions.getCharset()); 158 | 159 | this.reformat(pf, reformatOptions); 160 | pf.overwrite(file, ApronOptions.create().with(reformatOptions.getCharset())); 161 | } 162 | 163 | 164 | /** 165 | * Reformats the PropertyEntries in the given PropertyFile according to the default 166 | * {@link de.poiu.apron.reformatting.ReformatOptions}. 167 | *

168 | * This method actually changes the files on disk. 169 | * 170 | * @param propertyFile the PropertiesFile whose PropertyEntries to reformat 171 | * @throws de.poiu.apron.reformatting.InvalidFormatException if the given format string is invalid 172 | */ 173 | public void reformat(final PropertyFile propertyFile) { 174 | this.reformat(propertyFile, this.reformatOptions); 175 | } 176 | 177 | 178 | /** 179 | * Reformats the PropertyEntries in the given PropertyFile according to the given 180 | * ReformatOptions. 181 | *

182 | * This method does not change any files on disk. 183 | * 184 | * @param propertyFile the PropertiesFile whose PropertyEntries to reformat 185 | * @param reformatOptions the reformat options to use when reformatting the PropertyFile 186 | * @throws de.poiu.apron.reformatting.InvalidFormatException if the given format string is invalid 187 | */ 188 | public void reformat(final PropertyFile propertyFile, final ReformatOptions reformatOptions) { 189 | Objects.requireNonNull(propertyFile); 190 | Objects.requireNonNull(reformatOptions); 191 | 192 | final PropertyFormat propertyFormat= this.parseFormat(reformatOptions.getFormat()); 193 | 194 | final List formattedEntries= new ArrayList<>(); 195 | 196 | for (final Entry entry : propertyFile.getAllEntries()) { 197 | if (entry instanceof PropertyEntry) { 198 | // exchange leading whitespace, separator and line ending on PropertyEntries 199 | final PropertyEntry propertyEntry= (PropertyEntry) entry; 200 | final PropertyEntry formattedEntry= new PropertyEntry( 201 | propertyFormat.leadingWhitespace, 202 | reformatOptions.getReformatKeyAndValue() ? reformatKey(propertyEntry.getKey()) : propertyEntry.getKey(), 203 | propertyFormat.separator, 204 | reformatOptions.getReformatKeyAndValue() ? reformatValue(propertyEntry.getValue()) : propertyEntry.getValue(), 205 | propertyFormat.lineEnding); 206 | formattedEntries.add(formattedEntry); 207 | } else if (entry instanceof BasicEntry) { 208 | // exchange the line ending on BasicEntries 209 | final BasicEntry basicEntry= (BasicEntry) entry; 210 | final BasicEntry formattedEntry= new BasicEntry( 211 | basicEntry.toCharSequence().toString() 212 | .replaceAll("\\n", "") 213 | .replaceAll("\\r", "") 214 | .concat(propertyFormat.lineEnding.toString()) 215 | ); 216 | formattedEntries.add(formattedEntry); 217 | } else { 218 | throw new RuntimeException("Unexpected Entry type: "+entry.getClass()); 219 | } 220 | } 221 | 222 | propertyFile.clear(); 223 | propertyFile.setEntries(formattedEntries); 224 | } 225 | 226 | 227 | /** 228 | * Reorders the key-value pairs in the given .properties file alphabetically by the names 229 | * of its keys according to the default {@link de.poiu.apron.reformatting.ReformatOptions}. 230 | *

231 | * This method actually changes the file on disk. 232 | * 233 | * @param fileToReorder the file whose key-value pairs to reorder 234 | */ 235 | public void reorderByKey(final File fileToReorder) { 236 | this.reorderByKey(fileToReorder, this.reformatOptions); 237 | } 238 | 239 | 240 | /** 241 | * Reorders the key-value pairs in the given .properties file alphabetically by the names 242 | * of its keys according to the given ReformatOptions. 243 | *

244 | * This method actually changes the file on disk. 245 | * 246 | * @param fileToReorder the file whose key-value pairs to reorder 247 | * @param reformatOptions the reformat options to use when reordering the key-value pairs in the file 248 | */ 249 | public void reorderByKey(final File fileToReorder, final ReformatOptions reformatOptions) { 250 | Objects.requireNonNull(fileToReorder); 251 | Objects.requireNonNull(reformatOptions); 252 | 253 | final PropertyFile propertyFile= PropertyFile.from(fileToReorder, reformatOptions.getCharset()); 254 | this.reorderByKey(propertyFile, reformatOptions); 255 | propertyFile.overwrite(fileToReorder); 256 | } 257 | 258 | 259 | /** 260 | * Reorders the Entries in the given PropertyFile alphabetically by the names 261 | * of its PropertyEntries keys according to the default {@link de.poiu.apron.reformatting.ReformatOptions}. 262 | *

263 | * This method does not change any files on disk. 264 | * 265 | * @param fileToReorder the file whose Entries to reorder 266 | */ 267 | public void reorderByKey(final PropertyFile fileToReorder) { 268 | this.reorderByKey(fileToReorder, this.reformatOptions); 269 | } 270 | 271 | 272 | /** 273 | * Reorders a PropertyFiles entries alphabetically by the names of its PropertyEntries keys 274 | * according to the given ReformatOptions. 275 | *

276 | * This method does not change any files on disk. 277 | * 278 | * @param fileToReorder the file whose Entries to reorder 279 | * @param reformatOptions the reformat options to use when reordering the key-value pairs in the file 280 | */ 281 | public void reorderByKey(final PropertyFile fileToReorder, final ReformatOptions reformatOptions) { 282 | Objects.requireNonNull(fileToReorder); 283 | Objects.requireNonNull(reformatOptions); 284 | 285 | final List orderedEntries= new ArrayList<>(); 286 | 287 | // sort the entries by propertyKey 288 | final OrderableEntryList orderableEntries= reformatOptions.getAttachCommentsTo(). 289 | toOrderableEntries(fileToReorder.getAllEntries()); 290 | reformatOptions.getAttachCommentsTo().sort(orderableEntries.getAll()); 291 | 292 | //now add the sorted entries the ordered list 293 | orderableEntries.getAll() 294 | .forEach(_oe -> { 295 | orderedEntries.addAll(_oe.entries); 296 | }); 297 | 298 | //now write the reordered entries back to the PropertyFile (not yet to disk) 299 | fileToReorder.getAllEntries().clear(); 300 | fileToReorder.getAllEntries().addAll(orderedEntries); 301 | } 302 | 303 | 304 | /** 305 | * Reorders the key-value pairs in a .properties file according to the order of those keys in the given 306 | * reference file. 307 | *

308 | * Keys that only exist in the file to reorder, but not in the reference file will be put to the 309 | * end of the file to reorder. Those entries are not reordered. 310 | *

311 | * This method actually changes the fileToReorder on disk. The template file will not be modified. 312 | * 313 | * @param template the reference file to be used as template for the reordering 314 | * @param templateCharset the charset to use for loading the template file 315 | * @param fileToReorder the file whose key-value pairs to reorder according to the reference file 316 | * @param reformatOptions the reformat options to use when reordering the key-value pairs in the file 317 | */ 318 | public void reorderByTemplate(final File template, final Charset templateCharset, final File fileToReorder, final ReformatOptions reformatOptions) { 319 | Objects.requireNonNull(template); 320 | Objects.requireNonNull(fileToReorder); 321 | Objects.requireNonNull(reformatOptions); 322 | 323 | final PropertyFile reference= PropertyFile.from(template, templateCharset); 324 | this.reorderByTemplate(reference, fileToReorder, reformatOptions); 325 | } 326 | 327 | 328 | /** 329 | * Reorders the key-value pairs in a .properties file according to the order of those keys in the given 330 | * reference file. 331 | *

332 | * Keys that only exist in the file to reorder, but not in the reference file will be put to the 333 | * end of the file to reorder. Those entries are not reordered. 334 | *

335 | * This method actually changes the fileToReorder on disk. The template file will not be modified. 336 | * 337 | * @param template the reference file to be used as template for the reordering 338 | * @param fileToReorder the file whose key-value pairs to reorder according to the reference file 339 | * @param reformatOptions the reformat options to use when reordering the key-value pairs in the file 340 | */ 341 | public void reorderByTemplate(final File template, final File fileToReorder, final ReformatOptions reformatOptions) { 342 | Objects.requireNonNull(template); 343 | Objects.requireNonNull(fileToReorder); 344 | Objects.requireNonNull(reformatOptions); 345 | 346 | final PropertyFile reference= PropertyFile.from(template, reformatOptions.getCharset()); 347 | this.reorderByTemplate(reference, fileToReorder, reformatOptions); 348 | } 349 | 350 | 351 | /** 352 | * Reorders the key-value pairs in a .properties file according to the order of those keys in the given 353 | * reference file. 354 | *

355 | * Keys that only exist in the file to reorder, but not in the reference file will be put to the 356 | * end of the file to reorder. Those entries are not reordered. 357 | *

358 | * The default {@link de.poiu.apron.reformatting.ReformatOptions} will be used when reordering the key-value pairs. 359 | *

360 | * This method actually changes the fileToReorder on disk. The template file will not be modified. 361 | * 362 | * @param template the reference file to be used as template for the reordering 363 | * @param fileToReorder the file whose key-value pairs to reorder according to the reference file 364 | */ 365 | public void reorderByTemplate(final File template, final File fileToReorder) { 366 | this.reorderByTemplate(template, fileToReorder, this.reformatOptions); 367 | } 368 | 369 | 370 | /** 371 | * Reorders the key-value pairs in a .properties file according to the order of those entries keys of the given 372 | * reference file. 373 | *

374 | * Keys that only exist in the file to reorder, but not in the reference file will be put to the 375 | * end of the file to reorder. Those entries are not reordered. 376 | *

377 | * The default {@link de.poiu.apron.reformatting.ReformatOptions} will be used when reordering the key-value pairs. 378 | *

379 | * This method actually changes the fileToReorder on disk. The template file will not be modified. 380 | * 381 | * @param reference the reference file to be used as template for the reordering 382 | * @param fileToReorder the file whose key-value pairs to reorder according to the reference file 383 | */ 384 | public void reorderByTemplate(final PropertyFile reference, final File fileToReorder) { 385 | this.reorderByTemplate(reference, fileToReorder, this.reformatOptions); 386 | } 387 | 388 | 389 | /** 390 | * Reorders the key-value pairs in a .properties file according to the order of those entries keys of the given 391 | * reference file. 392 | *

393 | * Keys that only exist in the file to reorder, but not in the reference file will be put to the 394 | * end of the file to reorder. Those entries are not reordered. 395 | *

396 | * This method actually changes the fileToReorder on disk. The template file will not be modified. 397 | * 398 | * @param reference the reference file to be used as template for the reordering 399 | * @param fileToReorder the file whose key-value pairs to reorder according to the reference file 400 | * @param reformatOptions the reformat options to use when reordering the key-value pairs in the file 401 | */ 402 | public void reorderByTemplate(final PropertyFile reference, final File fileToReorder, final ReformatOptions reformatOptions) { 403 | Objects.requireNonNull(reference); 404 | Objects.requireNonNull(fileToReorder); 405 | Objects.requireNonNull(reformatOptions); 406 | 407 | final PropertyFile propertyFile= PropertyFile.from(fileToReorder, reformatOptions.getCharset()); 408 | this.reorderByTemplate(reference, propertyFile, reformatOptions); 409 | propertyFile.overwrite(fileToReorder); 410 | } 411 | 412 | 413 | /** 414 | * Reorders a PropertyFiles entries according to the order of those entries keys of the given 415 | * reference file. 416 | *

417 | * Keys that only exist in the file to reorder, but not in the reference file will be put to the 418 | * end of the PropertyFile. Those entries are not reordered. 419 | *

420 | * The default {@link de.poiu.apron.reformatting.ReformatOptions} will be used when reordering the key-value pairs. 421 | *

422 | * This method doesn't change any files on disk. 423 | * 424 | * @param reference the reference file to be used as template for the reordering 425 | * @param fileToReorder the file whose Entries to reorder according to the reference file 426 | */ 427 | public void reorderByTemplate(final PropertyFile reference, final PropertyFile fileToReorder) { 428 | this.reorderByTemplate(reference, fileToReorder, this.reformatOptions); 429 | } 430 | 431 | 432 | /** 433 | * Reorders a PropertyFiles entries according to the order of those entries keys of the given 434 | * reference file. 435 | *

436 | * Keys that only exist in the file to reorder, but not in the reference file will be put to the 437 | * end of the PropertyFile. Those entries are not reordered. 438 | *

439 | * This method doesn't change any files on disk. 440 | * 441 | * @param reference the reference file to be used as template for the reordering 442 | * @param fileToReorder the file whose Entries to reorder according to the reference file 443 | * @param reformatOptions the reformat options to use when reordering the key-value pairs in the file 444 | */ 445 | public void reorderByTemplate(final PropertyFile reference, final PropertyFile fileToReorder, final ReformatOptions reformatOptions) { 446 | Objects.requireNonNull(reference); 447 | Objects.requireNonNull(fileToReorder); 448 | Objects.requireNonNull(reformatOptions); 449 | 450 | final List orderedEntries= new ArrayList<>(); 451 | 452 | final OrderableEntryList orderableEntries= reformatOptions.getAttachCommentsTo() 453 | .toOrderableEntries(fileToReorder.getAllEntries()); 454 | for (final Entry refEntry : reference.getAllEntries()) { 455 | // only process PropertyEntries 456 | if (refEntry instanceof PropertyEntry) { 457 | final PropertyEntry refPEntry= (PropertyEntry) refEntry; 458 | orderableEntries.pop(refPEntry.getKey()) 459 | .ifPresent(_oe -> { 460 | orderedEntries.addAll(_oe.entries); 461 | }); 462 | } 463 | } 464 | 465 | //now add the remaining entries that have no counterpart in the reference PropertyFile 466 | orderableEntries.getAll() 467 | .forEach(_oe -> { 468 | orderedEntries.addAll(_oe.entries); 469 | }); 470 | 471 | //now write the reordered entries back to the PropertyFile (not yet to disk) 472 | fileToReorder.getAllEntries().clear(); 473 | fileToReorder.getAllEntries().addAll(orderedEntries); 474 | } 475 | 476 | 477 | /** 478 | * Parses a format string into a PropertyFormat object. 479 | * 480 | * @param format the format string to parse 481 | * @return the PropertyFormat for the given format string 482 | * @throws InvalidFormatException if the given format string is not valid according to {@link #PATTERN_PROPERTY_FORMAT}. 483 | */ 484 | private PropertyFormat parseFormat(final String format) { 485 | final Matcher matcher= PATTERN_PROPERTY_FORMAT.matcher(format); 486 | if (!matcher.matches()) { 487 | throw new InvalidFormatException("The format string is in an invalid format.\n" 488 | + "A usual format is \" = \\n\"\n" 489 | + "Please refer to the documention for a more detailed explantion of the allowed format.\n" 490 | + "The given format was: " + format); 491 | } 492 | 493 | return new PropertyFormat( 494 | convertEscapes(matcher.group("LEADINGWHITESPACE")), 495 | convertEscapes(matcher.group("SEPARATOR")), 496 | convertEscapes(matcher.group("LINEENDING")) 497 | ); 498 | } 499 | 500 | 501 | /** 502 | * Converts the literal escape sequences (of a format string) to their real instances. 503 | * @param s the string to convert 504 | * @return the given string with the literal escape sequences replaced by their real instances 505 | */ 506 | private String convertEscapes(final String s) { 507 | return s 508 | .replace("\\t", "\t") 509 | .replace("\\f", "\f") 510 | .replace("\\r", "\r") 511 | .replace("\\n", "\n") 512 | ; 513 | } 514 | 515 | 516 | /** 517 | * Reformats the (escaped) key of a key-value pair by removing all unnecessary whitespace and newlines. 518 | * The result is also escaped. 519 | * 520 | * @param key the (escaped) key to reformat 521 | * @return the (escaped) reformatted key 522 | */ 523 | protected CharSequence reformatKey(final CharSequence key) { 524 | return 525 | EscapeUtils.escapePropertyKey( 526 | EscapeUtils.unescape(key)); 527 | } 528 | 529 | 530 | /** 531 | * Reformats the (escaped) value of a key-value pair by removing all unnecessary whitespace and newlines. 532 | * The result is also escaped. 533 | * 534 | * @param value the (escaped) value to reformat 535 | * @return the (escaped) reformatted value 536 | */ 537 | protected CharSequence reformatValue(final CharSequence value) { 538 | return 539 | EscapeUtils.escapePropertyValue( 540 | EscapeUtils.unescape(value)); 541 | } 542 | } 543 | -------------------------------------------------------------------------------- /src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | module de.poiu.apron { 2 | requires java.logging; 3 | 4 | exports de.poiu.apron; 5 | exports de.poiu.apron.entry; 6 | exports de.poiu.apron.reformatting; 7 | } 8 | -------------------------------------------------------------------------------- /src/test/java/de/poiu/apron/escaping/EscapeUtilsTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Marco Herrn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package de.poiu.apron.escaping; 17 | 18 | import org.junit.Test; 19 | 20 | import static org.assertj.core.api.Assertions.assertThat; 21 | 22 | 23 | /** 24 | * 25 | * @author mherrn 26 | */ 27 | public class EscapeUtilsTest { 28 | 29 | @Test 30 | public void testUnescape() { 31 | assertThat(EscapeUtils.unescape("some normal string without escaping").toString()) 32 | .isEqualTo("some normal string without escaping"); 33 | assertThat(EscapeUtils.unescape("string\\ with\\ escaped\\ spaces").toString()) 34 | .isEqualTo("string with escaped spaces"); 35 | assertThat(EscapeUtils.unescape("key\\:\\=value").toString()) 36 | .isEqualTo("key:=value"); 37 | assertThat(EscapeUtils.unescape("double\\\\escaping").toString()) 38 | .isEqualTo("double\\escaping"); 39 | assertThat(EscapeUtils.unescape("escaped newline \\").toString()) 40 | .isEqualTo("escaped newline "); 41 | assertThat(EscapeUtils.unescape("non-escaped newline \nsecond line").toString()) 42 | .isEqualTo("non-escaped newline second line"); 43 | assertThat(EscapeUtils.unescape("escaped newline \\\nsecond line").toString()) 44 | .isEqualTo("escaped newline second line"); 45 | assertThat(EscapeUtils.unescape("literal newline \\n").toString()) 46 | .isEqualTo("literal newline \n"); 47 | } 48 | 49 | @Test 50 | public void testUnescapeUnicode() { 51 | assertThat(EscapeUtils.unescapeUnicode("\\u00fc").toString()) 52 | .isEqualTo("ü"); 53 | assertThat(EscapeUtils.unescapeUnicode("\\u1234").toString()) 54 | .isEqualTo("ሴ"); 55 | assertThat(EscapeUtils.unescapeUnicode("\\u7de8").toString()) 56 | .isEqualTo("編"); 57 | assertThat(EscapeUtils.unescapeUnicode("\\u042f").toString()) 58 | .isEqualTo("Я"); 59 | assertThat(EscapeUtils.unescapeUnicode("\\u0061").toString()) 60 | .isEqualTo("a"); 61 | 62 | assertThat(EscapeUtils.unescapeUnicode("abcd\\u1234abcd").toString()) 63 | .isEqualTo("abcdሴabcd"); 64 | 65 | //invalid unicode values are left as is 66 | assertThat(EscapeUtils.unescapeUnicode("\\u123").toString()) 67 | .isEqualTo("\\u123"); 68 | assertThat(EscapeUtils.unescapeUnicode("\\u123T").toString()) 69 | .isEqualTo("\\u123T"); 70 | 71 | //all other escapes need to remain as is 72 | assertThat(EscapeUtils.unescapeUnicode("some\\ test \\# with \\=\\: escaped\\ chars\\b\\n\\\\n").toString()) 73 | .isEqualTo("some\\ test \\# with \\=\\: escaped\\ chars\\b\\n\\\\n"); 74 | } 75 | 76 | 77 | @Test 78 | public void testUnescape_withUnicodeValues() { 79 | assertThat(EscapeUtils.unescape("hinzuf\\u00fcgen").toString()) 80 | .isEqualTo("hinzufügen"); 81 | assertThat(EscapeUtils.unescape("hinzuf\\u00FCgen").toString()) 82 | .isEqualTo("hinzufügen"); 83 | assertThat(EscapeUtils.unescape("Soll nicht ersetzt werden: \\\\u00fc!").toString()) 84 | .isEqualTo("Soll nicht ersetzt werden: \\u00fc!"); 85 | } 86 | 87 | 88 | @Test 89 | public void testUnescape_withInvalidUnicodeValues() { 90 | assertThat(EscapeUtils.unescape("hinzuf\\uTTTTgen").toString()) 91 | .isEqualTo("hinzuf\\uTTTTgen"); 92 | 93 | assertThat(EscapeUtils.unescape("hinzuf\\uu00fcgen").toString()) 94 | .isEqualTo("hinzuf\\uu00fcgen"); 95 | } 96 | 97 | 98 | @Test 99 | public void testEscapeUnicode() { 100 | assertThat(EscapeUtils.escapeUnicode('ü').toString()) 101 | .isEqualTo("\\u00fc"); 102 | assertThat(EscapeUtils.escapeUnicode('ሴ').toString()) 103 | .isEqualTo("\\u1234"); 104 | assertThat(EscapeUtils.escapeUnicode('編').toString()) 105 | .isEqualTo("\\u7de8"); 106 | assertThat(EscapeUtils.escapeUnicode('Я').toString()) 107 | .isEqualTo("\\u042f"); 108 | assertThat(EscapeUtils.escapeUnicode('a').toString()) 109 | .isEqualTo("\\u0061"); 110 | } 111 | 112 | 113 | @Test 114 | public void testTranslateUnicode() throws InvalidUnicodeCharacterException { 115 | assertThat(EscapeUtils.translateUnicode("\\u00fc")) 116 | .isEqualTo('ü').toString(); 117 | assertThat(EscapeUtils.translateUnicode("\\u1234")) 118 | .isEqualTo('ሴ').toString(); 119 | assertThat(EscapeUtils.translateUnicode("\\u7de8")) 120 | .isEqualTo('編').toString(); 121 | assertThat(EscapeUtils.translateUnicode("\\u042f")) 122 | .isEqualTo('Я').toString(); 123 | assertThat(EscapeUtils.translateUnicode("\\u0061")) 124 | .isEqualTo('a').toString(); 125 | } 126 | 127 | 128 | @Test 129 | public void testRemoveLeadingWhitespace() { 130 | assertThat(EscapeUtils.removeLeadingWhitespace(" first line \n" 131 | + " \tsecond line \r\n" 132 | + " \t\f third line").toString()) 133 | .isEqualTo("first line \n" 134 | + "second line \r\n" 135 | + "third line"); 136 | } 137 | 138 | 139 | @Test 140 | public void testUnescape_LiteralNewlineToRealNewline() { 141 | assertThat(EscapeUtils.unescape("first line \\n" 142 | + "second line").toString()) 143 | .isEqualTo("first line \n" 144 | + "second line"); 145 | assertThat(EscapeUtils.unescape("first line \\r" 146 | + "second line").toString()) 147 | .isEqualTo("first line \r" 148 | + "second line"); 149 | assertThat(EscapeUtils.unescape("first line \\r\\n" 150 | + "second line").toString()) 151 | .isEqualTo("first line \r\n" 152 | + "second line"); 153 | } 154 | 155 | 156 | @Test 157 | public void testUnescape_SilentlyDropUnnecessaryBackslashes() { 158 | assertThat(EscapeUtils.unescape("\\a\\b\\c\\d\\e").toString()) 159 | .isEqualTo("abcde"); 160 | } 161 | 162 | 163 | @Test 164 | public void testUnescape_RemoveNewlines() { 165 | assertThat(EscapeUtils.unescape("first line \n" 166 | + "second line").toString()) 167 | .isEqualTo("first line second line"); 168 | assertThat(EscapeUtils.unescape("first line \r" 169 | + "second line").toString()) 170 | .isEqualTo("first line second line"); 171 | assertThat(EscapeUtils.unescape("first line \r\n" 172 | + "second line").toString()) 173 | .isEqualTo("first line second line"); 174 | } 175 | 176 | @Test 177 | public void testUnescape_RemoveNewlinesAndLeadingWhitespace() { 178 | assertThat(EscapeUtils.unescape("first line \n" 179 | + "\tsecond line").toString()) 180 | .isEqualTo("first line second line"); 181 | } 182 | 183 | 184 | @Test 185 | public void testUnescapePropertyValue_UnescapeUnicodeDifferentLengths() { 186 | assertThat(EscapeUtils.unescape("Nudel\\u123456").toString()) 187 | .isEqualTo("Nudelሴ56"); 188 | assertThat(EscapeUtils.unescape("Nudel\\u12345").toString()) 189 | .isEqualTo("Nudelሴ5"); 190 | assertThat(EscapeUtils.unescape("Nudel\\u1234").toString()) 191 | .isEqualTo("Nudelሴ"); 192 | assertThat(EscapeUtils.unescape("Nudel\\u123").toString()) 193 | .isEqualTo("Nudel\\u123"); 194 | assertThat(EscapeUtils.unescape("Nudel\\u12").toString()) 195 | .isEqualTo("Nudel\\u12"); 196 | assertThat(EscapeUtils.unescape("Nudel\\u1").toString()) 197 | .isEqualTo("Nudel\\u1"); 198 | assertThat(EscapeUtils.unescape("Nudel\\u").toString()) 199 | .isEqualTo("Nudel\\u"); 200 | } 201 | 202 | 203 | @Test 204 | public void testEscapePropertyKey() { 205 | assertThat(EscapeUtils.escapePropertyKey("key with whitespace").toString()) 206 | .isEqualTo("key\\ with\\ whitespace"); 207 | 208 | assertThat(EscapeUtils.escapePropertyKey("key with \nnewline").toString()) 209 | .isEqualTo("key\\ with\\ \\\nnewline"); 210 | 211 | assertThat(EscapeUtils.escapePropertyKey("key with \rnewline").toString()) 212 | .isEqualTo("key\\ with\\ \\\rnewline"); 213 | 214 | assertThat(EscapeUtils.escapePropertyKey("key with \r\nnewline").toString()) 215 | .isEqualTo("key\\ with\\ \\\r\nnewline"); 216 | 217 | assertThat(EscapeUtils.escapePropertyKey("#key with commentchar").toString()) 218 | .isEqualTo("\\#key\\ with\\ commentchar"); 219 | 220 | assertThat(EscapeUtils.escapePropertyKey("key with #commentchar inside").toString()) 221 | .isEqualTo("key\\ with\\ \\#commentchar\\ inside"); 222 | 223 | assertThat(EscapeUtils.escapePropertyKey("key with :=").toString()) 224 | .isEqualTo("key\\ with\\ \\:\\="); 225 | } 226 | 227 | 228 | @Test 229 | public void testEscapePropertyValue() { 230 | assertThat(EscapeUtils.escapePropertyValue("value with whitespace").toString()) 231 | .isEqualTo("value with whitespace"); 232 | 233 | assertThat(EscapeUtils.escapePropertyValue("value with \nnewline").toString()) 234 | .isEqualTo("value with \\nnewline"); 235 | 236 | assertThat(EscapeUtils.escapePropertyValue("value with \rnewline").toString()) 237 | .isEqualTo("value with \\rnewline"); 238 | 239 | assertThat(EscapeUtils.escapePropertyValue("value with \r\nnewline").toString()) 240 | .isEqualTo("value with \\r\\nnewline"); 241 | 242 | assertThat(EscapeUtils.escapePropertyValue("#value with commentchar").toString()) 243 | .isEqualTo("#value with commentchar"); 244 | 245 | assertThat(EscapeUtils.escapePropertyValue("value with #commentchar inside").toString()) 246 | .isEqualTo("value with #commentchar inside"); 247 | 248 | assertThat(EscapeUtils.escapePropertyValue("value with :=").toString()) 249 | .isEqualTo("value with :="); 250 | } 251 | 252 | 253 | /** 254 | * This test checks bug #5: https://github.com/hupfdule/apron/issues/5 255 | */ 256 | @Test 257 | public void testEscape_Backslashes() { 258 | assertThat(EscapeUtils.escapePropertyKey("my\\key").toString()) 259 | .isEqualTo("my\\\\key"); 260 | assertThat(EscapeUtils.escapePropertyKey("my\\value").toString()) 261 | .isEqualTo("my\\\\value"); 262 | } 263 | 264 | 265 | @Test 266 | public void testComment() { 267 | assertThat(EscapeUtils.comment("" 268 | + "my key = my value \\\n" 269 | + " over \\\r" 270 | + " multiple \\\r\n" 271 | + " lines").toString()) 272 | .isEqualTo("" 273 | + "#my key = my value \\\n" 274 | + "# over \\\r" 275 | + "# multiple \\\r\n" 276 | + "# lines"); 277 | 278 | assertThat(EscapeUtils.comment("" 279 | + "my key = my value \n" 280 | + " over \r" 281 | + " multiple \r\n" 282 | + " lines").toString()) 283 | .isEqualTo("" 284 | + "#my key = my value \n" 285 | + "# over \r" 286 | + "# multiple \r\n" 287 | + "# lines"); 288 | 289 | assertThat(EscapeUtils.comment("" 290 | + "my key = my value \n" 291 | + " over \r" 292 | + " multiple \r\n" 293 | + " lines\n").toString()) 294 | .isEqualTo("" 295 | + "#my key = my value \n" 296 | + "# over \r" 297 | + "# multiple \r\n" 298 | + "# lines\n"); 299 | } 300 | 301 | 302 | @Test 303 | public void testUnescape_withUnicodeValuesAndRorN() { 304 | assertThat(EscapeUtils.unescape("Die Kommunikation ist gest\\u00f6rt.").toString()) 305 | .isEqualTo("Die Kommunikation ist gestört."); 306 | assertThat(EscapeUtils.unescape("\\u00c4ndern").toString()) 307 | .isEqualTo("Ändern"); 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /src/test/java/de/poiu/apron/io/PropertyFileWriterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Marco Herrn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package de.poiu.apron.io; 17 | 18 | import de.poiu.apron.ApronOptions; 19 | import de.poiu.apron.UnicodeHandling; 20 | import de.poiu.apron.entry.BasicEntry; 21 | import de.poiu.apron.entry.Entry; 22 | import de.poiu.apron.entry.PropertyEntry; 23 | import java.io.BufferedReader; 24 | import java.io.File; 25 | import java.io.FileInputStream; 26 | import java.io.IOException; 27 | import java.io.InputStreamReader; 28 | import org.junit.Test; 29 | 30 | import static java.nio.charset.StandardCharsets.ISO_8859_1; 31 | import static java.nio.charset.StandardCharsets.UTF_8; 32 | import static org.assertj.core.api.Assertions.assertThat; 33 | 34 | 35 | /** 36 | * 37 | * @author mherrn 38 | */ 39 | public class PropertyFileWriterTest { 40 | 41 | 42 | @Test 43 | public void testWriteEntries() throws IOException { 44 | // - preparation 45 | final Entry[] entries= { 46 | new BasicEntry("# Starting comment\n"), 47 | new BasicEntry("\n"), 48 | new PropertyEntry("", "keyA1", " = ", "valueA1", "\n"), 49 | new PropertyEntry(" ", "keyA2", ":", "valueA2", "\n"), 50 | }; 51 | 52 | final File file= this.createTestFile(); 53 | 54 | // - execution 55 | try(final PropertyFileWriter propertyFileWriter= new PropertyFileWriter(file);) { 56 | for (final Entry entry : entries) { 57 | propertyFileWriter.writeEntry(entry); 58 | } 59 | } 60 | 61 | // - verification 62 | final String fileContent= toString(file); 63 | assertThat(fileContent).isEqualTo( 64 | "# Starting comment\n" 65 | + "\n" 66 | + "keyA1 = valueA1\n" 67 | + " keyA2:valueA2\n" 68 | ); 69 | } 70 | 71 | 72 | @Test 73 | public void testWriteEntries_OnlyKeyAndValue() throws IOException { 74 | // - preparation 75 | final Entry[] entries= { 76 | new BasicEntry("# Starting comment\n"), 77 | new BasicEntry("\n"), 78 | new PropertyEntry("keyA1", "valueA1"), // this one has not leadingWhitespace, separator and line ending 79 | // and is therefore using the defaults 80 | new PropertyEntry(" ", "keyA2", ":", "valueA2", "\n"), 81 | }; 82 | 83 | final File file= this.createTestFile(); 84 | 85 | // - execution 86 | try(final PropertyFileWriter propertyFileWriter= new PropertyFileWriter(file);) { 87 | for (final Entry entry : entries) { 88 | propertyFileWriter.writeEntry(entry); 89 | } 90 | } 91 | 92 | // - verification 93 | final String fileContent= toString(file); 94 | assertThat(fileContent).isEqualTo( 95 | "# Starting comment\n" 96 | + "\n" 97 | + "keyA1 = valueA1\n" 98 | + " keyA2:valueA2\n" 99 | ); 100 | } 101 | 102 | 103 | @Test 104 | public void testWriteEntries_DifferentLineEndings() throws IOException { 105 | // - preparation 106 | final Entry[] entries= { 107 | new BasicEntry("# Starting comment\n"), 108 | new BasicEntry("\r"), 109 | new PropertyEntry("", "keyA1", " = ", "valueA1", "\r\n"), 110 | new PropertyEntry(" ", "keyA2", ":", "valueA2", ""), 111 | }; 112 | 113 | final File file= this.createTestFile(); 114 | 115 | // - execution 116 | try(final PropertyFileWriter propertyFileWriter= new PropertyFileWriter(file);) { 117 | for (final Entry entry : entries) { 118 | propertyFileWriter.writeEntry(entry); 119 | } 120 | } 121 | 122 | // - verification 123 | final String fileContent= toString(file); 124 | assertThat(fileContent).isEqualTo( 125 | "# Starting comment\n" 126 | + "\r" 127 | + "keyA1 = valueA1\r\n" 128 | + " keyA2:valueA2" 129 | ); 130 | } 131 | 132 | 133 | @Test 134 | public void testWriteEntries_Multilines() throws IOException { 135 | // - preparation 136 | final Entry[] entries= { 137 | new BasicEntry("# Starting comment\n"), 138 | new BasicEntry("\n"), 139 | new PropertyEntry("", "keyA1\\ \\\n\tover\\ multiple\\ lines", " = ", "valueA1 \r\tover multiple lines", "\n"), 140 | new PropertyEntry(" ", "keyA2", ":", "valueA2", "\n"), 141 | }; 142 | 143 | final File file= this.createTestFile(); 144 | 145 | // - execution 146 | try(final PropertyFileWriter propertyFileWriter= new PropertyFileWriter(file);) { 147 | for (final Entry entry : entries) { 148 | propertyFileWriter.writeEntry(entry); 149 | } 150 | } 151 | 152 | // - verification 153 | final String fileContent= toString(file); 154 | assertThat(fileContent).isEqualTo( 155 | "# Starting comment\n" 156 | + "\n" 157 | + "keyA1\\ \\\n\tover\\ multiple\\ lines = valueA1 \r\tover multiple lines\n" 158 | + " keyA2:valueA2\n" 159 | ); 160 | } 161 | 162 | 163 | @Test 164 | public void testWriteEntries_CharsetUTF8_UnicodeHandlingNOTHING() throws IOException { 165 | // - preparation 166 | final Entry[] entries= { 167 | new PropertyEntry("keyA1","Sch\\u00fcssel\\u069a"), 168 | new PropertyEntry("UTF-8-key-Äሴ", "UTF-8-value-編Я"), 169 | }; 170 | 171 | final File file= this.createTestFile(); 172 | 173 | // - execution 174 | try(final PropertyFileWriter propertyFileWriter= new PropertyFileWriter(file, 175 | ApronOptions.create() 176 | .with(UTF_8) 177 | .with(UnicodeHandling.DO_NOTHING));) { 178 | for (final Entry entry : entries) { 179 | propertyFileWriter.writeEntry(entry); 180 | } 181 | } 182 | 183 | // - verification 184 | final String fileContent= toString(file); 185 | assertThat(fileContent).isEqualTo("" 186 | + "keyA1 = Sch\\u00fcssel\\u069a\n" 187 | + "UTF-8-key-Äሴ = UTF-8-value-編Я\n" 188 | ); 189 | } 190 | 191 | 192 | @Test 193 | public void testWriteEntries_CharsetUTF8_UnicodeHandlingBYCHARSET() throws IOException { 194 | // - preparation 195 | final Entry[] entries= { 196 | new PropertyEntry("keyA1","Sch\\u00fcssel\\u069a"), 197 | new PropertyEntry("UTF-8-key-Äሴ", "UTF-8-value-編Я"), 198 | }; 199 | 200 | final File file= this.createTestFile(); 201 | 202 | // - execution 203 | try(final PropertyFileWriter propertyFileWriter= new PropertyFileWriter(file, 204 | ApronOptions.create() 205 | .with(UTF_8) 206 | .with(UnicodeHandling.BY_CHARSET));) { 207 | for (final Entry entry : entries) { 208 | propertyFileWriter.writeEntry(entry); 209 | } 210 | } 211 | 212 | // - verification 213 | final String fileContent= toString(file); 214 | assertThat(fileContent).isEqualTo("" 215 | + "keyA1 = Schüsselښ\n" 216 | + "UTF-8-key-Äሴ = UTF-8-value-編Я\n" 217 | ); 218 | } 219 | 220 | 221 | @Test 222 | public void testWriteEntries_CharsetUTF8_UnicodeHandlingUNICODE() throws IOException { 223 | // - preparation 224 | final Entry[] entries= { 225 | new PropertyEntry("keyA1","Sch\\u00fcssel\\u069a"), 226 | new PropertyEntry("UTF-8-key-Äሴ", "UTF-8-value-編Я"), 227 | }; 228 | 229 | final File file= this.createTestFile(); 230 | 231 | // - execution 232 | try(final PropertyFileWriter propertyFileWriter= new PropertyFileWriter(file, 233 | ApronOptions.create() 234 | .with(UTF_8) 235 | .with(UnicodeHandling.UNICODE));) { 236 | for (final Entry entry : entries) { 237 | propertyFileWriter.writeEntry(entry); 238 | } 239 | } 240 | 241 | // - verification 242 | final String fileContent= toString(file); 243 | assertThat(fileContent).isEqualTo("" 244 | + "keyA1 = Schüsselښ\n" 245 | + "UTF-8-key-Äሴ = UTF-8-value-編Я\n" 246 | ); 247 | } 248 | 249 | 250 | @Test 251 | public void testWriteEntries_CharsetUTF8_UnicodeHandlingESCAPE() throws IOException { 252 | // - preparation 253 | final Entry[] entries= { 254 | new PropertyEntry("keyA1","Sch\\u00fcssel\\u069a"), 255 | new PropertyEntry("UTF-8-key-Äሴ", "UTF-8-value-編Я"), 256 | }; 257 | 258 | final File file= this.createTestFile(); 259 | 260 | // - execution 261 | try(final PropertyFileWriter propertyFileWriter= new PropertyFileWriter(file, 262 | ApronOptions.create() 263 | .with(UTF_8) 264 | .with(UnicodeHandling.ESCAPE));) { 265 | for (final Entry entry : entries) { 266 | propertyFileWriter.writeEntry(entry); 267 | } 268 | } 269 | 270 | // - verification 271 | final String fileContent= toString(file); 272 | assertThat(fileContent).isEqualTo("" 273 | + "keyA1 = Sch\\u00fcssel\\u069a\n" 274 | + "UTF-8-key-\\u00c4\\u1234 = UTF-8-value-\\u7de8\\u042f\n" 275 | ); 276 | } 277 | 278 | 279 | @Test 280 | public void testWriteEntries_CharsetISO88591_UnicodeHandlingESCAPE() throws IOException { 281 | // - preparation 282 | final Entry[] entries= { 283 | new PropertyEntry("keyA1","Sch\\u00fcssel\\u069a"), 284 | new PropertyEntry("UTF-8-key-Äሴ", "UTF-8-value-編Я"), 285 | }; 286 | 287 | final File file= this.createTestFile(); 288 | 289 | // - execution 290 | try(final PropertyFileWriter propertyFileWriter= new PropertyFileWriter(file, 291 | ApronOptions.create() 292 | .with(ISO_8859_1) 293 | .with(UnicodeHandling.ESCAPE));) { 294 | for (final Entry entry : entries) { 295 | propertyFileWriter.writeEntry(entry); 296 | } 297 | } 298 | 299 | // - verification 300 | final String fileContent= toString(file); 301 | assertThat(fileContent).isEqualTo("" 302 | + "keyA1 = Sch\\u00fcssel\\u069a\n" 303 | + "UTF-8-key-\\u00c4\\u1234 = UTF-8-value-\\u7de8\\u042f\n" 304 | ); 305 | } 306 | 307 | 308 | @Test 309 | public void testWriteEntries_CharsetISO88591_UnicodeHandlingUNICODE() throws IOException { 310 | // UnicodeHandling needs to be ignored, since ISO-8859-1 does not allow unicode values 311 | // - preparation 312 | final Entry[] entries= { 313 | new PropertyEntry("keyA1","Sch\\u00fcssel\\u069a"), 314 | new PropertyEntry("UTF-8-key-Äሴ", "UTF-8-value-編Я"), 315 | }; 316 | 317 | final File file= this.createTestFile(); 318 | 319 | // - execution 320 | try(final PropertyFileWriter propertyFileWriter= new PropertyFileWriter(file, 321 | ApronOptions.create() 322 | .with(ISO_8859_1) 323 | .with(UnicodeHandling.ESCAPE));) { 324 | for (final Entry entry : entries) { 325 | propertyFileWriter.writeEntry(entry); 326 | } 327 | } 328 | 329 | // - verification 330 | final String fileContent= toString(file); 331 | assertThat(fileContent).isEqualTo("" 332 | + "keyA1 = Sch\\u00fcssel\\u069a\n" 333 | + "UTF-8-key-\\u00c4\\u1234 = UTF-8-value-\\u7de8\\u042f\n" 334 | ); 335 | } 336 | 337 | 338 | @Test 339 | public void testWriteEntries_CharsetISO88591_UnicodeHandlingNOTHING() throws IOException { 340 | // UnicodeHandling needs to be ignored, since ISO-8859-1 does not allow unicode values 341 | // - preparation 342 | final Entry[] entries= { 343 | new PropertyEntry("keyA1","Sch\\u00fcssel\\u069a"), 344 | new PropertyEntry("UTF-8-key-Äሴ", "UTF-8-value-編Я"), 345 | }; 346 | 347 | final File file= this.createTestFile(); 348 | 349 | // - execution 350 | try(final PropertyFileWriter propertyFileWriter= new PropertyFileWriter(file, 351 | ApronOptions.create() 352 | .with(ISO_8859_1) 353 | .with(UnicodeHandling.DO_NOTHING));) { 354 | for (final Entry entry : entries) { 355 | propertyFileWriter.writeEntry(entry); 356 | } 357 | } 358 | 359 | // - verification 360 | final String fileContent= toString(file); 361 | assertThat(fileContent).isEqualTo("" 362 | + "keyA1 = Sch\\u00fcssel\\u069a\n" 363 | + "UTF-8-key-\\u00c4\\u1234 = UTF-8-value-\\u7de8\\u042f\n" 364 | ); 365 | } 366 | 367 | 368 | 369 | private File createTestFile() { 370 | try { 371 | final File propertyTestFile= File.createTempFile("propertyFile", ".properties"); 372 | propertyTestFile.deleteOnExit(); 373 | 374 | return propertyTestFile; 375 | } catch (IOException ex) { 376 | throw new RuntimeException("Error in test preparation", ex); 377 | } 378 | } 379 | 380 | 381 | private String toString(final File file) throws IOException { 382 | final StringBuilder sb= new StringBuilder(); 383 | 384 | try(final BufferedReader reader= new BufferedReader(new InputStreamReader(new FileInputStream(file)));) { 385 | int cInt; 386 | while((cInt= reader.read()) != -1) { 387 | final char c= (char) cInt; 388 | sb.append(c); 389 | } 390 | } 391 | 392 | return sb.toString(); 393 | } 394 | 395 | 396 | private static void debugPrint(String fileContent) { 397 | System.out.println("debug ------"); 398 | System.out.println(">"+fileContent+"<"); 399 | System.out.println("debug end --"); 400 | } 401 | } 402 | -------------------------------------------------------------------------------- /src/test/java/de/poiu/apron/reformatting/ReformatterPatternTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Marco Herrn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package de.poiu.apron.reformatting; 17 | 18 | import java.util.regex.Matcher; 19 | import org.junit.Test; 20 | 21 | import static org.assertj.core.api.Assertions.assertThat; 22 | 23 | 24 | /** 25 | * 26 | * @author mherrn 27 | */ 28 | public class ReformatterPatternTest { 29 | 30 | @Test 31 | public void testPatternFormatString_BaseFormat() { 32 | final Matcher matcher= Reformatter.PATTERN_PROPERTY_FORMAT.matcher(" = \\n"); 33 | assertThat(matcher.matches()).isTrue(); 34 | assertThat(matcher.group("LEADINGWHITESPACE")).isEqualTo(""); 35 | assertThat(matcher.group("SEPARATOR")).isEqualTo(" = "); 36 | assertThat(matcher.group("LINEENDING")).isEqualTo("\\n"); 37 | } 38 | 39 | 40 | @Test 41 | public void testPatternFormatString_CaseInsensitive() { 42 | final Matcher matcher= Reformatter.PATTERN_PROPERTY_FORMAT.matcher(" = \\n"); 43 | assertThat(matcher.matches()).isTrue(); 44 | assertThat(matcher.group("LEADINGWHITESPACE")).isEqualTo(""); 45 | assertThat(matcher.group("SEPARATOR")).isEqualTo(" = "); 46 | assertThat(matcher.group("LINEENDING")).isEqualTo("\\n"); 47 | } 48 | 49 | 50 | @Test 51 | public void testPatternFormatString_DifferentWhitespace() { 52 | final Matcher matcher= Reformatter.PATTERN_PROPERTY_FORMAT.matcher(" \\t\\f\\f:\\t\\r\\n"); 53 | assertThat(matcher.matches()).isTrue(); 54 | assertThat(matcher.group("LEADINGWHITESPACE")).isEqualTo(" \\t\\f"); 55 | assertThat(matcher.group("SEPARATOR")).isEqualTo("\\f:\\t"); 56 | assertThat(matcher.group("LINEENDING")).isEqualTo("\\r\\n"); 57 | } 58 | 59 | 60 | @Test 61 | public void testPatternFormatString_WhitespaceAsSeparator() { 62 | final Matcher matcher= Reformatter.PATTERN_PROPERTY_FORMAT.matcher(" \\f \\t\\r"); 63 | assertThat(matcher.matches()).isTrue(); 64 | assertThat(matcher.group("LEADINGWHITESPACE")).isEqualTo(" "); 65 | assertThat(matcher.group("SEPARATOR")).isEqualTo("\\f \\t"); 66 | assertThat(matcher.group("LINEENDING")).isEqualTo("\\r"); 67 | } 68 | 69 | 70 | @Test 71 | public void testPatternFormatString_MissingSeparator() { 72 | final Matcher matcher= Reformatter.PATTERN_PROPERTY_FORMAT.matcher(" \\n"); 73 | assertThat(matcher.matches()).isFalse(); 74 | } 75 | 76 | 77 | @Test 78 | public void testPatternFormatString_MissingLineEnding() { 79 | final Matcher matcher= Reformatter.PATTERN_PROPERTY_FORMAT.matcher(" = "); 80 | assertThat(matcher.matches()).isFalse(); 81 | } 82 | 83 | 84 | @Test 85 | public void testPatternFormatString_InvalidLineEnding() { 86 | final Matcher matcher= Reformatter.PATTERN_PROPERTY_FORMAT.matcher(" = \\n\\r"); 87 | assertThat(matcher.matches()).isFalse(); 88 | } 89 | 90 | 91 | @Test 92 | public void testPatternFormatString_InvalidSeparator() { 93 | final Matcher matcher= Reformatter.PATTERN_PROPERTY_FORMAT.matcher(" == \\n"); 94 | assertThat(matcher.matches()).isFalse(); 95 | } 96 | 97 | 98 | @Test 99 | public void testPatternFormatString_LeadingWhitespace() { 100 | final Matcher matcher= Reformatter.PATTERN_PROPERTY_FORMAT.matcher("# = \\n"); 101 | assertThat(matcher.matches()).isFalse(); 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /src/test/java/de/poiu/apron/reformatting/ReformatterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Marco Herrn 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package de.poiu.apron.reformatting; 17 | 18 | import java.io.File; 19 | import java.io.FileOutputStream; 20 | import java.io.IOException; 21 | import java.io.OutputStreamWriter; 22 | import java.io.PrintWriter; 23 | import java.nio.file.Path; 24 | import java.util.stream.Stream; 25 | import org.junit.Rule; 26 | import org.junit.Test; 27 | import org.junit.rules.TemporaryFolder; 28 | 29 | import static java.nio.charset.StandardCharsets.UTF_8; 30 | import static org.assertj.core.api.Assertions.assertThat; 31 | import static org.assertj.core.api.Assertions.assertThatExceptionOfType; 32 | import static org.assertj.core.api.Assertions.contentOf; 33 | 34 | 35 | /** 36 | * 37 | * @author mherrn 38 | */ 39 | public class ReformatterTest { 40 | 41 | @Rule 42 | public TemporaryFolder tmpFolder= new TemporaryFolder(); 43 | 44 | 45 | 46 | @Test 47 | public void testReformat_differentFormat() throws IOException { 48 | 49 | // - preparation 50 | 51 | final Path propertiesRootDirectory= this.tmpFolder.getRoot().toPath(); 52 | final File f1= createI18nBundle(propertiesRootDirectory, "" 53 | + "key1 = value1\n" 54 | + "key2 : value2\r" 55 | + "key3 value3\r\n"); 56 | final File f2= createI18nBundle(propertiesRootDirectory, "" 57 | + " key1 = value1\n" 58 | + " key2 : value2\r" 59 | + "\tkey3 value3\r\n"); 60 | 61 | // - execution 62 | final Reformatter reformatter= new Reformatter( 63 | new ReformatOptions().withFormat("\\t=\\t\\n")); 64 | Stream.of(f1, f2) 65 | .forEachOrdered(_f -> { 66 | reformatter.reformat(_f); 67 | }); 68 | 69 | 70 | // - verification 71 | 72 | assertThat(contentOf(f1)).isEqualTo("" 73 | + "key1\t=\tvalue1\n" 74 | + "key2\t=\tvalue2\n" 75 | + "key3\t=\tvalue3\n"); 76 | assertThat(contentOf(f2)).isEqualTo("" 77 | + "key1\t=\tvalue1\n" 78 | + "key2\t=\tvalue2\n" 79 | + "key3\t=\tvalue3\n"); 80 | } 81 | 82 | 83 | @Test 84 | public void testReformat_invalidFormat() throws IOException { 85 | 86 | // - preparation 87 | 88 | final Path propertiesRootDirectory= this.tmpFolder.getRoot().toPath(); 89 | final File f1= createI18nBundle(propertiesRootDirectory, "" 90 | + "key1 = value1\n" 91 | + "key2 : value2\r" 92 | + "key3 value3\r\n"); 93 | 94 | // - execution 95 | // - verification 96 | 97 | assertThatExceptionOfType(InvalidFormatException.class) 98 | .isThrownBy(() -> { 99 | new Reformatter(ReformatOptions.create().withFormat(" = \\n")) 100 | .reformat(f1); 101 | }); 102 | } 103 | 104 | 105 | @Test 106 | public void testReformat_multilineProperties() throws IOException { 107 | 108 | // - preparation 109 | 110 | final Path propertiesRootDirectory= this.tmpFolder.getRoot().toPath(); 111 | final File f1= createI18nBundle(propertiesRootDirectory, "" 112 | + "\tkey\\ \\\n" 113 | + " one\\\n" 114 | + " : value \\\n" 115 | + " 1\n" 116 | + "key\\ \\\n" 117 | + " two\\\r" 118 | + " = \t value \\\r" 119 | + " 2\n" 120 | ); 121 | 122 | // - execution 123 | 124 | new Reformatter(ReformatOptions.create().withFormat(" = \\n")) 125 | .reformat(f1); 126 | 127 | // - verification 128 | 129 | assertThat(contentOf(f1)).isEqualTo("" 130 | + "key\\ \\\n" 131 | + " one\\\n" 132 | + " = value \\\n" 133 | + " 1\n" 134 | + "key\\ \\\n" 135 | + " two\\\r" 136 | + " = value \\\r" 137 | + " 2\n" 138 | ); 139 | } 140 | 141 | 142 | @Test 143 | public void testReformat_multilineProperties_ToSingleLine() throws IOException { 144 | 145 | // - preparation 146 | 147 | final Path propertiesRootDirectory= this.tmpFolder.getRoot().toPath(); 148 | final File f1= createI18nBundle(propertiesRootDirectory, "" 149 | + "\tkey\\ \\\n" 150 | + " one\\\n" 151 | + " : value \\\n" 152 | + " 1\n" 153 | + "key\\ \\\n" 154 | + " two\\\r" 155 | + " = \t value \\\r" 156 | + " 2\n" 157 | ); 158 | 159 | // - execution 160 | 161 | new Reformatter(ReformatOptions.create() 162 | .withFormat(" = \\n") 163 | .withReformatKeyAndValue(true)) 164 | .reformat(f1); 165 | 166 | // - verification 167 | 168 | assertThat(contentOf(f1)).isEqualTo("" 169 | + "key\\ one = value 1\n" 170 | + "key\\ two = value 2\n" 171 | ); 172 | } 173 | 174 | 175 | @Test 176 | public void testReorder_orderByLanguage() throws IOException { 177 | 178 | // - preparation 179 | 180 | final Path propertiesRootDirectory= this.tmpFolder.getRoot().toPath(); 181 | final File f1= createI18nBundle(propertiesRootDirectory, "" 182 | + "keyC = valueC\n" 183 | + "keyB = valueB\n" 184 | + "keyA = valueA\n" 185 | + "keyE = valueE\n" 186 | + "keyD = valueD\n" 187 | ); 188 | final File f2= createI18nBundle(propertiesRootDirectory, "" 189 | + "keyE = valueE\n" 190 | + "keyD = valueD\n" 191 | + "keyC = valueC\n" 192 | + "keyB = valueB\n" 193 | + "keyA = valueA\n" 194 | ); 195 | 196 | // - execution 197 | 198 | new Reformatter(ReformatOptions.create() 199 | .with(UTF_8) 200 | .with(AttachCommentsTo.NEXT_PROPERTY)) 201 | .reorderByTemplate(f1, f2); 202 | // .reorder(Arrays.asList(f1, f2), OrderPropertiesBy.LANGUAGE, AttachCommentsTo.NEXT_PROPERTY); 203 | 204 | // - verification 205 | 206 | assertThat(contentOf(f1)).isEqualTo("" 207 | + "keyC = valueC\n" 208 | + "keyB = valueB\n" 209 | + "keyA = valueA\n" 210 | + "keyE = valueE\n" 211 | + "keyD = valueD\n" 212 | ); 213 | assertThat(contentOf(f2)).isEqualTo("" 214 | + "keyC = valueC\n" 215 | + "keyB = valueB\n" 216 | + "keyA = valueA\n" 217 | + "keyE = valueE\n" 218 | + "keyD = valueD\n" 219 | ); 220 | } 221 | 222 | 223 | @Test 224 | public void testReorder_orderByLanguage_NothingInCommon() throws IOException { 225 | 226 | // - preparation 227 | 228 | final Path propertiesRootDirectory= this.tmpFolder.getRoot().toPath(); 229 | final File f1= createI18nBundle(propertiesRootDirectory, "" 230 | + "keyC = valueC\n" 231 | + "keyB = valueB\n" 232 | + "keyA = valueA\n" 233 | + "keyE = valueE\n" 234 | + "keyD = valueD\n" 235 | ); 236 | final File f2= createI18nBundle(propertiesRootDirectory, "" 237 | + "keyZ = valueZ\n" 238 | + "keyY = valueY\n" 239 | + "keyX = valueX\n" 240 | ); 241 | 242 | // - execution 243 | 244 | new Reformatter() 245 | .reorderByTemplate(f1, f2); 246 | 247 | // - verification 248 | 249 | assertThat(contentOf(f1)).isEqualTo("" 250 | + "keyC = valueC\n" 251 | + "keyB = valueB\n" 252 | + "keyA = valueA\n" 253 | + "keyE = valueE\n" 254 | + "keyD = valueD\n" 255 | ); 256 | assertThat(contentOf(f2)).isEqualTo("" 257 | + "keyZ = valueZ\n" 258 | + "keyY = valueY\n" 259 | + "keyX = valueX\n" 260 | ); 261 | } 262 | 263 | 264 | @Test 265 | public void testReorder_orderByName() throws IOException { 266 | 267 | // - preparation 268 | 269 | final Path propertiesRootDirectory= this.tmpFolder.getRoot().toPath(); 270 | final File f1= createI18nBundle(propertiesRootDirectory, "" 271 | + "keyC = valueC\n" 272 | + "keyB = valueB\n" 273 | + "keyA = valueA\n" 274 | + "keyE = valueE\n" 275 | + "keyD = valueD\n" 276 | ); 277 | final File f2= createI18nBundle(propertiesRootDirectory, "" 278 | + "keyE = valueE\n" 279 | + "keyD = valueD\n" 280 | + "keyC = valueC\n" 281 | + "keyB = valueB\n" 282 | + "keyA = valueA\n" 283 | ); 284 | 285 | // - execution 286 | 287 | final Reformatter reformatter= new Reformatter(); 288 | reformatter.reorderByKey(f1); 289 | reformatter.reorderByKey(f2); 290 | 291 | // - verification 292 | 293 | assertThat(contentOf(f1)).isEqualTo("" 294 | + "keyA = valueA\n" 295 | + "keyB = valueB\n" 296 | + "keyC = valueC\n" 297 | + "keyD = valueD\n" 298 | + "keyE = valueE\n" 299 | ); 300 | assertThat(contentOf(f2)).isEqualTo("" 301 | + "keyA = valueA\n" 302 | + "keyB = valueB\n" 303 | + "keyC = valueC\n" 304 | + "keyD = valueD\n" 305 | + "keyE = valueE\n" 306 | ); 307 | } 308 | 309 | 310 | @Test 311 | public void testReorder_attachCommentToNext() throws IOException { 312 | 313 | // - preparation 314 | 315 | final Path propertiesRootDirectory= this.tmpFolder.getRoot().toPath(); 316 | final File f1= createI18nBundle(propertiesRootDirectory, "" 317 | + "# Comment 1\n" 318 | + "key_F = F\n" 319 | + "key_L = L\n" 320 | + "\n" 321 | + "# Comment 2\n" 322 | + "key_B = B\n" 323 | + "# Comment 3\n" 324 | + "key_A = A\n" 325 | ); 326 | 327 | // - execution 328 | 329 | new Reformatter() 330 | .reorderByKey(f1); 331 | 332 | // - verification 333 | 334 | assertThat(contentOf(f1)).isEqualTo("" 335 | + "# Comment 3\n" 336 | + "key_A = A\n" 337 | + "\n" 338 | + "# Comment 2\n" 339 | + "key_B = B\n" 340 | + "# Comment 1\n" 341 | + "key_F = F\n" 342 | + "key_L = L\n" 343 | ); 344 | } 345 | 346 | 347 | @Test 348 | public void testReorder_attachCommentToPrev() throws IOException { 349 | 350 | // - preparation 351 | 352 | final Path propertiesRootDirectory= this.tmpFolder.getRoot().toPath(); 353 | final File f1= createI18nBundle(propertiesRootDirectory, "" 354 | + "# Comment 1\n" 355 | + "key_F = F\n" 356 | + "key_L = L\n" 357 | + "\n" 358 | + "# Comment 2\n" 359 | + "key_B = B\n" 360 | + "# Comment 3\n" 361 | + "key_A = A\n" 362 | ); 363 | 364 | // - execution 365 | 366 | new Reformatter(ReformatOptions.create().with(AttachCommentsTo.PREV_PROPERTY)) 367 | .reorderByKey(f1); 368 | 369 | // - verification 370 | 371 | assertThat(contentOf(f1)).isEqualTo("" 372 | + "# Comment 1\n" 373 | + "key_A = A\n" 374 | + "key_B = B\n" 375 | + "# Comment 3\n" 376 | + "key_F = F\n" 377 | + "key_L = L\n" 378 | + "\n" 379 | + "# Comment 2\n" 380 | ); 381 | } 382 | 383 | 384 | @Test 385 | public void testReorder_attachCommentToLine() throws IOException { 386 | 387 | // - preparation 388 | 389 | final Path propertiesRootDirectory= this.tmpFolder.getRoot().toPath(); 390 | final File f1= createI18nBundle(propertiesRootDirectory, "" 391 | + "# Comment 1\n" 392 | + "key_F = F\n" 393 | + "key_L = L\n" 394 | + "\n" 395 | + "# Comment 2\n" 396 | + "key_B = B\n" 397 | + "# Comment 3\n" 398 | + "key_A = A\n" 399 | ); 400 | 401 | // - execution 402 | 403 | new Reformatter(ReformatOptions.create().with(AttachCommentsTo.ORIG_LINE)) 404 | .reorderByKey(f1); 405 | 406 | // - verification 407 | 408 | assertThat(contentOf(f1)).isEqualTo("" 409 | + "# Comment 1\n" 410 | + "key_A = A\n" 411 | + "key_B = B\n" 412 | + "\n" 413 | + "# Comment 2\n" 414 | + "key_F = F\n" 415 | + "# Comment 3\n" 416 | + "key_L = L\n" 417 | ); 418 | } 419 | 420 | 421 | @Test 422 | public void testReorder_multilineProperties() throws IOException { 423 | 424 | // - preparation 425 | 426 | final Path propertiesRootDirectory= this.tmpFolder.getRoot().toPath(); 427 | final File f1= createI18nBundle(propertiesRootDirectory, "" 428 | + "key\\ \\\n" 429 | + " 2\\\n" 430 | + " = value \\\n" 431 | + " 2\n" 432 | + "\n" 433 | + " # some comment\n" 434 | + "key\\ \\\n" 435 | + " 1\\\n" 436 | + " = value \\\n" 437 | + " 1\n" 438 | + "key\\ \\\n" 439 | + " 3\\\n" 440 | + " = value \\\n" 441 | + " 3\n" 442 | ); 443 | 444 | // - execution 445 | 446 | new Reformatter(ReformatOptions.create().with(AttachCommentsTo.ORIG_LINE)) 447 | .reorderByKey(f1); 448 | 449 | // - verification 450 | 451 | assertThat(contentOf(f1)).isEqualTo("" 452 | + "key\\ \\\n" 453 | + " 1\\\n" 454 | + " = value \\\n" 455 | + " 1\n" 456 | + "\n" 457 | + " # some comment\n" 458 | + "key\\ \\\n" 459 | + " 2\\\n" 460 | + " = value \\\n" 461 | + " 2\n" 462 | + "key\\ \\\n" 463 | + " 3\\\n" 464 | + " = value \\\n" 465 | + " 3\n" 466 | ); 467 | } 468 | 469 | 470 | @Test 471 | public void testReformatAndReorder() throws IOException { 472 | 473 | // - preparation 474 | 475 | final Path propertiesRootDirectory= this.tmpFolder.getRoot().toPath(); 476 | final File f1= createI18nBundle(propertiesRootDirectory, "" 477 | + "# Comment 1\n" 478 | + "key_F = F\n" 479 | + "key_L = L\n" 480 | + "\n" 481 | + "# Comment 2\n" 482 | + "key_B = B\n" 483 | + "# Comment 3\n" 484 | + "key_A = A\n" 485 | ); 486 | 487 | // - execution 488 | 489 | final Reformatter reformatter= new Reformatter( 490 | ReformatOptions.create() 491 | .withFormat("\\t : \\r\\n") 492 | .with(UTF_8) 493 | .with(AttachCommentsTo.NEXT_PROPERTY)); 494 | reformatter.reformat(f1); 495 | reformatter.reorderByKey(f1); 496 | 497 | // - verification 498 | 499 | assertThat(contentOf(f1)).isEqualTo("" 500 | + "# Comment 3\r\n" 501 | + "\tkey_A : A\r\n" 502 | + "\r\n" 503 | + "# Comment 2\r\n" 504 | + "\tkey_B : B\r\n" 505 | + "# Comment 1\r\n" 506 | + "\tkey_F : F\r\n" 507 | + "\tkey_L : L\r\n" 508 | ); 509 | } 510 | 511 | 512 | 513 | private File createI18nBundle(final Path rootDirectory, final String content) { 514 | try { 515 | final File f= File.createTempFile("ReformatterTest", ".properties", rootDirectory.toFile()); 516 | 517 | try(final PrintWriter pw= new PrintWriter(new OutputStreamWriter(new FileOutputStream(f), UTF_8));) { 518 | pw.print(content); 519 | } 520 | 521 | return f; 522 | } catch (IOException ex) { 523 | throw new RuntimeException("Error in test preparation", ex); 524 | } 525 | } 526 | } 527 | -------------------------------------------------------------------------------- /src/test/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | --------------------------------------------------------------------------------