commit 81f3cc723d9eb3e94736b67c6bc72942f905da82 Author: Jörg Prante Date: Fri Jul 5 22:23:39 2024 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6970922 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +data +work +out +logs +/.idea +/target +/.settings +/.classpath +/.project +/.gradle +/plugins +/sessions +.DS_Store +*.iml +*~ +.secret +build +**/*.key +**/*.crt +**/*.pkcs8 +**/*.gz diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/NOTICE.txt b/NOTICE.txt new file mode 100644 index 0000000..01d0a3a --- /dev/null +++ b/NOTICE.txt @@ -0,0 +1,21 @@ +This work is based on + +JBoss LogManager https://github.com/jboss-logging/jboss-logmanager + +Apache License 2.0 + +JBoss log4j2-jboss-logmanager + +https://github.com/jboss-logging/log4j2-jboss-logmanager + +Apache License 2.0 + +and + +JBoss SLF4J LogManager + +https://github.com/jboss-logging/slf4j-jboss-logmanager + +Apache License 2.0 + +as of 1 July, 2024 diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..2e1866c --- /dev/null +++ b/build.gradle @@ -0,0 +1,35 @@ + +plugins { + id 'maven-publish' + id 'signing' + id "io.github.gradle-nexus.publish-plugin" version "2.0.0-rc-1" +} + +wrapper { + gradleVersion = libs.versions.gradle.get() + distributionType = Wrapper.DistributionType.BIN +} + +ext { + user = 'joerg' + name = 'logging' + description = 'Logging for Java 21+, a reimplementation of JBoss LogManager' + inceptionYear = '2024' + url = 'https://xbib.org/' + user + '/' + name + scmUrl = 'https://xbib.org/' + user + '/' + name + scmConnection = 'scm:git:git://xbib.org/' + user + '/' + name + '.git' + scmDeveloperConnection = 'scm:git:ssh://forgejo@xbib.org:' + user + '/' + name + '.git' + issueManagementSystem = 'Github' + issueManagementUrl = ext.scmUrl + '/issues' + licenseName = 'The Apache License, Version 2.0' + licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt' +} + +subprojects { + apply from: rootProject.file('gradle/repositories/maven.gradle') + apply from: rootProject.file('gradle/compile/java.gradle') + apply from: rootProject.file('gradle/test/junit5.gradle') + apply from: rootProject.file('gradle/publish/maven.gradle') +} +apply from: rootProject.file('gradle/publish/sonatype.gradle') +apply from: rootProject.file('gradle/publish/forgejo.gradle') diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..abd7b67 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,3 @@ +group = org.xbib +name = logging +version = 0.0.1 diff --git a/gradle/compile/groovy.gradle b/gradle/compile/groovy.gradle new file mode 100644 index 0000000..1abf883 --- /dev/null +++ b/gradle/compile/groovy.gradle @@ -0,0 +1,34 @@ +apply plugin: 'groovy' + +dependencies { + implementation "org.codehaus.groovy:groovy:${project.property('groovy.version')}:indy" +} + +compileGroovy { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +compileTestGroovy { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +tasks.withType(GroovyCompile) { + options.compilerArgs + if (!options.compilerArgs.contains("-processor")) { + options.compilerArgs << '-proc:none' + } + groovyOptions.optimizationOptions.indy = true +} + +task groovydocJar(type: Jar, dependsOn: 'groovydoc') { + from groovydoc.destinationDir + archiveClassifier.set('javadoc') +} + +configurations.all { + resolutionStrategy { + force "org.codehaus.groovy:groovy:${project.property('groovy.version')}:indy" + } +} diff --git a/gradle/compile/java.gradle b/gradle/compile/java.gradle new file mode 100644 index 0000000..d3ce818 --- /dev/null +++ b/gradle/compile/java.gradle @@ -0,0 +1,47 @@ +apply plugin: 'java-library' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } + modularity.inferModulePath.set(true) + withSourcesJar() + withJavadocJar() +} + +jar { + manifest { + attributes('Implementation-Version': project.version) + } + duplicatesStrategy = DuplicatesStrategy.INCLUDE +} + +tasks.withType(JavaCompile).configureEach { + doFirst { + options.fork = true + options.forkOptions.jvmArgs += ['-Duser.language=en', '-Duser.country=US'] + options.encoding = 'UTF-8' + // -classfile because of log4j2 issues "warning: Cannot find annotation method" + options.compilerArgs.add('-Xlint:all,-classfile') + options.compilerArgs.add("--module-version") + options.compilerArgs.add(project.version as String) + options.compilerArgs.add("--module-path") + options.compilerArgs.add(classpath.asPath) + classpath = files() + } +} + +tasks.withType(Javadoc).configureEach { + doFirst { + options.addStringOption('Xdoclint:none', '-quiet') + options.encoding = 'UTF-8' + } +} + +tasks.withType(JavaExec).configureEach { + doFirst { + jvmArguments.add("--module-path") + jvmArguments.add(classpath.asPath) + classpath = files() + } +} diff --git a/gradle/documentation/asciidoc.gradle b/gradle/documentation/asciidoc.gradle new file mode 100644 index 0000000..87ba22e --- /dev/null +++ b/gradle/documentation/asciidoc.gradle @@ -0,0 +1,55 @@ +apply plugin: 'org.xbib.gradle.plugin.asciidoctor' + +configurations { + asciidoclet +} + +dependencies { + asciidoclet "org.asciidoctor:asciidoclet:${project.property('asciidoclet.version')}" +} + + +asciidoctor { + backends 'html5' + outputDir = file("${rootProject.projectDir}/docs") + separateOutputDirs = false + attributes 'source-highlighter': 'coderay', + idprefix: '', + idseparator: '-', + toc: 'left', + doctype: 'book', + icons: 'font', + encoding: 'utf-8', + sectlink: true, + sectanchors: true, + linkattrs: true, + imagesdir: 'img', + stylesheet: "${projectDir}/src/docs/asciidoc/css/foundation.css" +} + + +/*javadoc { +options.docletpath = configurations.asciidoclet.files.asType(List) +options.doclet = 'org.asciidoctor.Asciidoclet' +//options.overview = "src/docs/asciidoclet/overview.adoc" +options.addStringOption "-base-dir", "${projectDir}" +options.addStringOption "-attribute", + "name=${project.name},version=${project.version},title-link=https://github.com/xbib/${project.name}" +configure(options) { + noTimestamp = true +} +}*/ + + +/*javadoc { + options.docletpath = configurations.asciidoclet.files.asType(List) + options.doclet = 'org.asciidoctor.Asciidoclet' + options.overview = "${rootProject.projectDir}/src/docs/asciidoclet/overview.adoc" + options.addStringOption "-base-dir", "${projectDir}" + options.addStringOption "-attribute", + "name=${project.name},version=${project.version},title-link=https://github.com/xbib/${project.name}" + options.destinationDirectory(file("${projectDir}/docs/javadoc")) + configure(options) { + noTimestamp = true + } +}*/ diff --git a/gradle/ide/idea.gradle b/gradle/ide/idea.gradle new file mode 100644 index 0000000..64e2167 --- /dev/null +++ b/gradle/ide/idea.gradle @@ -0,0 +1,13 @@ +apply plugin: 'idea' + +idea { + module { + outputDir file('build/classes/java/main') + testOutputDir file('build/classes/java/test') + } +} + +if (project.convention.findPlugin(JavaPluginConvention)) { + //sourceSets.main.output.classesDirs = file("build/classes/java/main") + //sourceSets.test.output.classesDirs = file("build/classes/java/test") +} diff --git a/gradle/publish/forgejo.gradle b/gradle/publish/forgejo.gradle new file mode 100644 index 0000000..b99b2fb --- /dev/null +++ b/gradle/publish/forgejo.gradle @@ -0,0 +1,16 @@ +if (project.hasProperty('forgeJoToken')) { + publishing { + repositories { + maven { + url 'https://xbib.org/api/packages/joerg/maven' + credentials(HttpHeaderCredentials) { + name = "Authorization" + value = "token ${project.property('forgeJoToken')}" + } + authentication { + header(HttpHeaderAuthentication) + } + } + } + } +} diff --git a/gradle/publish/ivy.gradle b/gradle/publish/ivy.gradle new file mode 100644 index 0000000..fe0a848 --- /dev/null +++ b/gradle/publish/ivy.gradle @@ -0,0 +1,27 @@ +apply plugin: 'ivy-publish' + +publishing { + repositories { + ivy { + url = "https://xbib.org/repo" + } + } + publications { + ivy(IvyPublication) { + from components.java + descriptor { + license { + name = 'The Apache License, Version 2.0' + url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' + } + author { + name = 'Jörg Prante' + url = 'http://example.com/users/jane' + } + descriptor.description { + text = rootProject.ext.description + } + } + } + } +} \ No newline at end of file diff --git a/gradle/publish/maven.gradle b/gradle/publish/maven.gradle new file mode 100644 index 0000000..ce6a26f --- /dev/null +++ b/gradle/publish/maven.gradle @@ -0,0 +1,51 @@ + +publishing { + publications { + "${project.name}"(MavenPublication) { + from components.java + pom { + artifactId = project.name + name = project.name + description = rootProject.ext.description + url = rootProject.ext.url + inceptionYear = rootProject.ext.inceptionYear + packaging = 'jar' + organization { + name = 'xbib' + url = 'https://xbib.org' + } + developers { + developer { + id = 'jprante' + name = 'Jörg Prante' + email = 'joergprante@gmail.com' + url = 'https://github.com/jprante' + } + } + scm { + url = rootProject.ext.scmUrl + connection = rootProject.ext.scmConnection + developerConnection = rootProject.ext.scmDeveloperConnection + } + issueManagement { + system = rootProject.ext.issueManagementSystem + url = rootProject.ext.issueManagementUrl + } + licenses { + license { + name = rootProject.ext.licenseName + url = rootProject.ext.licenseUrl + distribution = 'repo' + } + } + } + } + } +} + +if (project.hasProperty("signing.keyId")) { + apply plugin: 'signing' + signing { + sign publishing.publications."${project.name}" + } +} diff --git a/gradle/publish/sonatype.gradle b/gradle/publish/sonatype.gradle new file mode 100644 index 0000000..5d739de --- /dev/null +++ b/gradle/publish/sonatype.gradle @@ -0,0 +1,11 @@ +if (project.hasProperty('ossrhUsername') && project.hasProperty('ossrhPassword')) { + nexusPublishing { + repositories { + sonatype { + username = project.property('ossrhUsername') + password = project.property('ossrhPassword') + packageGroup = "org.xbib" + } + } + } +} diff --git a/gradle/quality/checkstyle.gradle b/gradle/quality/checkstyle.gradle new file mode 100644 index 0000000..85b8bd8 --- /dev/null +++ b/gradle/quality/checkstyle.gradle @@ -0,0 +1,19 @@ + +apply plugin: 'checkstyle' + +tasks.withType(Checkstyle) { + ignoreFailures = true + reports { + xml.getRequired().set(true) + html.getRequired().set(true) + } +} + +checkstyle { + configFile = rootProject.file('gradle/quality/checkstyle.xml') + ignoreFailures = true + showViolations = true + checkstyleMain { + source = sourceSets.main.allSource + } +} diff --git a/gradle/quality/checkstyle.xml b/gradle/quality/checkstyle.xml new file mode 100644 index 0000000..66a9aae --- /dev/null +++ b/gradle/quality/checkstyle.xml @@ -0,0 +1,333 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gradle/quality/cyclonedx.gradle b/gradle/quality/cyclonedx.gradle new file mode 100644 index 0000000..a6bf41b --- /dev/null +++ b/gradle/quality/cyclonedx.gradle @@ -0,0 +1,11 @@ +cyclonedxBom { + includeConfigs = [ 'runtimeClasspath' ] + skipConfigs = [ 'compileClasspath', 'testCompileClasspath' ] + projectType = "library" + schemaVersion = "1.4" + destination = file("build/reports") + outputName = "bom" + outputFormat = "json" + includeBomSerialNumber = true + componentVersion = "2.0.0" +} diff --git a/gradle/quality/pmd.gradle b/gradle/quality/pmd.gradle new file mode 100644 index 0000000..55fcfda --- /dev/null +++ b/gradle/quality/pmd.gradle @@ -0,0 +1,17 @@ + +apply plugin: 'pmd' + +tasks.withType(Pmd) { + ignoreFailures = true + reports { + xml.getRequired().set(true) + html.getRequired().set(true) + } +} + +pmd { + ignoreFailures = true + consoleOutput = false + toolVersion = "6.51.0" + ruleSetFiles = rootProject.files('gradle/quality/pmd/category/java/bestpractices.xml') +} diff --git a/gradle/quality/pmd/category/java/bestpractices.xml b/gradle/quality/pmd/category/java/bestpractices.xml new file mode 100644 index 0000000..56a764d --- /dev/null +++ b/gradle/quality/pmd/category/java/bestpractices.xml @@ -0,0 +1,1650 @@ + + + + + + Rules which enforce generally accepted best practices. + + + + + The abstract class does not contain any abstract methods. An abstract class suggests + an incomplete implementation, which is to be completed by subclasses implementing the + abstract methods. If the class is intended to be used as a base class only (not to be instantiated + directly) a protected constructor can be provided prevent direct instantiation. + + 3 + + + + + + + + + + + + + + + Instantiation by way of private constructors from outside of the constructor's class often causes the + generation of an accessor. A factory method, or non-privatization of the constructor can eliminate this + situation. The generated class file is actually an interface. It gives the accessing class the ability + to invoke a new hidden package scope constructor that takes the interface as a supplementary parameter. + This turns a private constructor effectively into one with package scope, and is challenging to discern. + + 3 + + + + + + + + When accessing a private field / method from another class, the Java compiler will generate a accessor methods + with package-private visibility. This adds overhead, and to the dex method count on Android. This situation can + be avoided by changing the visibility of the field / method from private to package-private. + + 3 + + + + + + + + Constructors and methods receiving arrays should clone objects and store the copy. + This prevents future changes from the user from affecting the original array. + + 3 + + + + + + + + Avoid printStackTrace(); use a logger call instead. + + 3 + + + + + + + + + + + + + + + Reassigning loop variables can lead to hard-to-find bugs. Prevent or limit how these variables can be changed. + + In foreach-loops, configured by the `foreachReassign` property: + - `deny`: Report any reassignment of the loop variable in the loop body. _This is the default._ + - `allow`: Don't check the loop variable. + - `firstOnly`: Report any reassignments of the loop variable, except as the first statement in the loop body. + _This is useful if some kind of normalization or clean-up of the value before using is permitted, but any other change of the variable is not._ + + In for-loops, configured by the `forReassign` property: + - `deny`: Report any reassignment of the control variable in the loop body. _This is the default._ + - `allow`: Don't check the control variable. + - `skip`: Report any reassignments of the control variable, except conditional increments/decrements (`++`, `--`, `+=`, `-=`). + _This prevents accidental reassignments or unconditional increments of the control variable._ + + 3 + + + + + + + + Reassigning values to incoming parameters is not recommended. Use temporary local variables instead. + + 2 + + + + + + + + StringBuffers/StringBuilders can grow considerably, and so may become a source of memory leaks + if held within objects with long lifetimes. + + 3 + + + + + + + + + + + + + + + Application with hard-coded IP addresses can become impossible to deploy in some cases. + Externalizing IP adresses is preferable. + + 3 + + + + + + + + Always check the return values of navigation methods (next, previous, first, last) of a ResultSet. + If the value return is 'false', it should be handled properly. + + 3 + + + + + + + + Avoid constants in interfaces. Interfaces should define types, constants are implementation details + better placed in classes or enums. See Effective Java, item 19. + + 3 + + + + + + + + + + + + + + + + By convention, the default label should be the last label in a switch statement. + + 3 + + + + + + + + + + + + + + + Reports loops that can be safely replaced with the foreach syntax. The rule considers loops over + lists, arrays and iterators. A loop is safe to replace if it only uses the index variable to + access an element of the list or array, only has one update statement, and loops through *every* + element of the list or array left to right. + + 3 + + l) { + for (int i = 0; i < l.size(); i++) { // pre Java 1.5 + System.out.println(l.get(i)); + } + + for (String s : l) { // post Java 1.5 + System.out.println(s); + } + } +} +]]> + + + + + + Having a lot of control variables in a 'for' loop makes it harder to see what range of values + the loop iterates over. By default this rule allows a regular 'for' loop with only one variable. + + 3 + + + + //ForInit/LocalVariableDeclaration[count(VariableDeclarator) > $maximumVariables] + + + + + + + + + + Whenever using a log level, one should check if the loglevel is actually enabled, or + otherwise skip the associate String creation and manipulation. + + 2 + + + + + + + + In JUnit 3, test suites are indicated by the suite() method. In JUnit 4, suites are indicated + through the @RunWith(Suite.class) annotation. + + 3 + + + + + + + + + + + + + + + In JUnit 3, the tearDown method was used to clean up all data entities required in running tests. + JUnit 4 skips the tearDown method and executes all methods annotated with @After after running each test. + JUnit 5 introduced @AfterEach and @AfterAll annotations to execute methods after each test or after all tests in the class, respectively. + + 3 + + + + + + + + + + + + + + + In JUnit 3, the setUp method was used to set up all data entities required in running tests. + JUnit 4 skips the setUp method and executes all methods annotated with @Before before all tests. + JUnit 5 introduced @BeforeEach and @BeforeAll annotations to execute methods before each test or before all tests in the class, respectively. + + 3 + + + + + + + + + + + + + + + In JUnit 3, the framework executed all methods which started with the word test as a unit test. + In JUnit 4, only methods annotated with the @Test annotation are executed. + In JUnit 5, one of the following annotations should be used for tests: @Test, @RepeatedTest, @TestFactory, @TestTemplate or @ParameterizedTest. + + 3 + + + + + + + + + + + + + + + + + JUnit assertions should include an informative message - i.e., use the three-argument version of + assertEquals(), not the two-argument version. + + 3 + + + + + + + + Unit tests should not contain too many asserts. Many asserts are indicative of a complex test, for which + it is harder to verify correctness. Consider breaking the test scenario into multiple, shorter test scenarios. + Customize the maximum number of assertions used by this Rule to suit your needs. + + This rule checks for JUnit4, JUnit5 and TestNG Tests, as well as methods starting with "test". + + 3 + + + + + $maximumAsserts] +]]> + + + + + + + + + + + JUnit tests should include at least one assertion. This makes the tests more robust, and using assert + with messages provide the developer a clearer idea of what the test does. + + 3 + + + + + + + + In JUnit4, use the @Test(expected) annotation to denote tests that should throw exceptions. + + 3 + + + + + + + + The use of implementation types (i.e., HashSet) as object references limits your ability to use alternate + implementations in the future as requirements change. Whenever available, referencing objects + by their interface types (i.e, Set) provides much more flexibility. + + 3 + + list = new ArrayList<>(); + + public HashSet getFoo() { + return new HashSet(); + } + + // preferred approach + private List list = new ArrayList<>(); + + public Set getFoo() { + return new HashSet(); + } +} +]]> + + + + + + Exposing internal arrays to the caller violates object encapsulation since elements can be + removed or replaced outside of the object that owns it. It is safer to return a copy of the array. + + 3 + + + + + + + + + Annotating overridden methods with @Override ensures at compile time that + the method really overrides one, which helps refactoring and clarifies intent. + + 3 + + + + + + + + Java allows the use of several variables declaration of the same type on one line. However, it + can lead to quite messy code. This rule looks for several declarations on the same line. + + 4 + + + + 1] + [$strictMode or count(distinct-values(VariableDeclarator/@BeginLine)) != count(VariableDeclarator)] +| +//FieldDeclaration + [count(VariableDeclarator) > 1] + [$strictMode or count(distinct-values(VariableDeclarator/@BeginLine)) != count(VariableDeclarator)] +]]> + + + + + + + + + + + + + Position literals first in comparisons, if the second argument is null then NullPointerExceptions + can be avoided, they will just return false. + + 3 + + + + + + + + + + + + + + + Position literals first in comparisons, if the second argument is null then NullPointerExceptions + can be avoided, they will just return false. + + 3 + + + + + + + + + + + + + + + Throwing a new exception from a catch block without passing the original exception into the + new exception will cause the original stack trace to be lost making it difficult to debug + effectively. + + 3 + + + + + + + + Consider replacing Enumeration usages with the newer java.util.Iterator + + 3 + + + + + + + + + + + + + + + Consider replacing Hashtable usage with the newer java.util.Map if thread safety is not required. + + 3 + + + //Type/ReferenceType/ClassOrInterfaceType[@Image='Hashtable'] + + + + + + + + + + Consider replacing Vector usages with the newer java.util.ArrayList if expensive thread-safe operations are not required. + + 3 + + + //Type/ReferenceType/ClassOrInterfaceType[@Image='Vector'] + + + + + + + + + + All switch statements should include a default option to catch any unspecified values. + + 3 + + + + + + + + + + + + + + References to System.(out|err).print are usually intended for debugging purposes and can remain in + the codebase even in production code. By using a logger one can enable/disable this behaviour at + will (and by priority) and avoid clogging the Standard out log. + + 2 + + + + + + + + + + + + + + + Avoid passing parameters to methods or constructors without actually referencing them in the method body. + + 3 + + + + + + + + Avoid unused import statements to prevent unwanted dependencies. + This rule will also find unused on demand imports, i.e. import com.foo.*. + + 4 + + + + + + + + Detects when a local variable is declared and/or assigned, but not used. + + 3 + + + + + + + + Detects when a private field is declared and/or assigned a value, but not used. + + 3 + + + + + + + + Unused Private Method detects when a private method is declared but is unused. + + 3 + + + + + + + + This rule detects JUnit assertions in object equality. These assertions should be made by more specific methods, like assertEquals. + + 3 + + + + + + + + + + + + + + + This rule detects JUnit assertions in object references equality. These assertions should be made by + more specific methods, like assertNull, assertNotNull. + + 3 + + + + + + + + + + + + + + + This rule detects JUnit assertions in object references equality. These assertions should be made + by more specific methods, like assertSame, assertNotSame. + + 3 + + + + + + + + + + + + + + + When asserting a value is the same as a literal or Boxed boolean, use assertTrue/assertFalse, instead of assertEquals. + + 3 + + + + + + + + + + + + + + + The isEmpty() method on java.util.Collection is provided to determine if a collection has any elements. + Comparing the value of size() to 0 does not convey intent as well as the isEmpty() method. + + 3 + + + + + + + + Java 7 introduced the try-with-resources statement. This statement ensures that each resource is closed at the end + of the statement. It avoids the need of explicitly closing the resources in a finally block. Additionally exceptions + are better handled: If an exception occurred both in the `try` block and `finally` block, then the exception from + the try block was suppressed. With the `try`-with-resources statement, the exception thrown from the try-block is + preserved. + + 3 + + + + + + + + + + + + + + + + + Java 5 introduced the varargs parameter declaration for methods and constructors. This syntactic + sugar provides flexibility for users of these methods and constructors, allowing them to avoid + having to deal with the creation of an array. + + 4 + + + + + + + + + + + + + + diff --git a/gradle/quality/pmd/category/java/categories.properties b/gradle/quality/pmd/category/java/categories.properties new file mode 100644 index 0000000..8ef5eac --- /dev/null +++ b/gradle/quality/pmd/category/java/categories.properties @@ -0,0 +1,10 @@ + +rulesets.filenames=\ + category/java/bestpractices.xml,\ + category/java/codestyle.xml,\ + category/java/design.xml,\ + category/java/documentation.xml,\ + category/java/errorprone.xml,\ + category/java/multithreading.xml,\ + category/java/performance.xml,\ + category/java/security.xml diff --git a/gradle/quality/pmd/category/java/codestyle.xml b/gradle/quality/pmd/category/java/codestyle.xml new file mode 100644 index 0000000..186ea4b --- /dev/null +++ b/gradle/quality/pmd/category/java/codestyle.xml @@ -0,0 +1,2176 @@ + + + + + + Rules which enforce a specific coding style. + + + + + Abstract classes should be named 'AbstractXXX'. + + This rule is deprecated and will be removed with PMD 7.0.0. The rule is replaced + by {% rule java/codestyle/ClassNamingConventions %}. + + 3 + + + + + + + + + + + + + + + + + + 3 + + + + + + + + Avoid using dollar signs in variable/method/class/interface names. + + 3 + + + + + + + Avoid using final local variables, turn them into fields. + 3 + + + + + + + + + + + + + + + Prefixing parameters by 'in' or 'out' pollutes the name of the parameters and reduces code readability. + To indicate whether or not a parameter will be modify in a method, its better to document method + behavior with Javadoc. + + This rule is deprecated and will be removed with PMD 7.0.0. The rule is replaced + by the more general rule {% rule java/codestyle/FormalParameterNamingConventions %}. + + 4 + + + + + + + + + + + + + + + + + + Do not use protected fields in final classes since they cannot be subclassed. + Clarify your intent by using private or package access modifiers instead. + + 3 + + + + + + + + + + + + + + + Do not use protected methods in most final classes since they cannot be subclassed. This should + only be allowed in final classes that extend other classes with protected methods (whose + visibility cannot be reduced). Clarify your intent by using private or package access modifiers instead. + + 3 + + + + + + + + + + + + + + + Unnecessary reliance on Java Native Interface (JNI) calls directly reduces application portability + and increases the maintenance burden. + + 2 + + + //Name[starts-with(@Image,'System.loadLibrary')] + + + + + + + + + + Methods that return boolean results should be named as predicate statements to denote this. + I.e, 'isReady()', 'hasValues()', 'canCommit()', 'willFail()', etc. Avoid the use of the 'get' + prefix for these methods. + + 4 + + + + + + + + + + + + + + + + It is a good practice to call super() in a constructor. If super() is not called but + another constructor (such as an overloaded constructor) is called, this rule will not report it. + + 3 + + + + 0 ] +/ClassOrInterfaceBody + /ClassOrInterfaceBodyDeclaration + /ConstructorDeclaration[ count (.//ExplicitConstructorInvocation)=0 ] +]]> + + + + + + + + + + + Configurable naming conventions for type declarations. This rule reports + type declarations which do not match the regex that applies to their + specific kind (e.g. enum or interface). Each regex can be configured through + properties. + + By default this rule uses the standard Java naming convention (Pascal case), + and reports utility class names not ending with 'Util'. + + 1 + + + + + + + + To avoid mistakes if we want that a Method, Constructor, Field or Nested class have a default access modifier + we must add a comment at the beginning of it's declaration. + By default the comment must be /* default */ or /* package */, if you want another, you have to provide a regular expression. + This rule ignores by default all cases that have a @VisibleForTesting annotation. Use the + property "ignoredAnnotations" to customize the recognized annotations. + + 3 + + + + + + + + Avoid negation within an "if" expression with an "else" clause. For example, rephrase: + `if (x != y) diff(); else same();` as: `if (x == y) same(); else diff();`. + + Most "if (x != y)" cases without an "else" are often return cases, so consistent use of this + rule makes the code easier to read. Also, this resolves trivial ordering problems, such + as "does the error case go first?" or "does the common case go first?". + + 3 + + + + + + + + Enforce a policy for braces on control statements. It is recommended to use braces on 'if ... else' + statements and loop statements, even if they are optional. This usually makes the code clearer, and + helps prepare the future when you need to add another statement. That said, this rule lets you control + which statements are required to have braces via properties. + + From 6.2.0 on, this rule supersedes WhileLoopMustUseBraces, ForLoopMustUseBraces, IfStmtMustUseBraces, + and IfElseStmtMustUseBraces. + + 3 + + + + + + + + + + + + + 1 + or (some $stmt (: in only the block statements until the next label :) + in following-sibling::BlockStatement except following-sibling::SwitchLabel[1]/following-sibling::BlockStatement + satisfies not($stmt/Statement/Block))] + ]]> + + + + + + + + + + Use explicit scoping instead of accidental usage of default package private level. + The rule allows methods and fields annotated with Guava's @VisibleForTesting. + + 3 + + + + + + + + + + + + Avoid importing anything from the package 'java.lang'. These classes are automatically imported (JLS 7.5.3). + + 4 + + + + + + + + Duplicate or overlapping import statements should be avoided. + + 4 + + + + + + + + Empty or auto-generated methods in an abstract class should be tagged as abstract. This helps to remove their inapproprate + usage by developers who should be implementing their own versions in the concrete subclasses. + + 1 + + + + + + + + + + + + + + No need to explicitly extend Object. + 4 + + + + + + + + + + + + + + + Fields should be declared at the top of the class, before any method declarations, constructors, initializers or inner classes. + + 3 + + + + + + + + + Configurable naming conventions for field declarations. This rule reports variable declarations + which do not match the regex that applies to their specific kind ---e.g. constants (static final), + enum constant, final field. Each regex can be configured through properties. + + By default this rule uses the standard Java naming convention (Camel case), and uses the ALL_UPPER + convention for constants and enum constants. + + 1 + + + + + + + + Some for loops can be simplified to while loops, this makes them more concise. + + 3 + + + + + + + + + + + + + + + Avoid using 'for' statements without using curly braces. If the code formatting or + indentation is lost then it becomes difficult to separate the code being controlled + from the rest. + + This rule is deprecated and will be removed with PMD 7.0.0. The rule is replaced + by the rule {% rule java/codestyle/ControlStatementBraces %}. + + 3 + + + //ForStatement[not(Statement/Block)] + + + + + + + + + + Configurable naming conventions for formal parameters of methods and lambdas. + This rule reports formal parameters which do not match the regex that applies to their + specific kind (e.g. lambda parameter, or final formal parameter). Each regex can be + configured through properties. + + By default this rule uses the standard Java naming convention (Camel case). + + 1 + + lambda1 = s_str -> { }; + + // lambda parameters with an explicit type can be configured separately + Consumer lambda1 = (String str) -> { }; + + } + + } + ]]> + + + + + + Names for references to generic values should be limited to a single uppercase letter. + + 4 + + + + 1 + or + string:upper-case(@Image) != @Image +] +]]> + + + + + extends BaseDao { + // This is ok... +} + +public interface GenericDao { + // Also this +} + +public interface GenericDao { + // 'e' should be an 'E' +} + +public interface GenericDao { + // 'EF' is not ok. +} +]]> + + + + + + + Identical `catch` branches use up vertical space and increase the complexity of code without + adding functionality. It's better style to collapse identical branches into a single multi-catch + branch. + + 3 + + + + + + + + Avoid using if..else statements without using surrounding braces. If the code formatting + or indentation is lost then it becomes difficult to separate the code being controlled + from the rest. + + This rule is deprecated and will be removed with PMD 7.0.0. The rule is replaced + by the rule {% rule java/codestyle/ControlStatementBraces %}. + + 3 + + + + + + + + + + + + + + + Avoid using if statements without using braces to surround the code block. If the code + formatting or indentation is lost then it becomes difficult to separate the code being + controlled from the rest. + + This rule is deprecated and will be removed with PMD 7.0.0. The rule is replaced + by the rule {% rule java/codestyle/ControlStatementBraces %}. + + 3 + + + + + + + + + + + + + + + This rule finds Linguistic Naming Antipatterns. It checks for fields, that are named, as if they should + be boolean but have a different type. It also checks for methods, that according to their name, should + return a boolean, but don't. Further, it checks, that getters return something and setters won't. + Finally, it checks that methods, that start with "to" - so called transform methods - actually return + something, since according to their name, they should convert or transform one object into another. + There is additionally an option, to check for methods that contain "To" in their name - which are + also transform methods. However, this is disabled by default, since this detection is prone to + false positives. + + For more information, see [Linguistic Antipatterns - What They Are and How + Developers Perceive Them](https://doi.org/10.1007/s10664-014-9350-8). + + 3 + + + + + + + + The Local Home interface of a Session EJB should be suffixed by 'LocalHome'. + + 4 + + + + + + + + + + + + + + + The Local Interface of a Session EJB should be suffixed by 'Local'. + + 4 + + + + + + + + + + + + + + + A local variable assigned only once can be declared final. + + 3 + + + + + + + + Configurable naming conventions for local variable declarations and other locally-scoped + variables. This rule reports variable declarations which do not match the regex that applies to their + specific kind (e.g. final variable, or catch-clause parameter). Each regex can be configured through + properties. + + By default this rule uses the standard Java naming convention (Camel case). + + 1 + + + + + + + + Fields, formal arguments, or local variable names that are too long can make the code difficult to follow. + + 3 + + + + + $minimum] +]]> + + + + + + + + + + + The EJB Specification states that any MessageDrivenBean or SessionBean should be suffixed by 'Bean'. + + 4 + + + + + + + + + + + + + + + A method argument that is never re-assigned within the method can be declared final. + + 3 + + + + + + + + Configurable naming conventions for method declarations. This rule reports + method declarations which do not match the regex that applies to their + specific kind (e.g. JUnit test or native method). Each regex can be + configured through properties. + + By default this rule uses the standard Java naming convention (Camel case). + + 1 + + + + + + + + Detects when a non-field has a name starting with 'm_'. This usually denotes a field and could be confusing. + + This rule is deprecated and will be removed with PMD 7.0.0. The rule is replaced + by the more general rule + {% rule java/codestyle/LocalVariableNamingConventions %}. + + 3 + + + + + + + + + + + + + + + Detects when a class or interface does not have a package definition. + + 3 + + + //ClassOrInterfaceDeclaration[count(preceding::PackageDeclaration) = 0] + + + + + + + + + + Since Java 1.7, numeric literals can use underscores to separate digits. This rule enforces that + numeric literals above a certain length use these underscores to increase readability. + + The rule only supports decimal (base 10) literals for now. The acceptable length under which literals + are not required to have underscores is configurable via a property. Even under that length, underscores + that are misplaced (not making groups of 3 digits) are reported. + + 3 + + + + + + + + + + + + + + + + + A method should have only one exit point, and that should be the last statement in the method. + + 3 + + 0) { + return "hey"; // first exit + } + return "hi"; // second exit + } +} +]]> + + + + + + Detects when a package definition contains uppercase characters. + + 3 + + + //PackageDeclaration/Name[lower-case(@Image)!=@Image] + + + + + + + + + + Checks for variables that are defined before they might be used. A reference is deemed to be premature if it is created right before a block of code that doesn't use it that also has the ability to return or throw an exception. + + 3 + + + + + + + + Remote Interface of a Session EJB should not have a suffix. + + 4 + + + + + + + + + + + + + + + A Remote Home interface type of a Session EJB should be suffixed by 'Home'. + + 4 + + + + + + + + + + + + + + + Short Classnames with fewer than e.g. five characters are not recommended. + + 4 + + + + + + + + + + + + + + + + Method names that are very short are not helpful to the reader. + + 3 + + + + + + + + + + + + + + + + Fields, local variables, or parameter names that are very short are not helpful to the reader. + + 3 + + + + + + + + + + + + + + + + + Field names using all uppercase characters - Sun's Java naming conventions indicating constants - should + be declared as final. + + This rule is deprecated and will be removed with PMD 7.0.0. The rule is replaced + by the more general rule {% rule java/codestyle/FieldNamingConventions %}. + + 3 + + + + + + + + + + + + + + + If you overuse the static import feature, it can make your program unreadable and + unmaintainable, polluting its namespace with all the static members you import. + Readers of your code (including you, a few months after you wrote it) will not know + which class a static member comes from (Sun 1.5 Language Guide). + + 3 + + + + + $maximumStaticImports] +]]> + + + + + + + + + + + + Avoid the use of value in annotations when it's the only element. + + 3 + + + + + + + + + This rule detects when a constructor is not necessary; i.e., when there is only one constructor and the + constructor is identical to the default constructor. The default constructor should has same access + modifier as the declaring class. In an enum type, the default constructor is implicitly private. + + 3 + + + + + + + + Import statements allow the use of non-fully qualified names. The use of a fully qualified name + which is covered by an import statement is redundant. Consider using the non-fully qualified name. + + 4 + + + + + + + + Avoid the creation of unnecessary local variables + + 3 + + + + + + + + Fields in interfaces and annotations are automatically `public static final`, and methods are `public abstract`. + Classes, interfaces or annotations nested in an interface or annotation are automatically `public static` + (all nested interfaces and annotations are automatically static). + Nested enums are automatically `static`. + For historical reasons, modifiers which are implied by the context are accepted by the compiler, but are superfluous. + + 3 + + + + + + + + Avoid the use of unnecessary return statements. + + 3 + + + + + + + + Use the diamond operator to let the type be inferred automatically. With the Diamond operator it is possible + to avoid duplication of the type parameters. + Instead, the compiler is now able to infer the parameter types for constructor calls, + which makes the code also more readable. + + 3 + + + + + + + + + strings = new ArrayList(); // unnecessary duplication of type parameters +List stringsWithDiamond = new ArrayList<>(); // using the diamond operator is more concise +]]> + + + + + Useless parentheses should be removed. + 4 + + + + 1] + /PrimaryPrefix/Expression + [not(./CastExpression)] + [not(./ConditionalExpression)] + [not(./AdditiveExpression)] + [not(./AssignmentOperator)] +| +//Expression[not(parent::PrimaryPrefix)]/PrimaryExpression[count(*)=1] + /PrimaryPrefix/Expression +| +//Expression/ConditionalAndExpression/PrimaryExpression/PrimaryPrefix/Expression[ + count(*)=1 and + count(./CastExpression)=0 and + count(./EqualityExpression/MultiplicativeExpression)=0 and + count(./ConditionalExpression)=0 and + count(./ConditionalOrExpression)=0] +| +//Expression/ConditionalOrExpression/PrimaryExpression/PrimaryPrefix/Expression[ + count(*)=1 and + not(./CastExpression) and + not(./ConditionalExpression) and + not(./EqualityExpression/MultiplicativeExpression)] +| +//Expression/ConditionalExpression/PrimaryExpression/PrimaryPrefix/Expression[ + count(*)=1 and + not(./CastExpression) and + not(./EqualityExpression)] +| +//Expression/AdditiveExpression[not(./PrimaryExpression/PrimaryPrefix/Literal[@StringLiteral='true'])] + /PrimaryExpression[1]/PrimaryPrefix/Expression[ + count(*)=1 and + not(./CastExpression) and + not(./AdditiveExpression[@Image = '-']) and + not(./ShiftExpression) and + not(./RelationalExpression) and + not(./InstanceOfExpression) and + not(./EqualityExpression) and + not(./AndExpression) and + not(./ExclusiveOrExpression) and + not(./InclusiveOrExpression) and + not(./ConditionalAndExpression) and + not(./ConditionalOrExpression) and + not(./ConditionalExpression)] +| +//Expression/EqualityExpression/PrimaryExpression/PrimaryPrefix/Expression[ + count(*)=1 and + not(./CastExpression) and + not(./AndExpression) and + not(./InclusiveOrExpression) and + not(./ExclusiveOrExpression) and + not(./ConditionalExpression) and + not(./ConditionalAndExpression) and + not(./ConditionalOrExpression) and + not(./EqualityExpression)] +]]> + + + + + + + + + + + Reports qualified this usages in the same class. + + 3 + + + + + + + + + + + + + + + A variable naming conventions rule - customize this to your liking. Currently, it + checks for final variables that should be fully capitalized and non-final variables + that should not include underscores. + + This rule is deprecated and will be removed with PMD 7.0.0. The rule is replaced + by the more general rules {% rule java/codestyle/FieldNamingConventions %}, + {% rule java/codestyle/FormalParameterNamingConventions %}, and + {% rule java/codestyle/LocalVariableNamingConventions %}. + + 1 + + + + + + + + Avoid using 'while' statements without using braces to surround the code block. If the code + formatting or indentation is lost then it becomes difficult to separate the code being + controlled from the rest. + + This rule is deprecated and will be removed with PMD 7.0.0. The rule is replaced + by the rule {% rule java/codestyle/ControlStatementBraces %}. + + 3 + + + //WhileStatement[not(Statement/Block)] + + + + + + + + diff --git a/gradle/quality/pmd/category/java/design.xml b/gradle/quality/pmd/category/java/design.xml new file mode 100644 index 0000000..ded3d80 --- /dev/null +++ b/gradle/quality/pmd/category/java/design.xml @@ -0,0 +1,1657 @@ + + + + + + Rules that help you discover design issues. + + + + + If an abstract class does not provides any methods, it may be acting as a simple data container + that is not meant to be instantiated. In this case, it is probably better to use a private or + protected constructor in order to prevent instantiation than make the class misleadingly abstract. + + 1 + + + + + + + + + + + + + + + Avoid catching generic exceptions such as NullPointerException, RuntimeException, Exception in try-catch block + + 3 + + + + + + + + + + + + + + + Avoid creating deeply nested if-then statements since they are harder to read and error-prone to maintain. + + 3 + + y) { + if (y>z) { + if (z==x) { + // !! too deep + } + } + } + } +} +]]> + + + + + + Catch blocks that merely rethrow a caught exception only add to code size and runtime complexity. + + 3 + + + + + + + + + + + + + + + Catch blocks that merely rethrow a caught exception wrapped inside a new instance of the same type only add to + code size and runtime complexity. + + 3 + + + + + + + + + + + + + + + *Effective Java, 3rd Edition, Item 72: Favor the use of standard exceptions* +> +>Arguably, every erroneous method invocation boils down to an illegal argument or state, +but other exceptions are standardly used for certain kinds of illegal arguments and states. +If a caller passes null in some parameter for which null values are prohibited, convention dictates that +NullPointerException be thrown rather than IllegalArgumentException. + +To implement that, you are encouraged to use `java.util.Objects.requireNonNull()` +(introduced in Java 1.7). This method is designed primarily for doing parameter +validation in methods and constructors with multiple parameters. + +Your parameter validation could thus look like the following: +``` +public class Foo { + private String exampleValue; + + void setExampleValue(String exampleValue) { + // check, throw and assignment in a single standard call + this.exampleValue = Objects.requireNonNull(exampleValue, "exampleValue must not be null!"); + } + } +``` +]]> + + 1 + + + + + + + + + + + + + + + Avoid throwing certain exception types. Rather than throw a raw RuntimeException, Throwable, + Exception, or Error, use a subclassed exception or error instead. + + 1 + + + + + + + + + + + + + + + A class with only private constructors should be final, unless the private constructor + is invoked by a inner class. + + 1 + + + + = 1 ] +[count(./ClassOrInterfaceBody/ClassOrInterfaceBodyDeclaration/ConstructorDeclaration[(@Public = 'true') or (@Protected = 'true') or (@PackagePrivate = 'true')]) = 0 ] +[not(.//ClassOrInterfaceDeclaration)] +]]> + + + + + + + + + + + Sometimes two consecutive 'if' statements can be consolidated by separating their conditions with a boolean short-circuit operator. + + 3 + + + + + + + + + + + + + + + This rule counts the number of unique attributes, local variables, and return types within an object. + A number higher than the specified threshold can indicate a high degree of coupling. + + 3 + + + + + + + = 10. +Additionnally, classes with many methods of moderate complexity get reported as well once the total of their +methods' complexities reaches 80, even if none of the methods was directly reported. + +Reported methods should be broken down into several smaller methods. Reported classes should probably be broken down +into subcomponents.]]> + + 3 + + + + + + + + Data Classes are simple data holders, which reveal most of their state, and + without complex functionality. The lack of functionality may indicate that + their behaviour is defined elsewhere, which is a sign of poor data-behaviour + proximity. By directly exposing their internals, Data Classes break encapsulation, + and therefore reduce the system's maintainability and understandability. Moreover, + classes tend to strongly rely on their data representation, which makes for a brittle + design. + + Refactoring a Data Class should focus on restoring a good data-behaviour proximity. In + most cases, that means moving the operations defined on the data back into the class. + In some other cases it may make sense to remove entirely the class and move the data + into the former client classes. + + 3 + + + + + + + + Errors are system exceptions. Do not extend them. + + 3 + + + + + + + + + + + + + + + Using Exceptions as form of flow control is not recommended as they obscure true exceptions when debugging. + Either add the necessary validation or use an alternate control structure. + + 3 + + + + + + + + Excessive class file lengths are usually indications that the class may be burdened with excessive + responsibilities that could be provided by external classes or functions. In breaking these methods + apart the code becomes more manageable and ripe for reuse. + + 3 + + + + + + + + A high number of imports can indicate a high degree of coupling within an object. This rule + counts the number of unique imports and reports a violation if the count is above the + user-specified threshold. + + 3 + + + + + + + + When methods are excessively long this usually indicates that the method is doing more than its + name/signature might suggest. They also become challenging for others to digest since excessive + scrolling causes readers to lose focus. + Try to reduce the method length by creating helper methods and removing any copy/pasted code. + + 3 + + + + + + + + Methods with numerous parameters are a challenge to maintain, especially if most of them share the + same datatype. These situations usually denote the need for new objects to wrap the numerous parameters. + + 3 + + + + + + + + Classes with large numbers of public methods and attributes require disproportionate testing efforts + since combinational side effects grow rapidly and increase risk. Refactoring these classes into + smaller ones not only increases testability and reliability but also allows new variations to be + developed easily. + + 3 + + + + + + + + If a final field is assigned to a compile-time constant, it could be made static, thus saving overhead + in each object at runtime. + + 3 + + + + + + + + + + + + + + + The God Class rule detects the God Class design flaw using metrics. God classes do too many things, + are very big and overly complex. They should be split apart to be more object-oriented. + The rule uses the detection strategy described in "Object-Oriented Metrics in Practice". + The violations are reported against the entire class. + + See also the references: + + Michele Lanza and Radu Marinescu. Object-Oriented Metrics in Practice: + Using Software Metrics to Characterize, Evaluate, and Improve the Design + of Object-Oriented Systems. Springer, Berlin, 1 edition, October 2006. Page 80. + + 3 + + + + + Identifies private fields whose values never change once object initialization ends either in the declaration + of the field or by a constructor. This helps in converting existing classes to becoming immutable ones. + + 3 + + + + + + + + The Law of Demeter is a simple rule, that says "only talk to friends". It helps to reduce coupling between classes + or objects. + + See also the references: + + * Andrew Hunt, David Thomas, and Ward Cunningham. The Pragmatic Programmer. From Journeyman to Master. Addison-Wesley Longman, Amsterdam, October 1999.; + * K.J. Lieberherr and I.M. Holland. Assuring good style for object-oriented programs. Software, IEEE, 6(5):38–48, 1989.; + * <http://www.ccs.neu.edu/home/lieber/LoD.html> + * <http://en.wikipedia.org/wiki/Law_of_Demeter> + + 3 + + + + + + + + Use opposite operator instead of negating the whole expression with a logic complement operator. + + 3 + + + + + + + + + = + return false; + } + + return true; +} +]]> + + + + + + Avoid using classes from the configured package hierarchy outside of the package hierarchy, + except when using one of the configured allowed classes. + + 3 + + + + + + + + Complexity directly affects maintenance costs is determined by the number of decision points in a method + plus one for the method entry. The decision points include 'if', 'while', 'for', and 'case labels' calls. + Generally, numbers ranging from 1-4 denote low complexity, 5-7 denote moderate complexity, 8-10 denote + high complexity, and 11+ is very high complexity. Modified complexity treats switch statements as a single + decision point. + + This rule is deprecated and will be removed with PMD 7.0.0. The rule is replaced + by the rule {% rule java/design/CyclomaticComplexity %}. + + 3 + + + + + + + + This rule uses the NCSS (Non-Commenting Source Statements) algorithm to determine the number of lines + of code for a given constructor. NCSS ignores comments, and counts actual statements. Using this algorithm, + lines of code that are split are counted as one. + + This rule is deprecated and will be removed with PMD 7.0.0. The rule is replaced + by the rule {% rule java/design/NcssCount %}. + + 3 + + + + + + + + This rule uses the NCSS (Non-Commenting Source Statements) metric to determine the number of lines + of code in a class, method or constructor. NCSS ignores comments, blank lines, and only counts actual + statements. For more details on the calculation, see the documentation of + the [NCSS metric](/pmd_java_metrics_index.html#non-commenting-source-statements-ncss). + + 3 + + + + + + + + This rule uses the NCSS (Non-Commenting Source Statements) algorithm to determine the number of lines + of code for a given method. NCSS ignores comments, and counts actual statements. Using this algorithm, + lines of code that are split are counted as one. + + This rule is deprecated and will be removed with PMD 7.0.0. The rule is replaced + by the rule {% rule java/design/NcssCount %}. + + 3 + + + + + + + + This rule uses the NCSS (Non-Commenting Source Statements) algorithm to determine the number of lines + of code for a given type. NCSS ignores comments, and counts actual statements. Using this algorithm, + lines of code that are split are counted as one. + + This rule is deprecated and will be removed with PMD 7.0.0. The rule is replaced + by the rule {% rule java/design/NcssCount %}. + + 3 + + + + + + + + The NPath complexity of a method is the number of acyclic execution paths through that method. + While cyclomatic complexity counts the number of decision points in a method, NPath counts the number of + full paths from the beginning to the end of the block of the method. That metric grows exponentially, as + it multiplies the complexity of statements in the same block. For more details on the calculation, see the + documentation of the [NPath metric](/pmd_java_metrics_index.html#npath-complexity-npath). + + A threshold of 200 is generally considered the point where measures should be taken to reduce + complexity and increase readability. + + 3 + + + + + + + + A method/constructor shouldn't explicitly throw the generic java.lang.Exception, since it + is unclear which exceptions that can be thrown from the methods. It might be + difficult to document and understand such vague interfaces. Use either a class + derived from RuntimeException or a checked exception. + + 3 + + + + + + + + + + 3 + + + + + + + + + + + + + + + Avoid negation in an assertTrue or assertFalse test. + + For example, rephrase: + + assertTrue(!expr); + + as: + + assertFalse(expr); + + + 3 + + + + + + + + + + + + + + + Avoid unnecessary comparisons in boolean expressions, they serve no purpose and impacts readability. + + 3 + + + + + + + + + + + + + + + Avoid unnecessary if-then-else statements when returning a boolean. The result of + the conditional test can be returned instead. + + 3 + + + + + + + + No need to check for null before an instanceof; the instanceof keyword returns false when given a null argument. + + 3 + + + + + + + + + + + + + + + Fields whose scopes are limited to just single methods do not rely on the containing + object to provide them to other methods. They may be better implemented as local variables + within those methods. + + 3 + + + + + + + + Complexity directly affects maintenance costs is determined by the number of decision points in a method + plus one for the method entry. The decision points include 'if', 'while', 'for', and 'case labels' calls. + Generally, numbers ranging from 1-4 denote low complexity, 5-7 denote moderate complexity, 8-10 denote + high complexity, and 11+ is very high complexity. + + This rule is deprecated and will be removed with PMD 7.0.0. The rule is replaced + by the rule {% rule java/design/CyclomaticComplexity %}. + + 3 + + + + + + + + A high ratio of statements to labels in a switch statement implies that the switch statement + is overloaded. Consider moving the statements into new methods or creating subclasses based + on the switch variable. + + 3 + + + + + + + + Classes that have too many fields can become unwieldy and could be redesigned to have fewer fields, + possibly through grouping related fields in new objects. For example, a class with individual + city/state/zip fields could park them within a single Address field. + + 3 + + + + + + + + A class with too many methods is probably a good suspect for refactoring, in order to reduce its + complexity and find a way to have more fine grained objects. + + 3 + + + + + + $maxmethods + ] +]]> + + + + + + + + The overriding method merely calls the same method defined in a superclass. + + 3 + + + + + + + + When you write a public method, you should be thinking in terms of an API. If your method is public, it means other class + will use it, therefore, you want (or need) to offer a comprehensive and evolutive API. If you pass a lot of information + as a simple series of Strings, you may think of using an Object to represent all those information. You'll get a simpler + API (such as doWork(Workload workload), rather than a tedious series of Strings) and more importantly, if you need at some + point to pass extra data, you'll be able to do so by simply modifying or extending Workload without any modification to + your API. + + 3 + + + + 3 +] +]]> + + + + + + + + + + + For classes that only have static methods, consider making them utility classes. + Note that this doesn't apply to abstract classes, since their subclasses may + well include non-static methods. Also, if you want this class to be a utility class, + remember to add a private constructor to prevent instantiation. + (Note, that this use was known before PMD 5.1.0 as UseSingleton). + + 3 + + + + + + diff --git a/gradle/quality/pmd/category/java/documentation.xml b/gradle/quality/pmd/category/java/documentation.xml new file mode 100644 index 0000000..34b351a --- /dev/null +++ b/gradle/quality/pmd/category/java/documentation.xml @@ -0,0 +1,144 @@ + + + + + + Rules that are related to code documentation. + + + + + A rule for the politically correct... we don't want to offend anyone. + + 3 + + + + + + + + Denotes whether comments are required (or unwanted) for specific language elements. + + 3 + + + + + + + + Determines whether the dimensions of non-header comments found are within the specified limits. + + 3 + + + + + + + + Uncommented Empty Constructor finds instances where a constructor does not + contain statements, but there is no comment. By explicitly commenting empty + constructors it is easier to distinguish between intentional (commented) + and unintentional empty constructors. + + 3 + + + + + + + + + + + + + + + + Uncommented Empty Method Body finds instances where a method body does not contain + statements, but there is no comment. By explicitly commenting empty method bodies + it is easier to distinguish between intentional (commented) and unintentional + empty methods. + + 3 + + + + + + + + + + + + + \ No newline at end of file diff --git a/gradle/quality/pmd/category/java/errorprone.xml b/gradle/quality/pmd/category/java/errorprone.xml new file mode 100644 index 0000000..5ee4e89 --- /dev/null +++ b/gradle/quality/pmd/category/java/errorprone.xml @@ -0,0 +1,3383 @@ + + + + + + Rules to detect constructs that are either broken, extremely confusing or prone to runtime errors. + + + + + Avoid assignments in operands; this can make code more complicated and harder to read. + + 3 + + + + + + + + Identifies a possible unsafe usage of a static field. + + 3 + + + + + + + + Methods such as getDeclaredConstructors(), getDeclaredConstructor(Class[]) and setAccessible(), + as the interface PrivilegedAction, allow for the runtime alteration of variable, class, or + method visibility, even if they are private. This violates the principle of encapsulation. + + 3 + + + + + + + + + + + + + + + Use of the term 'assert' will conflict with newer versions of Java since it is a reserved word. + + 2 + + + //VariableDeclaratorId[@Image='assert'] + + + + + + + + + + Using a branching statement as the last message of a loop may be a bug, and/or is confusing. + Ensure that the usage is not a bug, or consider using another approach. + + 2 + + 25) { + break; + } +} +]]> + + + + + + The method Object.finalize() is called by the garbage collector on an object when garbage collection determines + that there are no more references to the object. It should not be invoked by application logic. + + Note that Oracle has declared Object.finalize() as deprecated since JDK 9. + + 3 + + + + + + + + Code should never throw NullPointerExceptions under normal circumstances. A catch block may hide the + original error, causing other, more subtle problems later on. + + 3 + + + + + + + + + + + + + + + Catching Throwable errors is not recommended since its scope is very broad. It includes runtime issues such as + OutOfMemoryError that should be exposed and managed separately. + + 3 + + + + + + + + One might assume that the result of "new BigDecimal(0.1)" is exactly equal to 0.1, but it is actually + equal to .1000000000000000055511151231257827021181583404541015625. + This is because 0.1 cannot be represented exactly as a double (or as a binary fraction of any finite + length). Thus, the long value that is being passed in to the constructor is not exactly equal to 0.1, + appearances notwithstanding. + + The (String) constructor, on the other hand, is perfectly predictable: 'new BigDecimal("0.1")' is + exactly equal to 0.1, as one would expect. Therefore, it is generally recommended that the + (String) constructor be used in preference to this one. + + 3 + + + + + + + + + + + + + + + Code containing duplicate String literals can usually be improved by declaring the String as a constant field. + + 3 + + + + + + + + Use of the term 'enum' will conflict with newer versions of Java since it is a reserved word. + + 2 + + + //VariableDeclaratorId[@Image='enum'] + + + + + + + + + + It can be confusing to have a field name with the same name as a method. While this is permitted, + having information (field) and actions (method) is not clear naming. Developers versed in + Smalltalk often prefer this approach as the methods denote accessor methods. + + 3 + + + + + + + + It is somewhat confusing to have a field name matching the declaring class name. + This probably means that type and/or field names should be chosen more carefully. + + 3 + + + + + + + + Each caught exception type should be handled in its own catch clause. + + 3 + + + + + + + + + + + + + + + Avoid using hard-coded literals in conditional statements. By declaring them as static variables + or private members with descriptive names maintainability is enhanced. By default, the literals "-1" and "0" are ignored. + More exceptions can be defined with the property "ignoreMagicNumbers". + + 3 + + + + + + + + + + + = 0) { } // alternative approach + + if (aDouble > 0.0) {} // magic number 0.0 + if (aDouble >= Double.MIN_VALUE) {} // preferred approach +} +]]> + + + + + + Statements in a catch block that invoke accessors on the exception without using the information + only add to code size. Either remove the invocation, or use the return result. + + 2 + + + + + + + + + + + + + + + The use of multiple unary operators may be problematic, and/or confusing. + Ensure that the intended usage is not a bug, or consider simplifying the expression. + + 2 + + + + + + + + Integer literals should not start with zero since this denotes that the rest of literal will be + interpreted as an octal value. + + 3 + + + + + + + + Avoid equality comparisons with Double.NaN. Due to the implicit lack of representation + precision when comparing floating point numbers these are likely to cause logic errors. + + 3 + + + + + + + + + + + + + + + If a class is a bean, or is referenced by a bean directly or indirectly it needs to be serializable. + Member variables need to be marked as transient, static, or have accessor methods in the class. Marking + variables as transient is the safest and easiest modification. Accessor methods should follow the Java + naming conventions, i.e. for a variable named foo, getFoo() and setFoo() accessor methods should be provided. + + 3 + + + + + + + + The null check is broken since it will throw a NullPointerException itself. + It is likely that you used || instead of && or vice versa. + + 2 + + + + + + + Super should be called at the start of the method + 3 + + + + + + + + + + + + + + + Super should be called at the end of the method + + 3 + + + + + + + + + + + + + + + The skip() method may skip a smaller number of bytes than requested. Check the returned value to find out if it was the case or not. + + 3 + + + + + + + + When deriving an array of a specific class from your Collection, one should provide an array of + the same class as the parameter of the toArray() method. Doing otherwise you will will result + in a ClassCastException. + + 3 + + + + + + + + + + + + + + + The java Manual says "By convention, classes that implement this interface should override + Object.clone (which is protected) with a public method." + + 3 + + + + + + + + + + + + + + + The method clone() should only be implemented if the class implements the Cloneable interface with the exception of + a final method that only throws CloneNotSupportedException. + + The rule can also detect, if the class implements or extends a Cloneable class. + + 3 + + + + + + + + If a class implements cloneable the return type of the method clone() must be the class name. That way, the caller + of the clone method doesn't need to cast the returned clone to the correct type. + + Note: This is only possible with Java 1.5 or higher. + + 3 + + + + + + + + + + + + + + + The method clone() should throw a CloneNotSupportedException. + + 3 + + + + + + + + + + + + + + + Ensure that resources (like Connection, Statement, and ResultSet objects) are always closed after use. + + 3 + + + + + + + + Use equals() to compare object references; avoid comparing them with ==. + + 3 + + + + + + + + Calling overridable methods during construction poses a risk of invoking methods on an incompletely + constructed object and can be difficult to debug. + It may leave the sub-class unable to construct its superclass or forced to replicate the construction + process completely within itself, losing the ability to call super(). If the default constructor + contains a call to an overridable method, the subclass may be completely uninstantiable. Note that + this includes method calls throughout the control flow graph - i.e., if a constructor Foo() calls a + private method bar() that calls a public method buz(), this denotes a problem. + + 1 + + + + + + + The dataflow analysis tracks local definitions, undefinitions and references to variables on different paths on the data flow. + From those informations there can be found various problems. + + 1. UR - Anomaly: There is a reference to a variable that was not defined before. This is a bug and leads to an error. + 2. DU - Anomaly: A recently defined variable is undefined. These anomalies may appear in normal source text. + 3. DD - Anomaly: A recently defined variable is redefined. This is ominous but don't have to be a bug. + + 5 + + dd-anomaly + foo(buz); + buz = 2; +} // buz is undefined when leaving scope -> du-anomaly +]]> + + + + + + Calls to System.gc(), Runtime.getRuntime().gc(), and System.runFinalization() are not advised. Code should have the + same behavior whether the garbage collection is disabled using the option -Xdisableexplicitgc or not. + Moreover, "modern" jvms do a very good job handling garbage collections. If memory usage issues unrelated to memory + leaks develop within an application, it should be dealt with JVM options rather than within the code itself. + + 2 + + + + + + + + + + + + + + + Web applications should not call System.exit(), since only the web container or the + application server should stop the JVM. This rule also checks for the equivalent call Runtime.getRuntime().exit(). + + 3 + + + + + + + + + + + + + + + Extend Exception or RuntimeException instead of Throwable. + + 3 + + + + + + + + + + + + + + + Use Environment.getExternalStorageDirectory() instead of "/sdcard" + + 3 + + + //Literal[starts-with(@Image,'"/sdcard')] + + + + + + + + + + Throwing exceptions within a 'finally' block is confusing since they may mask other exceptions + or code defects. + Note: This is a PMD implementation of the Lint4j rule "A throw in a finally block" + + 4 + + + //FinallyStatement[descendant::ThrowStatement] + + + + + + + + + + Avoid importing anything from the 'sun.*' packages. These packages are not portable and are likely to change. + + 4 + + + + + + + + Don't use floating point for loop indices. If you must use floating point, use double + unless you're certain that float provides enough precision and you have a compelling + performance need (space or time). + + 3 + + + + + + + + + + + + + + + Empty Catch Block finds instances where an exception is caught, but nothing is done. + In most circumstances, this swallows an exception which should either be acted on + or reported. + + 3 + + + + + + + + + + + + + + + + + Empty finalize methods serve no purpose and should be removed. Note that Oracle has declared Object.finalize() as deprecated since JDK 9. + + 3 + + + + + + + + + + + + + + + Empty finally blocks serve no purpose and should be removed. + + 3 + + + + + + + + + + + + + + + Empty If Statement finds instances where a condition is checked but nothing is done about it. + + 3 + + + + + + + + + + + + + + + Empty initializers serve no purpose and should be removed. + + 3 + + + //Initializer/Block[count(*)=0] + + + + + + + + + + Empty block statements serve no purpose and should be removed. + + 3 + + + //BlockStatement/Statement/Block[count(*) = 0] + + + + + + + + + + An empty statement (or a semicolon by itself) that is not used as the sole body of a 'for' + or 'while' loop is probably a bug. It could also be a double semicolon, which has no purpose + and should be removed. + + 3 + + + + + + + + + + + + + + + Empty switch statements serve no purpose and should be removed. + + 3 + + + //SwitchStatement[count(*) = 1] + + + + + + + + + + Empty synchronized blocks serve no purpose and should be removed. + + 3 + + + //SynchronizedStatement/Block[1][count(*) = 0] + + + + + + + + + + Avoid empty try blocks - what's the point? + + 3 + + + + + + + + + + + + + + + Empty While Statement finds all instances where a while statement does nothing. + If it is a timing loop, then you should use Thread.sleep() for it; if it is + a while loop that does a lot in the exit expression, rewrite it to make it clearer. + + 3 + + + + + + + + + + + + + + + Tests for null should not use the equals() method. The '==' operator should be used instead. + + 1 + + + + + + + + + + + + + + + If the finalize() is implemented, its last action should be to call super.finalize. Note that Oracle has declared Object.finalize() as deprecated since JDK 9. + + 3 + + + + + + + + + + + + + + + + If the finalize() is implemented, it should do something besides just calling super.finalize(). Note that Oracle has declared Object.finalize() as deprecated since JDK 9. + + 3 + + + + + + + + + + + + + + + Methods named finalize() should not have parameters. It is confusing and most likely an attempt to + overload Object.finalize(). It will not be called by the VM. + + Note that Oracle has declared Object.finalize() as deprecated since JDK 9. + + 3 + + + + 0]] +]]> + + + + + + + + + + + When overriding the finalize(), the new method should be set as protected. If made public, + other classes may invoke it at inappropriate times. + + Note that Oracle has declared Object.finalize() as deprecated since JDK 9. + + 3 + + + + + + + + + + + + + + + Avoid idempotent operations - they have no effect. + + 3 + + + + + + + + There is no need to import a type that lives in the same package. + + 3 + + + + + + + + Avoid instantiating an object just to call getClass() on it; use the .class public member instead. + + 4 + + + + + + + + + + + + + + + Check for messages in slf4j loggers with non matching number of arguments and placeholders. + + 5 + + + + + + + + Avoid jumbled loop incrementers - its usually a mistake, and is confusing even if intentional. + + 3 + + + + + + + + + + + + + + Some JUnit framework methods are easy to misspell. + + 3 + + + + + + + + + + + + + + + The suite() method in a JUnit test needs to be both public and static. + + 3 + + + + + + + + + + + + + + + In most cases, the Logger reference can be declared as static and final. + + 2 + + + + + + + + + + + + + + + Non-constructor methods should not have the same name as the enclosing class. + + 3 + + + + + + + + The null check here is misplaced. If the variable is null a NullPointerException will be thrown. + Either the check is useless (the variable will never be "null") or it is incorrect. + + 3 + + + + + + + + + + + + + + + + + + Switch statements without break or return statements for each case option + may indicate problematic behaviour. Empty cases are ignored as these indicate an intentional fall-through. + + 3 + + + + + + + + + + + + + + + Serializable classes should provide a serialVersionUID field. + The serialVersionUID field is also needed for abstract base classes. Each individual class in the inheritance + chain needs an own serialVersionUID field. See also [Should an abstract class have a serialVersionUID](https://stackoverflow.com/questions/893259/should-an-abstract-class-have-a-serialversionuid). + + 3 + + + + + + + + + + + + + + + A class that has private constructors and does not have any static methods or fields cannot be used. + + 3 + + + + + + + + + + + + + + + Normally only one logger is used in each class. + + 2 + + + + + + + + A non-case label (e.g. a named break/continue label) was present in a switch statement. + This legal, but confusing. It is easy to mix up the case labels and the non-case labels. + + 3 + + + //SwitchStatement//BlockStatement/Statement/LabeledStatement + + + + + + + + + + A non-static initializer block will be called any time a constructor is invoked (just prior to + invoking the constructor). While this is a valid language construct, it is rarely used and is + confusing. + + 3 + + + + + + + + + + + + + + + Assigning a "null" to a variable (outside of its declaration) is usually bad form. Sometimes, this type + of assignment is an indication that the programmer doesn't completely understand what is going on in the code. + + NOTE: This sort of assignment may used in some cases to dereference objects and encourage garbage collection. + + 3 + + + + + + + + Override both public boolean Object.equals(Object other), and public int Object.hashCode(), or override neither. Even if you are inheriting a hashCode() from a parent class, consider implementing hashCode and explicitly delegating to your superclass. + + 3 + + + + + + + + Object clone() should be implemented with super.clone(). + + 2 + + + + 0 +] +]]> + + + + + + + + + + + A logger should normally be defined private static final and be associated with the correct class. + Private final Log log; is also allowed for rare cases where loggers need to be passed around, + with the restriction that the logger needs to be passed into the constructor. + + 3 + + + + + + + + + + + + + + + + For any method that returns an array, it is a better to return an empty array rather than a + null reference. This removes the need for null checking all results and avoids inadvertent + NullPointerExceptions. + + 1 + + + + + + + + + + + + + + + Avoid returning from a finally block, this can discard exceptions. + + 3 + + + + //FinallyStatement//ReturnStatement except //FinallyStatement//(MethodDeclaration|LambdaExpression)//ReturnStatement + + + + + + + + + + Be sure to specify a Locale when creating SimpleDateFormat instances to ensure that locale-appropriate + formatting is used. + + 3 + + + + + + + + + + + + + + + Some classes contain overloaded getInstance. The problem with overloaded getInstance methods + is that the instance created using the overloaded method is not cached and so, + for each call and new objects will be created for every invocation. + + 2 + + + + + + + + Some classes contain overloaded getInstance. The problem with overloaded getInstance methods + is that the instance created using the overloaded method is not cached and so, + for each call and new objects will be created for every invocation. + + 2 + + + + + + + + According to the J2EE specification, an EJB should not have any static fields + with write access. However, static read-only fields are allowed. This ensures proper + behavior especially when instances are distributed by the container on several JREs. + + 3 + + + + + + + + + + + + + + + Individual character values provided as initialization arguments will be converted into integers. + This can lead to internal buffer sizes that are larger than expected. Some examples: + + ``` + new StringBuffer() // 16 + new StringBuffer(6) // 6 + new StringBuffer("hello world") // 11 + 16 = 27 + new StringBuffer('A') // chr(A) = 65 + new StringBuffer("A") // 1 + 16 = 17 + + new StringBuilder() // 16 + new StringBuilder(6) // 6 + new StringBuilder("hello world") // 11 + 16 = 27 + new StringBuilder('C') // chr(C) = 67 + new StringBuilder("A") // 1 + 16 = 17 + ``` + + 4 + + + + + + + + + + + + + + + The method name and parameter number are suspiciously close to equals(Object), which can denote an + intention to override the equals(Object) method. + + 2 + + + + + + + + + + + + + + + The method name and return type are suspiciously close to hashCode(), which may denote an intention + to override the hashCode() method. + + 3 + + + + + + + + A suspicious octal escape sequence was found inside a String literal. + The Java language specification (section 3.10.6) says an octal + escape sequence inside a literal String shall consist of a backslash + followed by: + + OctalDigit | OctalDigit OctalDigit | ZeroToThree OctalDigit OctalDigit + + Any octal escape sequence followed by non-octal digits can be confusing, + e.g. "\038" is interpreted as the octal escape sequence "\03" followed by + the literal character "8". + + 3 + + + + + + + + Test classes end with the suffix Test. Having a non-test class with that name is not a good practice, + since most people will assume it is a test case. Test classes have test methods named testXXX. + + 3 + + + + + + + + Do not use "if" statements whose conditionals are always true or always false. + + 3 + + + + + + + + + + + + + + + A JUnit test assertion with a boolean literal is unnecessary since it always will evaluate to the same thing. + Consider using flow control (in case of assertTrue(false) or similar) or simply removing + statements like assertTrue(true) and assertFalse(false). If you just want a test to halt after finding + an error, use the fail() method and provide an indication message of why it did. + + 3 + + + + + + + + + + + + + + + Using equalsIgnoreCase() is faster than using toUpperCase/toLowerCase().equals() + + 3 + + + + + + + + Avoid the use temporary objects when converting primitives to Strings. Use the static conversion methods + on the wrapper classes instead. + + 3 + + + + + + + + After checking an object reference for null, you should invoke equals() on that object rather than passing it to another object's equals() method. + + 3 + + + + + + + + + + + + + + + To make sure the full stacktrace is printed out, use the logging statement with two arguments: a String and a Throwable. + + 3 + + + + + + + + + + + + + + + Using '==' or '!=' to compare strings only works if intern version is used on both sides. + Use the equals() method instead. + + 3 + + + + + + + + + + + + + + + An operation on an Immutable object (String, BigDecimal or BigInteger) won't change the object itself + since the result of the operation is a new object. Therefore, ignoring the operation result is an error. + + 3 + + + + + + + + When doing String.toLowerCase()/toUpperCase() conversions, use Locales to avoids problems with languages that + have unusual conventions, i.e. Turkish. + + 3 + + + + + + + + + + + + + + + In J2EE, the getClassLoader() method might not work as expected. Use + Thread.currentThread().getContextClassLoader() instead. + + 3 + + + //PrimarySuffix[@Image='getClassLoader'] + + + + + + + + diff --git a/gradle/quality/pmd/category/java/multithreading.xml b/gradle/quality/pmd/category/java/multithreading.xml new file mode 100644 index 0000000..d3e8327 --- /dev/null +++ b/gradle/quality/pmd/category/java/multithreading.xml @@ -0,0 +1,393 @@ + + + + + + Rules that flag issues when dealing with multiple threads of execution. + + + + + Method-level synchronization can cause problems when new code is added to the method. + Block-level synchronization helps to ensure that only the code that needs synchronization + gets it. + + 3 + + + //MethodDeclaration[@Synchronized='true'] + + + + + + + + + + Avoid using java.lang.ThreadGroup; although it is intended to be used in a threaded environment + it contains methods that are not thread-safe. + + 3 + + + + + + + + + + + + + + + Use of the keyword 'volatile' is generally used to fine tune a Java application, and therefore, requires + a good expertise of the Java Memory Model. Moreover, its range of action is somewhat misknown. Therefore, + the volatile keyword should not be used for maintenance purpose and portability. + + 2 + + + //FieldDeclaration[contains(@Volatile,'true')] + + + + + + + + + + The J2EE specification explicitly forbids the use of threads. + + 3 + + + //ClassOrInterfaceType[@Image = 'Thread' or @Image = 'Runnable'] + + + + + + + + + + Explicitly calling Thread.run() method will execute in the caller's thread of control. Instead, call Thread.start() for the intended behavior. + + 4 + + + + + + + + + + + + + + + Partially created objects can be returned by the Double Checked Locking pattern when used in Java. + An optimizing JRE may assign a reference to the baz variable before it calls the constructor of the object the + reference points to. + + Note: With Java 5, you can make Double checked locking work, if you declare the variable to be `volatile`. + + For more details refer to: <http://www.javaworld.com/javaworld/jw-02-2001/jw-0209-double.html> + or <http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html> + + 1 + + + + + + + + Non-thread safe singletons can result in bad state changes. Eliminate + static singletons if possible by instantiating the object directly. Static + singletons are usually not needed as only a single instance exists anyway. + Other possible fixes are to synchronize the entire method or to use an + [initialize-on-demand holder class](https://en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom). + + Refrain from using the double-checked locking pattern. The Java Memory Model doesn't + guarantee it to work unless the variable is declared as `volatile`, adding an uneeded + performance penalty. [Reference](http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html) + + See Effective Java, item 48. + + 3 + + + + + + + + SimpleDateFormat instances are not synchronized. Sun recommends using separate format instances + for each thread. If multiple threads must access a static formatter, the formatter must be + synchronized either on method or block level. + + This rule has been deprecated in favor of the rule {% rule UnsynchronizedStaticFormatter %}. + + 3 + + + + + + + + Instances of `java.text.Format` are generally not synchronized. + Sun recommends using separate format instances for each thread. + If multiple threads must access a static formatter, the formatter must be + synchronized either on method or block level. + + 3 + + + + + + + + Since Java5 brought a new implementation of the Map designed for multi-threaded access, you can + perform efficient map reads without blocking other threads. + + 3 + + + + + + + + + + + + + + + Thread.notify() awakens a thread monitoring the object. If more than one thread is monitoring, then only + one is chosen. The thread chosen is arbitrary; thus its usually safer to call notifyAll() instead. + + 3 + + + + + + + + + + + + + \ No newline at end of file diff --git a/gradle/quality/pmd/category/java/performance.xml b/gradle/quality/pmd/category/java/performance.xml new file mode 100644 index 0000000..1ce2d8d --- /dev/null +++ b/gradle/quality/pmd/category/java/performance.xml @@ -0,0 +1,1006 @@ + + + + + + Rules that flag suboptimal code. + + + + + The conversion of literals to strings by concatenating them with empty strings is inefficient. + It is much better to use one of the type-specific toString() methods instead. + + 3 + + + + + + + + + + + + + + + Avoid concatenating characters as strings in StringBuffer/StringBuilder.append methods. + + 3 + + + + + + + + Instead of manually copying data between two arrays, use the efficient Arrays.copyOf or System.arraycopy method instead. + + 3 + + + + + + + + + + + + + + + The FileInputStream and FileOutputStream classes contains a finalizer method which will cause garbage + collection pauses. + See [JDK-8080225](https://bugs.openjdk.java.net/browse/JDK-8080225) for details. + + The FileReader and FileWriter constructors instantiate FileInputStream and FileOutputStream, + again causing garbage collection issues while finalizer methods are called. + + * Use `Files.newInputStream(Paths.get(fileName))` instead of `new FileInputStream(fileName)`. + * Use `Files.newOutputStream(Paths.get(fileName))` instead of `new FileOutputStream(fileName)`. + * Use `Files.newBufferedReader(Paths.get(fileName))` instead of `new FileReader(fileName)`. + * Use `Files.newBufferedWriter(Paths.get(fileName))` instead of `new FileWriter(fileName)`. + + Please note, that the `java.nio` API does not throw a `FileNotFoundException` anymore, instead + it throws a `NoSuchFileException`. If your code dealt explicitly with a `FileNotFoundException`, + then this needs to be adjusted. Both exceptions are subclasses of `IOException`, so catching + that one covers both. + + 1 + + + + + + + + + + + + + + + New objects created within loops should be checked to see if they can created outside them and reused. + + 3 + + + + + + + + Java uses the 'short' type to reduce memory usage, not to optimize calculation. In fact, the JVM does not have any + arithmetic capabilities for the short type: the JVM must convert the short into an int, do the proper calculation + and convert the int back to a short. Thus any storage gains found through use of the 'short' type may be offset by + adverse impacts on performance. + + 1 + + + + + + + + + + + + + + + Don't create instances of already existing BigInteger (BigInteger.ZERO, BigInteger.ONE) and + for Java 1.5 onwards, BigInteger.TEN and BigDecimal (BigDecimal.ZERO, BigDecimal.ONE, BigDecimal.TEN) + + 3 + + + + + + + + Avoid instantiating Boolean objects; you can reference Boolean.TRUE, Boolean.FALSE, or call Boolean.valueOf() instead. + Note that new Boolean() is deprecated since JDK 9 for that reason. + + 2 + + + + + + + + Calling new Byte() causes memory allocation that can be avoided by the static Byte.valueOf(). + It makes use of an internal cache that recycles earlier instances making it more memory efficient. + Note that new Byte() is deprecated since JDK 9 for that reason. + + 2 + + + + + + + + + + + + + + + Consecutive calls to StringBuffer/StringBuilder .append should be chained, reusing the target object. This can improve the performance + by producing a smaller bytecode, reducing overhead and improving inlining. A complete analysis can be found [here](https://github.com/pmd/pmd/issues/202#issuecomment-274349067) + + 3 + + + + + + + + Consecutively calling StringBuffer/StringBuilder.append(...) with literals should be avoided. + Since the literals are constants, they can already be combined into a single String literal and this String + can be appended in a single method call. + + 3 + + + + + + + + + + 3 + + 0) { + doSomething(); + } +} +]]> + + + + + + Avoid concatenating non-literals in a StringBuffer constructor or append() since intermediate buffers will + need to be be created and destroyed by the JVM. + + 3 + + + + + + + + Failing to pre-size a StringBuffer or StringBuilder properly could cause it to re-size many times + during runtime. This rule attempts to determine the total number the characters that are actually + passed into StringBuffer.append(), but represents a best guess "worst case" scenario. An empty + StringBuffer/StringBuilder constructor initializes the object to 16 characters. This default + is assumed if the length of the constructor can not be determined. + + 3 + + + + + + + + Calling new Integer() causes memory allocation that can be avoided by the static Integer.valueOf(). + It makes use of an internal cache that recycles earlier instances making it more memory efficient. + Note that new Integer() is deprecated since JDK 9 for that reason. + + 2 + + + + + + + + + + + + + + + Calling new Long() causes memory allocation that can be avoided by the static Long.valueOf(). + It makes use of an internal cache that recycles earlier instances making it more memory efficient. + Note that new Long() is deprecated since JDK 9 for that reason. + + 2 + + + + + + + + + + + + + + + Calls to a collection's `toArray(E[])` method should specify a target array of zero size. This allows the JVM + to optimize the memory allocation and copying as much as possible. + + Previous versions of this rule (pre PMD 6.0.0) suggested the opposite, but current JVM implementations + perform always better, when they have full control over the target array. And allocation an array via + reflection is nowadays as fast as the direct allocation. + + See also [Arrays of Wisdom of the Ancients](https://shipilev.net/blog/2016/arrays-wisdom-ancients/) + + Note: If you don't need an array of the correct type, then the simple `toArray()` method without an array + is faster, but returns only an array of type `Object[]`. + + 3 + + + + + + + + + foos = getFoos(); + +// much better; this one allows the jvm to allocate an array of the correct size and effectively skip +// the zeroing, since each array element will be overridden anyways +Foo[] fooArray = foos.toArray(new Foo[0]); + +// inefficient, the array needs to be zeroed out by the jvm before it is handed over to the toArray method +Foo[] fooArray = foos.toArray(new Foo[foos.size()]); +]]> + + + + + + Java will initialize fields with known default values so any explicit initialization of those same defaults + is redundant and results in a larger class file (approximately three additional bytecode instructions per field). + + 3 + + + + + + + + Since it passes in a literal of length 1, calls to (string).startsWith can be rewritten using (string).charAt(0) + at the expense of some readability. + + 3 + + + + + + + + + + + + + + + Calling new Short() causes memory allocation that can be avoided by the static Short.valueOf(). + It makes use of an internal cache that recycles earlier instances making it more memory efficient. + Note that new Short() is deprecated since JDK 9 for that reason. + + 2 + + + + + + + + + + + + + + + Avoid instantiating String objects; this is usually unnecessary since they are immutable and can be safely shared. + + 2 + + + + + + + + Avoid calling toString() on objects already known to be string instances; this is unnecessary. + + 3 + + + + + + + + Switch statements are intended to be used to support complex branching behaviour. Using a switch for only a few + cases is ill-advised, since switches are not as easy to understand as if-then statements. In these cases use the + if-then statement to increase code readability. + + 3 + + + + + + + + + + + + + + + + Most wrapper classes provide static conversion methods that avoid the need to create intermediate objects + just to create the primitive forms. Using these avoids the cost of creating objects that also need to be + garbage-collected later. + + 3 + + + + + + + + ArrayList is a much better Collection implementation than Vector if thread-safe operation is not required. + + 3 + + + + 0] + //AllocationExpression/ClassOrInterfaceType + [@Image='Vector' or @Image='java.util.Vector'] +]]> + + + + + + + + + + + (Arrays.asList(...)) if that is inconvenient for you (e.g. because of concurrent access). +]]> + + 3 + + + + + + + + + l= new ArrayList<>(100); + for (int i=0; i< 100; i++) { + l.add(ints[i]); + } + for (int i=0; i< 100; i++) { + l.add(a[i].toString()); // won't trigger the rule + } + } +} +]]> + + + + + + Use String.indexOf(char) when checking for the index of a single character; it executes faster. + + 3 + + + + + + + + No need to call String.valueOf to append to a string; just use the valueOf() argument directly. + + 3 + + + + + + + + The use of the '+=' operator for appending strings causes the JVM to create and use an internal StringBuffer. + If a non-trivial number of these concatenations are being used then the explicit use of a StringBuilder or + threadsafe StringBuffer is recommended to avoid this. + + 3 + + + + + + + + Use StringBuffer.length() to determine StringBuffer length rather than using StringBuffer.toString().equals("") + or StringBuffer.toString().length() == ... + + 3 + + + + + + + + + diff --git a/gradle/quality/pmd/category/java/security.xml b/gradle/quality/pmd/category/java/security.xml new file mode 100644 index 0000000..dbad352 --- /dev/null +++ b/gradle/quality/pmd/category/java/security.xml @@ -0,0 +1,65 @@ + + + + + + Rules that flag potential security flaws. + + + + + Do not use hard coded values for cryptographic operations. Please store keys outside of source code. + + 3 + + + + + + + + Do not use hard coded initialization vector in cryptographic operations. Please use a randomly generated IV. + + 3 + + + + + + diff --git a/gradle/quality/sonarqube.gradle b/gradle/quality/sonarqube.gradle new file mode 100644 index 0000000..fe66cd0 --- /dev/null +++ b/gradle/quality/sonarqube.gradle @@ -0,0 +1,37 @@ + +subprojects { + + sonarqube { + properties { + property "sonar.projectName", "${project.group} ${project.name}" + property "sonar.sourceEncoding", "UTF-8" + property "sonar.tests", "src/test/java" + property "sonar.scm.provider", "git" + property "sonar.junit.reportsPath", "build/test-results/test/" + } + } + + + tasks.withType(Pmd) { + ignoreFailures = true + reports { + xml.enabled = true + html.enabled = true + } + } + + + spotbugs { + effort = "max" + reportLevel = "low" + //includeFilter = file("findbugs-exclude.xml") + } + + tasks.withType(com.github.spotbugs.SpotBugsTask) { + ignoreFailures = true + reports { + xml.enabled = false + html.enabled = true + } + } +} \ No newline at end of file diff --git a/gradle/quality/spotbugs.gradle b/gradle/quality/spotbugs.gradle new file mode 100644 index 0000000..2e5b0cd --- /dev/null +++ b/gradle/quality/spotbugs.gradle @@ -0,0 +1,15 @@ + +apply plugin: 'com.github.spotbugs' + +spotbugs { + effort = "max" + reportLevel = "low" + ignoreFailures = true +} + +spotbugsMain { + reports { + xml.getRequired().set(false) + html.getRequired().set(true) + } +} diff --git a/gradle/repositories/maven.gradle b/gradle/repositories/maven.gradle new file mode 100644 index 0000000..ec58acb --- /dev/null +++ b/gradle/repositories/maven.gradle @@ -0,0 +1,4 @@ +repositories { + mavenLocal() + mavenCentral() +} diff --git a/gradle/test/junit5.gradle b/gradle/test/junit5.gradle new file mode 100644 index 0000000..634ef8e --- /dev/null +++ b/gradle/test/junit5.gradle @@ -0,0 +1,29 @@ +dependencies { + testImplementation testLibs.junit.jupiter.api + testImplementation testLibs.hamcrest + testRuntimeOnly testLibs.junit.jupiter.engine + testRuntimeOnly testLibs.junit.jupiter.platform.launcher +} + +test { + useJUnitPlatform { + filter { + includeTestsMatching "*Test" + includeTestsMatching "*Tests" + excludeTestsMatching "*IT" + } + } + failFast = false + testLogging { + events 'STARTED', 'PASSED', 'FAILED', 'SKIPPED' + } + afterSuite { desc, result -> + if (!desc.parent) { + println "\nTest result: ${result.resultType}" + println "Test summary: ${result.testCount} tests, " + + "${result.successfulTestCount} succeeded, " + + "${result.failedTestCount} failed, " + + "${result.skippedTestCount} skipped" + } + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e644113 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..b82aa23 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..1aa94a4 --- /dev/null +++ b/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..7101f8e --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/logging-adapter-log4j/build.gradle b/logging-adapter-log4j/build.gradle new file mode 100644 index 0000000..e701382 --- /dev/null +++ b/logging-adapter-log4j/build.gradle @@ -0,0 +1,16 @@ +dependencies { + api project(':logging') + api libs.log4j.core +} + +def moduleName = 'org.xbib.logging.log4j.test' +def patchArgs = ['--patch-module', "$moduleName=" + files(sourceSets.test.resources.srcDirs).asPath ] + +tasks.named('compileTestJava') { + options.compilerArgs += patchArgs +} + +tasks.named('test') { + jvmArgs += patchArgs +} + diff --git a/logging-adapter-log4j/src/main/java/module-info.java b/logging-adapter-log4j/src/main/java/module-info.java new file mode 100644 index 0000000..2339bc8 --- /dev/null +++ b/logging-adapter-log4j/src/main/java/module-info.java @@ -0,0 +1,9 @@ +import org.apache.logging.log4j.spi.Provider; +import org.xbib.logging.log4j.XbibProvider; + +module org.xbib.logging.adapter.log4j { + requires transitive org.apache.logging.log4j; + requires transitive org.xbib.logging; + exports org.xbib.logging.log4j; + provides Provider with XbibProvider; +} diff --git a/logging-adapter-log4j/src/main/java/org/xbib/logging/log4j/LevelTranslator.java b/logging-adapter-log4j/src/main/java/org/xbib/logging/log4j/LevelTranslator.java new file mode 100644 index 0000000..7733dac --- /dev/null +++ b/logging-adapter-log4j/src/main/java/org/xbib/logging/log4j/LevelTranslator.java @@ -0,0 +1,94 @@ +package org.xbib.logging.log4j; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.logging.log4j.Level; + +/** + * A utility to translate levels. + */ +public class LevelTranslator { + + private static final Level DEFAULT_LOG4J_LEVEL = Level.DEBUG; + + private static final org.xbib.logging.Level DEFAULT_LEVEL = org.xbib.logging.Level.DEBUG; + + private final Map julToLog4j = new HashMap<>(); + + private final Map log4jToJul = new HashMap<>(); + + private static class Holder { + static final LevelTranslator INSTANCE = new LevelTranslator(); + } + + private LevelTranslator() { + // Add JUL levels + julToLog4j.put(java.util.logging.Level.FINEST.intValue(), Level.TRACE); + // This has a intValue() of 700 which is really between INFO and DEBUG, we'll default to DEBUG + julToLog4j.put(java.util.logging.Level.CONFIG.intValue(), Level.DEBUG); + + // Note these should be added last to override any values that match + julToLog4j.put(org.xbib.logging.Level.ALL.intValue(), Level.ALL); + julToLog4j.put(org.xbib.logging.Level.TRACE.intValue(), Level.TRACE); + julToLog4j.put(org.xbib.logging.Level.DEBUG.intValue(), Level.DEBUG); + julToLog4j.put(org.xbib.logging.Level.INFO.intValue(), Level.INFO); + julToLog4j.put(org.xbib.logging.Level.WARN.intValue(), Level.WARN); + julToLog4j.put(org.xbib.logging.Level.ERROR.intValue(), Level.ERROR); + julToLog4j.put(org.xbib.logging.Level.FATAL.intValue(), Level.FATAL); + julToLog4j.put(org.xbib.logging.Level.OFF.intValue(), Level.OFF); + + log4jToJul.put(Level.ALL.intLevel(), org.xbib.logging.Level.ALL); + log4jToJul.put(Level.TRACE.intLevel(), org.xbib.logging.Level.TRACE); + log4jToJul.put(Level.DEBUG.intLevel(), org.xbib.logging.Level.DEBUG); + log4jToJul.put(Level.INFO.intLevel(), org.xbib.logging.Level.INFO); + log4jToJul.put(Level.WARN.intLevel(), org.xbib.logging.Level.WARN); + log4jToJul.put(Level.ERROR.intLevel(), org.xbib.logging.Level.ERROR); + log4jToJul.put(Level.FATAL.intLevel(), org.xbib.logging.Level.FATAL); + log4jToJul.put(Level.OFF.intLevel(), org.xbib.logging.Level.OFF); + } + + /** + * Returns an instance of the level translator. + * + * @return an instance + */ + public static LevelTranslator getInstance() { + return Holder.INSTANCE; + } + + /** + * Translates a {@linkplain Level log4j level} to a {@linkplain java.util.logging.Level JUL level}. + * + * @param level the log4j level + * + * @return the closest match of a JUL level + */ + public java.util.logging.Level translateLevel(final Level level) { + final java.util.logging.Level result = level == null ? null : log4jToJul.get(level.intLevel()); + return result == null ? DEFAULT_LEVEL : result; + } + + /** + * Translates a {@linkplain java.util.logging.Level JUL level} to a {@linkplain Level log4j level}. + * + * @param level the JUL level + * + * @return the log4j level + */ + public Level translateLevel(final java.util.logging.Level level) { + return level == null ? DEFAULT_LOG4J_LEVEL : translateLevel(level.intValue()); + } + + /** + * Translates a {@linkplain java.util.logging.Level#intValue()} JUL level} to a {@linkplain Level log4j level}. + * + * @param level the JUL level int value + * + * @return the log4j level + */ + public Level translateLevel(final int level) { + final Level result = julToLog4j.get(level); + return result == null ? DEFAULT_LOG4J_LEVEL : result; + } +} diff --git a/logging-adapter-log4j/src/main/java/org/xbib/logging/log4j/ThreadContextMDCMap.java b/logging-adapter-log4j/src/main/java/org/xbib/logging/log4j/ThreadContextMDCMap.java new file mode 100644 index 0000000..9ae5d9e --- /dev/null +++ b/logging-adapter-log4j/src/main/java/org/xbib/logging/log4j/ThreadContextMDCMap.java @@ -0,0 +1,60 @@ +package org.xbib.logging.log4j; + +import java.util.Map; + +import org.apache.logging.log4j.spi.ThreadContextMap; +import org.xbib.logging.MDC; + +/** + * A {@link ThreadContextMap} implementation which delegates to {@link MDC}. + */ +public class ThreadContextMDCMap implements ThreadContextMap { + + public ThreadContextMDCMap() { + } + + @Override + public void clear() { + MDC.clear(); + } + + @Override + public boolean containsKey(final String key) { + return MDC.get(key) != null; + } + + @Override + public String get(final String key) { + return MDC.get(key); + } + + @Override + public Map getCopy() { + return MDC.copy(); + } + + @Override + public Map getImmutableMapOrNull() { + final Map copy = MDC.copy(); + return copy.isEmpty() ? null : Map.copyOf(copy); + } + + @Override + public boolean isEmpty() { + return MDC.isEmpty(); + } + + @Override + public void put(final String key, final String value) { + if (value == null) { + MDC.remove(key); + } else { + MDC.put(key, value); + } + } + + @Override + public void remove(final String key) { + MDC.remove(key); + } +} diff --git a/logging-adapter-log4j/src/main/java/org/xbib/logging/log4j/XbibLogger.java b/logging-adapter-log4j/src/main/java/org/xbib/logging/log4j/XbibLogger.java new file mode 100644 index 0000000..2ed0459 --- /dev/null +++ b/logging-adapter-log4j/src/main/java/org/xbib/logging/log4j/XbibLogger.java @@ -0,0 +1,161 @@ +package org.xbib.logging.log4j; + +import java.util.Collections; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.ThreadContext; +import org.apache.logging.log4j.message.Message; +import org.apache.logging.log4j.message.MessageFactory; +import org.apache.logging.log4j.spi.AbstractLogger; +import org.xbib.logging.ExtLogRecord; + +/** + * An implementation of a log4j2 {@linkplain org.apache.logging.log4j.Logger logger} that delegates + * to a LogManager logger. + *

+ * Only the {@linkplain Level level} is used to determine the result {@code isEnabled()} methods. All other parameters + * are ignored. + *

+ */ +@SuppressWarnings("serial") +class XbibLogger extends AbstractLogger { + + private transient final org.xbib.logging.Logger logger; + + private transient final LevelTranslator levelTranslator; + + XbibLogger(final org.xbib.logging.Logger logger, final MessageFactory messageFactory) { + super(logger.getName(), messageFactory); + this.logger = logger; + this.levelTranslator = LevelTranslator.getInstance(); + } + + @Override + public boolean isEnabled(final Level level, final Marker marker, final Message message, final Throwable t) { + return logger.isLoggable(levelTranslator.translateLevel(level)); + } + + @Override + public boolean isEnabled(final Level level, final Marker marker, final CharSequence message, final Throwable t) { + return logger.isLoggable(levelTranslator.translateLevel(level)); + } + + @Override + public boolean isEnabled(final Level level, final Marker marker, final Object message, final Throwable t) { + return logger.isLoggable(levelTranslator.translateLevel(level)); + } + + @Override + public boolean isEnabled(final Level level, final Marker marker, final String message, final Throwable t) { + return logger.isLoggable(levelTranslator.translateLevel(level)); + } + + @Override + public boolean isEnabled(final Level level, final Marker marker, final String message) { + return logger.isLoggable(levelTranslator.translateLevel(level)); + } + + @Override + public boolean isEnabled(final Level level, final Marker marker, final String message, final Object... params) { + return logger.isLoggable(levelTranslator.translateLevel(level)); + } + + @Override + public boolean isEnabled(final Level level, final Marker marker, final String message, final Object p0) { + return logger.isLoggable(levelTranslator.translateLevel(level)); + } + + @Override + public boolean isEnabled(final Level level, final Marker marker, final String message, final Object p0, final Object p1) { + return logger.isLoggable(levelTranslator.translateLevel(level)); + } + + @Override + public boolean isEnabled(final Level level, final Marker marker, final String message, final Object p0, final Object p1, + final Object p2) { + return logger.isLoggable(levelTranslator.translateLevel(level)); + } + + @Override + public boolean isEnabled(final Level level, final Marker marker, final String message, final Object p0, final Object p1, + final Object p2, final Object p3) { + return logger.isLoggable(levelTranslator.translateLevel(level)); + } + + @Override + public boolean isEnabled(final Level level, final Marker marker, final String message, final Object p0, final Object p1, + final Object p2, final Object p3, final Object p4) { + return logger.isLoggable(levelTranslator.translateLevel(level)); + } + + @Override + public boolean isEnabled(final Level level, final Marker marker, final String message, final Object p0, final Object p1, + final Object p2, final Object p3, final Object p4, final Object p5) { + return logger.isLoggable(levelTranslator.translateLevel(level)); + } + + @Override + public boolean isEnabled(final Level level, final Marker marker, final String message, final Object p0, final Object p1, + final Object p2, final Object p3, final Object p4, final Object p5, final Object p6) { + return logger.isLoggable(levelTranslator.translateLevel(level)); + } + + @Override + public boolean isEnabled(final Level level, final Marker marker, final String message, final Object p0, final Object p1, + final Object p2, final Object p3, final Object p4, final Object p5, final Object p6, final Object p7) { + return logger.isLoggable(levelTranslator.translateLevel(level)); + } + + @Override + public boolean isEnabled(final Level level, final Marker marker, final String message, final Object p0, final Object p1, + final Object p2, final Object p3, final Object p4, final Object p5, final Object p6, final Object p7, + final Object p8) { + return logger.isLoggable(levelTranslator.translateLevel(level)); + } + + @Override + public boolean isEnabled(final Level level, final Marker marker, final String message, final Object p0, final Object p1, + final Object p2, final Object p3, final Object p4, final Object p5, final Object p6, final Object p7, + final Object p8, final Object p9) { + return logger.isLoggable(levelTranslator.translateLevel(level)); + } + + @Override + public void logMessage(final String fqcn, final Level level, final Marker marker, final Message message, + final Throwable t) { + // Ignore null messages + if (message != null) { + final ExtLogRecord record = new ExtLogRecord(levelTranslator.translateLevel(level), + message.getFormattedMessage(), ExtLogRecord.FormatStyle.NO_FORMAT, fqcn); + if (message.getParameters() != null) { + record.setParameters(message.getParameters()); + } + if (ThreadContext.isEmpty()) { + record.setMdc(Collections.emptyMap()); + } else { + record.setMdc(ThreadContext.getContext()); + } + record.setNdc(getNdc()); + record.setThrown(t == null ? message.getThrowable() : t); + logger.log(record); + } + } + + @Override + public Level getLevel() { + final java.util.logging.Level level = logger.getLevel(); + if (level != null) { + return levelTranslator.translateLevel(level); + } + return levelTranslator.translateLevel(logger.getEffectiveLevel()); + } + + private String getNdc() { + final ThreadContext.ContextStack contextStack = ThreadContext.getImmutableStack(); + if (contextStack.isEmpty()) { + return ""; + } + return String.join(".", contextStack); + } +} diff --git a/logging-adapter-log4j/src/main/java/org/xbib/logging/log4j/XbibLoggerContext.java b/logging-adapter-log4j/src/main/java/org/xbib/logging/log4j/XbibLoggerContext.java new file mode 100644 index 0000000..1d7c58d --- /dev/null +++ b/logging-adapter-log4j/src/main/java/org/xbib/logging/log4j/XbibLoggerContext.java @@ -0,0 +1,125 @@ +package org.xbib.logging.log4j; + +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.apache.logging.log4j.message.MessageFactory; +import org.apache.logging.log4j.spi.AbstractLogger; +import org.apache.logging.log4j.spi.ExtendedLogger; +import org.apache.logging.log4j.spi.LoggerContext; +import org.apache.logging.log4j.spi.LoggerRegistry; +import org.xbib.logging.LogContext; + +/** + * Represents a {@link LoggerContext} backed by a {@link LogContext}. + */ +class XbibLoggerContext implements LoggerContext { + + private final LogContext logContext; + + private final Object externalContext; + + private final LoggerRegistry loggerRegistry = new LoggerRegistry<>(); + + private final ConcurrentMap map = new ConcurrentHashMap<>(); + + /** + * Creates a new logger context. + * + * @param logContext the LogManager context to use + * @param externalContext the external context provided + */ + XbibLoggerContext(final LogContext logContext, final Object externalContext) { + this.logContext = logContext; + this.externalContext = externalContext; + } + + @Override + public Object getExternalContext() { + return externalContext; + } + + @Override + public ExtendedLogger getLogger(final String name) { + return getLogger(name, null); + } + + @Override + public ExtendedLogger getLogger(final String name, final MessageFactory messageFactory) { + XbibLogger logger = loggerRegistry.getLogger(name, messageFactory); + if (logger != null) { + AbstractLogger.checkMessageFactory(logger, messageFactory); + return logger; + } + logger = new XbibLogger(logContext.getLogger(name), messageFactory); + loggerRegistry.putIfAbsent(name, messageFactory, logger); + return loggerRegistry.getLogger(name, messageFactory); + } + + @Override + public boolean hasLogger(final String name) { + return loggerRegistry.hasLogger(name); + } + + @Override + public boolean hasLogger(final String name, final MessageFactory messageFactory) { + return loggerRegistry.hasLogger(name, messageFactory); + } + + @Override + public boolean hasLogger(final String name, final Class messageFactoryClass) { + return loggerRegistry.hasLogger(name, messageFactoryClass); + } + + @Override + public Object getObject(final String key) { + return map.get(key); + } + + @Override + public Object putObject(final String key, final Object value) { + return map.put(key, value); + } + + @Override + public Object putObjectIfAbsent(final String key, final Object value) { + return map.putIfAbsent(key, value); + } + + @Override + public Object removeObject(final String key) { + return map.remove(key); + } + + @Override + public boolean removeObject(final String key, final Object value) { + return map.remove(key, value); + } + + @Override + public int hashCode() { + return Objects.hash(logContext, loggerRegistry, externalContext); + } + + @Override + public boolean equals(final Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof XbibLoggerContext other)) { + return false; + } + return Objects.equals(logContext, other.logContext) && Objects.equals(loggerRegistry, other.loggerRegistry) + && Objects.equals(externalContext, other.externalContext); + } + + /** + * Returns the LogManager log context associated with the log4j logger context. + * + * @return the LogManager log context + */ + LogContext getLogContext() { + return logContext; + } +} diff --git a/logging-adapter-log4j/src/main/java/org/xbib/logging/log4j/XbibLoggerContextFactory.java b/logging-adapter-log4j/src/main/java/org/xbib/logging/log4j/XbibLoggerContextFactory.java new file mode 100644 index 0000000..eb6b87f --- /dev/null +++ b/logging-adapter-log4j/src/main/java/org/xbib/logging/log4j/XbibLoggerContextFactory.java @@ -0,0 +1,124 @@ +package org.xbib.logging.log4j; + +import java.net.URI; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.locks.ReentrantLock; + +import org.apache.logging.log4j.spi.LoggerContext; +import org.apache.logging.log4j.spi.LoggerContextFactory; +import org.apache.logging.log4j.status.StatusLogger; +import org.xbib.logging.LogContext; +import org.xbib.logging.Logger; + +/** + * A context factory backed by LogManager. + */ +public class XbibLoggerContextFactory implements LoggerContextFactory { + + private static final Logger.AttachmentKey> CONTEXT_KEY = new Logger.AttachmentKey<>(); + + private static final String ROOT_LOGGER_NAME = ""; + + private final ReentrantLock lock = new ReentrantLock(); + + public XbibLoggerContextFactory() { + } + + @Override + public LoggerContext getContext(final String fqcn, final ClassLoader loader, final Object externalContext, + final boolean currentContext) { + return getLoggerContext(loader, externalContext, currentContext); + } + + @Override + public LoggerContext getContext(final String fqcn, final ClassLoader loader, final Object externalContext, + final boolean currentContext, final URI configLocation, final String name) { + try { + return getLoggerContext(loader, externalContext, currentContext); + } finally { + // Done in a finally block as the StatusLogger may not be configured until the call to getLoggerContext() + if (configLocation != null) { + StatusLogger.getLogger().warn( + "Configuration is not allowed for the LogManager binding. Ignoring configuration file {}", + configLocation); + } + } + } + + @Override + public void removeContext(final LoggerContext context) { + // Check the context type and if it's not a XbibLoggerContext there is nothing for us to do. + if (context instanceof XbibLoggerContext) { + final LogContext logContext = ((XbibLoggerContext) context).getLogContext(); + lock.lock(); + try { + final Map contexts = logContext.getAttachment(ROOT_LOGGER_NAME, CONTEXT_KEY); + if (contexts != null) { + final Iterator iter = contexts.values().iterator(); + while (iter.hasNext()) { + final LoggerContext c = iter.next(); + if (c.equals(context)) { + iter.remove(); + break; + } + } + if (contexts.isEmpty()) { + final Logger rootLogger = logContext.getLogger(ROOT_LOGGER_NAME); + detach(rootLogger); + XbibStatusListener.remove(logContext); + } + } + } finally { + lock.unlock(); + } + } + } + + private LoggerContext getLoggerContext(final ClassLoader classLoader, final Object externalContext, + final boolean currentContext) { + if (currentContext || classLoader == null) { + return getOrCreateLoggerContext(LogContext.getLogContext(), externalContext); + } + final ClassLoader current = getTccl(); + try { + setTccl(classLoader); + return getOrCreateLoggerContext(LogContext.getLogContext(), externalContext); + } finally { + setTccl(current); + } + } + + private LoggerContext getOrCreateLoggerContext(final LogContext logContext, final Object externalContext) { + final Logger rootLogger = logContext.getLogger(ROOT_LOGGER_NAME); + lock.lock(); + try { + Map contexts = rootLogger.getAttachment(CONTEXT_KEY); + if (contexts == null) { + contexts = new HashMap<>(); + attach(rootLogger, contexts); + } + XbibStatusListener.registerIfAbsent(logContext); + return contexts.computeIfAbsent(externalContext, o -> new XbibLoggerContext(logContext, externalContext)); + } finally { + lock.unlock(); + } + } + + private static void attach(final Logger logger, final Map value) { + logger.attach(CONTEXT_KEY, value); + } + + private static void detach(final Logger logger) { + logger.detach(CONTEXT_KEY); + } + + private static ClassLoader getTccl() { + return Thread.currentThread().getContextClassLoader(); + } + + private static void setTccl(final ClassLoader classLoader) { + Thread.currentThread().setContextClassLoader(classLoader); + } +} diff --git a/logging-adapter-log4j/src/main/java/org/xbib/logging/log4j/XbibProvider.java b/logging-adapter-log4j/src/main/java/org/xbib/logging/log4j/XbibProvider.java new file mode 100644 index 0000000..9606969 --- /dev/null +++ b/logging-adapter-log4j/src/main/java/org/xbib/logging/log4j/XbibProvider.java @@ -0,0 +1,21 @@ +package org.xbib.logging.log4j; + +import org.apache.logging.log4j.spi.Provider; +import org.apache.logging.log4j.spi.ThreadContextMap; + +public class XbibProvider extends Provider { + + public XbibProvider() { + super(500, "2.6.0", XbibLoggerContextFactory.class); + } + + @Override + public String getThreadContextMap() { + return ThreadContextMDCMap.class.getName(); + } + + @Override + public Class loadThreadContextMap() { + return ThreadContextMDCMap.class; + } +} diff --git a/logging-adapter-log4j/src/main/java/org/xbib/logging/log4j/XbibStatusListener.java b/logging-adapter-log4j/src/main/java/org/xbib/logging/log4j/XbibStatusListener.java new file mode 100644 index 0000000..22eb462 --- /dev/null +++ b/logging-adapter-log4j/src/main/java/org/xbib/logging/log4j/XbibStatusListener.java @@ -0,0 +1,92 @@ +package org.xbib.logging.log4j; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.status.StatusData; +import org.apache.logging.log4j.status.StatusListener; +import org.apache.logging.log4j.status.StatusLogger; +import org.xbib.logging.LogContext; +import org.xbib.logging.Logger; + +/** + * A status logger which logs to a LogManager Logger. + */ +public class XbibStatusListener implements StatusListener { + + private static final String NAME = "org.xbib.logging.log4j.status"; + + private static final Logger.AttachmentKey STATUS_LISTENER_KEY = new Logger.AttachmentKey<>(); + + private final Logger logger; + + private final LevelTranslator levelTranslator; + + private XbibStatusListener(final Logger logger, final LevelTranslator levelTranslator) { + this.logger = logger; + this.levelTranslator = levelTranslator; + } + + /** + * Registers a status listener with the log context if one does not already exist. + * + * @param logContext the log context to possibly register the status listener with + */ + public static void registerIfAbsent(final LogContext logContext) { + final LevelTranslator levelTranslator = LevelTranslator.getInstance(); + Logger logger = logContext.getLoggerIfExists(NAME); + if (logger == null) { + logger = logContext.getLogger(NAME); + logger.setLevel(levelTranslator.translateLevel(StatusLogger.getLogger().getFallbackListener().getStatusLevel())); + } + StatusListener listener = logger.getAttachment(STATUS_LISTENER_KEY); + if (listener == null) { + listener = new XbibStatusListener(logger, levelTranslator); + if (attachIfAbsent(logger, listener) == null) { + StatusLogger.getLogger().registerListener(listener); + } + } + } + + /** + * Removes the status listener from the log context. + * + * @param logContext the log context to remove the status listener from + */ + public static void remove(final LogContext logContext) { + final Logger logger = logContext.getLoggerIfExists(NAME); + if (logger != null) { + detach(logger); + } + } + + @Override + public void log(final StatusData data) { + // Verify we can log at this level + if (getStatusLevel().isLessSpecificThan(data.getLevel())) { + logger.log( + levelTranslator.translateLevel(data.getLevel()), + data.getMessage().getFormattedMessage(), + data.getThrowable()); + } + } + + @Override + public Level getStatusLevel() { + return levelTranslator.translateLevel(logger.getLevel()); + } + + @Override + public void close() { + detach(logger); + } + + private static StatusListener attachIfAbsent(final Logger logger, final StatusListener value) { + return logger.attachIfAbsent(STATUS_LISTENER_KEY, value); + } + + private static void detach(final Logger logger) { + final StatusListener listener = logger.detach(STATUS_LISTENER_KEY); + if (listener != null) { + StatusLogger.getLogger().removeListener(listener); + } + } +} diff --git a/logging-adapter-log4j/src/main/resources/META-INF/services/org.apache.logging.log4j.spi.Provider b/logging-adapter-log4j/src/main/resources/META-INF/services/org.apache.logging.log4j.spi.Provider new file mode 100644 index 0000000..48f59a5 --- /dev/null +++ b/logging-adapter-log4j/src/main/resources/META-INF/services/org.apache.logging.log4j.spi.Provider @@ -0,0 +1 @@ +org.xbib.logging.log4j.XbibProvider \ No newline at end of file diff --git a/logging-adapter-log4j/src/test/java/module-info.java b/logging-adapter-log4j/src/test/java/module-info.java new file mode 100644 index 0000000..af8a1c9 --- /dev/null +++ b/logging-adapter-log4j/src/test/java/module-info.java @@ -0,0 +1,8 @@ +module org.xbib.logging.log4j.test { + requires transitive java.logging; + requires org.apache.logging.log4j; + requires org.junit.jupiter.api; + requires transitive org.xbib.logging; + requires org.xbib.logging.adapter.log4j; + exports org.xbib.logging.log4j.test to org.junit.platform.commons; +} diff --git a/logging-adapter-log4j/src/test/java/org/xbib/logging/log4j/test/AbstractTest.java b/logging-adapter-log4j/src/test/java/org/xbib/logging/log4j/test/AbstractTest.java new file mode 100644 index 0000000..409dbda --- /dev/null +++ b/logging-adapter-log4j/src/test/java/org/xbib/logging/log4j/test/AbstractTest.java @@ -0,0 +1,85 @@ +package org.xbib.logging.log4j.test; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.logging.Formatter; + +import org.xbib.logging.ExtHandler; +import org.xbib.logging.ExtLogRecord; +import org.xbib.logging.LogContext; +import org.xbib.logging.LogContextSelector; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +public class AbstractTest { + + private static class TestLogContextSelector implements LogContextSelector { + private final LogContext logContext; + + private TestLogContextSelector(final LogContext logContext) { + this.logContext = logContext; + } + + @Override + public LogContext getLogContext() { + return logContext; + } + } + + @BeforeEach + public void logContextSetup() { + LogContext.setLogContextSelector(new TestLogContextSelector(LogContext.create())); + } + + @AfterEach + public void resetLogContext() { + LogContext.setLogContextSelector(LogContext.DEFAULT_LOG_CONTEXT_SELECTOR); + } + + static class TestQueueHandler extends ExtHandler { + private final Deque collected = new ArrayDeque<>(); + + TestQueueHandler() { + } + + TestQueueHandler(final Formatter formatter) { + setFormatter(formatter); + } + + @Override + protected void doPublish(final ExtLogRecord record) { + collected.addLast(record); + } + + ExtLogRecord pollFirst() { + return collected.pollFirst(); + } + + ExtLogRecord poll() { + return collected.pollLast(); + } + + String pollFormatted() { + return format(poll()); + } + + String pollFirstFormatted() { + return format(pollFirst()); + } + + boolean isEmpty() { + return collected.isEmpty(); + } + + private String format(final ExtLogRecord record) { + if (record == null) { + return null; + } + final Formatter formatter = getFormatter(); + if (formatter == null) { + return record.getMessage(); + } + return formatter.format(record); + } + } +} diff --git a/logging-adapter-log4j/src/test/java/org/xbib/logging/log4j/test/LevelTranslatorTest.java b/logging-adapter-log4j/src/test/java/org/xbib/logging/log4j/test/LevelTranslatorTest.java new file mode 100644 index 0000000..c36a4e2 --- /dev/null +++ b/logging-adapter-log4j/src/test/java/org/xbib/logging/log4j/test/LevelTranslatorTest.java @@ -0,0 +1,80 @@ +package org.xbib.logging.log4j.test; + +import org.apache.logging.log4j.Level; +import org.junit.jupiter.api.Test; +import org.xbib.logging.log4j.LevelTranslator; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class LevelTranslatorTest { + + private final LevelTranslator levelTranslator = LevelTranslator.getInstance(); + + @Test + public void testOff() { + testLevel(Level.OFF, java.util.logging.Level.OFF); + } + + @Test + public void testFatal() { + testLevel(Level.FATAL, org.xbib.logging.Level.FATAL); + } + + @Test + public void testError() { + testLevel(Level.ERROR, org.xbib.logging.Level.ERROR); + testLevel(Level.ERROR, java.util.logging.Level.SEVERE, org.xbib.logging.Level.ERROR); + } + + @Test + public void testWarn() { + testLevel(Level.WARN, org.xbib.logging.Level.WARN); + testLevel(Level.WARN, java.util.logging.Level.WARNING, org.xbib.logging.Level.WARN); + } + + @Test + public void testInfo() { + testLevel(Level.INFO, org.xbib.logging.Level.INFO); + testLevel(Level.INFO, java.util.logging.Level.INFO); + } + + @Test + public void testDebug() { + testLevel(Level.DEBUG, org.xbib.logging.Level.DEBUG); + testLevel(Level.DEBUG, java.util.logging.Level.FINE, org.xbib.logging.Level.DEBUG); + testLevel(Level.DEBUG, java.util.logging.Level.CONFIG, org.xbib.logging.Level.DEBUG); + } + + @Test + public void testTrace() { + testLevel(Level.TRACE, org.xbib.logging.Level.TRACE); + testLevel(Level.TRACE, java.util.logging.Level.FINER, org.xbib.logging.Level.TRACE); + testLevel(Level.TRACE, java.util.logging.Level.FINEST, org.xbib.logging.Level.TRACE); + } + + @Test + public void testAll() { + testLevel(Level.ALL, java.util.logging.Level.ALL); + } + + @Test + public void testNull() { + assertEquals(org.xbib.logging.Level.DEBUG, levelTranslator.translateLevel((Level) null), + "Expected null log4j level to map to INFO"); + assertEquals(Level.DEBUG, levelTranslator.translateLevel((java.util.logging.Level) null), + "Expected null JUL level to map to INFO"); + assertEquals(Level.DEBUG, levelTranslator.translateLevel(-1), "" + + "Expected a -1 effective level to map to INFO"); + } + + private void testLevel(final Level log4jLevel, final java.util.logging.Level julLevel) { + testLevel(log4jLevel, julLevel, julLevel); + } + + private void testLevel(final Level log4jLevel, final java.util.logging.Level julLevel, + final java.util.logging.Level expectedJulLevel) { + assertEquals(log4jLevel, levelTranslator.translateLevel(julLevel), + String.format("Expected log4j level %s to equal JUL level %s", log4jLevel, julLevel)); + assertEquals(expectedJulLevel, levelTranslator.translateLevel(log4jLevel), + String.format("Expected JUL level %s to equal log4j level %s", julLevel, log4jLevel)); + } +} diff --git a/logging-adapter-log4j/src/test/java/org/xbib/logging/log4j/test/LoggerContextFactoryTest.java b/logging-adapter-log4j/src/test/java/org/xbib/logging/log4j/test/LoggerContextFactoryTest.java new file mode 100644 index 0000000..4b83076 --- /dev/null +++ b/logging-adapter-log4j/src/test/java/org/xbib/logging/log4j/test/LoggerContextFactoryTest.java @@ -0,0 +1,16 @@ +package org.xbib.logging.log4j.test; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.spi.LoggerContextFactory; +import org.junit.jupiter.api.Test; +import org.xbib.logging.log4j.XbibLoggerContextFactory; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class LoggerContextFactoryTest { + + @Test + public void testCorrectFactory() { + final LoggerContextFactory factory = LogManager.getFactory(); + assertEquals(XbibLoggerContextFactory.class, factory.getClass()); + } +} diff --git a/logging-adapter-log4j/src/test/java/org/xbib/logging/log4j/test/LoggerContextTest.java b/logging-adapter-log4j/src/test/java/org/xbib/logging/log4j/test/LoggerContextTest.java new file mode 100644 index 0000000..965d110 --- /dev/null +++ b/logging-adapter-log4j/src/test/java/org/xbib/logging/log4j/test/LoggerContextTest.java @@ -0,0 +1,31 @@ +package org.xbib.logging.log4j.test; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.message.StringFormatterMessageFactory; +import org.apache.logging.log4j.spi.LoggerContext; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class LoggerContextTest extends AbstractTest { + + @Test + public void testHasLogger() { + final LoggerContext loggerContext = LogManager.getContext(); + final Logger logger = LogManager.getFormatterLogger(LoggerTest.class); + assertFalse(loggerContext.hasLogger("org.xbib.logging")); + assertFalse(loggerContext.hasLogger(logger.getName())); + assertTrue(loggerContext.hasLogger(logger.getName(), StringFormatterMessageFactory.INSTANCE)); + assertTrue(loggerContext.hasLogger(logger.getName(), StringFormatterMessageFactory.class)); + } + + @Test + public void testExternalContext() { + final Object externalContext = new Object(); + final LoggerContext loggerContext = LogManager.getContext(LoggerContextTest.class.getClassLoader(), true, + externalContext); + assertEquals(externalContext, loggerContext.getExternalContext()); + } +} diff --git a/logging-adapter-log4j/src/test/java/org/xbib/logging/log4j/test/LoggerTest.java b/logging-adapter-log4j/src/test/java/org/xbib/logging/log4j/test/LoggerTest.java new file mode 100644 index 0000000..fa37b55 --- /dev/null +++ b/logging-adapter-log4j/src/test/java/org/xbib/logging/log4j/test/LoggerTest.java @@ -0,0 +1,322 @@ +package org.xbib.logging.log4j.test; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Arrays; +import java.util.UUID; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.MarkerManager; +import org.apache.logging.log4j.message.AbstractMessageFactory; +import org.apache.logging.log4j.message.Message; +import org.apache.logging.log4j.message.MessageFactory; +import org.apache.logging.log4j.message.MessageFactory2; +import org.xbib.logging.ExtLogRecord; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class LoggerTest extends AbstractTest { + + private final String loggerName = LoggerTest.class.getPackage().getName(); + + private final Marker marker = MarkerManager.getMarker("test"); + + private TestQueueHandler handler; + + private org.xbib.logging.Logger lmLogger; + + @BeforeEach + public void setup() { + lmLogger = org.xbib.logging.Logger.getLogger("org.xbib.logging.log4j"); + lmLogger.setLevel(java.util.logging.Level.INFO); + final TestQueueHandler handler = new TestQueueHandler(); + lmLogger.addHandler(handler); + this.handler = handler; + } + + @AfterEach + public void tearDown() { + handler.close(); + lmLogger.removeHandler(handler); + } + + @Test + public void testNamedLogger() { + final Logger logger = LogManager.getLogger(loggerName); + logger.info("Test message"); + ExtLogRecord record = handler.poll(); + assertNotNull(record); + assertEquals("Test message", record.getMessage()); + logger.info("Test message parameter {}", 1); + record = handler.poll(); + assertNotNull(record); + assertEquals("Test message parameter 1", record.getMessage()); + } + + @SuppressWarnings("PlaceholderCountMatchesArgumentCount") + @Test + public void testNamedFormatterLogger() { + final Logger logger = LogManager.getFormatterLogger(loggerName); + logger.info("Test message parameter %s", 1); + final ExtLogRecord record = handler.poll(); + assertNotNull(record); + assertEquals("Test message parameter 1", record.getMessage()); + } + + @Test + public void testCurrentClassLogger() { + final String expectedName = LoggerTest.class.getName(); + final Logger logger = LogManager.getLogger(); + assertEquals(expectedName, logger.getName()); + logger.info("Test message"); + final ExtLogRecord record = handler.poll(); + assertNotNull(record); + assertEquals("Test message", record.getMessage()); + assertEquals(expectedName, record.getLoggerName()); + } + + @Test + public void testObjectLogger() { + final String expectedName = LoggerTest.class.getName(); + final Logger logger = LogManager.getLogger(this); + assertEquals(expectedName, logger.getName()); + logger.info("Test message"); + final ExtLogRecord record = handler.poll(); + assertNotNull(record); + assertEquals("Test message", record.getMessage()); + assertEquals(expectedName, record.getLoggerName()); + } + + @Test + public void testCurrentClassMessageFactoryLogger() { + final String prefix = generatePrefix(); + final String expectedName = LoggerTest.class.getName(); + final MessageFactory messageFactory = new TestMessageFactory(prefix); + final Logger logger = LogManager.getLogger(messageFactory); + assertEquals(expectedName, logger.getName()); + logger.info("Test message"); + final ExtLogRecord record = handler.poll(); + assertNotNull(record); + assertEquals(prefix + "Test message", record.getMessage()); + assertEquals(expectedName, record.getLoggerName()); + } + + @Test + public void testObjectMessageFactoryLogger() { + final String prefix = generatePrefix(); + final String expectedName = LoggerTest.class.getName(); + final MessageFactory messageFactory = new TestMessageFactory(prefix); + final Logger logger = LogManager.getLogger(this, messageFactory); + assertEquals(expectedName, logger.getName()); + logger.info("Test message"); + final ExtLogRecord record = handler.poll(); + assertNotNull(record); + assertEquals(prefix + "Test message", record.getMessage()); + assertEquals(expectedName, record.getLoggerName()); + } + + @Test + public void testNamedMessageFactoryLogger() { + final String prefix = generatePrefix(); + final String expectedName = loggerName; + final MessageFactory messageFactory = new TestMessageFactory(prefix); + final Logger logger = LogManager.getLogger(loggerName, messageFactory); + assertEquals(expectedName, logger.getName()); + logger.info("Test message"); + final ExtLogRecord record = handler.poll(); + assertNotNull(record); + assertEquals(prefix + "Test message", record.getMessage()); + assertEquals(expectedName, record.getLoggerName()); + } + + @Test + public void tesTypeMessageFactoryLogger() { + final String prefix = generatePrefix(); + final String expectedName = LoggerTest.class.getName(); + final MessageFactory messageFactory = new TestMessageFactory(prefix); + final Logger logger = LogManager.getLogger(LoggerTest.class, messageFactory); + assertEquals(expectedName, logger.getName()); + logger.info("Test message"); + final ExtLogRecord record = handler.poll(); + assertNotNull(record); + assertEquals(prefix + "Test message", record.getMessage()); + assertEquals(expectedName, record.getLoggerName()); + } + + @Test + public void testAllEnabled() { + lmLogger.setLevel(org.xbib.logging.Level.ALL); + testLevelEnabled(LogManager.getLogger(loggerName), Level.ALL); + testLevelEnabled(LogManager.getFormatterLogger(loggerName), Level.ALL); + } + + @Test + public void testTraceEnabled() { + lmLogger.setLevel(org.xbib.logging.Level.TRACE); + testLevelEnabled(LogManager.getLogger(loggerName), Level.TRACE); + testLevelEnabled(LogManager.getFormatterLogger(loggerName), Level.TRACE); + } + + @Test + public void testDebugEnabled() { + lmLogger.setLevel(org.xbib.logging.Level.DEBUG); + testLevelEnabled(LogManager.getLogger(loggerName), Level.DEBUG); + testLevelEnabled(LogManager.getFormatterLogger(loggerName), Level.DEBUG); + } + + @Test + public void testInfoEnabled() { + testLevelEnabled(LogManager.getFormatterLogger(loggerName), Level.INFO); + } + + @Test + public void testWarnEnabled() { + testLevelEnabled(LogManager.getLogger(loggerName), Level.WARN); + testLevelEnabled(LogManager.getFormatterLogger(loggerName), Level.WARN); + } + + @Test + public void testErrorEnabled() { + testLevelEnabled(LogManager.getLogger(loggerName), Level.ERROR); + testLevelEnabled(LogManager.getFormatterLogger(loggerName), Level.ERROR); + } + + @Test + public void testFatalEnabled() { + testLevelEnabled(LogManager.getLogger(loggerName), Level.FATAL); + testLevelEnabled(LogManager.getFormatterLogger(loggerName), Level.FATAL); + } + + @Test + public void testOffEnabled() { + assertFalse(LogManager.getLogger(loggerName).isEnabled(Level.OFF)); + assertFalse(LogManager.getLogger(loggerName).isEnabled(Level.OFF, marker)); + assertFalse(LogManager.getFormatterLogger(loggerName).isEnabled(Level.OFF)); + assertFalse(LogManager.getFormatterLogger(loggerName).isEnabled(Level.OFF, marker)); + } + + private void testLevelEnabled(final Logger logger, final Level level) { + final String msg = String.format("Expected level %s to be enabled on logger %s", level, logger); + final String markerMsg = String.format("Expected level %s to be enabled on logger %s with marker %s", level, logger, + marker); + assertTrue(logger.isEnabled(level), msg); + assertTrue(logger.isEnabled(level, marker), markerMsg); + if (level.equals(Level.FATAL)) { + assertTrue(logger.isFatalEnabled(), msg); + assertTrue(logger.isFatalEnabled(marker), markerMsg); + } else if (level.equals(Level.ERROR)) { + assertTrue(logger.isErrorEnabled(), msg); + assertTrue(logger.isErrorEnabled(marker), markerMsg); + } else if (level.equals(Level.WARN)) { + assertTrue(logger.isWarnEnabled(), msg); + assertTrue(logger.isWarnEnabled(marker), markerMsg); + } else if (level.equals(Level.INFO)) { + assertTrue(logger.isInfoEnabled(), msg); + assertTrue(logger.isInfoEnabled(marker), markerMsg); + } else if (level.equals(Level.DEBUG)) { + assertTrue(logger.isDebugEnabled(), msg); + assertTrue(logger.isDebugEnabled(marker), markerMsg); + } else if (level.equals(Level.TRACE)) { + assertTrue(logger.isTraceEnabled(), msg); + assertTrue(logger.isTraceEnabled(marker), markerMsg); + } + } + + private static String generatePrefix() { + return "[" + UUID.randomUUID() + "] "; + } + + @SuppressWarnings("serial") + private static class TestMessageFactory extends AbstractMessageFactory implements MessageFactory2 { + + private final String prefix; + + private TestMessageFactory(final String prefix) { + this.prefix = prefix; + } + + @Override + public Message newMessage(final CharSequence charSequence) { + return newMessage(String.valueOf(charSequence)); + } + + /* + * (non-Javadoc) + * + * @see org.apache.logging.log4j.message.MessageFactory#newMessage(java.lang.Object) + */ + @Override + public Message newMessage(final Object message) { + return newMessage(String.valueOf(message)); + } + + /* + * (non-Javadoc) + * + * @see org.apache.logging.log4j.message.MessageFactory#newMessage(java.lang.String) + */ + @Override + public Message newMessage(final String message) { + return new TestMessage(prefix, null, message); + } + + @Override + public Message newMessage(final String message, final Object... params) { + return new TestMessage(prefix, null, message, params); + } + } + + @SuppressWarnings("serial") + private static class TestMessage implements Message { + + private final String format; + + private transient final Object[] params; + + private final Throwable cause; + + TestMessage(final String prefix, final Throwable cause, final String format, final Object... params) { + this.format = prefix == null ? "" : prefix + format; + this.params = params == null ? new Object[0] : Arrays.copyOf(params, params.length); + this.cause = cause; + } + + @Override + public String getFormattedMessage() { + if (format == null) { + return null; + } + if (cause == null) { + return String.format(format, params); + } + final StringWriter writer = new StringWriter(); + writer.append(String.format(format, params)); + writer.write(System.lineSeparator()); + cause.printStackTrace(new PrintWriter(writer)); + return writer.toString(); + } + + @Override + public String getFormat() { + return format; + } + + @Override + public Object[] getParameters() { + return params; + } + + @Override + public Throwable getThrowable() { + return cause; + } + } +} diff --git a/logging-adapter-log4j/src/test/java/org/xbib/logging/log4j/test/StatusLoggerTest.java b/logging-adapter-log4j/src/test/java/org/xbib/logging/log4j/test/StatusLoggerTest.java new file mode 100644 index 0000000..f897585 --- /dev/null +++ b/logging-adapter-log4j/src/test/java/org/xbib/logging/log4j/test/StatusLoggerTest.java @@ -0,0 +1,109 @@ +package org.xbib.logging.log4j.test; + +import java.net.URI; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.spi.LoggerContext; +import org.apache.logging.log4j.status.StatusListener; +import org.apache.logging.log4j.status.StatusLogger; +import org.xbib.logging.Level; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.logging.log4j.XbibStatusListener; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +public class StatusLoggerTest extends AbstractTest { + + private TestQueueHandler handler; + + private org.xbib.logging.Logger lmLogger; + + private StatusLogger statusLogger; + + @BeforeEach + public void setup() { + lmLogger = org.xbib.logging.Logger.getLogger("org.xbib.logging.log4j.status"); + final TestQueueHandler handler = new TestQueueHandler(); + lmLogger.addHandler(handler); + this.handler = handler; + // Required to initialize the XbibStatusListener + LogManager.getContext(); + statusLogger = StatusLogger.getLogger(); + lmLogger.setLevel(Level.WARN); + } + + @AfterEach + public void tearDown() { + handler.close(); + lmLogger.removeHandler(handler); + } + + @Test + public void testListenerAttached() { + boolean found = false; + for (StatusListener listener : statusLogger.getListeners()) { + if (listener.getClass().equals(XbibStatusListener.class)) { + found = true; + break; + } + } + assertTrue(found, + "Expected to find " + XbibStatusListener.class.getName() + " registered: " + statusLogger.getListeners()); + } + + @Test + public void testError() { + // Log an error which should show up on the handler + statusLogger.error("Test status message"); + checkEmpty(false); + final String msg = handler.pollFirstFormatted(); + assertNotNull(msg); + assertEquals("Test status message", msg); + } + + @Test + public void testLevelChange() { + // Log a warning message which should be ignored + statusLogger.info("Test info message 1"); + checkEmpty(true); + + // Set the level to warn and log another message + lmLogger.setLevel(Level.INFO); + statusLogger.info("Test info message 2"); + checkEmpty(false); + assertEquals("Test info message 2", handler.pollFormatted()); + } + + @Test + public void testConfiguration() throws Exception { + final URI config = LoggerContextTest.class.getResource("/log4j2.xml").toURI(); + final LoggerContext loggerContext = LogManager.getContext(LoggerContextTest.class.getClassLoader(), true, config); + assertNotNull(loggerContext); + // The status logger should contain a message + checkEmpty(false); + final String foundMsg = handler.pollFirstFormatted(); + assertTrue(foundMsg.contains(config.toString()), + String.format("Expected the log message to contain %s. Found %s", config, foundMsg)); + } + + private void checkEmpty(final boolean expectEmpty) { + if (handler.isEmpty() != expectEmpty) { + final StringBuilder msg = new StringBuilder("Expect the data to "); + if (expectEmpty) { + msg.append("be empty, found:") + .append(System.lineSeparator()); + String logMsg; + while ((logMsg = handler.pollFirstFormatted()) != null) { + msg.append(logMsg).append(System.lineSeparator()); + } + } else { + msg.append("not be empty"); + } + fail(msg.toString()); + } + } +} diff --git a/logging-adapter-log4j/src/test/java/org/xbib/logging/log4j/test/ThreadContextMapTest.java b/logging-adapter-log4j/src/test/java/org/xbib/logging/log4j/test/ThreadContextMapTest.java new file mode 100644 index 0000000..8c69079 --- /dev/null +++ b/logging-adapter-log4j/src/test/java/org/xbib/logging/log4j/test/ThreadContextMapTest.java @@ -0,0 +1,140 @@ +package org.xbib.logging.log4j.test; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.ThreadContext; +import org.xbib.logging.MDC; +import org.xbib.logging.NDC; +import org.xbib.logging.formatters.PatternFormatter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ThreadContextMapTest extends AbstractTest { + + public ThreadContextMapTest() { + } + + @BeforeEach + public void clear() { + ThreadContext.clearAll(); + MDC.clear(); + NDC.clear(); + } + + @Test + public void clearThreadContext() { + final String key = "test.clear.key"; + ThreadContext.put(key, "test clear value"); + + assertEquals("test clear value", ThreadContext.get(key)); + assertEquals("test clear value", MDC.get(key)); + + ThreadContext.clearMap(); + assertTrue(ThreadContext.isEmpty()); + assertTrue(MDC.isEmpty()); + } + + @Test + public void clearMdc() { + final String key = "test.clear.key"; + ThreadContext.put(key, "test clear value"); + + assertEquals("test clear value", ThreadContext.get(key)); + assertEquals("test clear value", MDC.get(key)); + + MDC.clear(); + assertTrue(ThreadContext.isEmpty()); + assertTrue(MDC.isEmpty()); + } + + @Test + public void putThreadContext() { + final String key = "test.key"; + final TestQueueHandler handler = new TestQueueHandler(new PatternFormatter("%X{" + key + "}")); + org.xbib.logging.Logger.getLogger("").addHandler(handler); + ThreadContext.put(key, "test value"); + + final Logger logger = LogManager.getLogger(); + + logger.info("Test message"); + + assertEquals("test value", handler.pollFormatted()); + + ThreadContext.remove(key); + + logger.info("Test message"); + assertEquals("", handler.pollFormatted()); + } + + @Test + public void putMdc() { + final String key = "test.key"; + final TestQueueHandler handler = new TestQueueHandler(new PatternFormatter("%X{" + key + "}")); + org.xbib.logging.Logger.getLogger("").addHandler(handler); + MDC.put(key, "test value"); + + final Logger logger = LogManager.getLogger(); + + logger.info("Test message"); + + assertEquals("test value", handler.pollFormatted()); + + ThreadContext.remove(key); + + logger.info("Test message"); + assertEquals("", handler.pollFormatted()); + } + + @Test + public void pushThreadContext() { + final TestQueueHandler handler = new TestQueueHandler(new PatternFormatter("%x")); + org.xbib.logging.Logger.getLogger("").addHandler(handler); + + ThreadContext.push("value-1"); + ThreadContext.push("value-2"); + ThreadContext.push("value-3"); + + final Logger logger = LogManager.getLogger(); + + logger.info("Test message"); + assertEquals("value-1.value-2.value-3", handler.pollFormatted()); + + ThreadContext.trim(2); + assertEquals(2, ThreadContext.getDepth()); + + logger.info("Test message"); + assertEquals("value-1.value-2", handler.pollFormatted()); + } + + @Test + public void removeThreadContext() { + final String key = "test.clear.key"; + ThreadContext.put(key, "test clear value"); + + assertEquals("test clear value", ThreadContext.get(key)); + assertEquals("test clear value", MDC.get(key)); + + ThreadContext.remove(key); + + assertNull(ThreadContext.get(key)); + assertNull(MDC.get(key)); + } + + @Test + public void removeMdc() { + final String key = "test.clear.key"; + MDC.put(key, "test clear value"); + + assertEquals("test clear value", ThreadContext.get(key)); + assertEquals("test clear value", MDC.get(key)); + + MDC.remove(key); + + assertNull(ThreadContext.get(key)); + assertNull(MDC.get(key)); + } + +} diff --git a/logging-adapter-log4j/src/test/resources/log4j2.xml b/logging-adapter-log4j/src/test/resources/log4j2.xml new file mode 100644 index 0000000..4c7fe3e --- /dev/null +++ b/logging-adapter-log4j/src/test/resources/log4j2.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/logging-adapter-slf4j/build.gradle b/logging-adapter-slf4j/build.gradle new file mode 100644 index 0000000..99af2ed --- /dev/null +++ b/logging-adapter-slf4j/build.gradle @@ -0,0 +1,5 @@ +dependencies { + api project(':logging') + api libs.slf4j.api +} + diff --git a/logging-adapter-slf4j/src/main/java/module-info.java b/logging-adapter-slf4j/src/main/java/module-info.java new file mode 100644 index 0000000..8510b98 --- /dev/null +++ b/logging-adapter-slf4j/src/main/java/module-info.java @@ -0,0 +1,9 @@ +import org.slf4j.impl.XbibSlf4jServiceProvider; +import org.slf4j.spi.SLF4JServiceProvider; + +module org.xbib.logging.adapter.slf4j { + requires transitive org.slf4j; + requires transitive org.xbib.logging; + exports org.slf4j.impl; + provides SLF4JServiceProvider with XbibSlf4jServiceProvider; +} diff --git a/logging-adapter-slf4j/src/main/java/org/slf4j/impl/Slf4jLogger.java b/logging-adapter-slf4j/src/main/java/org/slf4j/impl/Slf4jLogger.java new file mode 100644 index 0000000..9f5e9f2 --- /dev/null +++ b/logging-adapter-slf4j/src/main/java/org/slf4j/impl/Slf4jLogger.java @@ -0,0 +1,553 @@ +package org.slf4j.impl; + +import org.xbib.logging.ExtLogRecord; +import org.xbib.logging.Level; +import org.xbib.logging.Logger; +import org.slf4j.Marker; +import org.slf4j.helpers.FormattingTuple; +import org.slf4j.helpers.MessageFormatter; +import org.slf4j.spi.LocationAwareLogger; + +public final class Slf4jLogger implements LocationAwareLogger { + + private final Logger logger; + + private static final String LOGGER_CLASS_NAME = Slf4jLogger.class.getName(); + + private static final int ALT_ERROR_INT = org.xbib.logging.Level.ERROR.intValue(); + private static final int ALT_WARN_INT = org.xbib.logging.Level.WARN.intValue(); + private static final int ALT_INFO_INT = org.xbib.logging.Level.INFO.intValue(); + private static final int ALT_DEBUG_INT = org.xbib.logging.Level.DEBUG.intValue(); + private static final int ALT_TRACE_INT = org.xbib.logging.Level.TRACE.intValue(); + + public Slf4jLogger(final Logger logger) { + this.logger = logger; + } + + public String getName() { + return logger.getName(); + } + + @Override + public void log(final Marker marker, final String fqcn, final int levelVal, final String fmt, final Object[] argArray, + final Throwable t) { + final java.util.logging.Level level = switch (levelVal) { + case LocationAwareLogger.TRACE_INT -> Level.TRACE; + case LocationAwareLogger.DEBUG_INT -> Level.DEBUG; + case LocationAwareLogger.INFO_INT -> Level.INFO; + case LocationAwareLogger.WARN_INT -> Level.WARN; + case LocationAwareLogger.ERROR_INT -> Level.ERROR; + default -> Level.DEBUG; + }; + if (logger.isLoggable(level)) { + final String message = MessageFormatter.arrayFormat(fmt, argArray).getMessage(); + log(marker, level, fqcn, message, t, argArray); + } + } + + @Override + public boolean isTraceEnabled() { + return logger.isLoggable(Level.TRACE); + } + + @Override + public void trace(final String msg) { + if (ALT_TRACE_INT < logger.getEffectiveLevel()) { + return; + } + log(null, org.xbib.logging.Level.TRACE, msg, null); + } + + @Override + public void trace(final String format, final Object arg) { + if (ALT_TRACE_INT < logger.getEffectiveLevel()) { + return; + } + final FormattingTuple formattingTuple = MessageFormatter.format(format, arg); + log(null, org.xbib.logging.Level.TRACE, formattingTuple.getMessage(), formattingTuple.getThrowable(), arg); + } + + @Override + public void trace(final String format, final Object arg1, final Object arg2) { + if (ALT_TRACE_INT < logger.getEffectiveLevel()) { + return; + } + final FormattingTuple formattingTuple = MessageFormatter.format(format, arg1, arg2); + log(null, org.xbib.logging.Level.TRACE, formattingTuple.getMessage(), formattingTuple.getThrowable(), arg1, arg2); + } + + @Override + public void trace(final String format, final Object... arguments) { + if (ALT_TRACE_INT < logger.getEffectiveLevel()) { + return; + } + final FormattingTuple formattingTuple = MessageFormatter.arrayFormat(format, arguments); + log(null, org.xbib.logging.Level.TRACE, formattingTuple.getMessage(), formattingTuple.getThrowable(), arguments); + } + + @Override + public void trace(final String msg, final Throwable t) { + if (ALT_TRACE_INT < logger.getEffectiveLevel()) { + return; + } + log(null, org.xbib.logging.Level.TRACE, msg, t); + } + + @Override + public boolean isTraceEnabled(Marker marker) { + return isTraceEnabled(); + } + + @Override + public void trace(Marker marker, String msg) { + if (ALT_TRACE_INT < logger.getEffectiveLevel()) { + return; + } + log(marker, org.xbib.logging.Level.TRACE, msg, null); + } + + @Override + public void trace(Marker marker, String format, Object arg) { + if (ALT_TRACE_INT < logger.getEffectiveLevel()) { + return; + } + final FormattingTuple formattingTuple = MessageFormatter.format(format, arg); + log(marker, org.xbib.logging.Level.TRACE, formattingTuple.getMessage(), formattingTuple.getThrowable(), arg); + } + + @Override + public void trace(Marker marker, String format, Object arg1, Object arg2) { + if (ALT_TRACE_INT < logger.getEffectiveLevel()) { + return; + } + final FormattingTuple formattingTuple = MessageFormatter.format(format, arg1, arg2); + log(marker, org.xbib.logging.Level.TRACE, formattingTuple.getMessage(), formattingTuple.getThrowable(), arg1, arg2); + } + + @Override + public void trace(Marker marker, String format, Object... arguments) { + if (ALT_TRACE_INT < logger.getEffectiveLevel()) { + return; + } + final FormattingTuple formattingTuple = MessageFormatter.arrayFormat(format, arguments); + log(marker, org.xbib.logging.Level.TRACE, formattingTuple.getMessage(), formattingTuple.getThrowable(), arguments); + + } + + @Override + public void trace(Marker marker, String msg, Throwable t) { + if (ALT_TRACE_INT < logger.getEffectiveLevel()) { + return; + } + log(marker, org.xbib.logging.Level.TRACE, msg, t); + } + + @Override + public boolean isDebugEnabled() { + return logger.isLoggable(Level.DEBUG); + } + + @Override + public void debug(final String msg) { + if (ALT_DEBUG_INT < logger.getEffectiveLevel()) { + return; + } + log(null, org.xbib.logging.Level.DEBUG, msg, null); + } + + @Override + public void debug(final String format, final Object arg) { + if (ALT_DEBUG_INT < logger.getEffectiveLevel()) { + return; + } + final FormattingTuple formattingTuple = MessageFormatter.format(format, arg); + log(null, org.xbib.logging.Level.DEBUG, formattingTuple.getMessage(), formattingTuple.getThrowable(), arg); + } + + @Override + public void debug(final String format, final Object arg1, final Object arg2) { + if (ALT_DEBUG_INT < logger.getEffectiveLevel()) { + return; + } + final FormattingTuple formattingTuple = MessageFormatter.format(format, arg1, arg2); + log(null, org.xbib.logging.Level.DEBUG, formattingTuple.getMessage(), formattingTuple.getThrowable(), arg1, arg2); + } + + @Override + public void debug(final String format, final Object... arguments) { + if (ALT_DEBUG_INT < logger.getEffectiveLevel()) { + return; + } + final FormattingTuple formattingTuple = MessageFormatter.arrayFormat(format, arguments); + log(null, org.xbib.logging.Level.DEBUG, formattingTuple.getMessage(), formattingTuple.getThrowable(), arguments); + } + + @Override + public void debug(final String msg, final Throwable t) { + if (ALT_DEBUG_INT < logger.getEffectiveLevel()) { + return; + } + log(null, org.xbib.logging.Level.DEBUG, msg, t); + } + + @Override + public boolean isDebugEnabled(Marker marker) { + return isDebugEnabled(); + } + + @Override + public void debug(Marker marker, String msg) { + if (ALT_DEBUG_INT < logger.getEffectiveLevel()) { + return; + } + log(marker, org.xbib.logging.Level.DEBUG, msg, null); + } + + @Override + public void debug(Marker marker, String format, Object arg) { + if (ALT_DEBUG_INT < logger.getEffectiveLevel()) { + return; + } + final FormattingTuple formattingTuple = MessageFormatter.format(format, arg); + log(marker, org.xbib.logging.Level.DEBUG, formattingTuple.getMessage(), formattingTuple.getThrowable(), arg); + } + + @Override + public void debug(Marker marker, String format, Object arg1, Object arg2) { + if (ALT_DEBUG_INT < logger.getEffectiveLevel()) { + return; + } + final FormattingTuple formattingTuple = MessageFormatter.format(format, arg1, arg2); + log(marker, org.xbib.logging.Level.DEBUG, formattingTuple.getMessage(), formattingTuple.getThrowable(), arg1, arg2); + } + + @Override + public void debug(Marker marker, String format, Object... arguments) { + if (ALT_DEBUG_INT < logger.getEffectiveLevel()) { + return; + } + final FormattingTuple formattingTuple = MessageFormatter.arrayFormat(format, arguments); + log(marker, org.xbib.logging.Level.DEBUG, formattingTuple.getMessage(), formattingTuple.getThrowable(), arguments); + } + + @Override + public void debug(Marker marker, String msg, Throwable t) { + if (ALT_DEBUG_INT < logger.getEffectiveLevel()) { + return; + } + log(marker, org.xbib.logging.Level.DEBUG, msg, t); + } + + @Override + public boolean isInfoEnabled() { + return logger.isLoggable(Level.INFO); + } + + @Override + public void info(final String msg) { + if (ALT_INFO_INT < logger.getEffectiveLevel()) { + return; + } + log(null, org.xbib.logging.Level.INFO, msg, null); + } + + @Override + public void info(final String format, final Object arg) { + if (ALT_INFO_INT < logger.getEffectiveLevel()) { + return; + } + final FormattingTuple formattingTuple = MessageFormatter.format(format, arg); + log(null, org.xbib.logging.Level.INFO, formattingTuple.getMessage(), formattingTuple.getThrowable(), arg); + } + + @Override + public void info(final String format, final Object arg1, final Object arg2) { + if (ALT_INFO_INT < logger.getEffectiveLevel()) { + return; + } + final FormattingTuple formattingTuple = MessageFormatter.format(format, arg1, arg2); + log(null, org.xbib.logging.Level.INFO, formattingTuple.getMessage(), formattingTuple.getThrowable(), arg1, arg2); + } + + @Override + public void info(final String format, final Object... arguments) { + if (ALT_INFO_INT < logger.getEffectiveLevel()) { + return; + } + final FormattingTuple formattingTuple = MessageFormatter.arrayFormat(format, arguments); + log(null, org.xbib.logging.Level.INFO, formattingTuple.getMessage(), formattingTuple.getThrowable(), arguments); + } + + @Override + public void info(final String msg, final Throwable t) { + if (ALT_INFO_INT < logger.getEffectiveLevel()) { + return; + } + log(null, org.xbib.logging.Level.INFO, msg, t); + } + + @Override + public boolean isInfoEnabled(Marker marker) { + return isInfoEnabled(); + } + + @Override + public void info(Marker marker, String msg) { + if (ALT_INFO_INT < logger.getEffectiveLevel()) { + return; + } + log(marker, org.xbib.logging.Level.INFO, msg, null); + } + + @Override + public void info(Marker marker, String format, Object arg) { + if (ALT_INFO_INT < logger.getEffectiveLevel()) { + return; + } + final FormattingTuple formattingTuple = MessageFormatter.format(format, arg); + log(marker, org.xbib.logging.Level.INFO, formattingTuple.getMessage(), formattingTuple.getThrowable(), arg); + } + + @Override + public void info(Marker marker, String format, Object arg1, Object arg2) { + if (ALT_INFO_INT < logger.getEffectiveLevel()) { + return; + } + final FormattingTuple formattingTuple = MessageFormatter.format(format, arg1, arg2); + log(marker, org.xbib.logging.Level.INFO, formattingTuple.getMessage(), formattingTuple.getThrowable(), arg1, arg2); + } + + @Override + public void info(Marker marker, String format, Object... arguments) { + if (ALT_INFO_INT < logger.getEffectiveLevel()) { + return; + } + final FormattingTuple formattingTuple = MessageFormatter.arrayFormat(format, arguments); + log(marker, org.xbib.logging.Level.INFO, formattingTuple.getMessage(), formattingTuple.getThrowable(), arguments); + } + + @Override + public void info(Marker marker, String msg, Throwable t) { + if (ALT_INFO_INT < logger.getEffectiveLevel()) { + return; + } + log(marker, org.xbib.logging.Level.INFO, msg, t); + } + + @Override + public boolean isWarnEnabled() { + return logger.isLoggable(Level.WARN); + } + + @Override + public void warn(final String msg) { + if (ALT_WARN_INT < logger.getEffectiveLevel()) { + return; + } + log(null, org.xbib.logging.Level.WARN, msg, null); + } + + @Override + public void warn(final String format, final Object arg) { + if (ALT_WARN_INT < logger.getEffectiveLevel()) { + return; + } + final FormattingTuple formattingTuple = MessageFormatter.format(format, arg); + log(null, org.xbib.logging.Level.WARN, formattingTuple.getMessage(), formattingTuple.getThrowable(), arg); + } + + @Override + public void warn(final String format, final Object... arguments) { + if (ALT_WARN_INT < logger.getEffectiveLevel()) { + return; + } + final FormattingTuple formattingTuple = MessageFormatter.arrayFormat(format, arguments); + log(null, org.xbib.logging.Level.WARN, formattingTuple.getMessage(), formattingTuple.getThrowable(), arguments); + } + + @Override + public void warn(final String format, final Object arg1, final Object arg2) { + if (ALT_WARN_INT < logger.getEffectiveLevel()) { + return; + } + final FormattingTuple formattingTuple = MessageFormatter.format(format, arg1, arg2); + log(null, org.xbib.logging.Level.WARN, formattingTuple.getMessage(), formattingTuple.getThrowable(), arg1, arg2); + } + + @Override + public void warn(final String msg, final Throwable t) { + if (ALT_WARN_INT < logger.getEffectiveLevel()) { + return; + } + log(null, org.xbib.logging.Level.WARN, msg, t); + } + + @Override + public boolean isWarnEnabled(Marker marker) { + return isWarnEnabled(); + } + + @Override + public void warn(Marker marker, String msg) { + if (ALT_WARN_INT < logger.getEffectiveLevel()) { + return; + } + log(marker, org.xbib.logging.Level.WARN, msg, null); + } + + @Override + public void warn(Marker marker, String format, Object arg) { + if (ALT_WARN_INT < logger.getEffectiveLevel()) { + return; + } + final FormattingTuple formattingTuple = MessageFormatter.format(format, arg); + log(marker, org.xbib.logging.Level.WARN, formattingTuple.getMessage(), formattingTuple.getThrowable(), arg); + } + + @Override + public void warn(Marker marker, String format, Object arg1, Object arg2) { + if (ALT_WARN_INT < logger.getEffectiveLevel()) { + return; + } + final FormattingTuple formattingTuple = MessageFormatter.format(format, arg1, arg2); + log(marker, org.xbib.logging.Level.WARN, formattingTuple.getMessage(), formattingTuple.getThrowable(), arg1, arg2); + } + + @Override + public void warn(Marker marker, String format, Object... arguments) { + if (ALT_WARN_INT < logger.getEffectiveLevel()) { + return; + } + final FormattingTuple formattingTuple = MessageFormatter.arrayFormat(format, arguments); + log(marker, org.xbib.logging.Level.WARN, formattingTuple.getMessage(), formattingTuple.getThrowable(), arguments); + } + + @Override + public void warn(Marker marker, String msg, Throwable t) { + if (ALT_WARN_INT < logger.getEffectiveLevel()) { + return; + } + log(marker, org.xbib.logging.Level.WARN, msg, t); + } + + @Override + public boolean isErrorEnabled() { + return logger.isLoggable(Level.ERROR); + } + + @Override + public void error(final String msg) { + if (ALT_ERROR_INT < logger.getEffectiveLevel()) { + return; + } + log(null, org.xbib.logging.Level.ERROR, msg, null); + } + + @Override + public void error(final String format, final Object arg) { + if (ALT_ERROR_INT < logger.getEffectiveLevel()) { + return; + } + final FormattingTuple formattingTuple = MessageFormatter.format(format, arg); + log(null, org.xbib.logging.Level.ERROR, formattingTuple.getMessage(), formattingTuple.getThrowable(), arg); + } + + @Override + public void error(final String format, final Object arg1, final Object arg2) { + if (ALT_ERROR_INT < logger.getEffectiveLevel()) { + return; + } + final FormattingTuple formattingTuple = MessageFormatter.format(format, arg1, arg2); + log(null, org.xbib.logging.Level.ERROR, formattingTuple.getMessage(), formattingTuple.getThrowable(), arg1, arg2); + } + + @Override + public void error(final String format, final Object... arguments) { + if (ALT_ERROR_INT < logger.getEffectiveLevel()) { + return; + } + final FormattingTuple formattingTuple = MessageFormatter.arrayFormat(format, arguments); + log(null, org.xbib.logging.Level.ERROR, formattingTuple.getMessage(), formattingTuple.getThrowable(), arguments); + } + + @Override + public void error(final String msg, final Throwable t) { + if (ALT_ERROR_INT < logger.getEffectiveLevel()) { + return; + } + log(null, org.xbib.logging.Level.ERROR, msg, t); + } + + @Override + public boolean isErrorEnabled(Marker marker) { + return isErrorEnabled(); + } + + @Override + public void error(Marker marker, String msg) { + if (ALT_ERROR_INT < logger.getEffectiveLevel()) { + return; + } + log(marker, org.xbib.logging.Level.ERROR, msg, null); + } + + @Override + public void error(Marker marker, String format, Object arg) { + if (ALT_ERROR_INT < logger.getEffectiveLevel()) { + return; + } + final FormattingTuple formattingTuple = MessageFormatter.format(format, arg); + log(marker, org.xbib.logging.Level.ERROR, formattingTuple.getMessage(), formattingTuple.getThrowable(), arg); + } + + @Override + public void error(Marker marker, String format, Object arg1, Object arg2) { + if (ALT_ERROR_INT < logger.getEffectiveLevel()) { + return; + } + final FormattingTuple formattingTuple = MessageFormatter.format(format, arg1, arg2); + log(marker, org.xbib.logging.Level.ERROR, formattingTuple.getMessage(), formattingTuple.getThrowable(), arg1, arg2); + } + + @Override + public void error(Marker marker, String format, Object... arguments) { + if (ALT_ERROR_INT < logger.getEffectiveLevel()) { + return; + } + final FormattingTuple formattingTuple = MessageFormatter.arrayFormat(format, arguments); + log(marker, org.xbib.logging.Level.ERROR, formattingTuple.getMessage(), formattingTuple.getThrowable(), arguments); + } + + @Override + public void error(Marker marker, String msg, Throwable t) { + if (ALT_ERROR_INT < logger.getEffectiveLevel()) { + return; + } + log(marker, org.xbib.logging.Level.ERROR, msg, t); + } + + private void log(final Marker marker, final java.util.logging.Level level, final String message, final Throwable t) { + final ExtLogRecord rec = new ExtLogRecord(level, message, LOGGER_CLASS_NAME); + rec.setThrown(t); + setMarker(rec, marker); + logger.logRaw(rec); + } + + private void log(final Marker marker, final java.util.logging.Level level, final String message, final Throwable t, + final Object... params) { + log(marker, level, LOGGER_CLASS_NAME, message, t, params); + } + + private void log(final Marker marker, final java.util.logging.Level level, final String fqcn, final String message, + final Throwable t, final Object[] params) { + final ExtLogRecord rec = new ExtLogRecord(level, message, ExtLogRecord.FormatStyle.NO_FORMAT, fqcn); + rec.setThrown(t); + rec.setParameters(params); + setMarker(rec, marker); + logger.logRaw(rec); + } + + private void setMarker(ExtLogRecord rec, Marker marker) { + rec.setMarker(marker); + } +} diff --git a/logging-adapter-slf4j/src/main/java/org/slf4j/impl/Slf4jLoggerFactory.java b/logging-adapter-slf4j/src/main/java/org/slf4j/impl/Slf4jLoggerFactory.java new file mode 100644 index 0000000..ce14074 --- /dev/null +++ b/logging-adapter-slf4j/src/main/java/org/slf4j/impl/Slf4jLoggerFactory.java @@ -0,0 +1,25 @@ +package org.slf4j.impl; + +import org.slf4j.ILoggerFactory; +import org.slf4j.Logger; +import org.xbib.logging.LogContext; + +public final class Slf4jLoggerFactory implements ILoggerFactory { + + private static final org.xbib.logging.Logger.AttachmentKey key = new org.xbib.logging.Logger.AttachmentKey<>(); + + public Slf4jLoggerFactory() { + } + + @Override + public Logger getLogger(final String name) { + final org.xbib.logging.Logger lmLogger = LogContext.getLogContext().getLogger(name); + final Logger logger = lmLogger.getAttachment(key); + if (logger != null) { + return logger; + } + final Logger newLogger = new Slf4jLogger(lmLogger); + final Logger appearingLogger = lmLogger.attachIfAbsent(key, newLogger); + return appearingLogger != null ? appearingLogger : newLogger; + } +} diff --git a/logging-adapter-slf4j/src/main/java/org/slf4j/impl/Slf4jMDCAdapter.java b/logging-adapter-slf4j/src/main/java/org/slf4j/impl/Slf4jMDCAdapter.java new file mode 100644 index 0000000..36da310 --- /dev/null +++ b/logging-adapter-slf4j/src/main/java/org/slf4j/impl/Slf4jMDCAdapter.java @@ -0,0 +1,50 @@ +package org.slf4j.impl; + +import java.util.Map; + +import org.slf4j.helpers.BasicMDCAdapter; +import org.slf4j.spi.MDCAdapter; +import org.xbib.logging.MDC; + +public final class Slf4jMDCAdapter extends BasicMDCAdapter implements MDCAdapter { + + public Slf4jMDCAdapter() { + } + + @Override + public void put(final String key, final String val) { + MDC.put(key, val); + } + + @Override + public String get(final String key) { + return MDC.get(key); + } + + @Override + public void remove(final String key) { + MDC.remove(key); + } + + @Override + public void clear() { + MDC.clear(); + } + + @Override + public Map getCopyOfContextMap() { + return MDC.copy(); + } + + @Override + public void setContextMap(final Map contextMap) { + MDC.clear(); + for (Map.Entry entry : ((Map) contextMap).entrySet()) { + final Object key = entry.getKey(); + final Object value = entry.getValue(); + if (key != null && value != null) { + MDC.put(key.toString(), value.toString()); + } + } + } +} diff --git a/logging-adapter-slf4j/src/main/java/org/slf4j/impl/StaticLoggerBinder.java b/logging-adapter-slf4j/src/main/java/org/slf4j/impl/StaticLoggerBinder.java new file mode 100644 index 0000000..3fbb910 --- /dev/null +++ b/logging-adapter-slf4j/src/main/java/org/slf4j/impl/StaticLoggerBinder.java @@ -0,0 +1,27 @@ +package org.slf4j.impl; + +import org.slf4j.ILoggerFactory; +import org.slf4j.spi.LoggerFactoryBinder; + +@Deprecated(forRemoval = true) +public final class StaticLoggerBinder implements LoggerFactoryBinder { + + public static final StaticLoggerBinder SINGLETON = new StaticLoggerBinder(); + + public StaticLoggerBinder() { + } + + @Override + public ILoggerFactory getLoggerFactory() { + return new Slf4jLoggerFactory(); + } + + @Override + public String getLoggerFactoryClassStr() { + return Slf4jLoggerFactory.class.getName(); + } + + public static StaticLoggerBinder getSingleton() { + return SINGLETON; + } +} diff --git a/logging-adapter-slf4j/src/main/java/org/slf4j/impl/StaticMDCBinder.java b/logging-adapter-slf4j/src/main/java/org/slf4j/impl/StaticMDCBinder.java new file mode 100644 index 0000000..7c73c9f --- /dev/null +++ b/logging-adapter-slf4j/src/main/java/org/slf4j/impl/StaticMDCBinder.java @@ -0,0 +1,20 @@ +package org.slf4j.impl; + +import org.slf4j.spi.MDCAdapter; + +@Deprecated(forRemoval = true) +public final class StaticMDCBinder { + + public static final StaticMDCBinder SINGLETON = new StaticMDCBinder(); + + private StaticMDCBinder() { + } + + public MDCAdapter getMDCA() { + return new Slf4jMDCAdapter(); + } + + public String getMDCAdapterClassStr() { + return Slf4jMDCAdapter.class.getName(); + } +} diff --git a/logging-adapter-slf4j/src/main/java/org/slf4j/impl/StaticMarkerBinder.java b/logging-adapter-slf4j/src/main/java/org/slf4j/impl/StaticMarkerBinder.java new file mode 100644 index 0000000..78d86e3 --- /dev/null +++ b/logging-adapter-slf4j/src/main/java/org/slf4j/impl/StaticMarkerBinder.java @@ -0,0 +1,24 @@ +package org.slf4j.impl; + +import org.slf4j.IMarkerFactory; +import org.slf4j.helpers.BasicMarkerFactory; +import org.slf4j.spi.MarkerFactoryBinder; + +@Deprecated(forRemoval = true) +public final class StaticMarkerBinder implements MarkerFactoryBinder { + + public static final StaticMarkerBinder SINGLETON = new StaticMarkerBinder(); + + private final IMarkerFactory markerFactory = new BasicMarkerFactory(); + + private StaticMarkerBinder() { + } + + public IMarkerFactory getMarkerFactory() { + return markerFactory; + } + + public String getMarkerFactoryClassStr() { + return BasicMarkerFactory.class.getName(); + } +} diff --git a/logging-adapter-slf4j/src/main/java/org/slf4j/impl/XbibSlf4jServiceProvider.java b/logging-adapter-slf4j/src/main/java/org/slf4j/impl/XbibSlf4jServiceProvider.java new file mode 100644 index 0000000..ddd1f0c --- /dev/null +++ b/logging-adapter-slf4j/src/main/java/org/slf4j/impl/XbibSlf4jServiceProvider.java @@ -0,0 +1,45 @@ +package org.slf4j.impl; + +import org.slf4j.ILoggerFactory; +import org.slf4j.IMarkerFactory; +import org.slf4j.helpers.BasicMarkerFactory; +import org.slf4j.spi.MDCAdapter; +import org.slf4j.spi.SLF4JServiceProvider; + +public class XbibSlf4jServiceProvider implements SLF4JServiceProvider { + + private final ILoggerFactory loggerFactory; + private final IMarkerFactory markerFactory; + private final MDCAdapter mdcAdapter; + + public XbibSlf4jServiceProvider() { + this.loggerFactory = new Slf4jLoggerFactory(); + this.markerFactory = new BasicMarkerFactory(); + this.mdcAdapter = new Slf4jMDCAdapter(); + } + + @Override + public ILoggerFactory getLoggerFactory() { + return loggerFactory; + } + + @Override + public IMarkerFactory getMarkerFactory() { + return markerFactory; + } + + @Override + public MDCAdapter getMDCAdapter() { + return mdcAdapter; + } + + @Override + public String getRequestedApiVersion() { + return "2.0.13"; + } + + @Override + public void initialize() { + // do nothing + } +} diff --git a/logging-adapter-slf4j/src/main/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider b/logging-adapter-slf4j/src/main/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider new file mode 100644 index 0000000..d1bbf5c --- /dev/null +++ b/logging-adapter-slf4j/src/main/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider @@ -0,0 +1,20 @@ +# +# JBoss, Home of Professional Open Source. +# +# Copyright 2023 Red Hat, Inc., and individual contributors +# as indicated by the @author tags. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +org.slf4j.impl.XbibSlf4jServiceProvider diff --git a/logging-adapter-slf4j/src/test/java/module-info.java b/logging-adapter-slf4j/src/test/java/module-info.java new file mode 100644 index 0000000..cd275f1 --- /dev/null +++ b/logging-adapter-slf4j/src/test/java/module-info.java @@ -0,0 +1,8 @@ +module org.xbib.logging.adapter.slf4j.test { + requires java.logging; + requires org.junit.jupiter.api; + requires org.slf4j; + requires org.xbib.logging; + requires org.xbib.logging.adapter.slf4j; + exports org.slf4j.impl.test to org.junit.platform.commons; +} diff --git a/logging-adapter-slf4j/src/test/java/org/slf4j/impl/test/LoggerTest.java b/logging-adapter-slf4j/src/test/java/org/slf4j/impl/test/LoggerTest.java new file mode 100644 index 0000000..37ac40d --- /dev/null +++ b/logging-adapter-slf4j/src/test/java/org/slf4j/impl/test/LoggerTest.java @@ -0,0 +1,144 @@ +package org.slf4j.impl.test; + +import java.util.concurrent.BlockingDeque; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.logging.LogRecord; + +import org.slf4j.impl.Slf4jLogger; +import org.slf4j.impl.Slf4jMDCAdapter; +import org.xbib.logging.ExtHandler; +import org.xbib.logging.ExtLogRecord; +import org.xbib.logging.LogContext; +import org.xbib.logging.LogContextSelector; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; +import org.slf4j.Marker; +import org.slf4j.helpers.BasicMarkerFactory; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class LoggerTest { + + private static final LogContext LOG_CONTEXT = LogContext.create(); + + private static final java.util.logging.Logger ROOT = LOG_CONTEXT.getLogger(""); + + private static final QueueHandler HANDLER = new QueueHandler(); + + private static final LogContextSelector DEFAULT_SELECTOR = LogContext.getLogContextSelector(); + + @BeforeAll + public static void configureLogManager() { + LogContext.setLogContextSelector(() -> LOG_CONTEXT); + ROOT.addHandler(HANDLER); + } + + @AfterAll + public static void cleanup() throws Exception { + LOG_CONTEXT.close(); + LogContext.setLogContextSelector(DEFAULT_SELECTOR); + } + + @AfterEach + public void clearHandler() { + HANDLER.close(); + } + + @Test + public void testLogger() { + final Logger logger = LoggerFactory.getLogger(LoggerTest.class); + assertTrue(logger instanceof Slf4jLogger, expectedTypeMessage(Slf4jLogger.class, logger.getClass())); + + // Ensure the logger logs something + final String testMsg = "This is a test message"; + logger.info(testMsg); + ExtLogRecord record = HANDLER.messages.poll(); + assertNotNull(record); + assertEquals(testMsg, record.getMessage()); + assertNull(record.getParameters()); + + // Test a formatted message + logger.info("This is a test formatted {}", "{message}"); + record = HANDLER.messages.poll(); + assertNotNull(record); + assertEquals("This is a test formatted {message}", record.getFormattedMessage()); + assertArrayEquals(new Object[] { "{message}" }, record.getParameters(), "Expected parameter not found."); + } + + @Test + public void testLoggerWithExceptions() { + final Logger logger = LoggerFactory.getLogger(LoggerTest.class); + + final RuntimeException e = new RuntimeException("Test exception"); + final String testMsg = "This is a test message"; + logger.info(testMsg, e); + LogRecord record = HANDLER.messages.poll(); + assertNotNull(record); + assertEquals(testMsg, record.getMessage()); + assertEquals(e, record.getThrown(), "Cause is different from the expected cause"); + + // Test format with the last parameter being the throwable which should set be set on the record + logger.info("This is a test formatted {}", "{message}", e); + record = HANDLER.messages.poll(); + assertNotNull(record); + assertEquals("This is a test formatted {message}", record.getMessage()); + assertEquals(e, record.getThrown(), "Cause is different from the expected cause"); + } + + @Test + public void testLoggerWithMarkers() { + final Logger logger = LoggerFactory.getLogger(LoggerTest.class); + final Marker marker = new BasicMarkerFactory().getMarker("test"); + + logger.info(marker, "log message"); + LogRecord record = HANDLER.messages.poll(); + assertNotNull(record); + // TODO: record.getMarker() must be same instance of "marker" + } + + @Test + public void testMDC() { + assertSame(MDC.getMDCAdapter() + .getClass(), Slf4jMDCAdapter.class, + expectedTypeMessage(Slf4jMDCAdapter.class, MDC.getMDCAdapter() + .getClass())); + final String key = Long.toHexString(System.currentTimeMillis()); + MDC.put(key, "value"); + assertEquals("value", MDC.get(key), "MDC value should be \"value\""); + assertEquals("value", org.xbib.logging.MDC.get(key), "MDC value should be \"value\""); + } + + private static Supplier expectedTypeMessage(final Class expected, final Class found) { + return () -> String.format("Expected type %s but found type %s", expected.getName(), found.getName()); + } + + private static class QueueHandler extends ExtHandler { + final BlockingDeque messages = new LinkedBlockingDeque<>(); + + @Override + protected void doPublish(final ExtLogRecord record) { + messages.add(record); + } + + @Override + public void flush() { + } + + @Override + public void close() throws SecurityException { + messages.clear(); + setLevel(Level.ALL); + } + } +} diff --git a/logging/build.gradle b/logging/build.gradle new file mode 100644 index 0000000..3afefd7 --- /dev/null +++ b/logging/build.gradle @@ -0,0 +1,67 @@ +test { + systemProperty 'java.util.logging.manager', 'org.xbib.logging.LogManager' + systemProperty 'javax.net.ssl.keyStore', 'src/test/resources/server-keystore.jks' + systemProperty 'javax.net.ssl.keyStorePassword', 'testpassword' + systemProperty 'javax.net.ssl.trustStore', 'src/test/resources/client-keystore.jks' + systemProperty 'javax.net.ssl.trustStorePassword', 'testpassword' +} + +def moduleName = 'org.xbib.logging.test' +def patchArgs = ['--patch-module', "$moduleName=" + files(sourceSets.test.resources.srcDirs).asPath ] + +tasks.named('compileTestJava') { + options.compilerArgs += patchArgs +} + +tasks.named('test') { + jvmArgs += patchArgs +} + +sourceSets { + integration { + java.srcDir "$projectDir/src/integration/java" + resources.srcDir "$projectDir/src/integration/resources" + compileClasspath += main.output + test.output + runtimeClasspath += main.output + test.output + } +} + +configurations { + integrationImplementation.extendsFrom testImplementation + integrationRuntime.extendsFrom testRuntime +} + +dependencies { + integrationImplementation testLibs.junit.jupiter.api + integrationImplementation testLibs.hamcrest + integrationRuntimeOnly testLibs.junit.jupiter.engine + integrationRuntimeOnly testLibs.junit.jupiter.platform.launcher +} + +tasks.register('integrationTest', Test) { + mustRunAfter test + testClassesDirs = sourceSets.integration.output.classesDirs + classpath = sourceSets.integration.runtimeClasspath + useJUnitPlatform() { + filter { + includeTestsMatching "*IT" + excludeTestsMatching "*Test" + } + } + failFast = false + ignoreFailures = true + testLogging { + events 'STARTED', 'PASSED', 'FAILED', 'SKIPPED' + } + afterSuite { desc, result -> + if (!desc.parent) { + println "\nIntegration result: ${result.resultType}" + println "Integration summary: ${result.testCount} integrations, " + + "${result.successfulTestCount} succeeded, " + + "${result.failedTestCount} failed, " + + "${result.skippedTestCount} skipped" + } + } +} + +check.dependsOn integrationTest diff --git a/logging/src/integration/java/module-info.java b/logging/src/integration/java/module-info.java new file mode 100644 index 0000000..2faa76d --- /dev/null +++ b/logging/src/integration/java/module-info.java @@ -0,0 +1,7 @@ +module org.xbib.logging.integration { + requires java.logging; + requires org.junit.jupiter.api; + requires org.xbib.logging; + requires org.xbib.logging.test; + exports org.xbib.logging.integration to org.junit.platform.commons; +} diff --git a/logging/src/integration/java/org/xbib/logging/integration/SystemLoggerIT.java b/logging/src/integration/java/org/xbib/logging/integration/SystemLoggerIT.java new file mode 100644 index 0000000..47a36a3 --- /dev/null +++ b/logging/src/integration/java/org/xbib/logging/integration/SystemLoggerIT.java @@ -0,0 +1,166 @@ +package org.xbib.logging.integration; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class SystemLoggerIT { + + private Path stdout; + private Path logFile; + + @BeforeEach + public void setup() throws Exception { + stdout = Files.createTempFile("stdout", ".txt"); + logFile = Files.createTempFile("system-logger", ".log"); + } + + @AfterEach + public void killProcess() throws IOException { + Files.deleteIfExists(stdout); + Files.deleteIfExists(logFile); + } + + @Test + public void testSystemLoggerActivated() throws Exception { + final Process process = createProcess(); + if (process.waitFor(3L, TimeUnit.SECONDS)) { + final int exitCode = process.exitValue(); + final StringBuilder msg = new StringBuilder("Expected exit value 0 got ") + .append(exitCode); + appendStdout(msg); + Assertions.assertEquals(0, exitCode, msg.toString()); + } else { + final Process destroyed = process.destroyForcibly(); + final StringBuilder msg = new StringBuilder("Failed to exit process within 3 seconds. Exit Code: ") + .append(destroyed.exitValue()); + appendStdout(msg); + Assertions.fail(msg.toString()); + } + + /* + final JsonObject json = readLogFile(logFile); + final JsonArray lines = json.getJsonArray("lines"); + Assertions.assertEquals(2, lines.size()); + // The first line should be from a SystemLogger + JsonObject line = lines.getJsonObject(0); + Assertions.assertEquals("org.xbib.logging.LoggerFinder$SystemLogger", line.getString("loggerClassName")); + JsonObject mdc = line.getJsonObject("mdc"); + Assertions.assertEquals("org.xbib.logging.LoggerFinder$SystemLogger", mdc.getString("logger.type")); + Assertions.assertEquals(LogManager.class.getName(), mdc.getString("java.util.logging.LogManager")); + Assertions.assertEquals(LogManager.class.getName(), mdc.getString("java.util.logging.manager")); + + + // The second line should be from a JUL logger + line = lines.getJsonObject(1); + Assertions.assertEquals(Logger.class.getName(), line.getString("loggerClassName")); + mdc = line.getJsonObject("mdc"); + Assertions.assertEquals(Logger.class.getName(), mdc.getString("logger.type")); + Assertions.assertEquals(LogManager.class.getName(), mdc.getString("java.util.logging.LogManager")); + Assertions.assertEquals(LogManager.class.getName(), mdc.getString("java.util.logging.manager")); + */ + } + + @Test + public void testSystemLoggerAccessedBeforeActivated() throws Exception { + final Process process = createProcess("-Dsystem.logger.test.jul=true", + "-Djava.util.logging.config.class=" + TestLogContextConfigurator.class.getName()); + if (process.waitFor(3L, TimeUnit.SECONDS)) { + final int exitCode = process.exitValue(); + final StringBuilder msg = new StringBuilder("Expected exit value 0 got ") + .append(exitCode); + appendStdout(msg); + Assertions.assertEquals(0, exitCode, msg.toString()); + } else { + final Process destroyed = process.destroyForcibly(); + final StringBuilder msg = new StringBuilder("Failed to exit process within 3 seconds. Exit Code: ") + .append(destroyed.exitValue()); + appendStdout(msg); + Assertions.fail(msg.toString()); + } + + /* + final JsonObject json = readLogFile(logFile); + final JsonArray lines = json.getJsonArray("lines"); + Assertions.assertEquals(3, lines.size()); + + // The first line should be an error indicating the java.util.logging.manager wasn't set before the LogManager + // was accessed + JsonObject line = lines.getJsonObject(0); + Assertions.assertEquals("ERROR", line.getString("level")); + final String message = line.getString("message"); + Assertions.assertNotNull(message); + Assertions.assertTrue(message.contains("java.util.logging.manager")); + + // The second line should be from a SystemLogger + line = lines.getJsonObject(1); + Assertions.assertEquals("org.xbib.logging.LoggerFinder$SystemLogger", line.getString("loggerClassName")); + JsonObject mdc = line.getJsonObject("mdc"); + Assertions.assertEquals("org.xbib.logging.LoggerFinder$SystemLogger", mdc.getString("logger.type")); + Assertions.assertEquals("java.util.logging.LogManager", mdc.getString("java.util.logging.LogManager")); + + // The third line should be from a JUL logger + line = lines.getJsonObject(2); + Assertions.assertEquals("java.util.logging.Logger", line.getString("loggerClassName")); + mdc = line.getJsonObject("mdc"); + Assertions.assertEquals("java.util.logging.Logger", mdc.getString("logger.type")); + Assertions.assertEquals("java.util.logging.LogManager", mdc.getString("java.util.logging.LogManager")); + */ + } + + private Process createProcess(final String... javaOpts) throws IOException { + final List cmd = new ArrayList<>(); + cmd.add(findJavaCommand()); + cmd.add("--add-modules=ALL-MODULE-PATH"); + cmd.add("-Dorg.xbib.logging.test.configure=true"); + cmd.add("-Dtest.log.file.name=" + logFile.toString()); + Collections.addAll(cmd, javaOpts); + cmd.add("-cp"); + cmd.add(System.getProperty("java.class.path")); // does not work in Gradle! + cmd.add(SystemLoggerMain.class.getName()); + System.out.println(cmd); + return new ProcessBuilder(cmd) + .redirectErrorStream(true) + .redirectOutput(stdout.toFile()) + .start(); + } + + private void appendStdout(final StringBuilder builder) throws IOException { + for (String line : Files.readAllLines(stdout)) { + builder.append(System.lineSeparator()) + .append(line); + } + } + + /*private static JsonObject readLogFile(final Path logFile) throws IOException { + final JsonArrayBuilder builder = Json.createArrayBuilder(); + try (BufferedReader reader = Files.newBufferedReader(logFile, StandardCharsets.UTF_8)) { + String line; + while ((line = reader.readLine()) != null) { + try (JsonReader jsonReader = Json.createReader(new StringReader(line))) { + builder.add(jsonReader.read()); + } + } + } + return Json.createObjectBuilder().add("lines", builder).build(); + }*/ + + // this hack does not respect Gradle toolchain + private static String findJavaCommand() { + String javaHome = System.getProperty("java.home"); + if (javaHome != null) { + return Paths.get(javaHome, "bin", "java").toAbsolutePath().toString(); + } + return "java"; + } +} diff --git a/logging/src/integration/java/org/xbib/logging/integration/SystemLoggerMain.java b/logging/src/integration/java/org/xbib/logging/integration/SystemLoggerMain.java new file mode 100644 index 0000000..ef69013 --- /dev/null +++ b/logging/src/integration/java/org/xbib/logging/integration/SystemLoggerMain.java @@ -0,0 +1,32 @@ +package org.xbib.logging.integration; + +import java.util.logging.LogManager; +import java.util.logging.Logger; +import org.xbib.logging.MDC; + +public class SystemLoggerMain { + + public SystemLoggerMain() { + } + + public static void main(final String[] args) { + if (Boolean.getBoolean("system.logger.test.jul")) { + // Access the log manager to ensure it's configured before the system property is set + LogManager.getLogManager(); + } + final System.Logger.Level level = System.Logger.Level.valueOf(System.getProperty("system.logger.test.level", "INFO")); + final String msgId = System.getProperty("system.logger.test.msg.id"); + final String msg = String.format("Test message from %s id %s", SystemLoggerMain.class.getName(), msgId); + final System.Logger systemLogger = System.getLogger(SystemLoggerMain.class.getName()); + MDC.put("logger.type", systemLogger.getClass().getName()); + MDC.put("java.util.logging.LogManager", LogManager.getLogManager().getClass().getName()); + MDC.put("java.util.logging.manager", System.getProperty("java.util.logging.manager")); + systemLogger.log(level, msg); + + final Logger logger = Logger.getLogger(SystemLoggerMain.class.getName()); + MDC.put("logger.type", logger.getClass().getName()); + MDC.put("java.util.logging.LogManager", LogManager.getLogManager().getClass().getName()); + MDC.put("java.util.logging.manager", System.getProperty("java.util.logging.manager")); + logger.info(msg); + } +} \ No newline at end of file diff --git a/logging/src/integration/java/org/xbib/logging/integration/TestLogContextConfigurator.java b/logging/src/integration/java/org/xbib/logging/integration/TestLogContextConfigurator.java new file mode 100644 index 0000000..3573943 --- /dev/null +++ b/logging/src/integration/java/org/xbib/logging/integration/TestLogContextConfigurator.java @@ -0,0 +1,77 @@ +package org.xbib.logging.integration; + +import java.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; +import java.io.UncheckedIOException; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.xbib.logging.ExtFormatter; +import org.xbib.logging.ExtLogRecord; +import org.xbib.logging.LogContext; +import org.xbib.logging.LogContextConfigurator; +import org.xbib.logging.handlers.FileHandler; + +public class TestLogContextConfigurator implements LogContextConfigurator { + + public TestLogContextConfigurator() { + this(true); + } + + TestLogContextConfigurator(final boolean assumedJul) { + if (assumedJul) { + configure(null); + } + } + + @Override + public void configure(final LogContext logContext, final InputStream inputStream) { + configure(logContext); + } + + private static void configure(final LogContext logContext) { + if (Boolean.getBoolean("org.xbib.logging.test.configure")) { + final Logger rootLogger; + if (logContext == null) { + rootLogger = Logger.getLogger(""); + } else { + rootLogger = logContext.getLogger(""); + } + try { + final String fileName = System.getProperty("test.log.file.name"); + final FileHandler handler = new FileHandler(fileName, false); + handler.setAutoFlush(true); + handler.setFormatter(new TestFormatter()); + rootLogger.addHandler(handler); + rootLogger.setLevel(Level.INFO); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } + + private static class TestFormatter extends ExtFormatter { + + @Override + public String format(final ExtLogRecord record) { + StringWriter stringWriter = new StringWriter(); + /*try (JsonGenerator generator = Json.createGenerator(stringWriter)) { + generator.writeStartObject(); + + generator.write("loggerClassName", record.getLoggerClassName()); + generator.write("level", record.getLevel().toString()); + generator.write("message", record.getMessage()); + generator.writeStartObject("mdc"); + final Map mdc = record.getMdcCopy(); + mdc.forEach(generator::write); + generator.writeEnd(); // end MDC + + generator.writeEnd(); // end object + generator.flush(); + stringWriter.write(System.lineSeparator()); + return stringWriter.toString(); + }*/ + return stringWriter.toString(); + } + } +} diff --git a/logging/src/main/java/module-info.java b/logging/src/main/java/module-info.java new file mode 100644 index 0000000..c028590 --- /dev/null +++ b/logging/src/main/java/module-info.java @@ -0,0 +1,17 @@ +module org.xbib.logging { + uses org.xbib.logging.LogContextInitializer; + uses org.xbib.logging.LogContextConfiguratorFactory; + uses org.xbib.logging.LogContextConfigurator; + uses org.xbib.logging.NDCProvider; + uses org.xbib.logging.MDCProvider; + requires transitive java.logging; + requires java.xml; + requires java.management; + exports org.xbib.logging; + exports org.xbib.logging.configuration; + exports org.xbib.logging.filters; + exports org.xbib.logging.formatters; + exports org.xbib.logging.handlers; + exports org.xbib.logging.net; + exports org.xbib.logging.util; +} diff --git a/logging/src/main/java/org/xbib/logging/CallerClassLoaderLogContextSelector.java b/logging/src/main/java/org/xbib/logging/CallerClassLoaderLogContextSelector.java new file mode 100644 index 0000000..888ec45 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/CallerClassLoaderLogContextSelector.java @@ -0,0 +1,151 @@ +package org.xbib.logging; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; +import org.xbib.logging.util.CopyOnWriteMap; +import org.xbib.logging.util.StackWalkerUtil; + +/** + * A log context selector which chooses a log context based on the caller's classloader. The first caller that is not + * a {@linkplain #addLogApiClassLoader(ClassLoader) log API} or does not have a {@code null} classloader will be the + * class loader used. + */ +@SuppressWarnings({"WeakerAccess", "unused"}) +public final class CallerClassLoaderLogContextSelector implements LogContextSelector { + + /** + * Construct a new instance. If no matching log context is found, the provided default selector is consulted. + * + * @param defaultSelector the selector to consult if no matching log context is found + */ + public CallerClassLoaderLogContextSelector(final LogContextSelector defaultSelector) { + this(defaultSelector, false); + } + + /** + * Construct a new instance. If no matching log context is found, the provided default selector is consulted. + *

+ * If the {@code checkParentClassLoaders} is set to {@code true} this selector recursively searches the class loader + * parents until a match is found or a {@code null} parent is found. + *

+ * + * @param defaultSelector the selector to consult if no matching log context is found + * @param checkParentClassLoaders {@code true} if the {@link LogContext log context} could not + * found for the class loader and the {@link ClassLoader#getParent() parent class + * loader} should be checked + */ + public CallerClassLoaderLogContextSelector(final LogContextSelector defaultSelector, + final boolean checkParentClassLoaders) { + this.defaultSelector = defaultSelector == null ? LogContext.DEFAULT_LOG_CONTEXT_SELECTOR : defaultSelector; + this.checkParentClassLoaders = checkParentClassLoaders; + } + + /** + * Construct a new instance. If no matching log context is found, the system context is used. + */ + public CallerClassLoaderLogContextSelector() { + this(false); + } + + /** + * Construct a new instance. If no matching log context is found, the system context is used. + *

+ * If the {@code checkParentClassLoaders} is set to {@code true} this selector recursively searches the class loader + * parents until a match is found or a {@code null} parent is found. + *

+ * + * @param checkParentClassLoaders {@code true} if the {@link LogContext log context} could not + * found for the class loader and the {@link ClassLoader#getParent() parent class + * loader} should be checked + */ + public CallerClassLoaderLogContextSelector(final boolean checkParentClassLoaders) { + this(LogContext.DEFAULT_LOG_CONTEXT_SELECTOR, checkParentClassLoaders); + } + + private final LogContextSelector defaultSelector; + + private final ConcurrentMap contextMap = new CopyOnWriteMap(); + private final Set logApiClassLoaders = Collections.newSetFromMap(new CopyOnWriteMap()); + private final boolean checkParentClassLoaders; + + /* private final PrivilegedAction logContextAction = new PrivilegedAction() { + + public LogContext run() { + final Class callingClass = JDKSpecific.findCallingClass(logApiClassLoaders); + return callingClass == null ? defaultSelector.getLogContext() : check(callingClass.getClassLoader()); + } + + };*/ + + private LogContext check(final ClassLoader classLoader) { + final LogContext context = contextMap.get(classLoader); + if (context != null) { + return context; + } + final ClassLoader parent = classLoader.getParent(); + if (parent != null && checkParentClassLoaders && !logApiClassLoaders.contains(parent)) { + return check(parent); + } + return defaultSelector.getLogContext(); + } + + /** + * {@inheritDoc} This instance will consult the call stack to see if the first callers classloader is associated + * with a log context. The first caller is determined by the first class loader that is not registered as a + * {@linkplain #addLogApiClassLoader(ClassLoader) log API}. + */ + @Override + public LogContext getLogContext() { + final Class callingClass = StackWalkerUtil.findCallingClass(logApiClassLoaders); + return callingClass == null ? defaultSelector.getLogContext() : check(callingClass.getClassLoader()); + } + + /** + * Register a class loader which is a known log API, and thus should be skipped over when searching for the + * log context to use for the caller class. + * + * @param apiClassLoader the API class loader + * @return {@code true} if this class loader was previously unknown, or {@code false} if it was already registered + */ + public boolean addLogApiClassLoader(ClassLoader apiClassLoader) { + return logApiClassLoaders.add(apiClassLoader); + } + + /** + * Remove a class loader from the known log APIs set. + * + * @param apiClassLoader the API class loader + * @return {@code true} if the class loader was removed, or {@code false} if it was not known to this selector + */ + public boolean removeLogApiClassLoader(ClassLoader apiClassLoader) { + return logApiClassLoaders.remove(apiClassLoader); + } + + /** + * Register a class loader with a log context. This method requires the {@code registerLogContext} + * {@link RuntimePermission}. + * + * @param classLoader the classloader + * @param logContext the log context + * @throws IllegalArgumentException if the classloader is already associated with a log context + */ + public void registerLogContext(ClassLoader classLoader, LogContext logContext) throws IllegalArgumentException { + if (contextMap.putIfAbsent(classLoader, logContext) != null) { + throw new IllegalArgumentException( + "ClassLoader instance is already registered to a log context (" + classLoader + ")"); + } + } + + /** + * Unregister a class loader/log context association. This method requires the {@code unregisterLogContext} + * {@link RuntimePermission}. + * + * @param classLoader the classloader + * @param logContext the log context + * @return {@code true} if the association exists and was removed, {@code false} otherwise + */ + public boolean unregisterLogContext(ClassLoader classLoader, LogContext logContext) { + return contextMap.remove(classLoader, logContext); + } +} diff --git a/logging/src/main/java/org/xbib/logging/ClassLoaderLogContextSelector.java b/logging/src/main/java/org/xbib/logging/ClassLoaderLogContextSelector.java new file mode 100644 index 0000000..55d86f6 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/ClassLoaderLogContextSelector.java @@ -0,0 +1,142 @@ +package org.xbib.logging; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; +import java.util.function.Function; +import org.xbib.logging.util.CopyOnWriteMap; +import org.xbib.logging.util.StackWalkerUtil; + +/** + * A log context selector which chooses a log context based on the caller's classloader. + */ +public final class ClassLoaderLogContextSelector implements LogContextSelector { + + /** + * Construct a new instance. If no matching log context is found, the provided default selector is consulted. + * + * @param defaultSelector the selector to consult if no matching log context is found + */ + public ClassLoaderLogContextSelector(final LogContextSelector defaultSelector) { + this(defaultSelector, false); + } + + /** + * Construct a new instance. If no matching log context is found, the provided default selector is consulted. + *

+ * If the {@code checkParentClassLoaders} is set to {@code true} this selector recursively searches the class loader + * parents until a match is found or a {@code null} parent is found. + * + * @param defaultSelector the selector to consult if no matching log context is found + * @param checkParentClassLoaders {@code true} if the {@link LogContext log context} could not + * found for the class loader and the {@link ClassLoader#getParent() parent class + * loader} should be checked + */ + public ClassLoaderLogContextSelector(final LogContextSelector defaultSelector, final boolean checkParentClassLoaders) { + this.defaultSelector = defaultSelector; + this.checkParentClassLoaders = checkParentClassLoaders; + } + + /** + * Construct a new instance. If no matching log context is found, the system context is used. + */ + public ClassLoaderLogContextSelector() { + this(false); + } + + /** + * Construct a new instance. If no matching log context is found, the system context is used. + *

+ * If the {@code checkParentClassLoaders} is set to {@code true} this selector recursively searches the class loader + * parents until a match is found or a {@code null} parent is found. + * + * @param checkParentClassLoaders {@code true} if the {@link LogContext log context} could not + * found for the class loader and the {@link ClassLoader#getParent() parent class + * loader} should be checked + */ + public ClassLoaderLogContextSelector(final boolean checkParentClassLoaders) { + this(LogContext.DEFAULT_LOG_CONTEXT_SELECTOR, checkParentClassLoaders); + } + + private final LogContextSelector defaultSelector; + + private final ConcurrentMap contextMap = new CopyOnWriteMap<>(); + private final Set logApiClassLoaders = Collections.newSetFromMap(new CopyOnWriteMap<>()); + private final boolean checkParentClassLoaders; + + private final Function logContextFinder = new Function<>() { + @Override + public LogContext apply(final ClassLoader classLoader) { + + final LogContext context = contextMap.get(classLoader); + if (context != null) { + return context; + } + final ClassLoader parent = classLoader.getParent(); + if (parent != null && checkParentClassLoaders && !logApiClassLoaders.contains(parent)) { + return apply(parent); + } + return null; + } + }; + + /** + * {@inheritDoc} This instance will consult the call stack to see if any calling classloader is associated + * with any log context. + */ + public LogContext getLogContext() { + final LogContext result = StackWalkerUtil.logContextFinder(logApiClassLoaders, logContextFinder); + if (result != null) { + return result; + } + return defaultSelector.getLogContext(); + } + + /** + * Register a class loader which is a known log API, and thus should be skipped over when searching for the + * log context to use for the caller class. + * + * @param apiClassLoader the API class loader + * @return {@code true} if this class loader was previously unknown, or {@code false} if it was already registered + */ + public boolean addLogApiClassLoader(ClassLoader apiClassLoader) { + return logApiClassLoaders.add(apiClassLoader); + } + + /** + * Remove a class loader from the known log APIs set. + * + * @param apiClassLoader the API class loader + * @return {@code true} if the class loader was removed, or {@code false} if it was not known to this selector + */ + public boolean removeLogApiClassLoader(ClassLoader apiClassLoader) { + return logApiClassLoaders.remove(apiClassLoader); + } + + /** + * Register a class loader with a log context. This method requires the {@code registerLogContext} + * {@link RuntimePermission}. + * + * @param classLoader the classloader + * @param logContext the log context + * @throws IllegalArgumentException if the classloader is already associated with a log context + */ + public void registerLogContext(ClassLoader classLoader, LogContext logContext) throws IllegalArgumentException { + if (contextMap.putIfAbsent(classLoader, logContext) != null) { + throw new IllegalArgumentException( + "ClassLoader instance is already registered to a log context (" + classLoader + ")"); + } + } + + /** + * Unregister a class loader/log context association. This method requires the {@code unregisterLogContext} + * {@link RuntimePermission}. + * + * @param classLoader the classloader + * @param logContext the log context + * @return {@code true} if the association exists and was removed, {@code false} otherwise + */ + public boolean unregisterLogContext(ClassLoader classLoader, LogContext logContext) { + return contextMap.remove(classLoader, logContext); + } +} diff --git a/logging/src/main/java/org/xbib/logging/ContextClassLoaderLogContextSelector.java b/logging/src/main/java/org/xbib/logging/ContextClassLoaderLogContextSelector.java new file mode 100644 index 0000000..21e5f59 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/ContextClassLoaderLogContextSelector.java @@ -0,0 +1,68 @@ +package org.xbib.logging; + +import java.util.concurrent.ConcurrentMap; +import org.xbib.logging.util.CopyOnWriteMap; +import static java.lang.Thread.currentThread; + +/** + * A log context selector which chooses a log context based on the thread context classloader. + */ +public final class ContextClassLoaderLogContextSelector implements LogContextSelector { + /** + * Construct a new instance. If no matching log context is found, the provided default selector is consulted. + * + * @param defaultSelector the selector to consult if no matching log context is found + */ + public ContextClassLoaderLogContextSelector(final LogContextSelector defaultSelector) { + this.defaultSelector = defaultSelector; + } + + /** + * Construct a new instance. If no matching log context is found, the system context is used. + */ + public ContextClassLoaderLogContextSelector() { + this(LogContext.DEFAULT_LOG_CONTEXT_SELECTOR); + } + + private final LogContextSelector defaultSelector; + + private final ConcurrentMap contextMap = new CopyOnWriteMap<>(); + + public LogContext getLogContext() { + ClassLoader cl = currentThread().getContextClassLoader(); + if (cl != null) { + final LogContext mappedContext = contextMap.get(cl); + if (mappedContext != null) { + return mappedContext; + } + } + return defaultSelector.getLogContext(); + } + + /** + * Register a class loader with a log context. This method requires the {@code registerLogContext} + * {@link RuntimePermission}. + * + * @param classLoader the classloader + * @param logContext the log context + * @throws IllegalArgumentException if the classloader is already associated with a log context + */ + public void registerLogContext(ClassLoader classLoader, LogContext logContext) throws IllegalArgumentException { + if (contextMap.putIfAbsent(classLoader, logContext) != null) { + throw new IllegalArgumentException( + "ClassLoader instance is already registered to a log context (" + classLoader + ")"); + } + } + + /** + * Unregister a class loader/log context association. This method requires the {@code unregisterLogContext} + * {@link RuntimePermission}. + * + * @param classLoader the classloader + * @param logContext the log context + * @return {@code true} if the association exists and was removed, {@code false} otherwise + */ + public boolean unregisterLogContext(ClassLoader classLoader, LogContext logContext) { + return contextMap.remove(classLoader, logContext); + } +} diff --git a/logging/src/main/java/org/xbib/logging/ExtErrorManager.java b/logging/src/main/java/org/xbib/logging/ExtErrorManager.java new file mode 100644 index 0000000..5f22668 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/ExtErrorManager.java @@ -0,0 +1,57 @@ +package org.xbib.logging; + +import java.util.logging.ErrorManager; + +/** + * An extended error manager, which contains additional useful utilities for error managers. + */ +public abstract class ExtErrorManager extends ErrorManager { + + public ExtErrorManager() { + } + + /** + * Get the name corresponding to the given error code. + * + * @param code the error code + * @return the corresponding name (not {@code null}) + */ + protected String nameForCode(int code) { + return switch (code) { + case CLOSE_FAILURE -> "CLOSE_FAILURE"; + case FLUSH_FAILURE -> "FLUSH_FAILURE"; + case FORMAT_FAILURE -> "FORMAT_FAILURE"; + case GENERIC_FAILURE -> "GENERIC_FAILURE"; + case OPEN_FAILURE -> "OPEN_FAILURE"; + case WRITE_FAILURE -> "WRITE_FAILURE"; + default -> "INVALID (" + code + ")"; + }; + } + + public void error(final String msg, final Exception ex, final int code) { + super.error(msg, ex, code); + } + + /** + * Convert the given error to a log record which can be published to handler(s) or stored. Care should + * be taken not to publish the log record to a logger that writes to the same handler that produced the error. + * + * @param msg the error message (possibly {@code null}) + * @param ex the error exception (possibly {@code null}) + * @param code the error code + * @return the log record (not {@code null}) + */ + protected ExtLogRecord errorToLogRecord(String msg, Exception ex, int code) { + final ExtLogRecord record = new ExtLogRecord(Level.ERROR, "Failed to publish log record (%s[%d]): %s", + ExtLogRecord.FormatStyle.PRINTF, getClass().getName()); + final String codeStr = nameForCode(code); + record.setParameters(new Object[]{ + codeStr, + code, + msg, + }); + record.setThrown(ex); + record.setLoggerName(""); + return record; + } +} diff --git a/logging/src/main/java/org/xbib/logging/ExtFormatter.java b/logging/src/main/java/org/xbib/logging/ExtFormatter.java new file mode 100644 index 0000000..318310c --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/ExtFormatter.java @@ -0,0 +1,196 @@ +package org.xbib.logging; + +import java.text.MessageFormat; +import java.util.MissingResourceException; +import java.util.Objects; +import java.util.ResourceBundle; +import java.util.logging.Formatter; +import java.util.logging.Handler; +import java.util.logging.LogRecord; + +/** + * A formatter which handles {@link ExtLogRecord ExtLogRecord} instances. + */ +public abstract class ExtFormatter extends Formatter { + /** + * Construct a new instance. + */ + public ExtFormatter() { + } + + /** + * Wrap an existing formatter with an {@link ExtFormatter}, optionally replacing message formatting with + * the default extended message formatting capability. + * + * @param formatter the formatter to wrap (must not be {@code null}) + * @param formatMessages {@code true} to replace message formatting, {@code false} to let the original formatter do it + * @return the extended formatter (not {@code null}) + */ + public static ExtFormatter wrap(Formatter formatter, boolean formatMessages) { + if (formatter instanceof ExtFormatter && !formatMessages) { + return (ExtFormatter) formatter; + } else { + return new WrappedFormatter(formatter, formatMessages); + } + } + + /** + * {@inheritDoc} + */ + public final String format(final LogRecord record) { + return format(ExtLogRecord.wrap(record)); + } + + /** + * Format a message using an extended log record. + * + * @param record the log record + * @return the formatted message + */ + public abstract String format(ExtLogRecord record); + + @Override + public String formatMessage(LogRecord record) { + final ResourceBundle bundle = record.getResourceBundle(); + String msg = record.getMessage(); + if (msg == null) { + return null; + } + if (bundle != null) { + try { + msg = bundle.getString(msg); + } catch (MissingResourceException ex) { + // ignore + } + } + final Object[] parameters = record.getParameters(); + if (parameters == null || parameters.length == 0) { + return formatMessageNone(record); + } + if (record instanceof ExtLogRecord extLogRecord) { + final ExtLogRecord.FormatStyle formatStyle = extLogRecord.getFormatStyle(); + if (formatStyle == ExtLogRecord.FormatStyle.PRINTF) { + return formatMessagePrintf(record); + } else if (formatStyle == ExtLogRecord.FormatStyle.NO_FORMAT) { + return formatMessageNone(record); + } + } + return msg.indexOf('{') >= 0 ? formatMessageLegacy(record) : formatMessageNone(record); + } + + /** + * Determines whether or not this formatter will require caller, source level, information when a log record is + * formatted. + * + * @return {@code true} if the formatter will need caller information, otherwise {@code false} + * @see LogRecord#getSourceClassName() + * @see ExtLogRecord#getSourceFileName() + * @see ExtLogRecord#getSourceLineNumber() + * @see LogRecord#getSourceMethodName() + */ + public boolean isCallerCalculationRequired() { + return true; + } + + /** + * Format the message text as if there are no parameters. The default implementation delegates to + * {@link LogRecord#getMessage() record.getMessage()}. + * + * @param record the record to format + * @return the formatted string + */ + protected String formatMessageNone(LogRecord record) { + return record.getMessage(); + } + + /** + * Format the message text as if there are no parameters. The default implementation delegates to + * {@link MessageFormat#format(String, Object[]) MessageFormat.format(record.getMessage(),record.getParameters())}. + * + * @param record the record to format + * @return the formatted string + */ + protected String formatMessageLegacy(LogRecord record) { + return MessageFormat.format(record.getMessage(), record.getParameters()); + } + + /** + * Format the message text as if there are no parameters. The default implementation delegates to + * {@link String#format(String, Object[]) String.format(record.getMessage(),record.getParameters())}. + * + * @param record the record to format + * @return the formatted string + */ + protected String formatMessagePrintf(LogRecord record) { + return String.format(record.getMessage(), record.getParameters()); + } + + static class WrappedFormatter extends ExtFormatter { + private final Formatter formatter; + private final boolean formatMessages; + + WrappedFormatter(Formatter formatter, boolean formatMessages) { + this.formatter = formatter; + this.formatMessages = formatMessages; + } + + @Override + public String format(ExtLogRecord record) { + return formatter.format(record); + } + + @Override + public String formatMessage(LogRecord record) { + return formatMessages ? super.formatMessage(record) : formatter.formatMessage(record); + } + + @Override + public String getHead(Handler h) { + return formatter.getHead(h); + } + + @Override + public String getTail(Handler h) { + return formatter.getTail(h); + } + } + + /** + * A base class for formatters which wrap other formatters. + */ + public abstract static class Delegating extends ExtFormatter { + /** + * The delegate formatter. + */ + protected final ExtFormatter delegate; + + /** + * Construct a new instance. + * + * @param delegate the delegate formatter (must not be {@code null}) + */ + public Delegating(final ExtFormatter delegate) { + this.delegate = Objects.requireNonNull(delegate, "delegate"); + } + + public String format(final ExtLogRecord record) { + return delegate.format(record); + } + + public String formatMessage(final LogRecord record) { + return delegate.formatMessage(record); + } + + public boolean isCallerCalculationRequired() { + return delegate.isCallerCalculationRequired(); + } + + public String getHead(final Handler h) { + return delegate.getHead(h); + } + + public String getTail(final Handler h) { + return delegate.getTail(h); + } + } +} \ No newline at end of file diff --git a/logging/src/main/java/org/xbib/logging/ExtHandler.java b/logging/src/main/java/org/xbib/logging/ExtHandler.java new file mode 100644 index 0000000..7139609 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/ExtHandler.java @@ -0,0 +1,494 @@ +package org.xbib.logging; + +import java.io.Flushable; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.concurrent.locks.ReentrantLock; +import java.util.logging.ErrorManager; +import java.util.logging.Filter; +import java.util.logging.Formatter; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import org.xbib.logging.errormanager.OnlyOnceErrorManager; +import org.xbib.logging.util.AtomicArray; + +/** + * An extended logger handler. Use this class as a base class for log handlers which require {@code ExtLogRecord} + * instances. + */ +public abstract class ExtHandler extends Handler implements AutoCloseable, Flushable { + + private static final ErrorManager DEFAULT_ERROR_MANAGER = new OnlyOnceErrorManager(); + + protected final ReentrantLock lock = new ReentrantLock(); + + // we keep our own copies of these fields so that they are protected with *our* lock: + private volatile Filter filter; + private volatile Formatter formatter; + private volatile Level level = Level.ALL; + private volatile ErrorManager errorManager; + // (skip `encoding` because we replace it with `charset` below) + + private volatile boolean autoFlush = true; + private volatile boolean enabled = true; + private volatile boolean closeChildren; + private volatile Charset charset = StandardCharsets.UTF_8; + + /** + * The sub-handlers for this handler. May only be updated using the {@link #handlersUpdater} atomic updater. The array + * instance should not be modified (treat as immutable). + */ + @SuppressWarnings("unused") + protected volatile Handler[] handlers; + + /** + * The atomic updater for the {@link #handlers} field. + */ + private static final AtomicArray handlersUpdater = + AtomicArray.create(AtomicReferenceFieldUpdater.newUpdater(ExtHandler.class, Handler[].class, "handlers"), Handler.class); + + /** + * Construct a new instance. + */ + protected ExtHandler() { + handlersUpdater.clear(this); + closeChildren = true; + errorManager = DEFAULT_ERROR_MANAGER; + } + + /** + * {@inheritDoc} + */ + public void publish(final LogRecord record) { + if (enabled && record != null && isLoggable(record)) { + doPublish(ExtLogRecord.wrap(record)); + } + } + + /** + * Publish an {@code ExtLogRecord}. + *

+ * The logging request was made initially to a Logger object, which initialized the LogRecord and forwarded it here. + *

+ * The {@code ExtHandler} is responsible for formatting the message, when and if necessary. The formatting should + * include localization. + * + * @param record the log record to publish + */ + public void publish(final ExtLogRecord record) { + if (enabled && record != null && isLoggable(record)) + try { + doPublish(record); + } catch (Exception e) { + reportError("Handler publication threw an exception", e, ErrorManager.WRITE_FAILURE); + } catch (Throwable ignored) { + } + } + + /** + * Publish a log record to each nested handler. + * + * @param record the log record to publish + */ + @SuppressWarnings("deprecation") // record.getFormattedMessage() + protected void publishToNestedHandlers(final ExtLogRecord record) { + if (record != null) { + LogRecord oldRecord = null; + for (Handler handler : getHandlers()) + try { + if (handler != null) { + if (handler instanceof ExtHandler || handler.getFormatter() instanceof ExtFormatter) { + handler.publish(record); + } else { + // old-style handlers generally don't know how to handle printf formatting + if (oldRecord == null) { + if (record.getFormatStyle() == ExtLogRecord.FormatStyle.PRINTF) { + // reformat it in a simple way, but only for legacy handler usage + oldRecord = new ExtLogRecord(record); + oldRecord.setMessage(record.getFormattedMessage()); + oldRecord.setParameters(null); + } else { + oldRecord = record; + } + } + handler.publish(oldRecord); + } + } + } catch (Exception e) { + reportError(handler, "Nested handler publication threw an exception", e, ErrorManager.WRITE_FAILURE); + } catch (Throwable ignored) { + } + } + } + + /** + * Do the actual work of publication; the record will have been filtered already. The default implementation + * does nothing except to flush if the {@code autoFlush} property is set to {@code true}; if this behavior is to be + * preserved in a subclass then this method should be called after the record is physically written. + * + * @param record the log record to publish + */ + protected void doPublish(final ExtLogRecord record) { + if (autoFlush) + flush(); + } + + /** + * Add a sub-handler to this handler. Some handler types do not utilize sub-handlers. + * + * @param handler the handler to add + */ + public void addHandler(Handler handler) { + if (handler == null) { + throw new NullPointerException("handler is null"); + } + handlersUpdater.add(this, handler); + } + + /** + * Remove a sub-handler from this handler. Some handler types do not utilize sub-handlers. + * + * @param handler the handler to remove + */ + public void removeHandler(Handler handler) { + if (handler == null) { + return; + } + handlersUpdater.remove(this, handler, true); + } + + /** + * Get a copy of the sub-handlers array. Since the returned value is a copy, it may be freely modified. + * + * @return a copy of the sub-handlers array + */ + public Handler[] getHandlers() { + final Handler[] handlers = this.handlers; + return handlers.length > 0 ? handlers.clone() : handlers; + } + + /** + * A convenience method to atomically get and clear all sub-handlers. + * + * @return the old sub-handler array + */ + public Handler[] clearHandlers() { + final Handler[] handlers = this.handlers; + handlersUpdater.clear(this); + return handlers.length > 0 ? handlers.clone() : handlers; + } + + /** + * A convenience method to atomically get and replace the sub-handler array. + * + * @param newHandlers the new sub-handlers + * @return the old sub-handler array + */ + public Handler[] setHandlers(final Handler[] newHandlers) { + if (newHandlers == null) { + throw new IllegalArgumentException("newHandlers is null"); + } + if (newHandlers.length == 0) { + return clearHandlers(); + } else { + final Handler[] handlers = handlersUpdater.getAndSet(this, newHandlers); + return handlers.length > 0 ? handlers.clone() : handlers; + } + } + + /** + * Determine if this handler will auto-flush. + * + * @return {@code true} if auto-flush is enabled + */ + public boolean isAutoFlush() { + return autoFlush; + } + + /** + * Change the autoflush setting for this handler. + * + * @param autoFlush {@code true} to automatically flush after each write; {@code false} otherwise + */ + public void setAutoFlush(final boolean autoFlush) { + this.autoFlush = autoFlush; + if (autoFlush) { + flush(); + } + } + + /** + * Enables or disables the handler based on the value passed in. + * + * @param enabled {@code true} to enable the handler or {@code false} to disable the handler. + */ + public final void setEnabled(final boolean enabled) { + this.enabled = enabled; + } + + /** + * Determine if the handler is enabled. + * + * @return {@code true} if the handler is enabled, otherwise {@code false}. + */ + public final boolean isEnabled() { + return enabled; + } + + /** + * Indicates whether or not children handlers should be closed when this handler is {@linkplain #close() closed}. + * + * @return {@code true} if the children handlers should be closed when this handler is closed, {@code false} if + * children handlers should not be closed when this handler is closed + */ + public boolean isCloseChildren() { + return closeChildren; + } + + /** + * Sets whether or not children handlers should be closed when this handler is {@linkplain #close() closed}. + * + * @param closeChildren {@code true} if all children handlers should be closed when this handler is closed, + * {@code false} if children handlers will not be closed when this handler + * is closed + */ + public void setCloseChildren(final boolean closeChildren) { + this.closeChildren = closeChildren; + } + + /** + * Flush all child handlers. + */ + @Override + public void flush() { + for (Handler handler : handlers) + try { + handler.flush(); + } catch (Exception ex) { + reportError("Failed to flush child handler", ex, ErrorManager.FLUSH_FAILURE); + } catch (Throwable ignored) { + } + } + + /** + * Close all child handlers. + */ + @Override + public void close() { + if (closeChildren) { + for (Handler handler : handlers) + try { + handler.close(); + } catch (Exception ex) { + reportError("Failed to close child handler", ex, ErrorManager.CLOSE_FAILURE); + } catch (Throwable ignored) { + } + } + } + + @Override + public void setFormatter(final Formatter newFormatter) { + Objects.requireNonNull(newFormatter); + lock.lock(); + try { + formatter = newFormatter; + } finally { + lock.unlock(); + } + } + + @Override + public Formatter getFormatter() { + return formatter; + } + + @Override + public void setFilter(final Filter newFilter) { + lock.lock(); + try { + filter = newFilter; + } finally { + lock.unlock(); + } + } + + @Override + public Filter getFilter() { + return filter; + } + + /** + * Set the handler's character set by name. This is roughly equivalent to calling {@link #setCharset(Charset)} with + * the results of {@link Charset#forName(String)}. + * + * @param encoding the name of the encoding + * @throws UnsupportedEncodingException if no character set could be found for the encoding name + */ + @Override + public void setEncoding(final String encoding) throws UnsupportedEncodingException { + if (encoding != null) { + try { + setCharset(Charset.forName(encoding)); + } catch (IllegalArgumentException e) { + final UnsupportedEncodingException e2 = new UnsupportedEncodingException( + "Unable to set encoding to \"" + encoding + "\""); + e2.initCause(e); + throw e2; + } + } else { + setCharset(StandardCharsets.UTF_8); + } + } + + /** + * Get the name of the {@linkplain #getCharset() handler's character set}. + * + * @return the handler character set name + */ + @Override + public String getEncoding() { + return getCharset().name(); + } + + /** + * Set the handler's character set. If not set, the handler's character set is initialized to the platform default + * character set. + * + * @param charset the character set (must not be {@code null}) + */ + public void setCharset(final Charset charset) { + setCharsetPrivate(charset); + } + + /** + * Set the handler's character set from within this handler. If not set, the handler's character set is initialized + * to the platform default character set. + * + * @param charset the character set (must not be {@code null}) + */ + protected void setCharsetPrivate(final Charset charset) { + Objects.requireNonNull(charset, "charset"); + lock.lock(); + try { + this.charset = charset; + } finally { + lock.unlock(); + } + } + + /** + * Get the handler's character set. + * + * @return the character set in use (not {@code null}) + */ + public Charset getCharset() { + return charset; + } + + @Override + public void setErrorManager(final ErrorManager em) { + Objects.requireNonNull(em); + lock.lock(); + try { + errorManager = em; + } finally { + lock.unlock(); + } + } + + @Override + public ErrorManager getErrorManager() { + return errorManager; + } + + @Override + public void setLevel(final Level newLevel) { + Objects.requireNonNull(newLevel); + lock.lock(); + try { + level = newLevel; + } finally { + lock.unlock(); + } + } + + @Override + public Level getLevel() { + return level; + } + + /** + * Indicates whether or not the {@linkplain #getFormatter() formatter} associated with this handler or a formatter + * from a {@linkplain #getHandlers() child handler} requires the caller to be calculated. + *

+ * Calculating the caller on a {@linkplain ExtLogRecord log record} can be an expensive operation. Some handlers + * may be required to copy some data from the log record, but may not need the caller information. If the + * {@linkplain #getFormatter() formatter} is a {@link ExtFormatter} the + * {@link ExtFormatter#isCallerCalculationRequired()} is used to determine if calculation of the caller is + * required. + *

+ * + * @return {@code true} if the caller should be calculated, otherwise {@code false} if it can be skipped + * @see LogRecord#getSourceClassName() + * @see ExtLogRecord#getSourceFileName() + * @see ExtLogRecord#getSourceLineNumber() + * @see LogRecord#getSourceMethodName() + */ + @SuppressWarnings("WeakerAccess") + public boolean isCallerCalculationRequired() { + Formatter formatter = getFormatter(); + if (formatterRequiresCallerCalculation(formatter)) { + return true; + } else + for (Handler handler : getHandlers()) { + if (handler instanceof ExtHandler) { + if (((ExtHandler) handler).isCallerCalculationRequired()) { + return true; + } + } else { + formatter = handler.getFormatter(); + if (formatterRequiresCallerCalculation(formatter)) { + return true; + } + } + } + return false; + } + + @Override + protected void reportError(String msg, Exception ex, int code) { + final ErrorManager errorManager = this.errorManager; + errorManager.error(msg, ex, code); + } + + /** + * Report an error using a handler's specific error manager, if any. + * + * @param handler the handler + * @param msg the error message + * @param ex the exception + * @param code the error code + */ + public static void reportError(Handler handler, String msg, Exception ex, int code) { + if (handler != null) { + ErrorManager errorManager = handler.getErrorManager(); + if (errorManager != null) + try { + errorManager.error(msg, ex, code); + } catch (Exception ex2) { + // use the same message as the JDK + System.err.println("Handler.reportError caught:"); + ex2.printStackTrace(); + } + } + } + + private static boolean formatterRequiresCallerCalculation(final Formatter formatter) { + return formatter != null + && (!(formatter instanceof ExtFormatter) || ((ExtFormatter) formatter).isCallerCalculationRequired()); + } +} diff --git a/logging/src/main/java/org/xbib/logging/ExtLogRecord.java b/logging/src/main/java/org/xbib/logging/ExtLogRecord.java new file mode 100644 index 0000000..d6d0d02 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/ExtLogRecord.java @@ -0,0 +1,615 @@ +package org.xbib.logging; + +import java.text.MessageFormat; +import java.util.Map; +import java.util.MissingResourceException; +import java.util.ResourceBundle; +import java.util.logging.LogRecord; +import org.xbib.logging.os.HostName; +import org.xbib.logging.os.Process; +import org.xbib.logging.util.FastCopyHashMap; +import org.xbib.logging.util.StackWalkerUtil; + +/** + * An extended log record, which includes additional information including MDC/NDC and correct + * caller location (even in the presence of a logging facade). + */ +@SuppressWarnings("serial") +public class ExtLogRecord extends LogRecord { + + /** + * The format style to use. + */ + public enum FormatStyle { + + /** + * Format the message using the {@link MessageFormat} parameter style. + */ + MESSAGE_FORMAT, + /** + * Format the message using the {@link java.util.Formatter} (also known as {@code printf()}) parameter style. + */ + PRINTF, + /** + * Do not format the message; parameters are ignored. + */ + NO_FORMAT, + } + + /** + * Construct a new instance. Grabs the current NDC immediately. MDC is deferred. + * + * @param level a logging level value + * @param msg the raw non-localized logging message (may be null) + * @param loggerClassName the name of the logger class + */ + public ExtLogRecord(final java.util.logging.Level level, final String msg, final String loggerClassName) { + this(level, msg, FormatStyle.MESSAGE_FORMAT, loggerClassName); + } + + /** + * Construct a new instance. Grabs the current NDC immediately. MDC is deferred. + * + * @param level a logging level value + * @param msg the raw non-localized logging message (may be null) + * @param formatStyle the parameter format style to use + * @param loggerClassName the name of the logger class + */ + public ExtLogRecord(final java.util.logging.Level level, final String msg, final FormatStyle formatStyle, + final String loggerClassName) { + super(level, msg); + this.formatStyle = formatStyle == null ? FormatStyle.MESSAGE_FORMAT : formatStyle; + this.loggerClassName = loggerClassName; + ndc = NDC.get(); + threadName = Thread.currentThread().getName(); + longThreadID = Thread.currentThread().threadId(); + hostName = HostName.getQualifiedHostName(); + processName = Process.getProcessName(); + processId = ProcessHandle.current().pid(); + } + + /** + * Make a copy of a log record. + * + * @param original the original + */ + public ExtLogRecord(final ExtLogRecord original) { + super(original.getLevel(), original.getMessage()); + // LogRecord fields + setLoggerName(original.getLoggerName()); + setInstant(original.getInstant()); + setParameters(original.getParameters()); + setResourceBundle(original.getResourceBundle()); + setResourceBundleName(original.getResourceBundleName()); + setSequenceNumber(original.getSequenceNumber()); + setThrown(original.getThrown()); + if (!original.calculateCaller) { + setSourceClassName(original.getSourceClassName()); + setSourceMethodName(original.getSourceMethodName()); + sourceFileName = original.sourceFileName; + sourceLineNumber = original.sourceLineNumber; + sourceModuleName = original.sourceModuleName; + sourceModuleVersion = original.sourceModuleVersion; + } + setLongThreadID(original.getLongThreadID()); + formatStyle = original.formatStyle; + marker = original.marker; + mdcCopy = original.mdcCopy; + ndc = original.ndc; + loggerClassName = original.loggerClassName; + threadName = original.threadName; + hostName = original.hostName; + processName = original.processName; + processId = original.processId; + } + + /** + * Wrap a JDK log record. If the target record is already an {@code ExtLogRecord}, it is simply returned. Otherwise + * a wrapper record is created and returned. + * + * @param rec the original record + * @return the wrapped record + */ + public static ExtLogRecord wrap(LogRecord rec) { + if (rec == null) { + return null; + } else if (rec instanceof ExtLogRecord) { + return (ExtLogRecord) rec; + } else { + return new WrappedExtLogRecord(rec); + } + } + + private final transient String loggerClassName; + private transient boolean calculateCaller = true; + + private String ndc; + private FormatStyle formatStyle; + private FastCopyHashMap mdcCopy; + private int sourceLineNumber = -1; + private String sourceFileName; + private String threadName; + private String hostName; + private String processName; + private long processId = -1; + private String sourceModuleName; + private String sourceModuleVersion; + private Object marker; + private long longThreadID; + + /** + * Disable caller calculation for this record. If the caller has already been calculated, leave it; otherwise + * set the caller to {@code "unknown"}. + */ + public void disableCallerCalculation() { + if (calculateCaller) { + setUnknownCaller(); + } + } + + /** + * Copy all fields and prepare this object to be passed to another thread. Calling this method + * more than once has no additional effect and will not incur extra copies. + */ + public void copyAll() { + copyMdc(); + calculateCaller(); + } + + /** + * Copy the MDC. Call this method before passing this log record to another thread. Calling this method + * more than once has no additional effect and will not incur extra copies. + */ + public void copyMdc() { + if (mdcCopy == null) { + mdcCopy = FastCopyHashMap.of(MDC.getMDCProvider().copyObject()); + } + } + + /** + * Get the value of an MDC property. + * + * @param key the property key + * @return the property value + */ + public String getMdc(String key) { + final Map mdcCopy = this.mdcCopy; + if (mdcCopy == null) { + return MDC.get(key); + } + final Object value = mdcCopy.get(key); + return value == null ? null : value.toString(); + } + + /** + * Get a copy of all the MDC properties for this log record. If the MDC has not yet been copied, this method will copy it. + * + * @return a copy of the MDC map + */ + public Map getMdcCopy() { + copyMdc(); + // Create a new map with string values + final FastCopyHashMap newMdc = new FastCopyHashMap(); + for (Map.Entry entry : mdcCopy.entrySet()) { + final String key = entry.getKey(); + final Object value = entry.getValue(); + newMdc.put(key, (value == null ? null : value.toString())); + } + return newMdc; + } + + /** + * Change an MDC value on this record. If the MDC has not yet been copied, this method will copy it. + * + * @param key the key to set + * @param value the value to set it to + * @return the old value, if any + */ + public String putMdc(String key, String value) { + copyMdc(); + final Object oldValue = mdcCopy.put(key, value); + return oldValue == null ? null : oldValue.toString(); + } + + /** + * Remove an MDC value on this record. If the MDC has not yet been copied, this method will copy it. + * + * @param key the key to remove + * @return the old value, if any + */ + public String removeMdc(String key) { + copyMdc(); + final Object oldValue = mdcCopy.remove(key); + return oldValue == null ? null : oldValue.toString(); + } + + /** + * Create a new MDC using a copy of the source map. + * + * @param sourceMap the source man, must not be {@code null} + */ + public void setMdc(Map sourceMap) { + final FastCopyHashMap newMdc = new FastCopyHashMap(); + for (Map.Entry entry : sourceMap.entrySet()) { + final Object key = entry.getKey(); + final Object value = entry.getValue(); + if (key != null && value != null) { + newMdc.put(key.toString(), value); + } + } + mdcCopy = newMdc; + } + + /** + * Get the NDC for this log record. + * + * @return the NDC + */ + public String getNdc() { + return ndc; + } + + /** + * Change the NDC for this log record. + * + * @param value the new NDC value + */ + public void setNdc(String value) { + ndc = value; + } + + /** + * Get the class name of the logger which created this record. + * + * @return the class name + */ + public String getLoggerClassName() { + return loggerClassName; + } + + /** + * Get the format style for the record. + * + * @return the format style + */ + public FormatStyle getFormatStyle() { + return formatStyle; + } + + /** + * Find the first stack frame below the call to the logger, and populate the log record with that information. + */ + private void calculateCaller() { + if (!calculateCaller) { + return; + } + calculateCaller = false; + StackWalkerUtil.calculateCaller(this); + } + + public void setUnknownCaller() { + setSourceClassName(null); + setSourceMethodName(null); + setSourceLineNumber(-1); + setSourceFileName(null); + setSourceModuleName(null); + setSourceModuleVersion(null); + } + + /** + * Get the source line number for this log record. + *

+ * Note that this line number is not verified and may be spoofed. This information may either have been + * provided as part of the logging call, or it may have been inferred automatically by the logging framework. In the + * latter case, the information may only be approximate and may in fact describe an earlier call on the stack frame. + * May be -1 if no information could be obtained. + * + * @return the source line number + */ + public int getSourceLineNumber() { + calculateCaller(); + return sourceLineNumber; + } + + /** + * Set the source line number for this log record. + * + * @param sourceLineNumber the source line number + */ + public void setSourceLineNumber(final int sourceLineNumber) { + calculateCaller = false; + this.sourceLineNumber = sourceLineNumber; + } + + /** + * Get the source file name for this log record. + *

+ * Note that this file name is not verified and may be spoofed. This information may either have been + * provided as part of the logging call, or it may have been inferred automatically by the logging framework. In the + * latter case, the information may only be approximate and may in fact describe an earlier call on the stack frame. + * May be {@code null} if no information could be obtained. + * + * @return the source file name + */ + public String getSourceFileName() { + calculateCaller(); + return sourceFileName; + } + + /** + * Set the source file name for this log record. + * + * @param sourceFileName the source file name + */ + public void setSourceFileName(final String sourceFileName) { + calculateCaller = false; + this.sourceFileName = sourceFileName; + } + + /** + * {@inheritDoc} + */ + public String getSourceClassName() { + calculateCaller(); + return super.getSourceClassName(); + } + + /** + * {@inheritDoc} + */ + public void setSourceClassName(final String sourceClassName) { + calculateCaller = false; + super.setSourceClassName(sourceClassName); + } + + /** + * {@inheritDoc} + */ + public String getSourceMethodName() { + calculateCaller(); + return super.getSourceMethodName(); + } + + /** + * {@inheritDoc} + */ + public void setSourceMethodName(final String sourceMethodName) { + calculateCaller = false; + super.setSourceMethodName(sourceMethodName); + } + + /** + * Get the name of the module that initiated the logging request, if known. + * + * @return the name of the module that initiated the logging request + */ + public String getSourceModuleName() { + calculateCaller(); + return sourceModuleName; + } + + /** + * Set the source module name of this record. + * + * @param sourceModuleName the source module name + */ + public void setSourceModuleName(final String sourceModuleName) { + calculateCaller = false; + this.sourceModuleName = sourceModuleName; + } + + /** + * Get the version of the module that initiated the logging request, if known. + * + * @return the version of the module that initiated the logging request + */ + public String getSourceModuleVersion() { + calculateCaller(); + return sourceModuleVersion; + } + + /** + * Set the source module version of this record. + * + * @param sourceModuleVersion the source module version + */ + public void setSourceModuleVersion(final String sourceModuleVersion) { + calculateCaller = false; + this.sourceModuleVersion = sourceModuleVersion; + } + + /** + * Get the fully formatted log record, with resources resolved and parameters applied. + * + * @return the formatted log record + * @deprecated The formatter should normally be used to format the message contents. + */ + @Deprecated + public String getFormattedMessage() { + final ResourceBundle bundle = getResourceBundle(); + String msg = getMessage(); + if (msg == null) + return null; + if (bundle != null) { + try { + msg = bundle.getString(msg); + } catch (MissingResourceException ex) { + // ignore + } + } + final Object[] parameters = getParameters(); + if (parameters == null || parameters.length == 0) { + return msg; + } + return switch (formatStyle) { + case PRINTF -> String.format(msg, parameters); + case MESSAGE_FORMAT -> msg.indexOf('{') >= 0 ? MessageFormat.format(msg, parameters) : msg; + default -> msg; + }; + } + + /** + * Get the resource key, if any. If the log message is not localized, then the key is {@code null}. + * + * @return the resource key + */ + public String getResourceKey() { + final String msg = getMessage(); + if (msg == null) + return null; + if (getResourceBundleName() == null && getResourceBundle() == null) + return null; + return msg; + } + + /** + * Get the thread name of this logging event. + * + * @return the thread name + */ + public String getThreadName() { + return threadName; + } + + /** + * Set the thread name of this logging event. + * + * @param threadName the thread name + */ + public void setThreadName(final String threadName) { + this.threadName = threadName; + } + + /** + * Get the host name of the record, if known. + * + * @return the host name of the record, if known + */ + public String getHostName() { + return hostName; + } + + /** + * Set the host name of the record. + * + * @param hostName the host name of the record + */ + public void setHostName(final String hostName) { + this.hostName = hostName; + } + + /** + * Get the process name of the record, if known. + * + * @return the process name of the record, if known + */ + public String getProcessName() { + return processName; + } + + /** + * Set the process name of the record. + * + * @param processName the process name of the record + */ + public void setProcessName(final String processName) { + this.processName = processName; + } + + /** + * Get the process ID of the record, if known. + * + * @return the process ID of the record, or -1 if not known + */ + public long getProcessId() { + return processId; + } + + /** + * Set the process ID of the record. + * + * @param processId the process ID of the record + */ + public void setProcessId(final long processId) { + this.processId = processId; + } + + /** + * Set the raw message. Any cached formatted message is discarded. The parameter format is set to be + * {@link MessageFormat}-style. + * + * @param message the new raw message + */ + public void setMessage(final String message) { + setMessage(message, FormatStyle.MESSAGE_FORMAT); + } + + /** + * Set the raw message. Any cached formatted message is discarded. The parameter format is set according to the + * given argument. + * + * @param message the new raw message + * @param formatStyle the format style to use + */ + public void setMessage(final String message, final FormatStyle formatStyle) { + this.formatStyle = formatStyle == null ? FormatStyle.MESSAGE_FORMAT : formatStyle; + super.setMessage(message); + } + + /** + * Set the parameters to the log message. Any cached formatted message is discarded. + * + * @param parameters the log message parameters. (may be null) + */ + public void setParameters(final Object[] parameters) { + super.setParameters(parameters); + } + + /** + * Set the localization resource bundle. Any cached formatted message is discarded. + * + * @param bundle localization bundle (may be null) + */ + public void setResourceBundle(final ResourceBundle bundle) { + super.setResourceBundle(bundle); + } + + /** + * Set the localization resource bundle name. Any cached formatted message is discarded. + * + * @param name localization bundle name (may be null) + */ + public void setResourceBundleName(final String name) { + super.setResourceBundleName(name); + } + + /** + * Set the marker for this event. Markers are used mostly by SLF4J and Log4j. + */ + public void setMarker(Object marker) { + this.marker = marker; + } + + public Object getMarker() { + return marker; + } + + @Deprecated + @SuppressWarnings("deprecation") + @Override + public void setThreadID(final int threadID) { + super.setThreadID(threadID); + this.longThreadID = threadID; + } + + @Override + public long getLongThreadID() { + return longThreadID; + } + + @Override + public ExtLogRecord setLongThreadID(final long id) { + this.longThreadID = id; + return this; + } +} diff --git a/logging/src/main/java/org/xbib/logging/Level.java b/logging/src/main/java/org/xbib/logging/Level.java new file mode 100644 index 0000000..763c2a3 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/Level.java @@ -0,0 +1,23 @@ +package org.xbib.logging; + +/** + * Log4j-like levels. + */ +@SuppressWarnings("serial") +public final class Level extends java.util.logging.Level { + + private Level(final String name, final int value) { + super(name, value); + } + + private Level(final String name, final int value, final String resourceBundleName) { + super(name, value, resourceBundleName); + } + + public static final Level FATAL = new Level("FATAL", 1100); + public static final Level ERROR = new Level("ERROR", 1000); + public static final Level WARN = new Level("WARN", 900); + public static final Level INFO = new Level("INFO", 800); + public static final Level DEBUG = new Level("DEBUG", 500); + public static final Level TRACE = new Level("TRACE", 400); +} diff --git a/logging/src/main/java/org/xbib/logging/LogContext.java b/logging/src/main/java/org/xbib/logging/LogContext.java new file mode 100644 index 0000000..916a3b2 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/LogContext.java @@ -0,0 +1,557 @@ +package org.xbib.logging; + +import java.io.IOException; +import java.lang.invoke.ConstantBootstraps; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.util.Collection; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.ReentrantLock; +import java.util.logging.Level; +import org.xbib.logging.ref.Reference; +import org.xbib.logging.ref.References; +import org.xbib.logging.util.CopyOnWriteMap; +import org.xbib.logging.util.CopyOnWriteWeakMap; + + +/** + * A logging context, for producing isolated logging environments. + */ +@SuppressWarnings({"unused", "WeakerAccess"}) +public final class LogContext implements AutoCloseable { + + private static final LogContext SYSTEM_CONTEXT = new LogContext(false, discoverDefaultInitializer()); + + private static LogContextInitializer discoverDefaultInitializer() { + return discoverDefaultInitializer0(); + } + + private static LogContextInitializer discoverDefaultInitializer0() { + // allow exceptions to bubble up, otherwise logging won't work with no indication as to why + final ServiceLoader loader = + ServiceLoader.load(LogContextInitializer.class, LogContext.class.getClassLoader()); + final Iterator iterator = loader.iterator(); + return iterator.hasNext() ? iterator.next() : LogContextInitializer.DEFAULT; + } + + private final LoggerNode rootLogger; + + private final boolean strong; + + private final LogContextInitializer initializer; + + private final Set pinnedSet; + + // must not be final because of VarHandle + private Map, Object> attachments; + + private static final VarHandle attachmentHandle = + ConstantBootstraps.fieldVarHandle(MethodHandles.lookup(), "attachments", VarHandle.class, LogContext.class, Map.class); + + /** + * This lazy holder class is required to prevent a problem due to a LogContext instance being constructed + * before the class init is complete. + */ + private static final class LazyHolder { + private static final HashMap> INITIAL_LEVEL_MAP; + + private LazyHolder() { + } + + private static void addStrong(Map> map, Level level) { + map.put(level.getName().toUpperCase(), References.create(Reference.Type.STRONG, level, null)); + } + + static { + final HashMap> map = new HashMap>(); + addStrong(map, Level.OFF); + addStrong(map, Level.ALL); + addStrong(map, Level.SEVERE); + addStrong(map, Level.WARNING); + addStrong(map, Level.CONFIG); + addStrong(map, Level.INFO); + addStrong(map, Level.FINE); + addStrong(map, Level.FINER); + addStrong(map, Level.FINEST); + + addStrong(map, org.xbib.logging.Level.FATAL); + addStrong(map, org.xbib.logging.Level.ERROR); + addStrong(map, org.xbib.logging.Level.WARN); + addStrong(map, org.xbib.logging.Level.INFO); + addStrong(map, org.xbib.logging.Level.DEBUG); + addStrong(map, org.xbib.logging.Level.TRACE); + + INITIAL_LEVEL_MAP = map; + } + } + + private final AtomicReference>> levelMapReference; + + private final Set closeHandlers; + + /** + * This lock is taken any time a change is made which affects multiple nodes in the hierarchy. + */ + final ReentrantLock treeLock = new ReentrantLock(); + + LogContext(final boolean strong, LogContextInitializer initializer) { + this.initializer = initializer; + this.strong = strong || initializer.useStrongReferences(); + levelMapReference = new AtomicReference>>(LazyHolder.INITIAL_LEVEL_MAP); + rootLogger = new LoggerNode(this); + closeHandlers = new LinkedHashSet<>(); + attachments = Map.of(); + pinnedSet = this.strong ? Set.of() : ConcurrentHashMap.newKeySet(); + } + + /** + * Create a new log context. + * {@link RuntimePermission RuntimePermission} to invoke this method. + * + * @param strong {@code true} if the context should use strong references, {@code false} to use (default) weak + * references for automatic logger GC + * @return a new log context + */ + public static LogContext create(boolean strong) { + return create(strong, LogContextInitializer.DEFAULT); + } + + /** + * Create a new log context. + * {@link RuntimePermission RuntimePermission} to invoke this method. + * + * @param strong {@code true} if the context should use strong references, {@code false} to use (default) weak + * references for automatic logger GC + * @param initializer the log context initializer to use (must not be {@code null}) + * @return a new log context + */ + public static LogContext create(boolean strong, LogContextInitializer initializer) { + return new LogContext(strong, initializer); + } + + /** + * Create a new log context. + * {@link RuntimePermission RuntimePermission} to invoke this method. + * + * @return a new log context + */ + public static LogContext create() { + return create(false); + } + + /** + * Create a new log context. + * {@link RuntimePermission RuntimePermission} to invoke this method. + * + * @param initializer the log context initializer to use (must not be {@code null}) + * @return a new log context + */ + public static LogContext create(LogContextInitializer initializer) { + return create(false, initializer); + } + + // Attachment mgmt + + /** + * Get the attachment value for a given key, or {@code null} if there is no such attachment. + * Log context attachments are placed on the root logger and can also be accessed there. + * + * @param key the key + * @param the attachment value type + * @return the attachment, or {@code null} if there is none for this key + */ + @SuppressWarnings("unchecked") + public V getAttachment(Logger.AttachmentKey key) { + return (V) attachments.get(key); + } + + /** + * Attach an object to this log context under a given key. + * A strong reference is maintained to the key and value for as long as this log context exists. + * Log context attachments are placed on the root logger and can also be accessed there. + * + * @param key the attachment key + * @param value the attachment value + * @param the attachment value type + * @return the old attachment, if there was one + * @throws IllegalArgumentException if the attachment cannot be added because the maximum has been reached + */ + @SuppressWarnings("unchecked") + public V attach(Logger.AttachmentKey key, V value) { + Map, Object> oldAttachments; + Map, Object> newAttachments; + V old; + do { + oldAttachments = attachments; + newAttachments = new HashMap<>(oldAttachments); + old = (V) newAttachments.put(key, value); + } while (!attachmentHandle.compareAndSet(this, oldAttachments, Map.copyOf(newAttachments))); + return old; + } + + /** + * Attach an object to this log context under a given key, if such an attachment does not already exist. + * A strong reference is maintained to the key and value for as long as this log context exists. + * Log context attachments are placed on the root logger and can also be accessed there. + * + * @param key the attachment key + * @param value the attachment value + * @param the attachment value type + * @return the current attachment, if there is one, or {@code null} if the value was successfully attached + * @throws IllegalArgumentException if the attachment cannot be added because the maximum has been reached + */ + @SuppressWarnings("unchecked") + public V attachIfAbsent(Logger.AttachmentKey key, V value) { + Map, Object> oldAttachments; + Map, Object> newAttachments; + do { + oldAttachments = attachments; + if (oldAttachments.containsKey(key)) { + return (V) oldAttachments.get(key); + } + newAttachments = new HashMap<>(oldAttachments); + newAttachments.put(key, value); + } while (!attachmentHandle.compareAndSet(this, oldAttachments, Map.copyOf(newAttachments))); + return null; + } + + /** + * Remove an attachment. + * Log context attachments are placed on the root logger and can also be accessed there. + * + * @param key the attachment key + * @param the attachment value type + * @return the old value, or {@code null} if there was none + */ + @SuppressWarnings("unchecked") + public V detach(Logger.AttachmentKey key) { + Map, Object> oldAttachments; + Map, Object> newAttachments; + V result; + do { + oldAttachments = attachments; + result = (V) oldAttachments.get(key); + if (result == null) { + return null; + } + final int size = oldAttachments.size(); + if (size == 1) { + // special case - the new map is empty + newAttachments = Map.of(); + } else { + newAttachments = new HashMap<>(oldAttachments); + newAttachments.remove(key); + } + } while (!attachmentHandle.compareAndSet(this, oldAttachments, Map.copyOf(newAttachments))); + return result; + } + + /** + * Get a logger with the given name from this logging context. + * + * @param name the logger name + * @return the logger instance + * @see java.util.logging.LogManager#getLogger(String) + */ + public Logger getLogger(String name) { + return rootLogger.getOrCreate(name).createLogger(); + } + + /** + * Get a logger with the given name from this logging context, if a logger node exists at that location. + * + * @param name the logger name + * @return the logger instance, or {@code null} if no such logger node exists + */ + public Logger getLoggerIfExists(String name) { + final LoggerNode node = rootLogger.getIfExists(name); + return node == null ? null : node.createLogger(); + } + + /** + * Get a logger attachment for a logger name, if it exists. + * + * @param loggerName the logger name + * @param key the attachment key + * @param the attachment value type + * @return the attachment or {@code null} if the logger or the attachment does not exist + */ + public V getAttachment(String loggerName, Logger.AttachmentKey key) { + final LoggerNode node = rootLogger.getIfExists(loggerName); + if (node == null) + return null; + return node.getAttachment(key); + } + + /** + * Get the level for a name. + * + * @param name the name + * @return the level + * @throws IllegalArgumentException if the name is not known + */ + public Level getLevelForName(String name) throws IllegalArgumentException { + if (name != null) { + final Map> map = levelMapReference.get(); + final Reference levelRef = map.get(name); + if (levelRef != null) { + final Level level = levelRef.get(); + if (level != null) { + return level; + } + } + } + throw new IllegalArgumentException("Unknown level \"" + name + "\""); + } + + /** + * Register a level instance with this log context. The level can then be looked up by name. Only a weak + * reference to the level instance will be kept. Any previous level registration for the given level's name + * will be overwritten. + * + * @param level the level to register + */ + public void registerLevel(Level level) { + registerLevel(level, false); + } + + /** + * Register a level instance with this log context. The level can then be looked up by name. Any previous level + * registration for the given level's name will be overwritten. + * + * @param level the level to register + * @param strong {@code true} to strongly reference the level, or {@code false} to weakly reference it + */ + public void registerLevel(Level level, boolean strong) { + for (; ; ) { + final Map> oldLevelMap = levelMapReference.get(); + final Map> newLevelMap = new HashMap<>(oldLevelMap.size()); + for (Map.Entry> entry : oldLevelMap.entrySet()) { + final String name = entry.getKey(); + final Reference levelRef = entry.getValue(); + if (levelRef.get() != null) { + newLevelMap.put(name, levelRef); + } + } + newLevelMap.put(level.getName(), + References.create(strong ? Reference.Type.STRONG : Reference.Type.WEAK, level, null)); + if (levelMapReference.compareAndSet(oldLevelMap, newLevelMap)) { + return; + } + } + } + + /** + * Unregister a previously registered level. Log levels that are not registered may still be used, they just will + * not be findable by name. + * + * @param level the level to unregister + */ + public void unregisterLevel(Level level) { + for (; ; ) { + final Map> oldLevelMap = levelMapReference.get(); + final Reference oldRef = oldLevelMap.get(level.getName()); + if (oldRef == null || oldRef.get() != level) { + // not registered, or the registration expired naturally + return; + } + final Map> newLevelMap = new HashMap<>(oldLevelMap.size()); + for (Map.Entry> entry : oldLevelMap.entrySet()) { + final String name = entry.getKey(); + final Reference levelRef = entry.getValue(); + final Level oldLevel = levelRef.get(); + if (oldLevel != null && oldLevel != level) { + newLevelMap.put(name, levelRef); + } + } + newLevelMap.put(level.getName(), References.create(Reference.Type.WEAK, level, null)); + if (levelMapReference.compareAndSet(oldLevelMap, newLevelMap)) { + return; + } + } + } + + /** + * Get the system log context. + * + * @return the system log context + */ + public static LogContext getSystemLogContext() { + return SYSTEM_CONTEXT; + } + + /** + * The default log context selector, which always returns the system log context. + */ + public static final LogContextSelector DEFAULT_LOG_CONTEXT_SELECTOR = new LogContextSelector() { + public LogContext getLogContext() { + return SYSTEM_CONTEXT; + } + }; + + private static volatile LogContextSelector logContextSelector = DEFAULT_LOG_CONTEXT_SELECTOR; + + /** + * Get the currently active log context. + * + * @return the currently active log context + */ + public static LogContext getLogContext() { + return logContextSelector.getLogContext(); + } + + /** + * Set a new log context selector. + * {@code "setLogContextSelector"} + * {@link RuntimePermission RuntimePermission} to invoke this method. + * + * @param newSelector the new selector. + */ + public static void setLogContextSelector(LogContextSelector newSelector) { + if (newSelector == null) { + throw new NullPointerException("newSelector is null"); + } + logContextSelector = newSelector; + } + + /** + * Returns the currently set log context selector. + * + * @return the log context selector + */ + public static LogContextSelector getLogContextSelector() { + return logContextSelector; + } + + @Override + public void close() throws IOException { + treeLock.lock(); + try { + // First we want to close all loggers + recursivelyClose(rootLogger); + // Next process the close handlers associated with this log context + for (AutoCloseable handler : closeHandlers) { + try { + handler.close(); + } catch (Exception e) { + throw new IOException(e); + } + } + final Map oldAttachments = (Map) attachmentHandle.getAndSet(this, Map.of()); + for (Object value : oldAttachments.values()) { + if (value instanceof AutoCloseable) { + try { + ((AutoCloseable) value).close(); + } catch (Exception ignore) { + } + } + } + if (!pinnedSet.isEmpty()) { + pinnedSet.clear(); + } + } finally { + treeLock.unlock(); + } + } + + /** + * Returns an enumeration of the logger names that have been created. This does not return names of loggers that + * may have been garbage collected. Logger names added after the enumeration has been retrieved may also be added to + * the enumeration. + * + * @return an enumeration of the logger names + * @see java.util.logging.LogManager#getLoggerNames() + */ + public Enumeration getLoggerNames() { + return rootLogger.getLoggerNames(); + } + + /** + * Adds a handler invoked during the {@linkplain #close() close} of this log context. The close handlers will be + * invoked in the order they are added. + *

+ * The loggers associated with this context will always be closed. + *

+ * + * @param closeHandler the close handler to use + */ + public void addCloseHandler(final AutoCloseable closeHandler) { + treeLock.lock(); + try { + closeHandlers.add(closeHandler); + } finally { + treeLock.unlock(); + } + } + + /** + * Gets the current close handlers associated with this log context. + * + * @return the current close handlers + */ + public Set getCloseHandlers() { + treeLock.lock(); + try { + return new LinkedHashSet<>(closeHandlers); + } finally { + treeLock.unlock(); + } + } + + /** + * Clears any current close handlers associated with log context, then adds the handlers to be invoked during + * the {@linkplain #close() close} of this log context. The close handlers will be invoked in the order they are + * added. + *

+ * The loggers associated with this context will always be closed. + *

+ * + * @param closeHandlers the close handlers to use + */ + public void setCloseHandlers(final Collection closeHandlers) { + treeLock.lock(); + try { + this.closeHandlers.clear(); + this.closeHandlers.addAll(closeHandlers); + } finally { + treeLock.unlock(); + } + } + + public LoggerNode getRootLoggerNode() { + return rootLogger; + } + + public ConcurrentMap createChildMap() { + return strong ? new CopyOnWriteMap<>() : new CopyOnWriteWeakMap<>(); + } + + public boolean pin(LoggerNode node) { + return !strong && pinnedSet.add(node); + } + + public boolean unpin(LoggerNode node) { + return !strong && pinnedSet.remove(node); + } + + public LogContextInitializer getInitializer() { + return initializer; + } + + private void recursivelyClose(final LoggerNode loggerNode) { + assert treeLock.isHeldByCurrentThread(); + for (LoggerNode child : loggerNode.getChildren()) { + recursivelyClose(child); + } + loggerNode.close(); + } +} diff --git a/logging/src/main/java/org/xbib/logging/LogContextConfigurator.java b/logging/src/main/java/org/xbib/logging/LogContextConfigurator.java new file mode 100644 index 0000000..9f5725d --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/LogContextConfigurator.java @@ -0,0 +1,25 @@ +package org.xbib.logging; + +import java.io.InputStream; + +/** + * A configurator for a log context. A log context configurator should set up all the log categories, + * handlers, formatters, filters, attachments, and other constructs as specified by the configuration. + */ +public interface LogContextConfigurator { + /** + * Configure the given log context according to this configurator's policy. If a configuration stream was + * provided, that is passed in to this method to be used or ignored. The stream should remain open after + * this method is called. + * + * @param logContext the log context to configure (not {@code null}) + * @param inputStream the input stream that was requested to be used, or {@code null} if none was provided + */ + void configure(LogContext logContext, InputStream inputStream); + + /** + * A constant representing an empty configuration. The configurator does nothing. + */ + LogContextConfigurator EMPTY = (logContext, inputStream) -> { + }; +} diff --git a/logging/src/main/java/org/xbib/logging/LogContextConfiguratorFactory.java b/logging/src/main/java/org/xbib/logging/LogContextConfiguratorFactory.java new file mode 100644 index 0000000..a7848f8 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/LogContextConfiguratorFactory.java @@ -0,0 +1,25 @@ +package org.xbib.logging; + +/** + * Used to create a {@link LogContextConfigurator}. The {@linkplain #priority() priority} is used to determine which + * factory should be used. The lowest priority factory is used. If two factories have the same priority the second + * factory will not be used. The order of loading the factories for determining priority is done via the + * {@link java.util.ServiceLoader#load(Class, ClassLoader)}. + */ +public interface LogContextConfiguratorFactory { + + /** + * Creates the {@link LogContextConfigurator}. + * + * @return the log context configurator + */ + LogContextConfigurator create(); + + /** + * The priority for the factory which is used to determine which factory should be used to create a + * {@link LogContextConfigurator}. The lowest priority factory will be used. + * + * @return the priority for this factory + */ + int priority(); +} diff --git a/logging/src/main/java/org/xbib/logging/LogContextInitializer.java b/logging/src/main/java/org/xbib/logging/LogContextInitializer.java new file mode 100644 index 0000000..16d5a8a --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/LogContextInitializer.java @@ -0,0 +1,77 @@ +package org.xbib.logging; + +import java.util.logging.Handler; +import java.util.logging.Level; + +/** + * An initializer for log contexts. The initializer provides initial values for log instances within the context + * for properties like levels, handlers, and so on. + *

+ * The initial log context will be configured using a context initializer that is located via the + * {@linkplain java.util.ServiceLoader JDK SPI mechanism}. + *

+ * This interface is intended to be forward-extensible. If new methods are added, they will include a default implementation. + * Implementations of this interface should accommodate the possibility of new methods being added; as a matter of convention, + * such methods should begin with the prefix {@code getInitial}, which will minimize the possibility of conflict. + */ +public interface LogContextInitializer { + /** + * An array containing zero handlers. + */ + Handler[] NO_HANDLERS = new Handler[0]; + + /** + * The default log context initializer, which is used when none is specified. This instance uses only + * default implementations for all the given methods. + */ + LogContextInitializer DEFAULT = new LogContextInitializer() { + }; + + /** + * Get the initial level for the given logger name. If the initializer returns a {@code null} level for the + * root logger, then a level of {@link org.xbib.logging.Level#INFO INFO} will be used. + *

+ * The default implementation returns {@code null}. + * + * @param loggerName the logger name (must not be {@code null}) + * @return the level to use, or {@code null} to inherit the level from the parent + */ + default Level getInitialLevel(String loggerName) { + return null; + } + + /** + * Get the minimum (most verbose) level allowed for the given logger name. If the initializer returns a + * {@code null} level for the root logger, then a level of {@link Level#ALL ALL} will be used. + *

+ * The default implementation returns {@code null}. + * + * @param loggerName the logger name (must not be {@code null}) + * @return the level to use, or {@code null} to inherit the level from the parent + */ + default Level getMinimumLevel(String loggerName) { + return null; + } + + /** + * Get the initial set of handlers to configure for the given logger name. + *

+ * The default implementation returns {@link #NO_HANDLERS}. A value of {@code null} is considered + * to be the same as {@link #NO_HANDLERS}. + * + * @param loggerName the logger name (must not be {@code null}) + * @return the handlers to use (should not be {@code null}) + */ + default Handler[] getInitialHandlers(String loggerName) { + return NO_HANDLERS; + } + + /** + * Establish whether strong references should be used for logger nodes. + * + * @return {@code true} to use strong references, or {@code false} to use weak references + */ + default boolean useStrongReferences() { + return false; + } +} diff --git a/logging/src/main/java/org/xbib/logging/LogContextSelector.java b/logging/src/main/java/org/xbib/logging/LogContextSelector.java new file mode 100644 index 0000000..788a62b --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/LogContextSelector.java @@ -0,0 +1,15 @@ +package org.xbib.logging; + +/** + * A mechanism for determining what the current log context is. This method is used primarily when constructing + * new loggers to determine what context the constructed logger should be installed into. + */ +public interface LogContextSelector { + + /** + * Get the current log context. + * + * @return the current log context + */ + LogContext getLogContext(); +} diff --git a/logging/src/main/java/org/xbib/logging/LogManager.java b/logging/src/main/java/org/xbib/logging/LogManager.java new file mode 100644 index 0000000..946e138 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/LogManager.java @@ -0,0 +1,186 @@ +package org.xbib.logging; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; +import java.util.ServiceConfigurationError; +import java.util.ServiceLoader; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiFunction; +import java.util.function.Function; +import org.xbib.logging.configuration.PropertyLogContextConfigurator; +import org.xbib.logging.util.StandardOutputStreams; + +/** + * Simplified log manager. Designed to work around the (many) design flaws of the JDK platform log manager. + */ +public final class LogManager extends java.util.logging.LogManager { + + static { + try { + // Ensure the StandardOutputStreams are initialized early to capture the current System.out and System.err. + Class.forName(StandardOutputStreams.class.getName()); + } catch (ClassNotFoundException ignore) { + // ignore + } + } + + /** + * Construct a new logmanager instance. Attempts to plug a known memory leak in {@link java.util.logging.Level} as + * well. + */ + public LogManager() { + } + + // Configuration + + private final AtomicReference configuratorRef = new AtomicReference<>(); + + /** + * Configure the system log context initially. + */ + public void readConfiguration() { + doConfigure(null); + } + + /** + * Configure the system log context initially withe given input stream. + * + * @param inputStream ignored + */ + public void readConfiguration(InputStream inputStream) { + doConfigure(inputStream); + } + + private void doConfigure(InputStream inputStream) { + final AtomicReference configuratorRef = this.configuratorRef; + LogContextConfigurator configurator = configuratorRef.get(); + if (configurator == null) { + synchronized (configuratorRef) { + configurator = configuratorRef.get(); + if (configurator == null) { + int best = Integer.MAX_VALUE; + LogContextConfiguratorFactory factory = null; + final ServiceLoader serviceLoader = + ServiceLoader.load(LogContextConfiguratorFactory.class, LogManager.class.getClassLoader()); + final Iterator iterator = serviceLoader.iterator(); + List problems = null; + for (; ; ) + try { + if (!iterator.hasNext()) + break; + final LogContextConfiguratorFactory f = iterator.next(); + if (f.priority() < best || factory == null) { + best = f.priority(); + factory = f; + } + } catch (Throwable t) { + if (problems == null) + problems = new ArrayList<>(4); + problems.add(t); + } + configurator = factory == null ? null : factory.create(); + if (configurator == null) { + if (problems == null) { + configuratorRef.set(configurator = new PropertyLogContextConfigurator()); + } else { + final ServiceConfigurationError e = new ServiceConfigurationError( + "Failed to configure log configurator service"); + for (Throwable problem : problems) { + e.addSuppressed(problem); + } + throw e; + } + } + } + } + } + configurator.configure(LogContext.getSystemLogContext(), inputStream); + } + + /** + * Does nothing. + * + * @param mapper not used + */ + public void updateConfiguration(final Function> mapper) throws IOException { + // no operation the configuration API should be used + } + + /** + * Does nothing. + * + * @param ins not used + * @param mapper not used + */ + public void updateConfiguration(final InputStream ins, final Function> mapper) + throws IOException { + // no operation the configuration API should be used + } + + /** + * Configuration listeners are not currently supported. + * + * @param listener not used + * @return this log manager + */ + public java.util.logging.LogManager addConfigurationListener(final Runnable listener) { + // no operation + return this; + } + + /** + * Configuration listeners are not currently supported. + * + * @param listener not used + */ + public void removeConfigurationListener(final Runnable listener) { + // no operation + } + + /** + * Does nothing. Properties are not supported. + * + * @param name ignored + * @return {@code null} + */ + public String getProperty(String name) { + // no properties + return null; + } + + /** + * Does nothing. This method only causes trouble. + */ + public void reset() { + // no operation! + } + + @Override + public Enumeration getLoggerNames() { + return LogContext.getLogContext().getLoggerNames(); + } + + /** + * Do nothing. Loggers are only added/acquired via {@link #getLogger(String)}. + * + * @param logger ignored + * @return {@code false} + */ + public boolean addLogger(java.util.logging.Logger logger) { + return false; + } + + /** + * Get or create a logger with the given name. + * + * @param name the logger name + * @return the corresponding logger + */ + public Logger getLogger(String name) { + return LogContext.getLogContext().getLogger(name); + } +} diff --git a/logging/src/main/java/org/xbib/logging/Logger.java b/logging/src/main/java/org/xbib/logging/Logger.java new file mode 100644 index 0000000..f9de1b7 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/Logger.java @@ -0,0 +1,987 @@ +package org.xbib.logging; + +import java.util.Arrays; +import java.util.Enumeration; +import java.util.Locale; +import java.util.ResourceBundle; +import java.util.function.Supplier; +import java.util.logging.Filter; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; + +/** + * An actual logger instance. This is the end-user interface into the logging system. + */ +public final class Logger extends java.util.logging.Logger { + + private static final ResourceBundle TOMBSTONE = new ResourceBundle() { + @Override + protected Object handleGetObject(final String key) { + return null; + } + + @Override + public Enumeration getKeys() { + return null; + } + }; + + /** + * The named logger tree node. + */ + private final LoggerNode loggerNode; + + /** + * The resource bundle for this logger. + */ + private volatile ResourceBundle resourceBundle; + + private static final String LOGGER_CLASS_NAME = Logger.class.getName(); + + /** + * Static logger factory method which returns a LogManager logger. + * + * @param name the logger name + * @return the logger + */ + public static Logger getLogger(final String name) { + return LogContext.getLogContext().getLogger(name); + } + + /** + * Static logger factory method which returns a LogManager logger. + * + * @param name the logger name + * @param bundleName the bundle name + * @return the logger + */ + public static Logger getLogger(final String name, final String bundleName) { + final Logger logger = LogContext.getLogContext().getLogger(name); + logger.resourceBundle = ResourceBundle.getBundle(bundleName, Locale.getDefault(), Logger.class.getClassLoader()); + return logger; + } + + /** + * Construct a new instance of an actual logger. + * + * @param loggerNode the node in the named logger tree + * @param name the fully-qualified name of this node + */ + Logger(final LoggerNode loggerNode, final String name) { + // Don't set up the bundle in the parent... + super(name, null); + // We have to propagate our level to an internal data structure in the superclass + super.setLevel(loggerNode.getLevel()); + this.loggerNode = loggerNode; + } + + // Filter mgmt + + /** + * {@inheritDoc} + */ + public void setFilter(Filter filter) { + loggerNode.setFilter(filter); + } + + /** + * {@inheritDoc} + */ + public Filter getFilter() { + return loggerNode.getFilter(); + } + + // Level mgmt + + /** + * {@inheritDoc} This implementation grabs a lock, so that only one thread may update the log level of any + * logger at a time, in order to allow readers to never block (though there is a window where retrieving the + * log level reflects an older effective level than the actual level). + */ + public void setLevel(Level newLevel) { + // We have to propagate our level to an internal data structure in the superclass + super.setLevel(newLevel); + loggerNode.setLevel(newLevel); + } + + /** + * Set the log level by name. Uses the parent logging context's name registry; otherwise behaves + * identically to {@link #setLevel(Level)}. + * + * @param newLevelName the name of the level to set + */ + public void setLevelName(String newLevelName) { + setLevel(loggerNode.getContext().getLevelForName(newLevelName)); + } + + /** + * Get the effective numerical log level, inherited from the parent. + * + * @return the effective level + */ + public int getEffectiveLevel() { + return loggerNode.getEffectiveLevel(); + } + + /** + * {@inheritDoc} + */ + public Level getLevel() { + return loggerNode.getLevel(); + } + + /** + * {@inheritDoc} + */ + public boolean isLoggable(Level level) { + return loggerNode.isLoggableLevel(level.intValue()); + } + + // Attachment mgmt + + /** + * Get the attachment value for a given key, or {@code null} if there is no such attachment. + * + * @param key the key + * @param the attachment value type + * @return the attachment, or {@code null} if there is none for this key + */ + @SuppressWarnings({"unchecked"}) + public V getAttachment(AttachmentKey key) { + return loggerNode.getAttachment(key); + } + + /** + * Attach an object to this logger under a given key. + * A strong reference is maintained to the key and value for as long as this logger exists. + * + * @param key the attachment key + * @param value the attachment value + * @param the attachment value type + * @return the old attachment, if there was one + * @throws IllegalArgumentException if the attachment cannot be added because the maximum has been reached + */ + public V attach(AttachmentKey key, V value) { + return loggerNode.attach(key, value); + } + + /** + * Attach an object to this logger under a given key, if such an attachment does not already exist. + * A strong reference is maintained to the key and value for as long as this logger exists. + * + * @param key the attachment key + * @param value the attachment value + * @param the attachment value type + * @return the current attachment, if there is one, or {@code null} if the value was successfully attached + * @throws IllegalArgumentException if the attachment cannot be added because the maximum has been reached + */ + @SuppressWarnings({"unchecked"}) + public V attachIfAbsent(AttachmentKey key, V value) { + return loggerNode.attachIfAbsent(key, value); + } + + /** + * Remove an attachment. + * + * @param key the attachment key + * @param the attachment value type + * @return the old value, or {@code null} if there was none + */ + @SuppressWarnings({"unchecked"}) + public V detach(AttachmentKey key) { + return loggerNode.detach(key); + } + + // Handler mgmt + + /** + * {@inheritDoc} + */ + public void addHandler(Handler handler) { + if (handler == null) { + throw new NullPointerException("handler is null"); + } + loggerNode.addHandler(handler); + } + + /** + * {@inheritDoc} + */ + public void removeHandler(Handler handler) { + if (handler == null) { + return; + } + loggerNode.removeHandler(handler); + } + + /** + * {@inheritDoc} + */ + public Handler[] getHandlers() { + final Handler[] handlers = loggerNode.getHandlers(); + return handlers.length > 0 ? handlers.clone() : handlers; + } + + /** + * A convenience method to atomically replace the handler list for this logger. + * + * @param handlers the new handlers + */ + public void setHandlers(final Handler[] handlers) { + final Handler[] safeHandlers = handlers.clone(); + for (Handler handler : safeHandlers) { + if (handler == null) { + throw new IllegalArgumentException("A handler is null"); + } + } + loggerNode.setHandlers(safeHandlers); + } + + /** + * Atomically get and set the handler list for this logger. + * + * @param handlers the new handler set + * @return the old handler set + */ + public Handler[] getAndSetHandlers(final Handler[] handlers) { + final Handler[] safeHandlers = handlers.clone(); + for (Handler handler : safeHandlers) { + if (handler == null) { + throw new IllegalArgumentException("A handler is null"); + } + } + return loggerNode.setHandlers(safeHandlers); + } + + /** + * Atomically compare and set the handler list for this logger. + * + * @param expected the expected list of handlers + * @param newHandlers the replacement list of handlers + * @return {@code true} if the handler list was updated or {@code false} if the current handlers did not match the expected + * handlers list + */ + public boolean compareAndSetHandlers(final Handler[] expected, final Handler[] newHandlers) { + final Handler[] safeExpectedHandlers = expected.clone(); + final Handler[] safeNewHandlers = newHandlers.clone(); + for (Handler handler : safeNewHandlers) { + if (handler == null) { + throw new IllegalArgumentException("A handler is null"); + } + } + Handler[] oldHandlers; + do { + oldHandlers = loggerNode.getHandlers(); + if (!Arrays.equals(oldHandlers, safeExpectedHandlers)) { + return false; + } + } while (!loggerNode.compareAndSetHandlers(oldHandlers, safeNewHandlers)); + return true; + } + + /** + * A convenience method to atomically get and clear all handlers. + */ + public Handler[] clearHandlers() { + return loggerNode.clearHandlers(); + } + + /** + * {@inheritDoc} + */ + public void setUseParentHandlers(boolean useParentHandlers) { + loggerNode.setUseParentHandlers(useParentHandlers); + } + + /** + * {@inheritDoc} + */ + public boolean getUseParentHandlers() { + return loggerNode.getUseParentHandlers(); + } + + /** + * Specify whether or not filters should be inherited from parent loggers. + *

+ * Setting this value to {@code false} has the same behaviour as {@linkplain java.util.logging.Logger}. + *

+ * + * @param useParentFilter {@code true} to inherit a parents filter, otherwise {@code false} + */ + public void setUseParentFilters(final boolean useParentFilter) { + loggerNode.setUseParentFilters(useParentFilter); + } + + /** + * Indicates whether or not this logger inherits filters from it's parent logger. + * + * @return {@code true} if filters are inherited, otherwise {@code false} + */ + public boolean getUseParentFilters() { + return loggerNode.getUseParentFilters(); + } + + // Parent/child + + /** + * {@inheritDoc} + */ + public Logger getParent() { + final LoggerNode parentNode = loggerNode.getParent(); + return parentNode == null ? null : parentNode.createLogger(); + } + + /** + * Not allowed. This method may never be called. + */ + public void setParent(java.util.logging.Logger parent) { + throw new UnsupportedOperationException(); + } + + /** + * Get the log context to which this logger belongs. + * + * @return the log context + */ + public LogContext getLogContext() { + return loggerNode.getContext(); + } + + // Logger + + static final int OFF_INT = Level.OFF.intValue(); + + static final int SEVERE_INT = Level.SEVERE.intValue(); + static final int WARNING_INT = Level.WARNING.intValue(); + static final int INFO_INT = Level.INFO.intValue(); + static final int CONFIG_INT = Level.CONFIG.intValue(); + static final int FINE_INT = Level.FINE.intValue(); + static final int FINER_INT = Level.FINER.intValue(); + static final int FINEST_INT = Level.FINEST.intValue(); + + /** + * {@inheritDoc} + */ + public void log(LogRecord record) { + if (!loggerNode.isLoggableLevel(record.getLevel().intValue())) { + return; + } + logRaw(record); + } + + /** + * {@inheritDoc} + */ + public void entering(final String sourceClass, final String sourceMethod) { + if (!loggerNode.isLoggableLevel(FINER_INT)) { + return; + } + final ExtLogRecord rec = new ExtLogRecord(Level.FINER, "ENTRY", LOGGER_CLASS_NAME); + rec.setSourceClassName(sourceClass); + rec.setSourceMethodName(sourceMethod); + logRaw(rec); + } + + /** + * {@inheritDoc} + */ + public void entering(final String sourceClass, final String sourceMethod, final Object param1) { + if (!loggerNode.isLoggableLevel(FINER_INT)) { + return; + } + final ExtLogRecord rec = new ExtLogRecord(Level.FINER, "ENTRY {0}", LOGGER_CLASS_NAME); + rec.setSourceClassName(sourceClass); + rec.setSourceMethodName(sourceMethod); + rec.setParameters(new Object[]{param1}); + logRaw(rec); + } + + /** + * {@inheritDoc} + */ + public void entering(final String sourceClass, final String sourceMethod, final Object[] params) { + if (!loggerNode.isLoggableLevel(FINER_INT)) { + return; + } + final StringBuilder builder = new StringBuilder("ENTRY"); + if (params != null) + for (int i = 0; i < params.length; i++) { + builder.append(" {").append(i).append('}'); + } + final ExtLogRecord rec = new ExtLogRecord(Level.FINER, builder.toString(), LOGGER_CLASS_NAME); + rec.setSourceClassName(sourceClass); + rec.setSourceMethodName(sourceMethod); + if (params != null) + rec.setParameters(params); + logRaw(rec); + } + + /** + * {@inheritDoc} + */ + public void exiting(final String sourceClass, final String sourceMethod) { + if (!loggerNode.isLoggableLevel(FINER_INT)) { + return; + } + final ExtLogRecord rec = new ExtLogRecord(Level.FINER, "RETURN", LOGGER_CLASS_NAME); + rec.setSourceClassName(sourceClass); + rec.setSourceMethodName(sourceMethod); + logRaw(rec); + } + + /** + * {@inheritDoc} + */ + public void exiting(final String sourceClass, final String sourceMethod, final Object result) { + if (!loggerNode.isLoggableLevel(FINER_INT)) { + return; + } + final ExtLogRecord rec = new ExtLogRecord(Level.FINER, "RETURN {0}", LOGGER_CLASS_NAME); + rec.setSourceClassName(sourceClass); + rec.setSourceMethodName(sourceMethod); + rec.setParameters(new Object[]{result}); + logRaw(rec); + } + + /** + * {@inheritDoc} + */ + public void throwing(final String sourceClass, final String sourceMethod, final Throwable thrown) { + if (!loggerNode.isLoggableLevel(FINER_INT)) { + return; + } + final ExtLogRecord rec = new ExtLogRecord(Level.FINER, "THROW", LOGGER_CLASS_NAME); + rec.setSourceClassName(sourceClass); + rec.setSourceMethodName(sourceMethod); + rec.setThrown(thrown); + logRaw(rec); + } + + /** + * {@inheritDoc} + */ + public void severe(final String msg) { + if (!loggerNode.isLoggableLevel(SEVERE_INT)) { + return; + } + final ExtLogRecord rec = new ExtLogRecord(Level.SEVERE, msg, LOGGER_CLASS_NAME); + logRaw(rec); + } + + @Override + public void severe(final Supplier msgSupplier) { + if (!loggerNode.isLoggableLevel(SEVERE_INT)) { + return; + } + final ExtLogRecord rec = new ExtLogRecord(Level.SEVERE, msgSupplier.get(), LOGGER_CLASS_NAME); + logRaw(rec); + } + + /** + * {@inheritDoc} + */ + public void warning(final String msg) { + if (!loggerNode.isLoggableLevel(WARNING_INT)) { + return; + } + final ExtLogRecord rec = new ExtLogRecord(Level.WARNING, msg, LOGGER_CLASS_NAME); + logRaw(rec); + } + + @Override + public void warning(final Supplier msgSupplier) { + if (!loggerNode.isLoggableLevel(WARNING_INT)) { + return; + } + final ExtLogRecord rec = new ExtLogRecord(Level.WARNING, msgSupplier.get(), LOGGER_CLASS_NAME); + logRaw(rec); + } + + /** + * {@inheritDoc} + */ + public void info(final String msg) { + if (!loggerNode.isLoggableLevel(INFO_INT)) { + return; + } + final ExtLogRecord rec = new ExtLogRecord(Level.INFO, msg, LOGGER_CLASS_NAME); + logRaw(rec); + } + + @Override + public void info(final Supplier msgSupplier) { + if (!loggerNode.isLoggableLevel(INFO_INT)) { + return; + } + final ExtLogRecord rec = new ExtLogRecord(Level.INFO, msgSupplier.get(), LOGGER_CLASS_NAME); + logRaw(rec); + } + + /** + * {@inheritDoc} + */ + public void config(final String msg) { + if (!loggerNode.isLoggableLevel(CONFIG_INT)) { + return; + } + final ExtLogRecord rec = new ExtLogRecord(Level.CONFIG, msg, LOGGER_CLASS_NAME); + logRaw(rec); + } + + @Override + public void config(final Supplier msgSupplier) { + if (!loggerNode.isLoggableLevel(CONFIG_INT)) { + return; + } + final ExtLogRecord rec = new ExtLogRecord(Level.CONFIG, msgSupplier.get(), LOGGER_CLASS_NAME); + logRaw(rec); + } + + /** + * {@inheritDoc} + */ + public void fine(final String msg) { + if (!loggerNode.isLoggableLevel(FINE_INT)) { + return; + } + final ExtLogRecord rec = new ExtLogRecord(Level.FINE, msg, LOGGER_CLASS_NAME); + logRaw(rec); + } + + @Override + public void fine(final Supplier msgSupplier) { + if (!loggerNode.isLoggableLevel(FINE_INT)) { + return; + } + final ExtLogRecord rec = new ExtLogRecord(Level.FINE, msgSupplier.get(), LOGGER_CLASS_NAME); + logRaw(rec); + } + + /** + * {@inheritDoc} + */ + public void finer(final String msg) { + if (!loggerNode.isLoggableLevel(FINER_INT)) { + return; + } + final ExtLogRecord rec = new ExtLogRecord(Level.FINER, msg, LOGGER_CLASS_NAME); + logRaw(rec); + } + + @Override + public void finer(final Supplier msgSupplier) { + if (!loggerNode.isLoggableLevel(FINER_INT)) { + return; + } + final ExtLogRecord rec = new ExtLogRecord(Level.FINER, msgSupplier.get(), LOGGER_CLASS_NAME); + logRaw(rec); + } + + /** + * {@inheritDoc} + */ + public void finest(final String msg) { + if (!loggerNode.isLoggableLevel(FINEST_INT)) { + return; + } + final ExtLogRecord rec = new ExtLogRecord(Level.FINEST, msg, LOGGER_CLASS_NAME); + logRaw(rec); + } + + @Override + public void finest(final Supplier msgSupplier) { + if (!loggerNode.isLoggableLevel(FINEST_INT)) { + return; + } + final ExtLogRecord rec = new ExtLogRecord(Level.FINEST, msgSupplier.get(), LOGGER_CLASS_NAME); + logRaw(rec); + } + + /** + * {@inheritDoc} + */ + public void log(final Level level, final String msg) { + if (!loggerNode.isLoggableLevel(level.intValue())) { + return; + } + final ExtLogRecord rec = new ExtLogRecord(level, msg, LOGGER_CLASS_NAME); + logRaw(rec); + } + + @Override + public void log(final Level level, final Supplier msgSupplier) { + if (!loggerNode.isLoggableLevel(level.intValue())) { + return; + } + final ExtLogRecord rec = new ExtLogRecord(level, msgSupplier.get(), LOGGER_CLASS_NAME); + logRaw(rec); + } + + /** + * {@inheritDoc} + */ + public void log(final Level level, final String msg, final Object param1) { + if (!loggerNode.isLoggableLevel(level.intValue())) { + return; + } + final ExtLogRecord rec = new ExtLogRecord(level, msg, LOGGER_CLASS_NAME); + rec.setParameters(new Object[]{param1}); + logRaw(rec); + } + + /** + * {@inheritDoc} + */ + public void log(final Level level, final String msg, final Object[] params) { + if (!loggerNode.isLoggableLevel(level.intValue())) { + return; + } + final ExtLogRecord rec = new ExtLogRecord(level, msg, LOGGER_CLASS_NAME); + if (params != null) + rec.setParameters(params); + logRaw(rec); + } + + /** + * {@inheritDoc} + */ + public void log(final Level level, final String msg, final Throwable thrown) { + if (!loggerNode.isLoggableLevel(level.intValue())) { + return; + } + final ExtLogRecord rec = new ExtLogRecord(level, msg, LOGGER_CLASS_NAME); + rec.setThrown(thrown); + logRaw(rec); + } + + @Override + public void log(final Level level, final Throwable thrown, final Supplier msgSupplier) { + if (!loggerNode.isLoggableLevel(level.intValue())) { + return; + } + final ExtLogRecord rec = new ExtLogRecord(level, msgSupplier.get(), LOGGER_CLASS_NAME); + rec.setThrown(thrown); + logRaw(rec); + } + + /** + * {@inheritDoc} + */ + public void logp(final Level level, final String sourceClass, final String sourceMethod, final String msg) { + if (!loggerNode.isLoggableLevel(level.intValue())) { + return; + } + final ExtLogRecord rec = new ExtLogRecord(level, msg, LOGGER_CLASS_NAME); + rec.setSourceClassName(sourceClass); + rec.setSourceMethodName(sourceMethod); + logRaw(rec); + } + + @Override + public void logp(final Level level, final String sourceClass, final String sourceMethod, + final Supplier msgSupplier) { + if (!loggerNode.isLoggableLevel(level.intValue())) { + return; + } + final ExtLogRecord rec = new ExtLogRecord(level, msgSupplier.get(), LOGGER_CLASS_NAME); + rec.setSourceClassName(sourceClass); + rec.setSourceMethodName(sourceMethod); + logRaw(rec); + } + + /** + * {@inheritDoc} + */ + public void logp(final Level level, final String sourceClass, final String sourceMethod, final String msg, + final Object param1) { + if (!loggerNode.isLoggableLevel(level.intValue())) { + return; + } + final ExtLogRecord rec = new ExtLogRecord(level, msg, LOGGER_CLASS_NAME); + rec.setSourceClassName(sourceClass); + rec.setSourceMethodName(sourceMethod); + rec.setParameters(new Object[]{param1}); + logRaw(rec); + } + + /** + * {@inheritDoc} + */ + public void logp(final Level level, final String sourceClass, final String sourceMethod, final String msg, + final Object[] params) { + if (!loggerNode.isLoggableLevel(level.intValue())) { + return; + } + final ExtLogRecord rec = new ExtLogRecord(level, msg, LOGGER_CLASS_NAME); + rec.setSourceClassName(sourceClass); + rec.setSourceMethodName(sourceMethod); + if (params != null) + rec.setParameters(params); + logRaw(rec); + } + + /** + * {@inheritDoc} + */ + public void logp(final Level level, final String sourceClass, final String sourceMethod, final String msg, + final Throwable thrown) { + if (!loggerNode.isLoggableLevel(level.intValue())) { + return; + } + final ExtLogRecord rec = new ExtLogRecord(level, msg, LOGGER_CLASS_NAME); + rec.setSourceClassName(sourceClass); + rec.setSourceMethodName(sourceMethod); + rec.setThrown(thrown); + logRaw(rec); + } + + @Override + public void logp(final Level level, final String sourceClass, final String sourceMethod, final Throwable thrown, + final Supplier msgSupplier) { + if (!loggerNode.isLoggableLevel(level.intValue())) { + return; + } + final ExtLogRecord rec = new ExtLogRecord(level, msgSupplier.get(), LOGGER_CLASS_NAME); + rec.setSourceClassName(sourceClass); + rec.setSourceMethodName(sourceMethod); + rec.setThrown(thrown); + logRaw(rec); + } + + /** + * {@inheritDoc} + */ + @Deprecated(since = "3.0", forRemoval = true) + public void logrb(final Level level, final String sourceClass, final String sourceMethod, final String bundleName, + final String msg) { + if (!loggerNode.isLoggableLevel(level.intValue())) { + return; + } + // No local check is needed here as this will delegate to log(LogRecord) + super.logrb(level, sourceClass, sourceMethod, bundleName, msg); + } + + /** + * {@inheritDoc} + */ + @Deprecated(since = "3.0", forRemoval = true) + public void logrb(final Level level, final String sourceClass, final String sourceMethod, final String bundleName, + final String msg, final Object param1) { + if (!loggerNode.isLoggableLevel(level.intValue())) { + return; + } + // No local check is needed here as this will delegate to log(LogRecord) + super.logrb(level, sourceClass, sourceMethod, bundleName, msg, param1); + } + + /** + * {@inheritDoc} + */ + @Deprecated(since = "3.0", forRemoval = true) + public void logrb(final Level level, final String sourceClass, final String sourceMethod, final String bundleName, + final String msg, final Object[] params) { + if (!loggerNode.isLoggableLevel(level.intValue())) { + return; + } + // No local check is needed here as this will delegate to log(LogRecord) + super.logrb(level, sourceClass, sourceMethod, bundleName, msg, params); + } + + /** + * {@inheritDoc} + */ + @Deprecated(since = "3.0", forRemoval = true) + public void logrb(final Level level, final String sourceClass, final String sourceMethod, final String bundleName, + final String msg, final Throwable thrown) { + if (!loggerNode.isLoggableLevel(level.intValue())) { + return; + } + // No local check is needed here as this will delegate to log(LogRecord) + super.logrb(level, sourceClass, sourceMethod, bundleName, msg, thrown); + } + + @Override + public void logrb(final Level level, final String sourceClass, final String sourceMethod, final ResourceBundle bundle, + final String msg, final Object... params) { + if (!loggerNode.isLoggableLevel(level.intValue())) { + return; + } + // No local check is needed here as this will delegate to log(LogRecord) + super.logrb(level, sourceClass, sourceMethod, bundle, msg, params); + } + + @Override + public void logrb(final Level level, final ResourceBundle bundle, final String msg, final Object... params) { + if (!loggerNode.isLoggableLevel(level.intValue())) { + return; + } + // No local check is needed here as this will delegate to log(LogRecord) + super.logrb(level, bundle, msg, params); + } + + @Override + public void logrb(final Level level, final String sourceClass, final String sourceMethod, final ResourceBundle bundle, + final String msg, final Throwable thrown) { + if (!loggerNode.isLoggableLevel(level.intValue())) { + return; + } + // No local check is needed here as this will delegate to log(LogRecord) + super.logrb(level, sourceClass, sourceMethod, bundle, msg, thrown); + } + + @Override + public void logrb(final Level level, final ResourceBundle bundle, final String msg, final Throwable thrown) { + if (!loggerNode.isLoggableLevel(level.intValue())) { + return; + } + // No local check is needed here as this will delegate to log(LogRecord) + super.logrb(level, bundle, msg, thrown); + } + // alternate SPI hooks + + /** + * SPI interface method to log a message at a given level, with a specific resource bundle. + * + * @param fqcn the fully qualified class name of the first logger class + * @param level the level to log at + * @param message the message + * @param bundleName the resource bundle name + * @param style the message format style + * @param params the log parameters + * @param t the throwable, if any + */ + public void log(final String fqcn, final Level level, final String message, final String bundleName, + final ExtLogRecord.FormatStyle style, final Object[] params, final Throwable t) { + if (level == null || fqcn == null || message == null + || !loggerNode.isLoggableLevel(level.intValue())) { + return; + } + final ExtLogRecord rec = new ExtLogRecord(level, message, style, fqcn); + rec.setResourceBundleName(bundleName); + rec.setParameters(params); + rec.setThrown(t); + logRaw(rec); + } + + /** + * SPI interface method to log a message at a given level. + * + * @param fqcn the fully qualified class name of the first logger class + * @param level the level to log at + * @param message the message + * @param style the message format style + * @param params the log parameters + * @param t the throwable, if any + */ + public void log(final String fqcn, final Level level, final String message, final ExtLogRecord.FormatStyle style, + final Object[] params, final Throwable t) { + if (level == null || fqcn == null || message == null + || !loggerNode.isLoggableLevel(level.intValue())) { + return; + } + final ExtLogRecord rec = new ExtLogRecord(level, message, style, fqcn); + rec.setParameters(params); + rec.setThrown(t); + logRaw(rec); + } + + /** + * SPI interface method to log a message at a given level. + * + * @param fqcn the fully qualified class name of the first logger class + * @param level the level to log at + * @param message the message + * @param t the throwable, if any + */ + public void log(final String fqcn, final Level level, final String message, final Throwable t) { + log(fqcn, level, message, ExtLogRecord.FormatStyle.MESSAGE_FORMAT, null, t); + } + + /** + * Do the logging with no level checks (they've already been done). + * + * @param record the extended log record + */ + public void logRaw(final ExtLogRecord record) { + record.setLoggerName(getName()); + final ResourceBundle bundle = getResourceBundle(); + if (bundle != null) { + record.setResourceBundleName(bundle.getBaseBundleName()); + record.setResourceBundle(bundle); + } + try { + if (!loggerNode.isLoggable(record)) { + return; + } + } catch (VirtualMachineError e) { + throw e; + } catch (Throwable t) { + // todo - error handler + // treat an errored filter as "pass" (I guess?) + } + loggerNode.publish(record); + } + + /** + * Set the resource bundle for this logger. + * + * @param resourceBundle the resource bundle (must not be {@code null}) + */ + @Override + public void setResourceBundle(ResourceBundle resourceBundle) { + super.setResourceBundle(resourceBundle); + synchronized (this) { + this.resourceBundle = resourceBundle; + } + } + + /** + * Get the resource bundle for this logger. + * + * @return the resource bundle, or {@code null} if none is configured for this logger + */ + @Override + public ResourceBundle getResourceBundle() { + if (resourceBundle == null) { + synchronized (this) { + if (resourceBundle == null) { + resourceBundle = super.getResourceBundle(); + if (resourceBundle == null) { + resourceBundle = TOMBSTONE; + } + } + } + } + return resourceBundle == TOMBSTONE ? null : resourceBundle; + } + + /** + * Do the logging with no level checks (they've already been done). Creates an extended log record if the + * provided record is not one. + * + * @param record the log record + */ + public void logRaw(final LogRecord record) { + logRaw(ExtLogRecord.wrap(record)); + } + + /** + * An attachment key instance. + * + * @param the attachment value type + */ + @SuppressWarnings({"UnusedDeclaration"}) + public static final class AttachmentKey { + + /** + * Construct a new instance. + */ + public AttachmentKey() { + } + } + + public String toString() { + return "Logger '" + getName() + "' in context " + loggerNode.getContext(); + } +} diff --git a/logging/src/main/java/org/xbib/logging/LoggerFinder.java b/logging/src/main/java/org/xbib/logging/LoggerFinder.java new file mode 100644 index 0000000..35902de --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/LoggerFinder.java @@ -0,0 +1,123 @@ +package org.xbib.logging; + +import java.util.EnumMap; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; + +/** + * Implements the {@code System.LoggerFinder}. It will make an attempt to set the {@code java.util.logging.manager} + * system property before a logger is accessed. + */ +public class LoggerFinder extends System.LoggerFinder { + + private static final Map LEVELS = new EnumMap<>(System.Logger.Level.class); + + private static final AtomicBoolean LOGGED = new AtomicBoolean(false); + + private static volatile boolean PROPERTY_SET = false; + + static { + LEVELS.put(System.Logger.Level.ALL, Level.ALL); + LEVELS.put(System.Logger.Level.TRACE, Level.TRACE); + LEVELS.put(System.Logger.Level.DEBUG, Level.DEBUG); + LEVELS.put(System.Logger.Level.INFO, Level.INFO); + LEVELS.put(System.Logger.Level.WARNING, Level.WARN); + LEVELS.put(System.Logger.Level.ERROR, Level.ERROR); + LEVELS.put(System.Logger.Level.OFF, Level.OFF); + } + + public LoggerFinder() { + super(); + } + + @Override + public System.Logger getLogger(final String name, final Module module) { + if (!PROPERTY_SET) { + synchronized (this) { + if (!PROPERTY_SET) { + if (System.getProperty("java.util.logging.manager") == null) { + System.setProperty("java.util.logging.manager", "org.xbib.logging.LogManager"); + } + } + PROPERTY_SET = true; + } + } + final java.util.logging.Logger logger = java.util.logging.Logger.getLogger(name); + if (!logger.getClass().getName().equals("org.xbib.logging.Logger")) { + if (LOGGED.compareAndSet(false, true)) { + logger.log(Level.ERROR, + "The LogManager accessed before the \"java.util.logging.manager\" system property was set to \"org.xbib.loggingr.LogManager\". Results may be unexpected."); + } + } + return new SystemLogger(logger); + } + + private static class SystemLogger implements System.Logger { + private static final String LOGGER_CLASS_NAME = SystemLogger.class.getName(); + private final java.util.logging.Logger delegate; + + private SystemLogger(final java.util.logging.Logger delegate) { + this.delegate = delegate; + } + + @Override + public String getName() { + return delegate.getName(); + } + + @Override + public boolean isLoggable(final Level level) { + return delegate.isLoggable(LEVELS.getOrDefault(level, java.util.logging.Level.INFO)); + } + + public void log(final Level level, final String msg) { + log(level, null, msg, (Object[]) null); + } + + public void log(final Level level, final Supplier msgSupplier) { + if (isLoggable(level)) { + log(level, null, msgSupplier.get(), (Object[]) null); + } + } + + public void log(final Level level, final Object obj) { + if (isLoggable(level)) { + this.log(level, null, obj.toString(), (Object[]) null); + } + } + + public void log(final Level level, final String msg, final Throwable thrown) { + this.log(level, null, msg, thrown); + } + + public void log(final Level level, final Supplier msgSupplier, final Throwable thrown) { + if (isLoggable(level)) { + this.log(level, null, msgSupplier.get(), thrown); + } + } + + public void log(final Level level, final String format, final Object... params) { + this.log(level, null, format, params); + } + + @Override + public void log(final Level level, final ResourceBundle bundle, final String msg, final Throwable thrown) { + final ExtLogRecord record = new ExtLogRecord(LEVELS.getOrDefault(level, java.util.logging.Level.INFO), msg, + LOGGER_CLASS_NAME); + record.setThrown(thrown); + record.setResourceBundle(bundle); + delegate.log(record); + } + + @Override + public void log(final Level level, final ResourceBundle bundle, final String format, final Object... params) { + final ExtLogRecord record = new ExtLogRecord(LEVELS.getOrDefault(level, java.util.logging.Level.INFO), format, + ExtLogRecord.FormatStyle.MESSAGE_FORMAT, LOGGER_CLASS_NAME); + record.setParameters(params); + record.setResourceBundle(bundle); + delegate.log(record); + } + } +} diff --git a/logging/src/main/java/org/xbib/logging/LoggerNode.java b/logging/src/main/java/org/xbib/logging/LoggerNode.java new file mode 100644 index 0000000..4739578 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/LoggerNode.java @@ -0,0 +1,621 @@ +package org.xbib.logging; + +import java.lang.invoke.ConstantBootstraps; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.lang.reflect.UndeclaredThrowableException; +import java.util.Collection; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.concurrent.locks.ReentrantLock; +import java.util.logging.ErrorManager; +import java.util.logging.Filter; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import org.xbib.logging.ref.PhantomReference; +import org.xbib.logging.ref.Reaper; +import org.xbib.logging.ref.Reference; +import org.xbib.logging.util.AtomicArray; +import org.xbib.logging.util.StandardOutputStreams; + +/** + * A node in the tree of logger names. Maintains weak references to children and a strong reference to its parent. + */ +public final class LoggerNode implements AutoCloseable { + + private static final Reaper REAPER = reference -> + reference.getAttachment().activeLoggers.remove(reference); + + private static final StackTraceElement[] EMPTY_STACK = new StackTraceElement[0]; + + /** + * The log context. + */ + private final LogContext context; + /** + * The parent node, or {@code null} if this is the root logger node. + */ + private final LoggerNode parent; + /** + * The fully-qualified name of this logger. + */ + private final String fullName; + + /** + * The map of names to child nodes. The child node references are weak. + */ + private final ConcurrentMap children; + + /** + * The handlers for this logger. May only be updated using the {@link #handlersUpdater} atomic updater. The array + * instance should not be modified (treat as immutable). + */ + @SuppressWarnings({"UnusedDeclaration"}) + private volatile Handler[] handlers; + + /** + * Flag to specify whether parent handlers are used. + */ + private volatile boolean useParentHandlers = true; + + /** + * The filter for this logger instance. + */ + private volatile Filter filter; + + /** + * Flag to specify whether parent filters are used. + */ + private volatile boolean useParentFilter = false; + + /** + * The set of phantom references to active loggers. + */ + private final Set> activeLoggers = + ConcurrentHashMap.newKeySet(); + + // must not be final + private Map, Object> attachments; + + private static final VarHandle attachmentHandle = + ConstantBootstraps.fieldVarHandle(MethodHandles.lookup(), "attachments", VarHandle.class, LoggerNode.class, Map.class); + + /** + * The atomic updater for the {@link #handlers} field. + */ + private static final AtomicArray handlersUpdater = + AtomicArray.create(AtomicReferenceFieldUpdater.newUpdater(LoggerNode.class, Handler[].class, "handlers"), Handler.class); + + /** + * The actual level. May only be modified when the context's level change lock is held; in addition, changing + * this field must be followed immediately by recursively updating the effective loglevel of the child tree. + */ + private volatile Level level; + /** + * The effective level. May only be modified when the context's level change lock is held; in addition, changing + * this field must be followed immediately by recursively updating the effective loglevel of the child tree. + */ + private volatile int effectiveLevel; + + /** + * The effective minimum level, which may not be modified. + */ + private final int effectiveMinLevel; + + /** + * Construct a new root instance. + * + * @param context the logmanager + */ + public LoggerNode(final LogContext context) { + parent = null; + fullName = ""; + this.context = context; + final LogContextInitializer initializer = context.getInitializer(); + final Level minLevel = initializer.getMinimumLevel(fullName); + effectiveMinLevel = Objects.requireNonNullElse(minLevel, Level.ALL).intValue(); + final Level initialLevel = initializer.getInitialLevel(fullName); + if (initialLevel != null) { + level = initialLevel; + effectiveLevel = initialLevel.intValue(); + } else { + effectiveLevel = Logger.INFO_INT; + } + handlers = safeCloneHandlers(initializer.getInitialHandlers(fullName)); + children = context.createChildMap(); + attachments = Map.of(); + } + + /** + * Construct a child instance. + * + * @param context the logmanager + * @param parent the parent node + * @param nodeName the name of this subnode + */ + private LoggerNode(LogContext context, LoggerNode parent, String nodeName) { + nodeName = nodeName.trim(); + if (nodeName.isEmpty() && parent == null) { + throw new IllegalArgumentException("nodeName is empty, or just whitespace and has no parent"); + } + this.parent = parent; + if (parent.parent == null) { + if (nodeName.isEmpty()) { + fullName = "."; + } else { + fullName = nodeName; + } + } else { + fullName = parent.fullName + "." + nodeName; + } + this.context = context; + final LogContextInitializer initializer = context.getInitializer(); + final Level minLevel = initializer.getMinimumLevel(fullName); + effectiveMinLevel = minLevel != null ? minLevel.intValue() : parent.effectiveMinLevel; + final Level initialLevel = initializer.getInitialLevel(fullName); + if (initialLevel != null) { + level = initialLevel; + effectiveLevel = initialLevel.intValue(); + } else { + effectiveLevel = parent.effectiveLevel; + } + handlers = safeCloneHandlers(initializer.getInitialHandlers(fullName)); + children = context.createChildMap(); + attachments = Map.of(); + } + + public static Handler[] safeCloneHandlers(Handler... initialHandlers) { + if (initialHandlers == null || initialHandlers.length == 0) { + return LogContextInitializer.NO_HANDLERS; + } + final Handler[] clone = initialHandlers.clone(); + final int length = clone.length; + for (int i = 0; i < length; i++) { + if (clone[i] == null) { + // our clone contains nulls; we have to clone again to be safe + int cnt; + for (cnt = 1, i++; i < length; i++) { + if (clone[i] == null) + cnt++; + } + final int newLen = length - cnt; + if (newLen == 0) { + return LogContextInitializer.NO_HANDLERS; + } + final Handler[] newClone = new Handler[newLen]; + for (int j = 0, k = 0; j < length; j++) { + if (clone[j] != null) + newClone[k++] = clone[j]; + } + return newClone; + } + } + // original contained no nulls, so carry on + return clone; + } + + @Override + public void close() { + final ReentrantLock treeLock = context.treeLock; + treeLock.lock(); + try { + // Reset everything to defaults + filter = null; + if ("".equals(fullName)) { + level = Level.INFO; + effectiveLevel = Level.INFO.intValue(); + } else { + level = null; + effectiveLevel = parent.effectiveLevel; + } + handlersUpdater.clear(this); + useParentFilter = false; + useParentHandlers = true; + attachmentHandle.setVolatile(this, Map.of()); + children.clear(); + } finally { + treeLock.unlock(); + } + } + + /** + * Get or create a relative logger node. The name is relatively qualified to this node. + * + * @param name the name + * @return the corresponding logger node + */ + public LoggerNode getOrCreate(final String name) { + if (name == null || name.isEmpty()) { + return this; + } else { + int i = name.indexOf('.'); + final String nextName = i == -1 ? name : name.substring(0, i); + LoggerNode nextNode = children.get(nextName); + if (nextNode == null) { + nextNode = new LoggerNode(context, this, nextName); + LoggerNode appearingNode = children.putIfAbsent(nextName, nextNode); + if (appearingNode != null) { + nextNode = appearingNode; + } + } + if (i == -1) { + return nextNode; + } else { + return nextNode.getOrCreate(name.substring(i + 1)); + } + } + } + + /** + * Get a relative logger, if it exists. + * + * @param name the name + * @return the corresponding logger + */ + public LoggerNode getIfExists(final String name) { + if (name == null || name.isEmpty()) { + return this; + } else { + int i = name.indexOf('.'); + final String nextName = i == -1 ? name : name.substring(0, i); + LoggerNode nextNode = children.get(nextName); + if (nextNode == null) { + return null; + } + if (i == -1) { + return nextNode; + } else { + return nextNode.getIfExists(name.substring(i + 1)); + } + } + } + + public Logger createLogger() { + final Logger logger = new Logger(this, fullName); + activeLoggers.add(new PhantomReference<>(logger, LoggerNode.this, REAPER)); + return logger; + } + + /** + * Get the children of this logger. + * + * @return the children + */ + public Collection getChildren() { + return children.values(); + } + + /** + * Get the log context. + * + * @return the log context + */ + public LogContext getContext() { + return context; + } + + /** + * Update the effective level if it is inherited from a parent. Must only be called while the logmanager's level + * change lock is held. + * + * @param newLevel the new effective level + */ + public void setEffectiveLevel(int newLevel) { + if (level == null) { + effectiveLevel = newLevel; + for (LoggerNode node : children.values()) { + if (node != null) { + node.setEffectiveLevel(newLevel); + } + } + } + } + + public void setFilter(final Filter filter) { + this.filter = filter; + if (filter != null) { + context.pin(this); + } + } + + public Filter getFilter() { + return filter; + } + + public boolean getUseParentFilters() { + return useParentFilter; + } + + public void setUseParentFilters(final boolean useParentFilter) { + this.useParentFilter = useParentFilter; + if (useParentFilter) { + context.pin(this); + } + } + + public int getEffectiveLevel() { + // this can be inlined + return effectiveLevel; + } + + public boolean isLoggableLevel(int level) { + // this can be inlined + return level != Logger.OFF_INT && level >= effectiveMinLevel && level >= effectiveLevel; + } + + public Handler[] getHandlers() { + Handler[] handlers = this.handlers; + if (handlers == null) { + synchronized (this) { + handlers = this.handlers; + if (handlers == null) { + handlers = this.handlers = safeCloneHandlers(context.getInitializer().getInitialHandlers(fullName)); + } + } + } + return handlers; + } + + public Handler[] clearHandlers() { + final Handler[] handlers = this.handlers; + handlersUpdater.clear(this); + return safeCloneHandlers(handlers); + } + + public void removeHandler(final Handler handler) { + getHandlers(); + handlersUpdater.remove(this, handler, true); + } + + public void addHandler(final Handler handler) { + getHandlers(); + handlersUpdater.add(this, handler); + context.pin(this); + } + + public Handler[] setHandlers(final Handler[] handlers) { + if (handlers.length > 0) { + context.pin(this); + } + return handlersUpdater.getAndSet(this, handlers); + } + + public boolean compareAndSetHandlers(final Handler[] oldHandlers, final Handler[] newHandlers) { + return handlersUpdater.compareAndSet(this, oldHandlers, newHandlers); + } + + public boolean getUseParentHandlers() { + return useParentHandlers; + } + + public void setUseParentHandlers(final boolean useParentHandlers) { + this.useParentHandlers = useParentHandlers; + if (!useParentHandlers) { + context.pin(this); + } + } + + public void publish(final ExtLogRecord record) { + LogRecord oldRecord = null; + for (Handler handler : getHandlers()) + try { + if (handler instanceof ExtHandler || handler.getFormatter() instanceof ExtFormatter) { + handler.publish(record); + } else { + // old-style handlers generally don't know how to handle printf formatting + if (oldRecord == null) { + if (record.getFormatStyle() == ExtLogRecord.FormatStyle.PRINTF) { + // reformat it in a simple way, but only for legacy handler usage + oldRecord = new ExtLogRecord(record); + oldRecord.setMessage(record.getFormattedMessage()); + oldRecord.setParameters(null); + } else { + oldRecord = record; + } + } + handler.publish(oldRecord); + } + } catch (VirtualMachineError e) { + throw e; + } catch (Throwable t) { + ErrorManager errorManager = handler.getErrorManager(); + if (errorManager != null) { + Exception e; + if (t instanceof Exception) { + e = (Exception) t; + } else { + e = new UndeclaredThrowableException(t); + e.setStackTrace(EMPTY_STACK); + } + try { + errorManager.error("Handler publication threw an exception", e, ErrorManager.WRITE_FAILURE); + } catch (Throwable t2) { + StandardOutputStreams.printError(t2, "Handler.reportError caught an exception"); + } + } + } + if (useParentHandlers) { + final LoggerNode parent = this.parent; + if (parent != null) + parent.publish(record); + } + } + + public void setLevel(final Level newLevel) { + final ReentrantLock treeLock = context.treeLock; + treeLock.lock(); + try { + final int oldEffectiveLevel = effectiveLevel; + final int newEffectiveLevel; + if (newLevel != null) { + level = newLevel; + newEffectiveLevel = newLevel.intValue(); + context.pin(this); + } else { + final LoggerNode parent = this.parent; + if (parent == null) { + level = Level.INFO; + newEffectiveLevel = Logger.INFO_INT; + } else { + level = null; + newEffectiveLevel = parent.effectiveLevel; + } + } + effectiveLevel = newEffectiveLevel; + if (oldEffectiveLevel != newEffectiveLevel) { + // our level changed, recurse down to children + for (LoggerNode node : children.values()) { + if (node != null) { + node.setEffectiveLevel(newEffectiveLevel); + } + } + } + } finally { + treeLock.unlock(); + } + } + + public Level getLevel() { + return level; + } + + @SuppressWarnings({"unchecked"}) + public V getAttachment(final Logger.AttachmentKey key) { + return (V) attachments.get(key); + } + + @SuppressWarnings({"unchecked"}) + public V attach(final Logger.AttachmentKey key, final V value) { + Map, Object> oldAttachments; + Map, Object> newAttachments; + V old; + do { + oldAttachments = attachments; + newAttachments = new HashMap<>(oldAttachments); + old = (V) newAttachments.put(key, value); + } while (!attachmentHandle.compareAndSet(this, oldAttachments, Map.copyOf(newAttachments))); + return old; + } + + @SuppressWarnings({"unchecked"}) + public V attachIfAbsent(final Logger.AttachmentKey key, final V value) { + Map, Object> oldAttachments; + Map, Object> newAttachments; + do { + oldAttachments = attachments; + if (oldAttachments.containsKey(key)) { + return (V) oldAttachments.get(key); + } + newAttachments = new HashMap<>(oldAttachments); + newAttachments.put(key, value); + } while (!attachmentHandle.compareAndSet(this, oldAttachments, Map.copyOf(newAttachments))); + return null; + } + + @SuppressWarnings({"unchecked"}) + public V detach(final Logger.AttachmentKey key) { + Map, Object> oldAttachments; + Map, Object> newAttachments; + V result; + do { + oldAttachments = attachments; + result = (V) oldAttachments.get(key); + if (result == null) { + return null; + } + final int size = oldAttachments.size(); + if (size == 1) { + // special case - the new map is empty + newAttachments = Map.of(); + } else { + newAttachments = new HashMap<>(oldAttachments); + newAttachments.remove(key); + } + } while (!attachmentHandle.compareAndSet(this, oldAttachments, Map.copyOf(newAttachments))); + return result; + } + + public String getFullName() { + return fullName; + } + + public LoggerNode getParent() { + return parent; + } + + /** + * Checks the filter to see if the record is loggable. If the {@link #getUseParentFilters()} is set to {@code true} + * the parent loggers are checked. + * + * @param record the log record to check against the filter + * @return {@code true} if the record is loggable, otherwise {@code false} + */ + public boolean isLoggable(final ExtLogRecord record) { + if (!useParentFilter) { + final Filter filter = this.filter; + return filter == null || filter.isLoggable(record); + } + final ReentrantLock treeLock = context.treeLock; + treeLock.lock(); + try { + return isLoggable(this, record); + } finally { + treeLock.unlock(); + } + } + + private static boolean isLoggable(final LoggerNode loggerNode, final ExtLogRecord record) { + if (loggerNode == null) { + return true; + } + final Filter filter = loggerNode.filter; + return !(filter != null && !filter.isLoggable(record)) + && (!loggerNode.useParentFilter || isLoggable(loggerNode.getParent(), record)); + } + + public Enumeration getLoggerNames() { + return new Enumeration<>() { + final Iterator children = getChildren().iterator(); + String next = activeLoggers.isEmpty() ? null : fullName; + Enumeration sub; + + @Override + public boolean hasMoreElements() { + while (next == null) { + while (sub == null) { + if (children.hasNext()) { + final LoggerNode child = children.next(); + sub = child.getLoggerNames(); + } else { + return false; + } + } + if (sub.hasMoreElements()) { + next = sub.nextElement(); + return true; + } else { + sub = null; + } + } + return true; + } + + @Override + public String nextElement() { + try { + return next; + } finally { + next = null; + } + } + }; + } +} diff --git a/logging/src/main/java/org/xbib/logging/MDC.java b/logging/src/main/java/org/xbib/logging/MDC.java new file mode 100644 index 0000000..6842686 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/MDC.java @@ -0,0 +1,135 @@ +package org.xbib.logging; + +import java.util.Iterator; +import java.util.Map; +import java.util.ServiceConfigurationError; +import java.util.ServiceLoader; + +/** + * Mapped diagnostic context. This is a thread-local map used to hold loggable information. + */ +public final class MDC { + private static final MDCProvider mdcProvider = getDefaultMDCProvider(); + + private MDC() { + } + + static MDCProvider getMDCProvider() { + return mdcProvider; + } + + private static MDCProvider getDefaultMDCProvider() { + return doGetDefaultMDCProvider(); + } + + private static MDCProvider doGetDefaultMDCProvider() { + final ServiceLoader configLoader = ServiceLoader.load(MDCProvider.class, MDC.class.getClassLoader()); + final Iterator iterator = configLoader.iterator(); + for (; ; ) + try { + if (!iterator.hasNext()) { + return new ThreadLocalMDC(); + } + return iterator.next(); + } catch (ServiceConfigurationError | RuntimeException e) { + System.err.print("Warning: failed to load MDC Provider: "); + e.printStackTrace(System.err); + } + } + + /** + * Get the value for a key, or {@code null} if there is no mapping. + * + * @param key the key + * @return the value + */ + public static String get(String key) { + return mdcProvider.get(key); + } + + /** + * Get the value for a key, or {@code null} if there is no mapping. + * + * @param key the key + * @return the value + */ + public static Object getObject(String key) { + return mdcProvider.getObject(key); + } + + /** + * Set the value of a key, returning the old value (if any) or {@code null} if there was none. + * + * @param key the key + * @param value the new value + * @return the old value or {@code null} if there was none + */ + public static String put(String key, String value) { + return mdcProvider.put(key, value); + } + + /** + * Set the value of a key, returning the old value (if any) or {@code null} if there was none. + * + * @param key the key + * @param value the new value + * @return the old value or {@code null} if there was none + */ + public static Object putObject(String key, Object value) { + return mdcProvider.putObject(key, value); + } + + /** + * Remove a key. + * + * @param key the key + * @return the old value or {@code null} if there was none + */ + public static String remove(String key) { + return mdcProvider.remove(key); + } + + /** + * Remove a key. + * + * @param key the key + * @return the old value or {@code null} if there was none + */ + public static Object removeObject(String key) { + return mdcProvider.removeObject(key); + } + + /** + * Get a copy of the MDC map. This is a relatively expensive operation. + * + * @return a copy of the map + */ + public static Map copy() { + return mdcProvider.copy(); + } + + /** + * Get a copy of the MDC map. This is a relatively expensive operation. + * + * @return a copy of the map + */ + public static Map copyObject() { + return mdcProvider.copyObject(); + } + + /** + * Checks of the MDC map is empty. + * + * @return {@code true} if the MDC map is empty, otherwise {@code false} + */ + public static boolean isEmpty() { + return mdcProvider.isEmpty(); + } + + /** + * Clear the current MDC map. + */ + public static void clear() { + mdcProvider.clear(); + } +} diff --git a/logging/src/main/java/org/xbib/logging/MDCProvider.java b/logging/src/main/java/org/xbib/logging/MDCProvider.java new file mode 100644 index 0000000..2f49265 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/MDCProvider.java @@ -0,0 +1,83 @@ +package org.xbib.logging; + +import java.util.Map; + +public interface MDCProvider { + + /** + * Get the value for a key, or {@code null} if there is no mapping. + * + * @param key the key + * @return the value + */ + String get(String key); + + /** + * Get the value for a key, or {@code null} if there is no mapping. + * + * @param key the key + * @return the value + */ + Object getObject(String key); + + /** + * Set the value of a key, returning the old value (if any) or {@code null} if there was none. + * + * @param key the key + * @param value the new value + * @return the old value or {@code null} if there was none + */ + String put(String key, String value); + + /** + * Set the value of a key, returning the old value (if any) or {@code null} if there was none. + * + * @param key the key + * @param value the new value + * @return the old value or {@code null} if there was none + */ + Object putObject(String key, Object value); + + /** + * Remove a key. + * + * @param key the key + * @return the old value or {@code null} if there was none + */ + String remove(String key); + + /** + * Remove a key. + * + * @param key the key + * @return the old value or {@code null} if there was none + */ + Object removeObject(String key); + + /** + * Get a copy of the MDC map. This is a relatively expensive operation. + * + * @return a copy of the map + */ + Map copy(); + + /** + * Get a copy of the MDC map. This is a relatively expensive operation. + * + * @return a copy of the map + */ + Map copyObject(); + + /** + * Returns {@code true} if the backing MDC map is empty, otherwise {@code false}. + * + * @return {@code true} if the MDC map is empty + */ + boolean isEmpty(); + + /** + * Clear the current MDC map. + */ + void clear(); + +} diff --git a/logging/src/main/java/org/xbib/logging/NDC.java b/logging/src/main/java/org/xbib/logging/NDC.java new file mode 100644 index 0000000..8edb181 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/NDC.java @@ -0,0 +1,103 @@ +package org.xbib.logging; + +import java.util.Iterator; +import java.util.ServiceConfigurationError; +import java.util.ServiceLoader; + +/** + * Nested diagnostic context. This is basically a thread-local stack that holds a string which can be included + * in a log message. + */ +public final class NDC { + private static final NDCProvider ndcProvider = getDefaultNDCProvider(); + + private NDC() { + } + + static NDCProvider getNDCProvider() { + return ndcProvider; + } + + static NDCProvider getDefaultNDCProvider() { + return doGetDefaultNDCProvider(); + } + + static NDCProvider doGetDefaultNDCProvider() { + final ServiceLoader configLoader = ServiceLoader.load(NDCProvider.class, NDC.class.getClassLoader()); + final Iterator iterator = configLoader.iterator(); + for (; ; ) + try { + if (!iterator.hasNext()) { + return new ThreadLocalNDC(); + } + return iterator.next(); + } catch (ServiceConfigurationError | RuntimeException e) { + System.err.print("Warning: failed to load NDC Provider: "); + e.printStackTrace(System.err); + } + } + + /** + * Push a value on to the NDC stack, returning the new stack depth which should later be used to restore the stack. + * + * @param context the new value + * @return the new stack depth + */ + public static int push(String context) { + return ndcProvider.push(context); + } + + /** + * Pop the topmost value from the NDC stack and return it. + * + * @return the old topmost value + */ + public static String pop() { + return ndcProvider.pop(); + } + + /** + * Clear the thread's NDC stack. + */ + public static void clear() { + ndcProvider.clear(); + } + + /** + * Trim the thread NDC stack down to no larger than the given size. Used to restore the stack to the depth returned + * by a {@code push()}. + * + * @param size the new size + */ + public static void trimTo(int size) { + ndcProvider.trimTo(size); + } + + /** + * Get the current NDC stack depth. + * + * @return the stack depth + */ + public static int getDepth() { + return ndcProvider.getDepth(); + } + + /** + * Get the current NDC value. + * + * @return the current NDC value, or {@code ""} if there is none + */ + public static String get() { + return ndcProvider.get(); + } + + /** + * Provided for compatibility with log4j. Get the NDC value that is {@code n} entries from the bottom. + * + * @param n the index + * @return the value or {@code null} if there is none + */ + public static String get(int n) { + return ndcProvider.get(n); + } +} diff --git a/logging/src/main/java/org/xbib/logging/NDCProvider.java b/logging/src/main/java/org/xbib/logging/NDCProvider.java new file mode 100644 index 0000000..99e395d --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/NDCProvider.java @@ -0,0 +1,55 @@ +package org.xbib.logging; + +public interface NDCProvider { + + /** + * Push a value on to the NDC stack, returning the new stack depth which should later be used to restore the stack. + * + * @param context the new value + * @return the new stack depth + */ + int push(String context); + + /** + * Pop the topmost value from the NDC stack and return it. + * + * @return the old topmost value + */ + String pop(); + + /** + * Clear the thread's NDC stack. + */ + void clear(); + + /** + * Trim the thread NDC stack down to no larger than the given size. Used to restore the stack to the depth returned + * by a {@code push()}. + * + * @param size the new size + */ + void trimTo(int size); + + /** + * Get the current NDC stack depth. + * + * @return the stack depth + */ + int getDepth(); + + /** + * Get the current NDC value. + * + * @return the current NDC value, or {@code ""} if there is none + */ + String get(); + + /** + * Provided for compatibility with log4j. Get the NDC value that is {@code n} entries from the bottom. + * + * @param n the index + * @return the value or {@code null} if there is none + */ + String get(int n); + +} diff --git a/logging/src/main/java/org/xbib/logging/ThreadLocalLogContextSelector.java b/logging/src/main/java/org/xbib/logging/ThreadLocalLogContextSelector.java new file mode 100644 index 0000000..468b1c6 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/ThreadLocalLogContextSelector.java @@ -0,0 +1,57 @@ +package org.xbib.logging; + +/** + * A log context selector which stores the chosen log context in a thread-local. + */ +public final class ThreadLocalLogContextSelector implements LogContextSelector { + + private final Object securityKey; + private final LogContextSelector delegate; + private final ThreadLocal context = new ThreadLocal<>(); + + /** + * Construct a new instance. + * + * @param delegate the selector to delegate to if no context is chosen + */ + public ThreadLocalLogContextSelector(final LogContextSelector delegate) { + this(null, delegate); + } + + /** + * Construct a new instance. + * + * @param securityKey the security key required to push or pop a log context. + * @param delegate the selector to delegate to if no context is chosen + */ + public ThreadLocalLogContextSelector(final Object securityKey, final LogContextSelector delegate) { + this.securityKey = securityKey; + this.delegate = delegate; + } + + public LogContext getLogContext() { + final LogContext localContext = context.get(); + return localContext != null ? localContext : delegate.getLogContext(); + } + + /** + * Get and set the log context. + * + * @param securityKey the security key to check (ignored if none was set on construction) + * @param newValue the new log context value, or {@code null} to clear + * @return the previous log context value, or {@code null} if none was set + */ + public LogContext getAndSet(Object securityKey, LogContext newValue) { + if (this.securityKey != null && securityKey != this.securityKey) { + throw new IllegalArgumentException("invalid security key for ThreadLocalLogContextSelector modification"); + } + try { + return context.get(); + } finally { + if (newValue == null) + context.remove(); + else + context.set(newValue); + } + } +} diff --git a/logging/src/main/java/org/xbib/logging/ThreadLocalMDC.java b/logging/src/main/java/org/xbib/logging/ThreadLocalMDC.java new file mode 100644 index 0000000..4c6447b --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/ThreadLocalMDC.java @@ -0,0 +1,84 @@ +package org.xbib.logging; + +import java.util.Map; +import org.xbib.logging.util.FastCopyHashMap; + +final class ThreadLocalMDC implements MDCProvider { + private static final Holder mdc = new Holder(); + + @Override + public String get(String key) { + final Object value = getObject(key); + return value == null ? null : value.toString(); + } + + @Override + public Object getObject(String key) { + return mdc.get().get(key); + } + + @Override + public String put(String key, String value) { + final Object oldValue = putObject(key, value); + return oldValue == null ? null : oldValue.toString(); + } + + @Override + public Object putObject(String key, Object value) { + if (key == null) { + throw new NullPointerException("key is null"); + } + if (value == null) { + throw new NullPointerException("value is null"); + } + return mdc.get().put(key, value); + } + + @Override + public String remove(String key) { + final Object oldValue = removeObject(key); + return oldValue == null ? null : oldValue.toString(); + } + + @Override + public Object removeObject(String key) { + return mdc.get().remove(key); + } + + @Override + public Map copy() { + final FastCopyHashMap result = new FastCopyHashMap<>(); + for (Map.Entry entry : mdc.get().entrySet()) { + result.put(entry.getKey(), entry.getValue().toString()); + } + return result; + } + + @Override + public Map copyObject() { + return mdc.get().clone(); + } + + @Override + public boolean isEmpty() { + return mdc.get().isEmpty(); + } + + @Override + public void clear() { + mdc.get().clear(); + } + + private static final class Holder extends InheritableThreadLocal> { + + @Override + protected FastCopyHashMap childValue(final FastCopyHashMap parentValue) { + return new FastCopyHashMap<>(parentValue); + } + + @Override + protected FastCopyHashMap initialValue() { + return new FastCopyHashMap<>(); + } + } +} diff --git a/logging/src/main/java/org/xbib/logging/ThreadLocalNDC.java b/logging/src/main/java/org/xbib/logging/ThreadLocalNDC.java new file mode 100644 index 0000000..7694aa8 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/ThreadLocalNDC.java @@ -0,0 +1,122 @@ +package org.xbib.logging; + +import java.util.Arrays; + +final class ThreadLocalNDC implements NDCProvider { + private static final Holder ndc = new Holder(); + + @Override + public int push(String context) { + final Stack stack = ndc.get(); + try { + return stack.depth(); + } finally { + stack.push(context); + } + } + + @Override + public String pop() { + final Stack stack = ndc.get(); + if (stack.isEmpty()) { + return ""; + } else { + return stack.pop(); + } + } + + @Override + public void clear() { + ndc.get().trimTo(0); + } + + @Override + public void trimTo(int size) { + ndc.get().trimTo(size); + } + + @Override + public int getDepth() { + return ndc.get().depth(); + } + + @Override + public String get() { + final Stack stack = ndc.get(); + if (stack.isEmpty()) { + return ""; + } else { + return stack.toString(); + } + } + + @Override + public String get(int n) { + return ndc.get().get(n); + } + + private static final class Holder extends ThreadLocal> { + protected Stack initialValue() { + return new Stack<>(); + } + } + + private static final class Stack { + private Object[] data = new Object[32]; + private int sp; + + public void push(T value) { + if (sp == data.length) { + data = Arrays.copyOf(data, (data.length << 1) + data.length >>> 1); + } + data[sp++] = value; + } + + @SuppressWarnings("unchecked") + public T pop() { + try { + return (T) data[--sp]; + } finally { + data[sp] = null; + } + } + + @SuppressWarnings("unchecked") + public T top() { + return (T) data[sp - 1]; + } + + public boolean isEmpty() { + return sp == 0; + } + + public int depth() { + return sp; + } + + public void trimTo(int max) { + final int sp = this.sp; + if (sp > max) { + Arrays.fill(data, max, sp - 1, null); + this.sp = max; + } + } + + @SuppressWarnings("unchecked") + public T get(int n) { + return n < sp ? (T) data[n] : null; + } + + public String toString() { + final StringBuilder b = new StringBuilder(); + final int sp = this.sp; + for (int i = 0; i < sp; i++) { + b.append(data[i]); + if ((i + 1) < sp) { + b.append('.'); + } + } + return b.toString(); + } + } +} diff --git a/logging/src/main/java/org/xbib/logging/WrappedExtLogRecord.java b/logging/src/main/java/org/xbib/logging/WrappedExtLogRecord.java new file mode 100644 index 0000000..16da652 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/WrappedExtLogRecord.java @@ -0,0 +1,230 @@ +package org.xbib.logging; + +import java.time.Instant; +import java.util.ResourceBundle; +import java.util.logging.Level; +import java.util.logging.LogRecord; + +@SuppressWarnings("serial") +class WrappedExtLogRecord extends ExtLogRecord { + + private static final String LOGGER_CLASS_NAME = java.util.logging.Logger.class.getName(); + + private transient final LogRecord logRecord; + + private transient boolean resolved; + + WrappedExtLogRecord(final LogRecord logRecord) { + super(logRecord.getLevel(), logRecord.getMessage(), LOGGER_CLASS_NAME); + this.logRecord = logRecord; + } + + public String getLoggerName() { + return logRecord.getLoggerName(); + } + + public void setLoggerName(final String name) { + super.setLoggerName(name); + logRecord.setLoggerName(name); + } + + @Override + public ResourceBundle getResourceBundle() { + return logRecord.getResourceBundle(); + } + + @Override + public void setResourceBundle(final ResourceBundle bundle) { + super.setResourceBundle(bundle); + logRecord.setResourceBundle(bundle); + } + + @Override + public String getResourceBundleName() { + return logRecord.getResourceBundleName(); + } + + @Override + public void setResourceBundleName(final String name) { + super.setResourceBundleName(name); + logRecord.setResourceBundleName(name); + } + + @Override + public Level getLevel() { + return logRecord.getLevel(); + } + + @Override + public void setLevel(final Level level) { + super.setLevel(level); + logRecord.setLevel(level); + } + + @Override + public long getSequenceNumber() { + return logRecord.getSequenceNumber(); + } + + @Override + public void setSequenceNumber(final long seq) { + super.setSequenceNumber(seq); + logRecord.setSequenceNumber(seq); + } + + @Override + public String getSourceClassName() { + if (!resolved) { + resolve(); + } + return super.getSourceClassName(); + } + + @Override + public void setSourceClassName(final String sourceClassName) { + resolved = true; + super.setSourceClassName(sourceClassName); + logRecord.setSourceClassName(sourceClassName); + } + + @Override + public String getSourceMethodName() { + if (!resolved) { + resolve(); + } + return super.getSourceMethodName(); + } + + @Override + public void setSourceMethodName(final String sourceMethodName) { + resolved = true; + super.setSourceMethodName(sourceMethodName); + logRecord.setSourceMethodName(sourceMethodName); + } + + private void resolve() { + resolved = true; + final String sourceMethodName = logRecord.getSourceMethodName(); + final String sourceClassName = logRecord.getSourceClassName(); + super.setSourceMethodName(sourceMethodName); + super.setSourceClassName(sourceClassName); + final StackTraceElement[] st = new Throwable().getStackTrace(); + for (StackTraceElement element : st) { + if (element.getClassName().equals(sourceClassName) && element.getMethodName().equals(sourceMethodName)) { + super.setSourceLineNumber(element.getLineNumber()); + super.setSourceFileName(element.getFileName()); + return; + } + } + } + + @Override + public int getSourceLineNumber() { + if (!resolved) { + resolve(); + } + return super.getSourceLineNumber(); + } + + public void setSourceLineNumber(final int sourceLineNumber) { + resolved = true; + super.setSourceLineNumber(sourceLineNumber); + } + + public String getSourceFileName() { + if (!resolved) { + resolve(); + } + return super.getSourceFileName(); + } + + @Override + public void setSourceFileName(final String sourceFileName) { + resolved = true; + super.setSourceFileName(sourceFileName); + } + + @Override + public String getMessage() { + return logRecord.getMessage(); + } + + @Override + public void setMessage(final String message) { + super.setMessage(message); + logRecord.setMessage(message); + } + + @Override + public void setMessage(String message, FormatStyle formatStyle) { + super.setMessage(message, formatStyle); + logRecord.setMessage(message); + } + + @Override + public Object[] getParameters() { + return logRecord.getParameters(); + } + + @Override + public void setParameters(final Object[] parameters) { + logRecord.setParameters(parameters); + } + + @Deprecated + @SuppressWarnings("deprecation") + public int getThreadID() { + return logRecord.getThreadID(); + } + + @Deprecated + @SuppressWarnings("deprecation") + public void setThreadID(final int threadID) { + super.setThreadID(threadID); + logRecord.setThreadID(threadID); + } + + @Override + public long getLongThreadID() { + return logRecord.getLongThreadID(); + } + + @Override + public ExtLogRecord setLongThreadID(final long id) { + super.setLongThreadID(id); + logRecord.setLongThreadID(id); + return this; + } + + @Override + public long getMillis() { + return logRecord.getMillis(); + } + + @Deprecated + @SuppressWarnings("deprecation") + @Override + public void setMillis(final long millis) { + logRecord.setMillis(millis); + } + + @Override + public Instant getInstant() { + return logRecord.getInstant(); + } + + @Override + public void setInstant(Instant instant) { + logRecord.setInstant(instant); + } + + @Override + public Throwable getThrown() { + return logRecord.getThrown(); + } + + @Override + public void setThrown(final Throwable thrown) { + logRecord.setThrown(thrown); + } +} diff --git a/logging/src/main/java/org/xbib/logging/configuration/ConfigurationResource.java b/logging/src/main/java/org/xbib/logging/configuration/ConfigurationResource.java new file mode 100644 index 0000000..87e30af --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/configuration/ConfigurationResource.java @@ -0,0 +1,35 @@ +package org.xbib.logging.configuration; + +import java.util.function.Supplier; + +/** + * Represents a configuration resource. If the resource is a {@link AutoCloseable}, then invoking {@link #close()} on + * this resource will close the resource. + */ +public interface ConfigurationResource extends Supplier, AutoCloseable { + + /** + * Creates a configuration resource which lazily invokes the supplier. Note that {@link #close()} will only close + * the resource if {@link #get()} was first invoked to retrieve the value from the supplier. + * + * @param supplier the supplier used to create the configuration resource + * @return the configuration resource represented by a lazy instance + */ + static ConfigurationResource of(final Supplier supplier) { + if (supplier instanceof ConfigurationResource) { + return (ConfigurationResource) supplier; + } + return new LazyConfigurationResource<>(supplier); + } + + /** + * Creates a configuration resource with the instance as a constant. Note that if {@link #close()} is invoked, + * {@link #get()} will return {@code null}. + * + * @param instance the constant instance + * @return the configuration resource represented by a constant instance + */ + static ConfigurationResource of(final T instance) { + return new ConstantConfigurationResource<>(instance); + } +} diff --git a/logging/src/main/java/org/xbib/logging/configuration/ConstantConfigurationResource.java b/logging/src/main/java/org/xbib/logging/configuration/ConstantConfigurationResource.java new file mode 100644 index 0000000..5cf68aa --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/configuration/ConstantConfigurationResource.java @@ -0,0 +1,25 @@ +package org.xbib.logging.configuration; + +import java.util.concurrent.atomic.AtomicReference; + +class ConstantConfigurationResource implements ConfigurationResource { + + private final AtomicReference instance; + + ConstantConfigurationResource(final T instance) { + this.instance = new AtomicReference<>(instance); + } + + @Override + public T get() { + return instance.get(); + } + + @Override + public void close() throws Exception { + final T instance = this.instance.getAndSet(null); + if (instance instanceof AutoCloseable) { + ((AutoCloseable) instance).close(); + } + } +} diff --git a/logging/src/main/java/org/xbib/logging/configuration/ContextConfiguration.java b/logging/src/main/java/org/xbib/logging/configuration/ContextConfiguration.java new file mode 100644 index 0000000..168592b --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/configuration/ContextConfiguration.java @@ -0,0 +1,398 @@ +package org.xbib.logging.configuration; + +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; +import java.util.logging.ErrorManager; +import java.util.logging.Filter; +import java.util.logging.Formatter; +import java.util.logging.Handler; +import org.xbib.logging.LogContext; +import org.xbib.logging.Logger; + +/** + * A configuration which can be stored on a {@linkplain LogContext log context} to store information + * about the configured error managers, handlers, filters, formatters and objects that might be associated with a + * configured object. + *

+ * The {@link #addObject(String, Supplier)} can be used to allow objects to be set when configuring error managers, + * handlers, filters and formatters. + *

+ *

+ * If the {@linkplain Supplier supplier} os not already an instance of a {@link ConfigurationResource}, then it is + * wrapped and considered a {@linkplain ConfigurationResource#of(Supplier) lazy resource}. + *

+ */ +@SuppressWarnings({"UnusedReturnValue", "unused"}) +public class ContextConfiguration implements AutoCloseable { + public static final Logger.AttachmentKey CONTEXT_CONFIGURATION_KEY = new Logger.AttachmentKey<>(); + private final LogContext context; + private final Map> errorManagers; + private final Map> filters; + private final Map> formatters; + private final Map> handlers; + private final Map> objects; + + /** + * Creates a new context configuration. + */ + public ContextConfiguration(final LogContext context) { + this.context = context; + errorManagers = new ConcurrentHashMap<>(); + handlers = new ConcurrentHashMap<>(); + formatters = new ConcurrentHashMap<>(); + filters = new ConcurrentHashMap<>(); + objects = new ConcurrentHashMap<>(); + } + + /** + * Returns the {@linkplain LogContext context} for this configuration. + * + * @return the context for this configuration + */ + public LogContext getContext() { + return context; + } + + /** + * Checks if the logger exists in this context. + * + * @param name the logger name + * @return {@code true} if the logger exists in this context, otherwise {@code false} + */ + public boolean hasLogger(final String name) { + return getContext().getLoggerIfExists(Objects.requireNonNull(name, "The name cannot be null")) != null; + } + + /** + * Gets the logger if it exists. + * + * @param name the name of the logger + * @return the logger or {@code null} if the logger does not exist + */ + public Logger getLogger(final String name) { + return getContext().getLogger(Objects.requireNonNull(name, "The name cannot be null")); + } + + /** + * Returns an unmodifiable set of the configured logger names + * + * @return an unmodified set of the logger names + */ + public Set getLoggers() { + return Set.copyOf(Collections.list(getContext().getLoggerNames())); + } + + /** + * Adds an error manager to the context configuration. + * + * @param name the name for the error manager + * @param errorManager the error manager to add + * @return the previous error manager associated with the name or {@code null} if one did not exist + */ + public ConfigurationResource addErrorManager(final String name, final Supplier errorManager) { + if (errorManager == null) { + return removeErrorManager(name); + } + return errorManagers.putIfAbsent(Objects.requireNonNull(name, "The name cannot be null"), + ConfigurationResource.of(errorManager)); + } + + /** + * Removes the error manager from the context configuration. + * + * @param name the name of the error manager + * @return the error manager removed or {@code null} if the error manager did not exist + */ + public ConfigurationResource removeErrorManager(final String name) { + return errorManagers.remove(Objects.requireNonNull(name, "The name cannot be null")); + } + + /** + * Checks if the error manager exists with the name provided. + * + * @param name the name for the error manager + * @return {@code true} if the error manager exists in this context, otherwise {@code false} + */ + public boolean hasErrorManager(final String name) { + return errorManagers.containsKey(Objects.requireNonNull(name, "The name cannot be null")); + } + + /** + * Gets the error manager if it exists. + * + * @param name the name of the error manager + * @return the error manager or {@code null} if the error manager does not exist + */ + public ErrorManager getErrorManager(final String name) { + if (errorManagers.containsKey(Objects.requireNonNull(name, "The name cannot be null"))) { + return errorManagers.get(name).get(); + } + return null; + } + + /** + * Returns an unmodifiable map of the error managers and the suppliers used to create them. + * + * @return an unmodified map of the error managers + */ + public Map> getErrorManagers() { + return Collections.unmodifiableMap(errorManagers); + } + + /** + * Adds a handler to the context configuration. + * + * @param name the name for the handler + * @param handler the handler to add + * @return the previous handler associated with the name or {@code null} if one did not exist + */ + public ConfigurationResource addHandler(final String name, final Supplier handler) { + if (handler == null) { + return removeHandler(name); + } + return handlers.putIfAbsent(Objects.requireNonNull(name, "The name cannot be null"), + ConfigurationResource.of(handler)); + } + + /** + * Removes the handler from the context configuration. + * + * @param name the name of the handler + * @return the handler removed or {@code null} if the handler did not exist + */ + public ConfigurationResource removeHandler(final String name) { + return handlers.remove(Objects.requireNonNull(name, "The name cannot be null")); + } + + /** + * Checks if the handler exists with the name provided. + * + * @param name the name for the handler + * @return {@code true} if the handler exists in this context, otherwise {@code false} + */ + public boolean hasHandler(final String name) { + return handlers.containsKey(Objects.requireNonNull(name, "The name cannot be null")); + } + + /** + * Gets the handler if it exists. + * + * @param name the name of the handler + * @return the handler or {@code null} if the handler does not exist + */ + public Handler getHandler(final String name) { + if (handlers.containsKey(Objects.requireNonNull(name, "The name cannot be null"))) { + return handlers.get(name).get(); + } + return null; + } + + /** + * Returns an unmodifiable map of the handlers and the suppliers used to create them. + * + * @return an unmodified map of the handlers + */ + public Map> getHandlers() { + return Collections.unmodifiableMap(handlers); + } + + /** + * Adds a formatter to the context configuration. + * + * @param name the name for the formatter + * @param formatter the formatter to add + * @return the previous formatter associated with the name or {@code null} if one did not exist + */ + public ConfigurationResource addFormatter(final String name, final Supplier formatter) { + if (formatter == null) { + return removeFormatter(name); + } + return formatters.putIfAbsent(Objects.requireNonNull(name, "The name cannot be null"), + ConfigurationResource.of(formatter)); + } + + /** + * Removes the formatter from the context configuration. + * + * @param name the name of the formatter + * @return the formatter removed or {@code null} if the formatter did not exist + */ + public ConfigurationResource removeFormatter(final String name) { + return formatters.remove(Objects.requireNonNull(name, "The name cannot be null")); + } + + /** + * Checks if the formatter exists with the name provided. + * + * @param name the name for the formatter + * @return {@code true} if the formatter exists in this context, otherwise {@code false} + */ + public boolean hasFormatter(final String name) { + return formatters.containsKey(Objects.requireNonNull(name, "The name cannot be null")); + } + + /** + * Gets the formatter if it exists. + * + * @param name the name of the formatter + * @return the formatter or {@code null} if the formatter does not exist + */ + public Formatter getFormatter(final String name) { + if (formatters.containsKey(Objects.requireNonNull(name, "The name cannot be null"))) { + return formatters.get(name).get(); + } + return null; + } + + /** + * Returns an unmodifiable map of the formatters and the suppliers used to create them. + * + * @return an unmodified map of the formatters + */ + public Map> getFormatters() { + return Collections.unmodifiableMap(formatters); + } + + /** + * Adds a filter to the context configuration. + * + * @param name the name for the filter + * @param filter the filter to add + * @return the previous filter associated with the name or {@code null} if one did not exist + */ + public ConfigurationResource addFilter(final String name, final Supplier filter) { + if (filter == null) { + return removeFilter(name); + } + return filters.putIfAbsent(Objects.requireNonNull(name, "The name cannot be null"), + ConfigurationResource.of(filter)); + } + + /** + * Removes the filter from the context configuration. + * + * @param name the name of the filter + * @return the filter removed or {@code null} if the filter did not exist + */ + public ConfigurationResource removeFilter(final String name) { + return filters.remove(Objects.requireNonNull(name, "The name cannot be null")); + } + + /** + * Checks if the filter exists with the name provided. + * + * @param name the name for the filter + * @return {@code true} if the filter exists in this context, otherwise {@code false} + */ + public boolean hasFilter(final String name) { + return filters.containsKey(Objects.requireNonNull(name, "The name cannot be null")); + } + + /** + * Gets the filter if it exists. + * + * @param name the name of the filter + * @return the filer or {@code null} if the filter does not exist + */ + public Filter getFilter(final String name) { + if (filters.containsKey(Objects.requireNonNull(name, "The name cannot be null"))) { + return filters.get(name).get(); + } + return null; + } + + /** + * Returns an unmodifiable map of the filters and the suppliers used to create them. + * + * @return an unmodified map of the filters + */ + public Map> getFilters() { + return Collections.unmodifiableMap(filters); + } + + /** + * Adds an object that can be used as a configuration property for another configuration type. This is used for + * cases when an object cannot simply be converted from a string. + * + * @param name the name for the configuration object + * @param object the configuration object to add + * @return the previous configuration object associated with the name or {@code null} if one did not exist + */ + public ConfigurationResource addObject(final String name, final Supplier object) { + if (object == null) { + return removeObject(name); + } + return objects.putIfAbsent(Objects.requireNonNull(name, "The name cannot be null"), + ConfigurationResource.of(object)); + } + + /** + * Removes the configuration object from the context configuration. + * + * @param name the name of the configuration object + * @return the configuration object removed or {@code null} if the configuration object did not exist + */ + public ConfigurationResource removeObject(final String name) { + return objects.remove(Objects.requireNonNull(name, "The name cannot be null")); + } + + /** + * Checks if the configuration object exists with the name provided. + * + * @param name the name for the configuration object + * @return {@code true} if the configuration object exists in this context, otherwise {@code false} + */ + public boolean hasObject(final String name) { + return objects.containsKey(Objects.requireNonNull(name, "The name cannot be null")); + } + + /** + * Gets the configuration object if it exists. + * + * @param name the name of the configuration object + * @return the configuration object or {@code null} if the configuration object does not exist + */ + public Object getObject(final String name) { + if (objects.containsKey(Objects.requireNonNull(name, "The name cannot be null"))) { + return objects.get(name).get(); + } + return null; + } + + /** + * Returns an unmodifiable map of the configuration objects and the suppliers used to create them. + * + * @return an unmodified map of the configuration objects + */ + public Map> getObjects() { + return Collections.unmodifiableMap(objects); + } + + @Override + public void close() throws Exception { + context.close(); + closeResources(handlers); + closeResources(filters); + closeResources(formatters); + closeResources(errorManagers); + closeResources(objects); + } + + private static void closeResources(final Map> resources) { + final var iter = resources.entrySet().iterator(); + while (iter.hasNext()) { + var entry = iter.next(); + iter.remove(); + try { + entry.getValue().close(); + } catch (Throwable ignore) { + // do nothing + } + } + } + +} diff --git a/logging/src/main/java/org/xbib/logging/configuration/DefaultLogContextConfiguratorFactory.java b/logging/src/main/java/org/xbib/logging/configuration/DefaultLogContextConfiguratorFactory.java new file mode 100644 index 0000000..cd0da1b --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/configuration/DefaultLogContextConfiguratorFactory.java @@ -0,0 +1,23 @@ +package org.xbib.logging.configuration; + +import org.xbib.logging.LogContextConfigurator; +import org.xbib.logging.LogContextConfiguratorFactory; + +/** + * The default configuration factory which has a priority of 100. + */ +public class DefaultLogContextConfiguratorFactory implements LogContextConfiguratorFactory { + + public DefaultLogContextConfiguratorFactory() { + } + + @Override + public LogContextConfigurator create() { + return new PropertyLogContextConfigurator(); + } + + @Override + public int priority() { + return 100; + } +} diff --git a/logging/src/main/java/org/xbib/logging/configuration/LazyConfigurationResource.java b/logging/src/main/java/org/xbib/logging/configuration/LazyConfigurationResource.java new file mode 100644 index 0000000..16909c1 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/configuration/LazyConfigurationResource.java @@ -0,0 +1,37 @@ +package org.xbib.logging.configuration; + +import java.util.function.Supplier; + +/** + * A configuration resource + */ +class LazyConfigurationResource implements ConfigurationResource { + private final Supplier supplier; + private volatile T instance; + + LazyConfigurationResource(final Supplier supplier) { + this.supplier = supplier; + } + + @Override + public T get() { + if (instance == null) { + synchronized (this) { + if (instance == null) { + instance = supplier.get(); + } + } + } + return instance; + } + + @Override + public void close() throws Exception { + synchronized (this) { + if (instance instanceof AutoCloseable) { + ((AutoCloseable) instance).close(); + } + instance = null; + } + } +} diff --git a/logging/src/main/java/org/xbib/logging/configuration/ObjectBuilder.java b/logging/src/main/java/org/xbib/logging/configuration/ObjectBuilder.java new file mode 100644 index 0000000..4871092 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/configuration/ObjectBuilder.java @@ -0,0 +1,345 @@ +package org.xbib.logging.configuration; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.nio.charset.Charset; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.TimeZone; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.regex.Pattern; + +/** + * Helper to lazily build an object. + */ +@SuppressWarnings({"UnusedReturnValue"}) +class ObjectBuilder { + private final ContextConfiguration contextConfiguration; + private final Class baseClass; + private final String className; + private final Map constructorProperties; + private final Map properties; + private final Set definedProperties; + private final Set postConstructMethods; + private String moduleName; + + private ObjectBuilder(final ContextConfiguration contextConfiguration, + final Class baseClass, final String className) { + this.contextConfiguration = contextConfiguration; + this.baseClass = baseClass; + this.className = className; + constructorProperties = new LinkedHashMap<>(); + properties = new LinkedHashMap<>(); + definedProperties = new LinkedHashSet<>(); + postConstructMethods = new LinkedHashSet<>(); + } + + /** + * Create a new {@link ObjectBuilder}. + * + * @param baseClass the base type + * @param className the name of the class to create + * @param the type being created + * @return a new {@link ObjectBuilder} + */ + static ObjectBuilder of(final ContextConfiguration contextConfiguration, + final Class baseClass, final String className) { + return new ObjectBuilder<>(contextConfiguration, baseClass, className); + } + + /** + * Adds a property used for constructing the object. + *

+ * The {@code name} must be the base name for a getter or setter so the type can be determined. + *

+ * + * @param name the name of the property + * @param value a string representing the value + * @return this builder + */ + ObjectBuilder addConstructorProperty(final String name, final String value) { + constructorProperties.put(name, value); + return this; + } + + /** + * Adds a method name to be executed after the object is created and the properties are set. The method must not + * have any parameters. + * + * @param methodNames the name of the method to execute + * @return this builder + */ + ObjectBuilder addPostConstructMethods(final String... methodNames) { + if (methodNames != null) { + Collections.addAll(postConstructMethods, methodNames); + } + return this; + } + + /** + * Adds a property to be set on the object after it has been created. + * + * @param name the name of the property + * @param value a string representing the value + * @return this builder + */ + ObjectBuilder addProperty(final String name, final String value) { + properties.put(name, value); + return this; + } + + /** + * Adds a defined property to be set after the object is created. + * + * @param name the name of the property + * @param type the type of the property + * @param value a supplier for the property value + * @return this builder + */ + ObjectBuilder addDefinedProperty(final String name, final Class type, final Supplier value) { + definedProperties.add(new PropertyValue(name, type, value)); + return this; + } + + /** + * Sets the name of the module used to load the object being created. + * + * @param moduleName the module name or {@code null} to use this class loader + * @return this builder + */ + ObjectBuilder setModuleName(final String moduleName) { + this.moduleName = moduleName; + return this; + } + + /** + * Creates a the object when the {@linkplain Supplier#get() supplier} value is accessed. + * + * @return a supplier which can create the object + */ + Supplier build() { + if (className == null) { + throw new IllegalArgumentException("className is null"); + } + final Map constructorProperties = new LinkedHashMap<>(this.constructorProperties); + final Map properties = new LinkedHashMap<>(this.properties); + final Set postConstructMethods = new LinkedHashSet<>(this.postConstructMethods); + return () -> { + final ClassLoader classLoader = getClass().getClassLoader(); + final Class actualClass; + try { + actualClass = Class.forName(className, true, classLoader).asSubclass(baseClass); + } catch (Exception e) { + throw new IllegalArgumentException(String.format("Failed to load class \"%s\"", className), e); + } + final int length = constructorProperties.size(); + final Class[] paramTypes = new Class[length]; + final Object[] params = new Object[length]; + int i = 0; + for (Map.Entry entry : constructorProperties.entrySet()) { + final String property = entry.getKey(); + final Class type = getConstructorPropertyType(actualClass, property); + if (type == null) { + throw new IllegalArgumentException( + String.format("No property named \"%s\" in \"%s\"", property, className)); + } + paramTypes[i] = type; + params[i] = getValue(actualClass, property, type, entry.getValue()); + i++; + } + final Constructor constructor; + try { + constructor = actualClass.getConstructor(paramTypes); + } catch (Exception e) { + throw new IllegalArgumentException(String.format("Failed to locate constructor in class \"%s\"", className), e); + } + + // Get all the setters + final Map setters = new LinkedHashMap<>(); + for (Map.Entry entry : properties.entrySet()) { + final Method method = getPropertySetter(actualClass, entry.getKey()); + if (method == null) { + throw new IllegalArgumentException(String + .format("Failed to locate setter for property \"%s\" on type \"%s\"", entry.getKey(), className)); + } + // Get the value type for the setter + Class type = getPropertyType(method); + if (type == null) { + throw new IllegalArgumentException(String + .format("Failed to determine type for setter \"%s\" on type \"%s\"", method.getName(), className)); + } + setters.put(method, getValue(actualClass, entry.getKey(), type, entry.getValue())); + } + + // Define known type parameters + for (PropertyValue value : definedProperties) { + final String methodName = getPropertySetterName(value.name); + try { + final Method method = actualClass.getMethod(methodName, value.type); + setters.put(method, value.value.get()); + } catch (NoSuchMethodException e) { + throw new IllegalArgumentException(String.format( + "Failed to find setter method for property \"%s\" on type \"%s\"", value.name, className), e); + } + } + + // Get all the post construct methods + final Set postConstruct = new LinkedHashSet<>(); + for (String methodName : postConstructMethods) { + try { + postConstruct.add(actualClass.getMethod(methodName)); + } catch (NoSuchMethodException e) { + throw new IllegalArgumentException( + String.format("Failed to find post construct method \"%s\" on type \"%s\"", methodName, className), + e); + } + } + try { + T instance = constructor.newInstance(params); + + // Execute setters + for (Map.Entry entry : setters.entrySet()) { + entry.getKey().invoke(instance, entry.getValue()); + } + + // Execute post construct methods + for (Method method : postConstruct) { + method.invoke(instance); + } + + return instance; + } catch (Exception e) { + throw new IllegalArgumentException(String.format("Failed to instantiate class \"%s\"", className), e); + } + }; + } + + @SuppressWarnings("unchecked") + private Object getValue(final Class objClass, final String propertyName, final Class paramType, final String value) { + if (value == null) { + if (paramType.isPrimitive()) { + throw new IllegalArgumentException( + String.format("Cannot assign null value to primitive property \"%s\" of %s", propertyName, objClass)); + } + return null; + } + final var trimmedValue = value.trim(); + if (paramType == String.class) { + // Don't use the trimmed value for strings + return value; + } else if (paramType == Level.class) { + return contextConfiguration.getContext().getLevelForName(trimmedValue); + } else if (paramType == java.util.logging.Logger.class) { + return contextConfiguration.getContext().getLogger(trimmedValue); + } else if (paramType == boolean.class || paramType == Boolean.class) { + return Boolean.valueOf(trimmedValue); + } else if (paramType == byte.class || paramType == Byte.class) { + return Byte.valueOf(trimmedValue); + } else if (paramType == short.class || paramType == Short.class) { + return Short.valueOf(trimmedValue); + } else if (paramType == int.class || paramType == Integer.class) { + return Integer.valueOf(trimmedValue); + } else if (paramType == long.class || paramType == Long.class) { + return Long.valueOf(trimmedValue); + } else if (paramType == float.class || paramType == Float.class) { + return Float.valueOf(trimmedValue); + } else if (paramType == double.class || paramType == Double.class) { + return Double.valueOf(trimmedValue); + } else if (paramType == char.class || paramType == Character.class) { + return !trimmedValue.isEmpty() ? trimmedValue.charAt(0) : 0; + } else if (paramType == TimeZone.class) { + return TimeZone.getTimeZone(trimmedValue); + } else if (paramType == Charset.class) { + return Charset.forName(trimmedValue); + } else if (paramType.isAssignableFrom(Level.class)) { + return Level.parse(trimmedValue); + } else if (paramType.isEnum()) { + return Enum.valueOf(paramType.asSubclass(Enum.class), trimmedValue); + } else if (contextConfiguration.hasObject(trimmedValue)) { + return contextConfiguration.getObject(trimmedValue); + } else if (definedPropertiesContains(propertyName)) { + final PropertyValue propertyValue = findDefinedProperty(propertyName); + if (propertyValue == null) { + throw new IllegalArgumentException("Unknown parameter type for property " + propertyName + " on " + objClass); + } + return propertyValue.value.get(); + } else { + throw new IllegalArgumentException("Unknown parameter type for property " + propertyName + " on " + objClass); + } + } + + private boolean definedPropertiesContains(final String name) { + return findDefinedProperty(name) != null; + } + + private PropertyValue findDefinedProperty(final String name) { + for (PropertyValue value : definedProperties) { + if (name.equals(value.name)) { + return value; + } + } + return null; + } + + private static Class getPropertyType(Class clazz, String propertyName) { + return getPropertyType(getPropertySetter(clazz, propertyName)); + } + + private static Class getPropertyType(final Method setter) { + return setter != null ? setter.getParameterTypes()[0] : null; + } + + private static Class getConstructorPropertyType(Class clazz, String propertyName) { + final Method getter = getPropertyGetter(clazz, propertyName); + return getter != null ? getter.getReturnType() : getPropertyType(clazz, propertyName); + } + + private static Method getPropertySetter(Class clazz, String propertyName) { + final String set = getPropertySetterName(propertyName); + for (Method method : clazz.getMethods()) { + if ((method.getName().equals(set) && Modifier.isPublic(method.getModifiers())) + && method.getParameterTypes().length == 1) { + return method; + } + } + return null; + } + + private static Method getPropertyGetter(Class clazz, String propertyName) { + final String upperPropertyName = Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1); + final Pattern pattern = Pattern.compile("(get|has|is)(" + Pattern.quote(upperPropertyName) + ")"); + for (Method method : clazz.getMethods()) { + if ((pattern.matcher(method.getName()).matches() && Modifier.isPublic(method.getModifiers())) + && method.getParameterTypes().length == 0) { + return method; + } + } + return null; + } + + private static String getPropertySetterName(final String propertyName) { + return "set" + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1); + } + + private static class PropertyValue implements Comparable { + final String name; + final Class type; + final Supplier value; + + private PropertyValue(final String name, final Class type, final Supplier value) { + this.name = name; + this.type = type; + this.value = value; + } + + @Override + public int compareTo(final PropertyValue o) { + return name.compareTo(o.name); + } + } +} diff --git a/logging/src/main/java/org/xbib/logging/configuration/PropertyContextConfiguration.java b/logging/src/main/java/org/xbib/logging/configuration/PropertyContextConfiguration.java new file mode 100644 index 0000000..4c6bae4 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/configuration/PropertyContextConfiguration.java @@ -0,0 +1,306 @@ +package org.xbib.logging.configuration; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Properties; +import java.util.function.Supplier; +import java.util.logging.ErrorManager; +import java.util.logging.Filter; +import java.util.logging.Formatter; +import java.util.logging.Handler; +import java.util.logging.Level; +import org.xbib.logging.LogContext; +import org.xbib.logging.Logger; +import org.xbib.logging.configuration.filters.FilterExpressions; +import org.xbib.logging.expression.Expression; +import org.xbib.logging.filters.AcceptAllFilter; +import org.xbib.logging.filters.DenyAllFilter; +import org.xbib.logging.util.StandardOutputStreams; + +/** + * A utility to parse a {@code logging.properties} file and configure a {@link LogContext}. + */ +@SuppressWarnings("WeakerAccess") +public class PropertyContextConfiguration extends ContextConfiguration { + + private static final String[] EMPTY_STRINGS = new String[0]; + private final Properties properties; + + private PropertyContextConfiguration(final LogContext logContext, final Properties properties) { + super(logContext); + this.properties = properties; + } + + /** + * Configures the {@link LogContext} based on the properties. + * + * @param logContext the log context to configure + * @param properties the properties used to configure the log context + * @return the context configuration for the properties + */ + public static PropertyContextConfiguration configure(final LogContext logContext, final Properties properties) { + final PropertyContextConfiguration config = new PropertyContextConfiguration( + Objects.requireNonNull(logContext), + Objects.requireNonNull(properties)); + config.doConfigure(); + return config; + } + + private void doConfigure() { + // Start with the list of loggers to configure. The root logger is always on the list. + configureLogger(""); + // And, for each logger name, configure any filters, handlers, etc. + for (String loggerName : getStringCsvArray("loggers")) { + configureLogger(loggerName); + } + // Configure any declared handlers. + for (String handlerName : getStringCsvArray("handlers")) { + configureHandler(handlerName); + } + // Configure any declared filters. + for (String filterName : getStringCsvArray("filters")) { + configureFilter(filterName); + } + // Configure any declared formatters. + for (String formatterName : getStringCsvArray("formatters")) { + configureFormatter(formatterName); + } + // Configure any declared error managers. + for (String errorManagerName : getStringCsvArray("errorManagers")) { + configureErrorManager(errorManagerName); + } + } + + @SuppressWarnings({"ConstantConditions"}) + private void configureLogger(final String loggerName) { + final Logger logger = getContext().getLogger(loggerName); + // Get logger level + final String levelName = getStringProperty(getKey("logger", loggerName, "level")); + if (levelName != null) { + logger.setLevel(Level.parse(levelName)); + } + // Get logger filters + final String filterName = getStringProperty(getKey("logger", loggerName, "filter")); + if (filterName != null) { + configureFilter(filterName); + logger.setFilter(getFilter(filterName)); + } + // Get logger handlers + final String[] handlerNames = getStringCsvArray(getKey("logger", loggerName, "handlers")); + for (String name : handlerNames) { + if (configureHandler(name)) { + logger.addHandler(getHandler(name)); + } + } + // Get logger properties + final String useParentHandlersString = getStringProperty(getKey("logger", loggerName, "useParentHandlers")); + if (useParentHandlersString != null) { + logger.setUseParentHandlers(resolveBooleanExpression(useParentHandlersString)); + } + final String useParentFiltersString = getStringProperty(getKey("logger", loggerName, "useParentFilters")); + if (useParentFiltersString != null) { + if (logger instanceof Logger) { + logger.setUseParentFilters(resolveBooleanExpression(useParentHandlersString)); + } + } + } + + private boolean configureHandler(final String handlerName) { + if (hasHandler(handlerName)) { + // already configured! + return true; + } + final String className = getStringProperty(getKey("handler", handlerName), true, false); + if (className == null) { + StandardOutputStreams.printError("Handler %s is not defined%n", handlerName); + return false; + } + + final ObjectBuilder handlerBuilder = ObjectBuilder.of(this, Handler.class, className) + .setModuleName(getStringProperty(getKey("handler", handlerName, "module"))) + .addPostConstructMethods(getStringCsvArray(getKey("handler", handlerName, "postConfiguration"))); + + // Configure the constructor properties + configureProperties(handlerBuilder, "handler", handlerName); + final String encoding = getStringProperty(getKey("handler", handlerName, "encoding")); + if (encoding != null) { + handlerBuilder.addProperty("encoding", encoding); + } + + final String filter = getStringProperty(getKey("handler", handlerName, "filter")); + if (filter != null) { + configureFilter(filter); + handlerBuilder.addDefinedProperty("filter", Filter.class, getFilters().get(filter)); + } + final String levelName = getStringProperty(getKey("handler", handlerName, "level")); + if (levelName != null) { + handlerBuilder.addProperty("level", levelName); + } + final String formatterName = getStringProperty(getKey("handler", handlerName, "formatter")); + if (formatterName != null) { + if (configureFormatter(formatterName)) { + handlerBuilder.addDefinedProperty("formatter", Formatter.class, getFormatters() + .get(formatterName)); + } + } + final String errorManagerName = getStringProperty(getKey("handler", handlerName, "errorManager")); + if (errorManagerName != null) { + if (configureErrorManager(errorManagerName)) { + handlerBuilder.addDefinedProperty("errorManager", ErrorManager.class, getErrorManagers() + .get(errorManagerName)); + } + } + + final String[] handlerNames = getStringCsvArray(getKey("handler", handlerName, "handlers")); + if (handlerNames.length > 0) { + final List> subhandlers = new ArrayList<>(); + for (String name : handlerNames) { + if (configureHandler(name)) { + subhandlers.add(getHandlers().get(name)); + } + } + handlerBuilder.addDefinedProperty("handlers", Handler[].class, (Supplier) () -> { + if (subhandlers.isEmpty()) { + return new Handler[0]; + } + final Handler[] result = new Handler[subhandlers.size()]; + int i = 0; + for (Supplier supplier : subhandlers) { + result[i++] = supplier.get(); + } + return result; + }); + } + addHandler(handlerName, handlerBuilder.build()); + return true; + } + + private boolean configureFormatter(final String formatterName) { + if (hasFilter(formatterName)) { + // already configured! + return true; + } + final String className = getStringProperty(getKey("formatter", formatterName), true, false); + if (className == null) { + StandardOutputStreams.printError("Formatter %s is not defined%n", formatterName); + return false; + } + final ObjectBuilder formatterBuilder = ObjectBuilder.of(this, Formatter.class, className) + .setModuleName(getStringProperty(getKey("formatter", formatterName, "module"))) + .addPostConstructMethods(getStringCsvArray(getKey("formatter", formatterName, "postConfiguration"))); + configureProperties(formatterBuilder, "formatter", formatterName); + addFormatter(formatterName, formatterBuilder.build()); + return true; + } + + private boolean configureErrorManager(final String errorManagerName) { + if (hasErrorManager(errorManagerName)) { + // already configured! + return true; + } + final String className = getStringProperty(getKey("errorManager", errorManagerName), true, false); + if (className == null) { + StandardOutputStreams.printError("Error manager %s is not defined%n", errorManagerName); + return false; + } + final ObjectBuilder errorManagerBuilder = ObjectBuilder.of(this, ErrorManager.class, className) + .setModuleName(getStringProperty(getKey("errorManager", errorManagerName, "module"))) + .addPostConstructMethods(getStringCsvArray(getKey("errorManager", errorManagerName, "postConfiguration"))); + configureProperties(errorManagerBuilder, "errorManager", errorManagerName); + addErrorManager(errorManagerName, errorManagerBuilder.build()); + return true; + } + + private void configureFilter(final String filterName) { + if (hasFilter(filterName)) { + return; + } + // First determine if we're using a defined filters or filters expression. We assume a defined filters if there is + // a filters.NAME property. + String filterValue = getStringProperty(getKey("filter", filterName), true, false); + if (filterValue == null) { + // We are a filters expression, parse the expression and create a filters + addFilter(filterName, () -> FilterExpressions.parse(getContext(), filterName)); + } else { + // The AcceptAllFilter and DenyAllFilter are singletons. + if (AcceptAllFilter.class.getName().equals(filterValue)) { + addFilter(filterName, AcceptAllFilter::getInstance); + } else if (DenyAllFilter.class.getName().equals(filterValue)) { + addFilter(filterName, DenyAllFilter::getInstance); + } else { + // We assume we're a defined filter + final ObjectBuilder filterBuilder = ObjectBuilder.of(this, Filter.class, filterValue) + .setModuleName(getStringProperty(getKey("filter", filterName, "module"))) + .addPostConstructMethods(getStringCsvArray(getKey("filter", filterName, "postConfiguration"))); + configureProperties(filterBuilder, "errorManager", filterName); + addFilter(filterName, filterBuilder.build()); + } + } + } + + private String getStringProperty(final String key) { + return getStringProperty(key, true, true); + } + + private String getStringProperty(final String key, final boolean trim, final boolean resolveExpression) { + String value = properties.getProperty(key); + if (resolveExpression && value != null) { + value = resolveExpression(value); + } else if (value != null && trim) { + value = value.trim(); + } + return value; + } + + private String[] getStringCsvArray(final String key) { + final String property = properties.getProperty(key, ""); + if (property == null) { + return EMPTY_STRINGS; + } + final String value = property.trim(); + if (value.isEmpty()) { + return EMPTY_STRINGS; + } + return value.split("\\s*,\\s*"); + } + + private void configureProperties(final ObjectBuilder builder, final String prefix, final String name) { + // First configure constructor properties + final String[] constructorPropertyNames = getStringCsvArray(getKey(prefix, name, "constructorProperties")); + for (String propertyName : constructorPropertyNames) { + final String valueString = getStringProperty(getKey(prefix, name, propertyName), false, true); + if (valueString != null) + builder.addConstructorProperty(propertyName, valueString); + } + + // Next configure setter properties + final String[] propertyNames = getStringCsvArray(getKey(prefix, name, "properties")); + for (String propertyName : propertyNames) { + final String valueString = getStringProperty(getKey(prefix, name, propertyName), false, true); + if (valueString != null) + builder.addProperty(propertyName, valueString); + } + } + + private static String getKey(final String prefix, final String objectName) { + return !objectName.isEmpty() ? prefix + "." + objectName : prefix; + } + + private static String getKey(final String prefix, final String objectName, final String key) { + return !objectName.isEmpty() ? prefix + "." + objectName + "." + key : prefix + "." + key; + } + + private static boolean resolveBooleanExpression(final String possibleExpression) { + final String value = resolveExpression(possibleExpression); + return !value.toLowerCase(Locale.ROOT).equals("false"); + } + + private static String resolveExpression(final String possibleExpression) { + final EnumSet flags = EnumSet.noneOf(Expression.Flag.class); + final Expression expression = Expression.compile(possibleExpression, flags); + return expression.evaluateWithPropertiesAndEnvironment(false); + } +} diff --git a/logging/src/main/java/org/xbib/logging/configuration/PropertyLogContextConfigurator.java b/logging/src/main/java/org/xbib/logging/configuration/PropertyLogContextConfigurator.java new file mode 100644 index 0000000..91ea0a2 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/configuration/PropertyLogContextConfigurator.java @@ -0,0 +1,93 @@ +package org.xbib.logging.configuration; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Iterator; +import java.util.Properties; +import java.util.ServiceLoader; +import java.util.logging.Logger; +import org.xbib.logging.Level; +import org.xbib.logging.LogContext; +import org.xbib.logging.LogContextConfigurator; +import org.xbib.logging.formatters.PatternFormatter; +import org.xbib.logging.handlers.ConsoleHandler; +import org.xbib.logging.util.StandardOutputStreams; + +/** + * A default log context configuration. + *

+ * If the {@linkplain #configure(LogContext, InputStream) input stream} is {@code null} an attempt is made to find a + * {@code logging.properties} file. If the file is not found, a {@linkplain ServiceLoader service loader} is + * used to find the first implementation of a {@link LogContextConfigurator}. If that fails a default + * {@link ConsoleHandler} is configured with the pattern {@code %d{yyyy-MM-dd'T'HH:mm:ssXXX} %-5p [%c] (%t) %s%e%n}. + *

+ * + *

+ * Locating the {@code logging.properties} happens in the following order: + *

    + *
  • The {@code logging.configuration} system property is checked
  • + *
  • The current threads {@linkplain ClassLoader#getResourceAsStream(String)} class loader} for a + * {@code logging.properties}
  • + *
  • Finally {@link Class#getResourceAsStream(String)} is used to locate a {@code logging.properties}
  • + *
+ *

+ */ +public class PropertyLogContextConfigurator implements LogContextConfigurator { + + public PropertyLogContextConfigurator() { + } + + @Override + public void configure(final LogContext logContext, final InputStream inputStream) { + final InputStream configIn = inputStream != null ? inputStream : findConfiguration(); + final LogContext context = logContext == null ? LogContext.getLogContext() : logContext; + // Configure the log context based on a property file + if (configIn != null) { + final Properties properties = new Properties(); + try (Reader reader = new InputStreamReader(configIn, StandardCharsets.UTF_8)) { + properties.load(reader); + } catch (IOException e) { + throw new RuntimeException("Failed to configure log manager with configuration file", e); + } + final PropertyContextConfiguration configurator = PropertyContextConfiguration.configure(context, properties); + context.attachIfAbsent(ContextConfiguration.CONTEXT_CONFIGURATION_KEY, configurator); + } else { + // Next check the service loader + final Iterator serviceLoader = + ServiceLoader.load(LogContextConfigurator.class, PropertyLogContextConfigurator.class.getClassLoader()).iterator(); + if (serviceLoader.hasNext()) { + serviceLoader.next().configure(context, null); + } else { + // Configure a default console handler, pattern formatter and associated with the root logger + final ConsoleHandler handler = + new ConsoleHandler(new PatternFormatter("%d{yyyy-MM-dd'T'HH:mm:ssXXX} %-5p [%c] (%t) %s%e%n")); + handler.setLevel(Level.INFO); + handler.setAutoFlush(true); + final Logger rootLogger = context.getLogger(""); + rootLogger.setLevel(Level.INFO); + rootLogger.addHandler(handler); + } + } + } + + private static InputStream findConfiguration() { + final String propLoc = System.getProperty("logging.configuration"); + if (propLoc != null) { + try { + return URI.create(propLoc).toURL().openStream(); + } catch (IOException e) { + StandardOutputStreams.printError("Unable to read the logging configuration from '%s' (%s)%n", propLoc, e); + } + } + final ClassLoader cl = PropertyLogContextConfigurator.class.getClassLoader(); + try { + return cl.getResourceAsStream("logging.properties"); + } catch (Exception ignore) { + return null; + } + } +} diff --git a/logging/src/main/java/org/xbib/logging/configuration/filters/FilterExpressions.java b/logging/src/main/java/org/xbib/logging/configuration/filters/FilterExpressions.java new file mode 100644 index 0000000..b51f5d8 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/configuration/filters/FilterExpressions.java @@ -0,0 +1,257 @@ +package org.xbib.logging.configuration.filters; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.logging.Filter; +import java.util.logging.Level; +import org.xbib.logging.LogContext; +import org.xbib.logging.filters.AcceptAllFilter; +import org.xbib.logging.filters.AllFilter; +import org.xbib.logging.filters.AnyFilter; +import org.xbib.logging.filters.DenyAllFilter; +import org.xbib.logging.filters.InvertFilter; +import org.xbib.logging.filters.LevelChangingFilter; +import org.xbib.logging.filters.LevelFilter; +import org.xbib.logging.filters.LevelRangeFilter; +import org.xbib.logging.filters.RegexFilter; +import org.xbib.logging.filters.SubstituteFilter; +import static java.lang.Character.isJavaIdentifierPart; +import static java.lang.Character.isJavaIdentifierStart; +import static java.lang.Character.isWhitespace; + +/** + * Helper class to parse filter expressions. + */ +public class FilterExpressions { + + private static final String ACCEPT = "accept"; + private static final String ALL = "all"; + private static final String ANY = "any"; + private static final String DENY = "deny"; + private static final String LEVELS = "levels"; + private static final String LEVEL_CHANGE = "levelChange"; + private static final String LEVEL_RANGE = "levelRange"; + private static final String MATCH = "match"; + private static final String NOT = "not"; + private static final String SUBSTITUTE = "substitute"; + private static final String SUBSTITUTE_ALL = "substituteAll"; + + /** + * Pareses a filter expression and returns the parsed filter. + * + * @param logContext the log context this filter is for + * @param expression the filter expression + * @return the created filter + */ + public static Filter parse(final LogContext logContext, final String expression) { + final Iterator iterator = tokens(expression).iterator(); + return parseFilterExpression(logContext, iterator, true); + } + + private static Filter parseFilterExpression(final LogContext logContext, final Iterator iterator, + final boolean outermost) { + if (!iterator.hasNext()) { + if (outermost) { + return null; + } + throw endOfExpression(); + } + final String token = iterator.next(); + switch (token) { + case ACCEPT -> { + return AcceptAllFilter.getInstance(); + } + case DENY -> { + return DenyAllFilter.getInstance(); + } + case NOT -> { + expect("(", iterator); + final Filter nested = parseFilterExpression(logContext, iterator, false); + expect(")", iterator); + return new InvertFilter(nested); + } + case ALL -> { + expect("(", iterator); + final List filters = new ArrayList<>(); + do { + filters.add(parseFilterExpression(logContext, iterator, false)); + } while (expect(",", ")", iterator)); + return new AllFilter(filters); + } + case ANY -> { + expect("(", iterator); + final List filters = new ArrayList<>(); + do { + filters.add(parseFilterExpression(logContext, iterator, false)); + } while (expect(",", ")", iterator)); + return new AnyFilter(filters); + } + case LEVEL_CHANGE -> { + expect("(", iterator); + final Level level = logContext.getLevelForName(expectName(iterator)); + expect(")", iterator); + return new LevelChangingFilter(level); + } + case LEVELS -> { + expect("(", iterator); + final Set levels = new HashSet<>(); + do { + levels.add(logContext.getLevelForName(expectName(iterator))); + } while (expect(",", ")", iterator)); + return new LevelFilter(levels); + } + case LEVEL_RANGE -> { + final boolean minInclusive = expect("[", "(", iterator); + final Level minLevel = logContext.getLevelForName(expectName(iterator)); + expect(",", iterator); + final Level maxLevel = logContext.getLevelForName(expectName(iterator)); + final boolean maxInclusive = expect("]", ")", iterator); + return new LevelRangeFilter(minLevel, minInclusive, maxLevel, maxInclusive); + } + case MATCH -> { + expect("(", iterator); + final String pattern = expectString(iterator); + expect(")", iterator); + return new RegexFilter(pattern); + } + case SUBSTITUTE -> { + expect("(", iterator); + final String pattern = expectString(iterator); + expect(",", iterator); + final String replacement = expectString(iterator); + expect(")", iterator); + return new SubstituteFilter(pattern, replacement, false); + } + case SUBSTITUTE_ALL -> { + expect("(", iterator); + final String pattern = expectString(iterator); + expect(",", iterator); + final String replacement = expectString(iterator); + expect(")", iterator); + return new SubstituteFilter(pattern, replacement, true); + } + case null, default -> { + final String name = expectName(iterator); + throw new IllegalArgumentException(String.format("No filter named \"%s\" is defined", name)); + } + } + } + + private static String expectName(Iterator iterator) { + if (iterator.hasNext()) { + final String next = iterator.next(); + if (isJavaIdentifierStart(next.codePointAt(0))) { + return next; + } + } + throw new IllegalArgumentException("Expected identifier next in filter expression"); + } + + private static String expectString(final Iterator iterator) { + if (iterator.hasNext()) { + final String next = iterator.next(); + if (next.codePointAt(0) == '"') { + return next.substring(1); + } + } + throw new IllegalArgumentException("Expected string next in filter expression"); + } + + private static boolean expect(final String trueToken, final String falseToken, final Iterator iterator) { + final boolean hasNext = iterator.hasNext(); + final String next = hasNext ? iterator.next() : null; + final boolean result; + if (!hasNext || !((result = trueToken.equals(next)) || falseToken.equals(next))) { + throw new IllegalArgumentException( + "Expected '" + trueToken + "' or '" + falseToken + "' next in filter expression"); + } + return result; + } + + private static void expect(String token, Iterator iterator) { + if (!iterator.hasNext() || !token.equals(iterator.next())) { + throw new IllegalArgumentException("Expected '" + token + "' next in filter expression"); + } + } + + private static IllegalArgumentException endOfExpression() { + return new IllegalArgumentException("Unexpected end of filter expression"); + } + + @SuppressWarnings("UnusedAssignment") + private static List tokens(final String source) { + final List tokens = new ArrayList<>(); + final int length = source.length(); + int idx = 0; + while (idx < length) { + int ch; + ch = source.codePointAt(idx); + if (isWhitespace(ch)) { + ch = source.codePointAt(idx); + idx = source.offsetByCodePoints(idx, 1); + } else if (isJavaIdentifierStart(ch)) { + int start = idx; + do { + idx = source.offsetByCodePoints(idx, 1); + } while (idx < length && isJavaIdentifierPart(ch = source.codePointAt(idx))); + tokens.add(source.substring(start, idx)); + } else if (ch == '"') { + final StringBuilder b = new StringBuilder(); + // tag token as a string + b.append('"'); + idx = source.offsetByCodePoints(idx, 1); + while (idx < length && (ch = source.codePointAt(idx)) != '"') { + ch = source.codePointAt(idx); + if (ch == '\\') { + idx = source.offsetByCodePoints(idx, 1); + if (idx == length) { + throw new IllegalArgumentException("Truncated filter expression string"); + } + ch = source.codePointAt(idx); + switch (ch) { + case '\\': + b.append('\\'); + break; + case '\'': + b.append('\''); + break; + case '"': + b.append('"'); + break; + case 'b': + b.append('\b'); + break; + case 'f': + b.append('\f'); + break; + case 'n': + b.append('\n'); + break; + case 'r': + b.append('\r'); + break; + case 't': + b.append('\t'); + break; + default: + throw new IllegalArgumentException("Invalid escape found in filter expression string"); + } + } else { + b.appendCodePoint(ch); + } + idx = source.offsetByCodePoints(idx, 1); + } + idx = source.offsetByCodePoints(idx, 1); + tokens.add(b.toString()); + } else { + int start = idx; + idx = source.offsetByCodePoints(idx, 1); + tokens.add(source.substring(start, idx)); + } + } + return tokens; + } +} diff --git a/logging/src/main/java/org/xbib/logging/errormanager/HandlerErrorManager.java b/logging/src/main/java/org/xbib/logging/errormanager/HandlerErrorManager.java new file mode 100644 index 0000000..53f3c81 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/errormanager/HandlerErrorManager.java @@ -0,0 +1,33 @@ +package org.xbib.logging.errormanager; + +import java.util.logging.Handler; +import org.xbib.logging.ExtErrorManager; + +/** + * An error manager which publishes errors to a handler. + */ +public final class HandlerErrorManager extends ExtErrorManager { + private static final ThreadLocal handlingError = new ThreadLocal<>(); + + private final Handler handler; + + /** + * Construct a new instance. + * + * @param handler the handler to set (must not be {@code null}) + */ + public HandlerErrorManager(final Handler handler) { + this.handler = handler; + } + + public void error(final String msg, final Exception ex, final int code) { + if (handlingError.get() != Boolean.TRUE) { + handlingError.set(Boolean.TRUE); + try { + handler.publish(errorToLogRecord(msg, ex, code)); + } finally { + handlingError.remove(); + } + } + } +} diff --git a/logging/src/main/java/org/xbib/logging/errormanager/OnlyOnceErrorManager.java b/logging/src/main/java/org/xbib/logging/errormanager/OnlyOnceErrorManager.java new file mode 100644 index 0000000..9035fa5 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/errormanager/OnlyOnceErrorManager.java @@ -0,0 +1,47 @@ +package org.xbib.logging.errormanager; + +import java.util.logging.ErrorManager; +import org.xbib.logging.ExtErrorManager; + +/** + * An error manager which runs only once and delegates to the given error manager. + */ +public final class OnlyOnceErrorManager extends ExtErrorManager { + + private volatile ErrorManager delegate; + + /** + * Construct a new instance. + * + * @param delegate the delegate error manager + */ + public OnlyOnceErrorManager(final ErrorManager delegate) { + this.delegate = delegate; + } + + /** + * Construct a new instance with a {@link SimpleErrorManager} as a delegate. + */ + public OnlyOnceErrorManager() { + this(new SimpleErrorManager()); + } + + /** + * {@inheritDoc} + */ + public void error(final String msg, final Exception ex, final int code) { + ErrorManager delegate = this.delegate; + if (delegate == null) { + return; + } else { + synchronized (this) { + delegate = this.delegate; + if (delegate == null) { + return; + } + this.delegate = null; + } + } + delegate.error(msg, ex, code); + } +} diff --git a/logging/src/main/java/org/xbib/logging/errormanager/SimpleErrorManager.java b/logging/src/main/java/org/xbib/logging/errormanager/SimpleErrorManager.java new file mode 100644 index 0000000..372141d --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/errormanager/SimpleErrorManager.java @@ -0,0 +1,13 @@ +package org.xbib.logging.errormanager; + +import org.xbib.logging.ExtErrorManager; +import org.xbib.logging.util.StandardOutputStreams; + +/** + * An error manager which simply prints a message the system error stream. + */ +public class SimpleErrorManager extends ExtErrorManager { + public void error(final String msg, final Exception ex, final int code) { + StandardOutputStreams.printError(ex, "LogManager error of type %s: %s%n", nameForCode(code), msg); + } +} diff --git a/logging/src/main/java/org/xbib/logging/expression/CompositeNode.java b/logging/src/main/java/org/xbib/logging/expression/CompositeNode.java new file mode 100644 index 0000000..daa758a --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/expression/CompositeNode.java @@ -0,0 +1,38 @@ +package org.xbib.logging.expression; + +import java.util.HashSet; +import java.util.List; + +final class CompositeNode extends Node { + private final Node[] subNodes; + + CompositeNode(final Node[] subNodes) { + this.subNodes = subNodes; + } + + CompositeNode(final List subNodes) { + this.subNodes = subNodes.toArray(NO_NODES); + } + + void emit(final ResolveContext context, + final ExceptionBiConsumer, StringBuilder, E> resolveFunction) throws E { + for (Node subNode : subNodes) { + subNode.emit(context, resolveFunction); + } + } + + void catalog(final HashSet strings) { + for (Node node : subNodes) { + node.catalog(strings); + } + } + + public String toString() { + StringBuilder b = new StringBuilder(); + b.append('*'); + for (Node subNode : subNodes) { + b.append('<').append(subNode.toString()).append('>'); + } + return b.toString(); + } +} diff --git a/logging/src/main/java/org/xbib/logging/expression/ExceptionBiConsumer.java b/logging/src/main/java/org/xbib/logging/expression/ExceptionBiConsumer.java new file mode 100644 index 0000000..6bf9745 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/expression/ExceptionBiConsumer.java @@ -0,0 +1,28 @@ +package org.xbib.logging.expression; + +/** + * A two-argument consumer which can throw an exception. + */ +@FunctionalInterface +public interface ExceptionBiConsumer { + /** + * Performs this operation on the given arguments. + * + * @param t the first argument + * @param u the second argument + * @throws E if an exception occurs + */ + void accept(T t, U u) throws E; + + default ExceptionBiConsumer andThen(ExceptionBiConsumer after) { + return (t, u) -> { + accept(t, u); + after.accept(t, u); + }; + } + + default ExceptionRunnable compose(ExceptionSupplier before1, + ExceptionSupplier before2) { + return () -> accept(before1.get(), before2.get()); + } +} diff --git a/logging/src/main/java/org/xbib/logging/expression/ExceptionBiFunction.java b/logging/src/main/java/org/xbib/logging/expression/ExceptionBiFunction.java new file mode 100644 index 0000000..4e57fca --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/expression/ExceptionBiFunction.java @@ -0,0 +1,31 @@ +package org.xbib.logging.expression; + +/** + * A two-argument function which can throw an exception. + */ +@FunctionalInterface +public interface ExceptionBiFunction { + /** + * Applies this function to the given arguments. + * + * @param t the first argument + * @param u the second argument + * @return the function result + * @throws E if an exception occurs + */ + R apply(T t, U u) throws E; + + default ExceptionBiFunction andThen(ExceptionFunction after) { + return (t, u) -> after.apply(apply(t, u)); + } + + @SuppressWarnings("overloads") + default ExceptionBiConsumer andThen(ExceptionConsumer after) { + return (t, u) -> after.accept(apply(t, u)); + } + + default ExceptionSupplier compose(ExceptionSupplier before1, + ExceptionSupplier before2) { + return () -> apply(before1.get(), before2.get()); + } +} diff --git a/logging/src/main/java/org/xbib/logging/expression/ExceptionBiPredicate.java b/logging/src/main/java/org/xbib/logging/expression/ExceptionBiPredicate.java new file mode 100644 index 0000000..4129ee1 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/expression/ExceptionBiPredicate.java @@ -0,0 +1,33 @@ +package org.xbib.logging.expression; + +/** + * A two-argument predicate which can throw an exception. + */ +@FunctionalInterface +public interface ExceptionBiPredicate { + /** + * Evaluate this predicate on the given arguments. + * + * @param t the first argument + * @param u the second argument + * @return {@code true} if the predicate passes, {@code false} otherwise + * @throws E if an exception occurs + */ + boolean test(T t, U u) throws E; + + default ExceptionBiPredicate and(ExceptionBiPredicate other) { + return (t, u) -> test(t, u) && other.test(t, u); + } + + default ExceptionBiPredicate or(ExceptionBiPredicate other) { + return (t, u) -> test(t, u) || other.test(t, u); + } + + default ExceptionBiPredicate xor(ExceptionBiPredicate other) { + return (t, u) -> test(t, u) != other.test(t, u); + } + + default ExceptionBiPredicate not() { + return (t, u) -> !test(t, u); + } +} diff --git a/logging/src/main/java/org/xbib/logging/expression/ExceptionConsumer.java b/logging/src/main/java/org/xbib/logging/expression/ExceptionConsumer.java new file mode 100644 index 0000000..fdc3104 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/expression/ExceptionConsumer.java @@ -0,0 +1,33 @@ +package org.xbib.logging.expression; + +/** + * A one-argument consumer which can throw an exception. + */ +@FunctionalInterface +public interface ExceptionConsumer { + /** + * Performs this operation on the given argument. + * + * @param t the argument + * @throws E if an exception occurs + */ + void accept(T t) throws E; + + default ExceptionConsumer andThen(ExceptionConsumer after) { + return t -> { + accept(t); + after.accept(t); + }; + } + + default ExceptionConsumer compose(ExceptionConsumer before) { + return t -> { + accept(t); + before.accept(t); + }; + } + + default ExceptionRunnable compose(ExceptionSupplier before) { + return () -> accept(before.get()); + } +} diff --git a/logging/src/main/java/org/xbib/logging/expression/ExceptionFunction.java b/logging/src/main/java/org/xbib/logging/expression/ExceptionFunction.java new file mode 100644 index 0000000..c38bdf3 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/expression/ExceptionFunction.java @@ -0,0 +1,54 @@ +package org.xbib.logging.expression; + +/** + * A one-argument function which can throw an exception. + */ +@FunctionalInterface +public interface ExceptionFunction { + /** + * Applies this function to the given arguments. + * + * @param t the argument + * @return the function result + * @throws E if an exception occurs + */ + R apply(T t) throws E; + + default ExceptionFunction andThen(ExceptionFunction after) { + return t -> after.apply(apply(t)); + } + + default ExceptionFunction andThen( + ExceptionBiFunction after) { + return t -> after.apply(t, apply(t)); + } + + @SuppressWarnings("overloads") + default ExceptionFunction compose(ExceptionFunction before) { + return t -> apply(before.apply(t)); + } + + @SuppressWarnings("overloads") + default ExceptionConsumer andThen(ExceptionConsumer after) { + return t -> after.accept(apply(t)); + } + + @SuppressWarnings("overloads") + default ExceptionConsumer andThen(ExceptionBiConsumer after) { + return t -> after.accept(t, apply(t)); + } + + @SuppressWarnings("overloads") + default ExceptionPredicate andThen(ExceptionPredicate after) { + return t -> after.test(apply(t)); + } + + @SuppressWarnings("overloads") + default ExceptionPredicate andThen(ExceptionBiPredicate after) { + return t -> after.test(t, apply(t)); + } + + default ExceptionSupplier compose(ExceptionSupplier before) { + return () -> apply(before.get()); + } +} diff --git a/logging/src/main/java/org/xbib/logging/expression/ExceptionPredicate.java b/logging/src/main/java/org/xbib/logging/expression/ExceptionPredicate.java new file mode 100644 index 0000000..447d554 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/expression/ExceptionPredicate.java @@ -0,0 +1,36 @@ +package org.xbib.logging.expression; + +/** + * A one-argument predicate which can throw an exception. + */ +@FunctionalInterface +public interface ExceptionPredicate { + /** + * Evaluate this predicate on the given arguments. + * + * @param t the first argument + * @return {@code true} if the predicate passes, {@code false} otherwise + * @throws E if an exception occurs + */ + boolean test(T t) throws E; + + default ExceptionPredicate and(ExceptionPredicate other) { + return t -> test(t) && other.test(t); + } + + default ExceptionPredicate or(ExceptionPredicate other) { + return t -> test(t) || other.test(t); + } + + default ExceptionPredicate xor(ExceptionPredicate other) { + return t -> test(t) != other.test(t); + } + + default ExceptionPredicate not() { + return t -> !test(t); + } + + default ExceptionBiPredicate with(ExceptionPredicate other) { + return (t, u) -> test(t) && other.test(u); + } +} diff --git a/logging/src/main/java/org/xbib/logging/expression/ExceptionRunnable.java b/logging/src/main/java/org/xbib/logging/expression/ExceptionRunnable.java new file mode 100644 index 0000000..8c18647 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/expression/ExceptionRunnable.java @@ -0,0 +1,27 @@ +package org.xbib.logging.expression; + +/** + * An operation that can throw an exception. + */ +public interface ExceptionRunnable { + /** + * Run the operation. + * + * @throws E if an exception occurs + */ + void run() throws E; + + default ExceptionRunnable andThen(ExceptionRunnable after) { + return () -> { + run(); + after.run(); + }; + } + + default ExceptionRunnable compose(ExceptionRunnable before) { + return () -> { + before.run(); + run(); + }; + } +} diff --git a/logging/src/main/java/org/xbib/logging/expression/ExceptionSupplier.java b/logging/src/main/java/org/xbib/logging/expression/ExceptionSupplier.java new file mode 100644 index 0000000..eb90d76 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/expression/ExceptionSupplier.java @@ -0,0 +1,24 @@ +package org.xbib.logging.expression; + +/** + * A supplier which can throw an exception. + */ +@FunctionalInterface +public interface ExceptionSupplier { + /** + * Gets a result. + * + * @return the result + * @throws E if an exception occurs + */ + T get() throws E; + + default ExceptionRunnable andThen(ExceptionConsumer after) { + return () -> after.accept(get()); + } + + @SuppressWarnings("overloads") + default ExceptionSupplier andThen(ExceptionFunction after) { + return () -> after.apply(get()); + } +} diff --git a/logging/src/main/java/org/xbib/logging/expression/Expression.java b/logging/src/main/java/org/xbib/logging/expression/Expression.java new file mode 100644 index 0000000..c9dea3a --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/expression/Expression.java @@ -0,0 +1,693 @@ +package org.xbib.logging.expression; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.BiConsumer; + +/** + * A compiled property-expansion expression string. An expression string is a mix of plain strings and expression + * segments, which are wrapped by the sequence "{@code ${ ... }}". + */ +public final class Expression { + private final Node content; + private final Set referencedStrings; + + Expression(Node content) { + this.content = content; + HashSet strings = new HashSet<>(); + content.catalog(strings); + referencedStrings = strings.isEmpty() ? Collections.emptySet() + : strings.size() == 1 ? Collections.singleton(strings.iterator().next()) : Collections.unmodifiableSet(strings); + } + + /** + * Get the immutable set of string keys that are referenced by expressions in this compiled expression. If there + * are no expansions in this expression, the set is empty. Note that this will not include any string keys + * that themselves contain expressions, in the case that {@link Flag#NO_RECURSE_KEY} was not specified. + * + * @return the immutable set of strings (not {@code null}) + */ + public Set getReferencedStrings() { + return referencedStrings; + } + + /** + * Evaluate the expression with the given expansion function, which may throw a checked exception. The given "function" + * is a predicate which returns {@code true} if the expansion succeeded or {@code false} if it failed (in which case + * a default value may be used). If expansion succeeds, the expansion function should append the result to the + * given {@link StringBuilder}. + * + * @param expandFunction the expansion function to apply (must not be {@code null}) + * @param the exception type thrown by the expansion function + * @return the expanded string + * @throws E if the expansion function throws an exception + */ + public String evaluateException( + final ExceptionBiConsumer, StringBuilder, E> expandFunction) throws E { + final StringBuilder b = new StringBuilder(); + content.emit(new ResolveContext(expandFunction, b), expandFunction); + return b.toString(); + } + + /** + * Evaluate the expression with the given expansion function. The given "function" + * is a predicate which returns {@code true} if the expansion succeeded or {@code false} if it failed (in which case + * a default value may be used). If expansion succeeds, the expansion function should append the result to the + * given {@link StringBuilder}. + * + * @param expandFunction the expansion function to apply (must not be {@code null}) + * @return the expanded string + */ + public String evaluate(BiConsumer, StringBuilder> expandFunction) { + return evaluateException(expandFunction::accept); + } + + /** + * Evaluate the expression using a default expansion function that evaluates system and environment properties + * in the style using the prefix {@code "env."} to designate an environment property. + * + * @param failOnNoDefault {@code true} to throw an {@link IllegalArgumentException} if an unresolvable key has no + * default value; {@code false} to expand such keys to an empty string + * @return the expanded string + */ + public String evaluateWithPropertiesAndEnvironment(boolean failOnNoDefault) { + return evaluate((c, b) -> { + final String key = c.getKey(); + if (key.startsWith("env.")) { + final String env = key.substring(4); + final String val = System.getenv(env); + if (val == null) { + if (failOnNoDefault && !c.hasDefault()) { + throw new IllegalArgumentException(); // Messages.msg.unresolvedEnvironmentProperty(env); + } + c.expandDefault(); + } else { + b.append(val); + } + } else { + final String val = System.getProperty(key); + if (val == null) { + if (failOnNoDefault && !c.hasDefault()) { + throw new IllegalArgumentException(); //Messages.msg.unresolvedSystemProperty(key); + } + c.expandDefault(); + } else { + b.append(val); + } + } + }); + } + + /** + * Evaluate the expression using a default expansion function that evaluates system properties. + * + * @param failOnNoDefault {@code true} to throw an {@link IllegalArgumentException} if an unresolvable key has no + * default value; {@code false} to expand such keys to an empty string + * @return the expanded string + */ + public String evaluateWithProperties(boolean failOnNoDefault) { + return evaluate((c, b) -> { + final String key = c.getKey(); + final String val = System.getProperty(key); + if (val == null) { + if (failOnNoDefault && !c.hasDefault()) { + throw new IllegalArgumentException(); // Messages.msg.unresolvedSystemProperty(key); + } + c.expandDefault(); + } else { + b.append(val); + } + }); + } + + /** + * Evaluate the expression using a default expansion function that evaluates environment properties. + * + * @param failOnNoDefault {@code true} to throw an {@link IllegalArgumentException} if an unresolvable key has no + * default value; {@code false} to expand such keys to an empty string + * @return the expanded string + */ + public String evaluateWithEnvironment(boolean failOnNoDefault) { + return evaluate((c, b) -> { + final String key = c.getKey(); + final String val = System.getenv(key); + if (val == null) { + if (failOnNoDefault && !c.hasDefault()) { + throw new IllegalArgumentException(); //Messages.msg.unresolvedEnvironmentProperty(key); + } + c.expandDefault(); + } else { + b.append(val); + } + }); + } + + /** + * Compile an expression string. + * + * @param string the expression string (must not be {@code null}) + * @param flags optional flags to apply which affect the compilation + * @return the compiled expression (not {@code null}) + */ + public static Expression compile(String string, Flag... flags) { + return compile(string, flags == null || flags.length == 0 ? NO_FLAGS : EnumSet.of(flags[0], flags)); + } + + /** + * Compile an expression string. + * + * @param string the expression string (must not be {@code null}) + * @param flags optional flags to apply which affect the compilation (must not be {@code null}) + * @return the compiled expression (not {@code null}) + */ + public static Expression compile(String string, EnumSet flags) { + final Node content; + final Itr itr; + if (flags.contains(Flag.NO_TRIM)) { + itr = new Itr(string); + } else { + itr = new Itr(string.trim()); + } + content = parseString(itr, true, false, false, flags); + return content == Node.NULL ? EMPTY : new Expression(content); + } + + private static final Expression EMPTY = new Expression(Node.NULL); + + static final class Itr { + private final String str; + private int idx; + + Itr(final String str) { + this.str = str; + } + + boolean hasNext() { + return idx < str.length(); + } + + int next() { + final int idx = this.idx; + try { + return str.codePointAt(idx); + } finally { + this.idx = str.offsetByCodePoints(idx, 1); + } + } + + int prev() { + final int idx = this.idx; + try { + return str.codePointBefore(idx); + } finally { + this.idx = str.offsetByCodePoints(idx, -1); + } + } + + int getNextIdx() { + return idx; + } + + int getPrevIdx() { + return str.offsetByCodePoints(idx, -1); + } + + String getStr() { + return str; + } + + int peekNext() { + return str.codePointAt(idx); + } + + int peekPrev() { + return str.codePointBefore(idx); + } + + void rewind(final int newNext) { + idx = newNext; + } + } + + private static Node parseString(Itr itr, final boolean allowExpr, final boolean endOnBrace, final boolean endOnColon, + final EnumSet flags) { + int ignoreBraceLevel = 0; + final List list = new ArrayList<>(); + int start = itr.getNextIdx(); + while (itr.hasNext()) { + // index of this character + int idx = itr.getNextIdx(); + int ch = itr.next(); + switch (ch) { + case '$': { + if (!allowExpr) { + // TP 1 + // treat as plain content + continue; + } + // check to see if it's a dangling $ + if (!itr.hasNext()) { + if (!flags.contains(Flag.LENIENT_SYNTAX)) { + // TP 2 + throw invalidExpressionSyntax(itr.getStr(), idx); + } + // TP 3 + list.add(new LiteralNode(itr.getStr(), start, itr.getNextIdx())); + start = itr.getNextIdx(); + continue; + } + // enqueue what we have acquired so far + if (idx > start) { + // TP 4 + list.add(new LiteralNode(itr.getStr(), start, idx)); + } + // next char should be an expression starter of some sort + idx = itr.getNextIdx(); + ch = itr.next(); + switch (ch) { + case '{': { + // ${ + boolean general = flags.contains(Flag.GENERAL_EXPANSION) && itr.hasNext() && itr.peekNext() == '{'; + // consume double-{ + if (general) + itr.next(); + // set start to the beginning of the key for later + start = itr.getNextIdx(); + // the expression name starts in the next position + Node keyNode = parseString(itr, !flags.contains(Flag.NO_RECURSE_KEY), true, true, flags); + if (!itr.hasNext()) { + if (!flags.contains(Flag.LENIENT_SYNTAX)) { + // TP 5 + throw invalidExpressionSyntax(itr.getStr(), itr.getNextIdx()); + } + // TP 6 + // otherwise treat it as a properly terminated expression + list.add(new ExpressionNode(general, keyNode, Node.NULL)); + start = itr.getNextIdx(); + continue; + } else if (itr.peekNext() == ':') { + if (flags.contains(Flag.DOUBLE_COLON)) { + itr.next(); + if (itr.hasNext() && itr.peekNext() == ':') { + // TP 7 + // OK actually the whole thing is really going to be part of the key + // Best approach is, rewind and do it over again, but without end-on-colon + itr.rewind(start); + keyNode = parseString(itr, !flags.contains(Flag.NO_RECURSE_KEY), true, false, flags); + list.add(new ExpressionNode(general, keyNode, Node.NULL)); + } else { + // TP 7.5 + // A single colon was found, so it is a default + final Node defaultValueNode = parseString(itr, !flags.contains(Flag.NO_RECURSE_DEFAULT), + true, false, flags); + list.add(new ExpressionNode(general, keyNode, defaultValueNode)); + } + } else { + // TP 8 + itr.next(); // consume it + final Node defaultValueNode = parseString(itr, !flags.contains(Flag.NO_RECURSE_DEFAULT), + true, false, flags); + list.add(new ExpressionNode(general, keyNode, defaultValueNode)); + } + // now expect } + if (!itr.hasNext()) { + if (!flags.contains(Flag.LENIENT_SYNTAX)) { + // TP 9 + throw invalidExpressionSyntax(itr.getStr(), itr.getNextIdx()); + } + // TP 10 + // otherwise treat it as a properly terminated expression + start = itr.getNextIdx(); + continue; + } else { + // TP 11 + assert itr.peekNext() == '}'; + itr.next(); // consume + if (general) { + if (!itr.hasNext()) { + if (!flags.contains(Flag.LENIENT_SYNTAX)) { + // TP 11_1 + throw invalidExpressionSyntax(itr.getStr(), itr.getNextIdx()); + } + // TP 11_2 + // otherwise treat it as a properly terminated expression + start = itr.getNextIdx(); + continue; + } else { + if (itr.peekNext() == '}') { + itr.next(); // consume it + // TP 11_3 + start = itr.getNextIdx(); + continue; + } else { + if (!flags.contains(Flag.LENIENT_SYNTAX)) { + // TP 11_4 + throw invalidExpressionSyntax(itr.getStr(), itr.getNextIdx()); + } + // otherwise treat it as a properly terminated expression + start = itr.getNextIdx(); + continue; + } + } + } else { + start = itr.getNextIdx(); + continue; + } + //throw Assert.unreachableCode(); + } + } else { + // TP 12 + assert itr.peekNext() == '}'; + itr.next(); // consume + list.add(new ExpressionNode(general, keyNode, Node.NULL)); + if (general) { + if (!itr.hasNext()) { + if (!flags.contains(Flag.LENIENT_SYNTAX)) { + // TP 12_1 + throw invalidExpressionSyntax(itr.getStr(), itr.getNextIdx()); + } + // TP 12_2 + // otherwise treat it as a properly terminated expression + start = itr.getNextIdx(); + continue; + } else { + if (itr.peekNext() == '}') { + itr.next(); // consume it + // TP 12_3 + start = itr.getNextIdx(); + continue; + } else { + if (!flags.contains(Flag.LENIENT_SYNTAX)) { + // TP 12_4 + throw invalidExpressionSyntax(itr.getStr(), itr.getNextIdx()); + } + // otherwise treat it as a properly terminated expression + start = itr.getNextIdx(); + continue; + } + } + } + start = itr.getNextIdx(); + continue; + } + //throw Assert.unreachableCode(); + } + case '$': { + // $$ + if (flags.contains(Flag.MINI_EXPRS)) { + // TP 13 + list.add(new ExpressionNode(false, LiteralNode.DOLLAR, Node.NULL)); + } else { + // just resolve $$ to $ + // TP 14 + list.add(LiteralNode.DOLLAR); + } + start = itr.getNextIdx(); + continue; + } + case '}': { + // $} + if (flags.contains(Flag.MINI_EXPRS)) { + // TP 15 + list.add(new ExpressionNode(false, LiteralNode.CLOSE_BRACE, Node.NULL)); + start = itr.getNextIdx(); + continue; + } else if (endOnBrace) { + if (flags.contains(Flag.LENIENT_SYNTAX)) { + // TP 16 + // just treat the $ that we got like plain text, and return + list.add(LiteralNode.DOLLAR); + itr.prev(); // back up to point at } again + return Node.fromList(list); + } else { + // TP 17 + throw invalidExpressionSyntax(itr.getStr(), idx); + } + } else { + if (flags.contains(Flag.LENIENT_SYNTAX)) { + // TP 18 + // just treat $} like plain text + list.add(LiteralNode.DOLLAR); + list.add(LiteralNode.CLOSE_BRACE); + start = itr.getNextIdx(); + continue; + } else { + // TP 19 + throw invalidExpressionSyntax(itr.getStr(), idx); + } + } + //throw Assert.unreachableCode(); + } + case ':': { + // $: + if (flags.contains(Flag.MINI_EXPRS)) { + // $: is an expression + // TP 20 + list.add(new ExpressionNode(false, LiteralNode.COLON, Node.NULL)); + start = itr.getNextIdx(); + continue; + } else if (endOnColon) { + if (flags.contains(Flag.LENIENT_SYNTAX)) { + // TP 21 + // just treat the $ that we got like plain text, and return + itr.prev(); // back up to point at : again + list.add(LiteralNode.DOLLAR); + return Node.fromList(list); + } else { + // TP 22 + throw invalidExpressionSyntax(itr.getStr(), idx); + } + } else { + if (flags.contains(Flag.LENIENT_SYNTAX)) { + // TP 23 + // just treat $: like plain text + list.add(LiteralNode.DOLLAR); + list.add(LiteralNode.COLON); + start = itr.getNextIdx(); + continue; + } else { + // TP 24 + throw invalidExpressionSyntax(itr.getStr(), idx); + } + } + //throw Assert.unreachableCode(); + } + default: { + // $ followed by anything else + if (flags.contains(Flag.MINI_EXPRS)) { + // TP 25 + list.add(new ExpressionNode(false, new LiteralNode(itr.getStr(), idx, itr.getNextIdx()), + Node.NULL)); + start = itr.getNextIdx(); + continue; + } else if (flags.contains(Flag.LENIENT_SYNTAX)) { + // TP 26 + // just treat it as literal + start = itr.getPrevIdx() - 1; // we can use 1 here because unicode '$' is one char in size + continue; + } else { + // TP 27 + throw invalidExpressionSyntax(itr.getStr(), idx); + } + //throw Assert.unreachableCode(); + } + } + //throw Assert.unreachableCode(); + } + case ':': { + if (endOnColon) { + // TP 28 + itr.prev(); // back up to point at : again + if (idx > start) { + list.add(new LiteralNode(itr.getStr(), start, idx)); + } + return Node.fromList(list); + } else { + // TP 29 + // plain content always + continue; + } + //throw Assert.unreachableCode(); + } + case '{': { + if (!flags.contains(Flag.NO_SMART_BRACES)) { + // TP 1.2 + ignoreBraceLevel++; + } + // TP 1.3 + continue; + } + case '}': { + if (!flags.contains(Flag.NO_SMART_BRACES) && ignoreBraceLevel > 0) { + // TP 1.1 + ignoreBraceLevel--; + continue; + } else if (endOnBrace) { + // TP 30 + itr.prev(); // back up to point at } again + // TP 46 // allow an empty default value + if (idx >= start) { + list.add(new LiteralNode(itr.getStr(), start, idx)); + } + return Node.fromList(list); + } else { + // TP 31 + // treat as plain content + continue; + } + //throw Assert.unreachableCode(); + } + case '\\': { + if (flags.contains(Flag.ESCAPES)) { + if (idx > start) { + list.add(new LiteralNode(itr.getStr(), start, idx)); + start = idx; + } + if (!itr.hasNext()) { + if (flags.contains(Flag.LENIENT_SYNTAX)) { + // just treat it like plain content + // TP 33 + continue; + } else { + // TP 34 + throw invalidExpressionSyntax(itr.getStr(), idx); + } + } else { + ch = itr.next(); + final LiteralNode node; + switch (ch) { + case 'n': { + // TP 35 + node = LiteralNode.NEWLINE; + break; + } + case 'r': { + // TP 36 + node = LiteralNode.CARRIAGE_RETURN; + break; + } + case 't': { + // TP 37 + node = LiteralNode.TAB; + break; + } + case 'b': { + // TP 38 + node = LiteralNode.BACKSPACE; + break; + } + case 'f': { + // TP 39 + node = LiteralNode.FORM_FEED; + break; + } + case '\\': { + // TP 45 + node = LiteralNode.BACKSLASH; + break; + } + default: { + if (flags.contains(Flag.LENIENT_SYNTAX)) { + // TP 40 + // just append the literal character after the \, whatever it was + start = itr.getPrevIdx(); + continue; + } + // TP 41 + throw invalidExpressionSyntax(itr.getStr(), idx); + } + } + list.add(node); + start = itr.getNextIdx(); + continue; + } + } + // TP 42 + // otherwise, just... + continue; + } + default: { + // TP 43 + // treat as plain content + //noinspection UnnecessaryContinue + continue; + } + } + //throw Assert.unreachableCode(); + } + final int length = itr.getStr().length(); + if (length > start) { + // TP 44 + list.add(new LiteralNode(itr.getStr(), start, length)); + } + return Node.fromList(list); + } + + private static IllegalArgumentException invalidExpressionSyntax(final String string, final int index) { + String msg = "invalid expression syntax at " + index; //Messages.msg.invalidExpressionSyntax(index); + StringBuilder b = new StringBuilder(msg.length() + string.length() + string.length() + 5); + b.append(msg); + b.append('\n').append('\t').append(string); + b.append('\n').append('\t'); + for (int i = 0; i < index; i = string.offsetByCodePoints(i, 1)) { + final int cp = string.codePointAt(i); + if (Character.isWhitespace(cp)) { + b.append(cp); + } else if (Character.isValidCodePoint(cp) && !Character.isISOControl(cp)) { + b.append(' '); + } + } + b.append('^'); + return new IllegalArgumentException(b.toString()); + } + + private static final EnumSet NO_FLAGS = EnumSet.noneOf(Flag.class); + + /** + * Flags that can apply to a property expression compilation + */ + public enum Flag { + /** + * Do not trim leading and trailing whitespace off of the expression string before parsing it. + */ + NO_TRIM, + /** + * Ignore syntax problems instead of throwing an exception. + */ + LENIENT_SYNTAX, + /** + * Support single-character expressions that can be interpreted without wrapping in curly braces. + */ + MINI_EXPRS, + /** + * Do not support recursive expression expansion in the key part of the expression. + */ + NO_RECURSE_KEY, + /** + * Do not support recursion in default values. + */ + NO_RECURSE_DEFAULT, + /** + * Do not support smart braces. + */ + NO_SMART_BRACES, + /** + * Support {@code Policy} file style "general" expansion alternate expression syntax. "Smart" braces + * will only work if the opening brace is not the first character in the expression key. + */ + GENERAL_EXPANSION, + /** + * Support standard escape sequences in plain text and default value fields, which begin with a backslash ("{@code \}") + * character. + */ + ESCAPES, + /** + * Treat expressions containing a double-colon delimiter as special, encoding the entire content into the key. + */ + DOUBLE_COLON, + } +} diff --git a/logging/src/main/java/org/xbib/logging/expression/ExpressionNode.java b/logging/src/main/java/org/xbib/logging/expression/ExpressionNode.java new file mode 100644 index 0000000..f2937a9 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/expression/ExpressionNode.java @@ -0,0 +1,50 @@ +package org.xbib.logging.expression; + +import java.util.HashSet; + +class ExpressionNode extends Node { + private final boolean generalExpression; + private final Node key; + private final Node defaultValue; + + ExpressionNode(final boolean generalExpression, final Node key, final Node defaultValue) { + this.generalExpression = generalExpression; + this.key = key; + this.defaultValue = defaultValue; + } + + void emit(final ResolveContext context, + final ExceptionBiConsumer, StringBuilder, E> resolveFunction) throws E { + ExpressionNode oldCurrent = context.setCurrent(this); + try { + resolveFunction.accept(context, context.getStringBuilder()); + } finally { + context.setCurrent(oldCurrent); + } + } + + void catalog(final HashSet strings) { + if (key instanceof LiteralNode) { + strings.add(key.toString()); + } else { + key.catalog(strings); + } + defaultValue.catalog(strings); + } + + boolean isGeneralExpression() { + return generalExpression; + } + + Node getKey() { + return key; + } + + Node getDefaultValue() { + return defaultValue; + } + + public String toString() { + return String.format("Expr<%s:%s>", key, defaultValue); + } +} diff --git a/logging/src/main/java/org/xbib/logging/expression/LiteralNode.java b/logging/src/main/java/org/xbib/logging/expression/LiteralNode.java new file mode 100644 index 0000000..7989b7b --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/expression/LiteralNode.java @@ -0,0 +1,45 @@ +package org.xbib.logging.expression; + +import java.io.File; +import java.util.HashSet; + +class LiteralNode extends Node { + static final LiteralNode DOLLAR = new LiteralNode("$"); + static final LiteralNode CLOSE_BRACE = new LiteralNode("}"); + static final LiteralNode FILE_SEP = new LiteralNode(File.separator); + static final LiteralNode COLON = new LiteralNode(":"); + static final LiteralNode NEWLINE = new LiteralNode("\n"); + static final LiteralNode CARRIAGE_RETURN = new LiteralNode("\r"); + static final LiteralNode TAB = new LiteralNode("\t"); + static final LiteralNode BACKSPACE = new LiteralNode("\b"); + static final LiteralNode FORM_FEED = new LiteralNode("\f"); + static final LiteralNode BACKSLASH = new LiteralNode("\\"); + + private final String literalValue; + private final int start; + private final int end; + private String toString; + + LiteralNode(final String literalValue, final int start, final int end) { + this.literalValue = literalValue; + this.start = start; + this.end = end; + } + + LiteralNode(final String literalValue) { + this(literalValue, 0, literalValue.length()); + } + + void emit(final ResolveContext context, + final ExceptionBiConsumer, StringBuilder, E> resolveFunction) throws E { + context.getStringBuilder().append(literalValue, start, end); + } + + void catalog(final HashSet strings) { + } + + public String toString() { + final String toString = this.toString; + return toString != null ? toString : (this.toString = literalValue.substring(start, end)); + } +} diff --git a/logging/src/main/java/org/xbib/logging/expression/Node.java b/logging/src/main/java/org/xbib/logging/expression/Node.java new file mode 100644 index 0000000..a7ac760 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/expression/Node.java @@ -0,0 +1,40 @@ +package org.xbib.logging.expression; + +import java.util.HashSet; +import java.util.List; + +abstract class Node { + + static final Node[] NO_NODES = new Node[0]; + + Node() { + } + + static Node fromList(List list) { + if (list == null || list.isEmpty()) { + return NULL; + } else if (list.size() == 1) { + return list.get(0); + } else { + return new CompositeNode(list); + } + } + + static final Node NULL = new Node() { + void emit(final ResolveContext context, + final ExceptionBiConsumer, StringBuilder, E> resolveFunction) throws E { + } + + void catalog(final HashSet strings) { + } + + public String toString() { + return "<>"; + } + }; + + abstract void emit(final ResolveContext context, + final ExceptionBiConsumer, StringBuilder, E> resolveFunction) throws E; + + abstract void catalog(final HashSet strings); +} diff --git a/logging/src/main/java/org/xbib/logging/expression/ResolveContext.java b/logging/src/main/java/org/xbib/logging/expression/ResolveContext.java new file mode 100644 index 0000000..228f1c5 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/expression/ResolveContext.java @@ -0,0 +1,125 @@ +package org.xbib.logging.expression; + +/** + * The expression resolve context, which can be used to query the current expression key, write out expansions or + * default values, or perform validation. + *

+ * The expression context is not thread-safe and is not valid outside of the property expansion function body. + * + * @param the exception type that can be thrown by the expansion function + */ +public final class ResolveContext { + private final ExceptionBiConsumer, StringBuilder, E> function; + private StringBuilder builder; + private ExpressionNode current; + + ResolveContext(final ExceptionBiConsumer, StringBuilder, E> function, final StringBuilder builder) { + this.function = function; + this.builder = builder; + } + + /** + * Get the expression resolution key, as a string. If the key contains an expression, it will have been expanded + * unless {@link Expression.Flag#NO_RECURSE_KEY} was given. + * The result is not cached and will be re-expanded every time this method is called. + * + * @return the expanded key (not {@code null}) + * @throws E if the recursive expansion threw an exception + */ + public String getKey() throws E { + if (current == null) + throw new IllegalStateException(); + final Node key = current.getKey(); + if (key instanceof LiteralNode) { + return key.toString(); + } else if (key == Node.NULL) { + return ""; + } + final StringBuilder b = new StringBuilder(); + emitToBuilder(b, key); + return b.toString(); + } + + /** + * Expand the default value to the given string builder. If the default value contains an expression, it will + * have been expanded unless {@link Expression.Flag#NO_RECURSE_DEFAULT} was given. + * The result is not cached and will be re-expanded every time this method is called. + * + * @param target the string builder target + * @throws E if the recursive expansion threw an exception + */ + public void expandDefault(StringBuilder target) throws E { + if (current == null) + throw new IllegalStateException(); + emitToBuilder(target, current.getDefaultValue()); + } + + private void emitToBuilder(final StringBuilder target, final Node node) throws E { + if (node == Node.NULL) { + } else if (node instanceof LiteralNode) { + target.append(node); + } else { + final StringBuilder old = builder; + try { + builder = target; + node.emit(this, function); + } finally { + builder = old; + } + } + } + + /** + * Expand the default value to the current target string builder. If the default value contains an expression, it will + * have been expanded unless {@link Expression.Flag#NO_RECURSE_DEFAULT} was given. + * The result is not cached and will be re-expanded every time this method is called. + * + * @throws E if the recursive expansion threw an exception + */ + public void expandDefault() throws E { + expandDefault(builder); + } + + /** + * Expand the default value to a string. If the default value contains an expression, it will + * have been expanded unless {@link Expression.Flag#NO_RECURSE_DEFAULT} was given. + * The result is not cached and will be re-expanded every time this method is called. + * + * @return the expanded string (not {@code null}) + * @throws E if the recursive expansion threw an exception + */ + public String getExpandedDefault() throws E { + if (current == null) + throw new IllegalStateException(); + final Node defaultValue = current.getDefaultValue(); + if (defaultValue instanceof LiteralNode) { + return defaultValue.toString(); + } else if (defaultValue == Node.NULL) { + return ""; + } + final StringBuilder b = new StringBuilder(); + emitToBuilder(b, defaultValue); + return b.toString(); + } + + /** + * Determine if the current expression has a default value. + * + * @return {@code true} if there is a default value, {@code false} otherwise + */ + public boolean hasDefault() { + return current.getDefaultValue() != Node.NULL; + } + + StringBuilder getStringBuilder() { + return builder; + } + + ExpressionNode setCurrent(final ExpressionNode current) { + try { + return this.current; + } finally { + this.current = current; + } + } +} diff --git a/logging/src/main/java/org/xbib/logging/filters/AcceptAllFilter.java b/logging/src/main/java/org/xbib/logging/filters/AcceptAllFilter.java new file mode 100644 index 0000000..57c7a20 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/filters/AcceptAllFilter.java @@ -0,0 +1,33 @@ +package org.xbib.logging.filters; + +import java.util.logging.Filter; +import java.util.logging.LogRecord; + +/** + * An accept-all filter. + */ +public final class AcceptAllFilter implements Filter { + private AcceptAllFilter() { + } + + private static final AcceptAllFilter INSTANCE = new AcceptAllFilter(); + + /** + * Always returns {@code true}. + * + * @param record ignored + * @return {@code true} + */ + public boolean isLoggable(final LogRecord record) { + return true; + } + + /** + * Get the filter instance. + * + * @return the filter instance + */ + public static AcceptAllFilter getInstance() { + return INSTANCE; + } +} \ No newline at end of file diff --git a/logging/src/main/java/org/xbib/logging/filters/AllFilter.java b/logging/src/main/java/org/xbib/logging/filters/AllFilter.java new file mode 100644 index 0000000..cd0bfe1 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/filters/AllFilter.java @@ -0,0 +1,70 @@ +package org.xbib.logging.filters; + +import java.util.Iterator; +import java.util.logging.Filter; +import java.util.logging.LogRecord; + +/** + * A filter consisting of several filters in a chain. If any filter finds the log message to be unloggable, + * the message will not be logged and subsequent filters will not be checked. If there are no nested filters, + * this instance always returns {@code true}. + */ +public final class AllFilter implements Filter { + private final Filter[] filters; + + /** + * Construct a new instance. + * + * @param filters the constituent filters + */ + public AllFilter(final Filter[] filters) { + this.filters = filters.clone(); + } + + /** + * Construct a new instance. + * + * @param filters the constituent filters + */ + public AllFilter(final Iterable filters) { + this(filters.iterator()); + } + + /** + * Construct a new instance. + * + * @param filters the constituent filters + */ + public AllFilter(final Iterator filters) { + this.filters = unroll(filters, 0); + } + + private static Filter[] unroll(Iterator iter, int cnt) { + if (iter.hasNext()) { + final Filter filter = iter.next(); + if (filter == null) { + throw new NullPointerException("filter at index " + cnt + " is null"); + } + final Filter[] filters = unroll(iter, cnt + 1); + filters[cnt] = filter; + return filters; + } else { + return new Filter[cnt]; + } + } + + /** + * Determine whether the record is loggable. + * + * @param record the log record + * @return {@code true} if all the constituent filters return {@code true} + */ + public boolean isLoggable(final LogRecord record) { + for (Filter filter : filters) { + if (!filter.isLoggable(record)) { + return false; + } + } + return true; + } +} diff --git a/logging/src/main/java/org/xbib/logging/filters/AnyFilter.java b/logging/src/main/java/org/xbib/logging/filters/AnyFilter.java new file mode 100644 index 0000000..79e9bcb --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/filters/AnyFilter.java @@ -0,0 +1,70 @@ +package org.xbib.logging.filters; + +import java.util.Iterator; +import java.util.logging.Filter; +import java.util.logging.LogRecord; + +/** + * A filter consisting of several filters in a chain. If any filter finds the log message to be loggable, + * the message will be logged and subsequent filters will not be checked. If there are no nested filters, this + * instance always returns {@code false}. + */ +public final class AnyFilter implements Filter { + private final Filter[] filters; + + /** + * Construct a new instance. + * + * @param filters the constituent filters + */ + public AnyFilter(final Filter[] filters) { + this.filters = filters.clone(); + } + + /** + * Construct a new instance. + * + * @param filters the constituent filters + */ + public AnyFilter(final Iterable filters) { + this(filters.iterator()); + } + + /** + * Construct a new instance. + * + * @param filters the constituent filters + */ + public AnyFilter(final Iterator filters) { + this.filters = unroll(filters, 0); + } + + private static Filter[] unroll(Iterator iter, int cnt) { + if (iter.hasNext()) { + final Filter filter = iter.next(); + if (filter == null) { + throw new NullPointerException("filter at index " + cnt + " is null"); + } + final Filter[] filters = unroll(iter, cnt + 1); + filters[cnt] = filter; + return filters; + } else { + return new Filter[cnt]; + } + } + + /** + * Determine whether the record is loggable. + * + * @param record the log record + * @return {@code true} if any of the constituent filters return {@code true} + */ + public boolean isLoggable(final LogRecord record) { + for (Filter filter : filters) { + if (filter.isLoggable(record)) { + return true; + } + } + return false; + } +} diff --git a/logging/src/main/java/org/xbib/logging/filters/DenyAllFilter.java b/logging/src/main/java/org/xbib/logging/filters/DenyAllFilter.java new file mode 100644 index 0000000..86be1f4 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/filters/DenyAllFilter.java @@ -0,0 +1,33 @@ +package org.xbib.logging.filters; + +import java.util.logging.Filter; +import java.util.logging.LogRecord; + +/** + * A deny-all filter. + */ +public final class DenyAllFilter implements Filter { + private DenyAllFilter() { + } + + private static final DenyAllFilter INSTANCE = new DenyAllFilter(); + + /** + * Always returns {@code false}. + * + * @param record ignored + * @return {@code false} + */ + public boolean isLoggable(final LogRecord record) { + return false; + } + + /** + * Get the filter instance. + * + * @return the filter instance + */ + public static DenyAllFilter getInstance() { + return INSTANCE; + } +} diff --git a/logging/src/main/java/org/xbib/logging/filters/InvertFilter.java b/logging/src/main/java/org/xbib/logging/filters/InvertFilter.java new file mode 100644 index 0000000..3dce29a --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/filters/InvertFilter.java @@ -0,0 +1,30 @@ +package org.xbib.logging.filters; + +import java.util.logging.Filter; +import java.util.logging.LogRecord; + +/** + * An inverting filter. + */ +public final class InvertFilter implements Filter { + private final Filter target; + + /** + * Construct a new instance. + * + * @param target the target filter + */ + public InvertFilter(final Filter target) { + this.target = target; + } + + /** + * Determine whether a log record passes this filter. + * + * @param record the log record + * @return {@code true} if the target filter returns {@code false}, {@code false} otherwise + */ + public boolean isLoggable(final LogRecord record) { + return !target.isLoggable(record); + } +} diff --git a/logging/src/main/java/org/xbib/logging/filters/LevelChangingFilter.java b/logging/src/main/java/org/xbib/logging/filters/LevelChangingFilter.java new file mode 100644 index 0000000..3ba317a --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/filters/LevelChangingFilter.java @@ -0,0 +1,34 @@ +package org.xbib.logging.filters; + +import java.util.logging.Filter; +import java.util.logging.Level; +import java.util.logging.LogRecord; + +/** + * A filter which modifies the log record with a new level if the nested filter evaluates {@code true} for that + * record. + */ +public final class LevelChangingFilter implements Filter { + + private final Level newLevel; + + /** + * Construct a new instance. + * + * @param newLevel the level to change to + */ + public LevelChangingFilter(final Level newLevel) { + this.newLevel = newLevel; + } + + /** + * Apply the filter to this log record. + * + * @param record the record to inspect and possibly update + * @return {@code true} always + */ + public boolean isLoggable(final LogRecord record) { + record.setLevel(newLevel); + return true; + } +} diff --git a/logging/src/main/java/org/xbib/logging/filters/LevelFilter.java b/logging/src/main/java/org/xbib/logging/filters/LevelFilter.java new file mode 100644 index 0000000..97f96c0 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/filters/LevelFilter.java @@ -0,0 +1,44 @@ +package org.xbib.logging.filters; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.logging.Filter; +import java.util.logging.Level; +import java.util.logging.LogRecord; + +/** + * A filter which excludes messages of a certain level or levels + */ +public final class LevelFilter implements Filter { + private final Set includedLevels; + + /** + * Construct a new instance. + * + * @param includedLevel the level to include + */ + public LevelFilter(final Level includedLevel) { + includedLevels = Collections.singleton(includedLevel); + } + + /** + * Construct a new instance. + * + * @param includedLevels the levels to exclude + */ + public LevelFilter(final Collection includedLevels) { + this.includedLevels = new HashSet(includedLevels); + } + + /** + * Determine whether the message is loggable. + * + * @param record the log record + * @return {@code true} if the level is in the inclusion list + */ + public boolean isLoggable(final LogRecord record) { + return includedLevels.contains(record.getLevel()); + } +} diff --git a/logging/src/main/java/org/xbib/logging/filters/LevelRangeFilter.java b/logging/src/main/java/org/xbib/logging/filters/LevelRangeFilter.java new file mode 100644 index 0000000..0950394 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/filters/LevelRangeFilter.java @@ -0,0 +1,44 @@ +package org.xbib.logging.filters; + +import java.util.logging.Filter; +import java.util.logging.Level; +import java.util.logging.LogRecord; + +/** + * Log only messages that fall within a level range. + */ +public final class LevelRangeFilter implements Filter { + private final int min; + private final int max; + private final boolean minInclusive; + private final boolean maxInclusive; + + /** + * Create a new instance. + * + * @param min the minimum (least severe) level, inclusive + * @param minInclusive {@code true} if the {@code min} value is inclusive, {@code false} if it is exclusive + * @param max the maximum (most severe) level, inclusive + * @param maxInclusive {@code true} if the {@code max} value is inclusive, {@code false} if it is exclusive + */ + public LevelRangeFilter(final Level min, final boolean minInclusive, final Level max, final boolean maxInclusive) { + this.minInclusive = minInclusive; + this.maxInclusive = maxInclusive; + this.min = min.intValue(); + this.max = max.intValue(); + if (this.max < this.min) { + throw new IllegalArgumentException("Max level cannot be less than min level"); + } + } + + /** + * Determine if a record is loggable. + * + * @param record the log record + * @return {@code true} if the record's level falls within the range specified for this instance + */ + public boolean isLoggable(final LogRecord record) { + final int iv = record.getLevel().intValue(); + return (minInclusive ? min <= iv : min < iv) && (maxInclusive ? iv <= max : iv < max); + } +} diff --git a/logging/src/main/java/org/xbib/logging/filters/RegexFilter.java b/logging/src/main/java/org/xbib/logging/filters/RegexFilter.java new file mode 100644 index 0000000..1157478 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/filters/RegexFilter.java @@ -0,0 +1,50 @@ +package org.xbib.logging.filters; + +import java.text.MessageFormat; +import java.util.logging.Filter; +import java.util.logging.LogRecord; +import java.util.regex.Pattern; +import org.xbib.logging.ExtLogRecord; + +/** + * A regular-expression-based filter. Used to exclude log records which match or don't match the expression. The + * regular expression is checked against the raw (unformatted) message. + */ +public final class RegexFilter implements Filter { + private final Pattern pattern; + + /** + * Create a new instance. + * + * @param pattern the pattern to match + */ + public RegexFilter(final Pattern pattern) { + this.pattern = pattern; + } + + /** + * Create a new instance. + * + * @param patternString the pattern string to match + */ + public RegexFilter(final String patternString) { + this(Pattern.compile(patternString)); + } + + /** + * Determine if this log record is loggable. + * + * @param record the log record + * @return {@code true} if the log record is loggable + */ + @Override + public boolean isLoggable(final LogRecord record) { + final String msg; + if (record instanceof ExtLogRecord) { + msg = ((ExtLogRecord) record).getFormattedMessage(); + } else { + msg = MessageFormat.format(record.getMessage(), record.getParameters()); + } + return pattern.matcher(String.valueOf(msg)).find(); + } +} diff --git a/logging/src/main/java/org/xbib/logging/filters/SubstituteFilter.java b/logging/src/main/java/org/xbib/logging/filters/SubstituteFilter.java new file mode 100644 index 0000000..df4945f --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/filters/SubstituteFilter.java @@ -0,0 +1,75 @@ +package org.xbib.logging.filters; + +import java.text.MessageFormat; +import java.util.logging.Filter; +import java.util.logging.LogRecord; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.xbib.logging.ExtLogRecord; +import org.xbib.logging.ExtLogRecord.FormatStyle; + +/** + * A filter which applies a text substitution on the message if the nested filter matches. + */ +public final class SubstituteFilter implements Filter { + + private final Pattern pattern; + private final String replacement; + private final boolean replaceAll; + + /** + * Construct a new instance. + * + * @param pattern the pattern to match + * @param replacement the string replacement + * @param replaceAll {@code true} if all occurrences should be replaced; {@code false} if only the first occurrence + */ + public SubstituteFilter(final Pattern pattern, final String replacement, final boolean replaceAll) { + this.pattern = pattern; + this.replacement = replacement; + this.replaceAll = replaceAll; + } + + /** + * Construct a new instance. + * + * @param patternString the pattern to match + * @param replacement the string replacement + * @param replaceAll {@code true} if all occurrences should be replaced; {@code false} if only the first occurrence + */ + public SubstituteFilter(final String patternString, final String replacement, final boolean replaceAll) { + this(Pattern.compile(patternString), replacement, replaceAll); + } + + /** + * Apply the filter to the given log record. + *

+ * The {@link FormatStyle format style} will always be set to {@link FormatStyle#NO_FORMAT} as the formatted + * message will be the one used in the replacement. + * + * @param record the log record to inspect and modify + * @return {@code true} always + */ + @Override + public boolean isLoggable(final LogRecord record) { + final String currentMsg; + if (record instanceof ExtLogRecord) { + currentMsg = ((ExtLogRecord) record).getFormattedMessage(); + } else { + currentMsg = MessageFormat.format(record.getMessage(), record.getParameters()); + } + final Matcher matcher = pattern.matcher(String.valueOf(currentMsg)); + final String msg; + if (replaceAll) { + msg = matcher.replaceAll(replacement); + } else { + msg = matcher.replaceFirst(replacement); + } + if (record instanceof ExtLogRecord) { + ((ExtLogRecord) record).setMessage(msg, FormatStyle.NO_FORMAT); + } else { + record.setMessage(msg); + } + return true; + } +} diff --git a/logging/src/main/java/org/xbib/logging/formatters/ColorMap.java b/logging/src/main/java/org/xbib/logging/formatters/ColorMap.java new file mode 100644 index 0000000..b9f4440 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/formatters/ColorMap.java @@ -0,0 +1,241 @@ +package org.xbib.logging.formatters; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.NavigableMap; +import java.util.TreeMap; +import org.xbib.logging.Level; + +public class ColorMap { + private static final String DARK_BLACK = "\033[30m"; + private static final String DARK_RED = "\033[31m"; + private static final String DARK_GREEN = "\033[32m"; + private static final String DARK_YELLOW = "\033[33m"; + private static final String DARK_BLUE = "\033[34m"; + private static final String DARK_MAGENTA = "\033[35m"; + private static final String DARK_CYAN = "\033[36m"; + private static final String DARK_WHITE = "\033[37m"; + + private static final String BRIGHT_BLACK = "\033[1;30m"; + private static final String BRIGHT_RED = "\033[1;31m"; + private static final String BRIGHT_GREEN = "\033[1;32m"; + private static final String BRIGHT_YELLOW = "\033[1;33m"; + private static final String BRIGHT_BLUE = "\033[1;34m"; + private static final String BRIGHT_MAGENTA = "\033[1;35m"; + private static final String BRIGHT_CYAN = "\033[1;36m"; + private static final String BRIGHT_WHITE = "\033[1;37m"; + + private static final String CLEAR = "\033[0m"; + + static final boolean SUPPORTS_COLOR; + + private static final Map codes; + private static final Map reverseCodes; + private static final Map levels = new HashMap(); + private static final Map reverseLevels = new HashMap(); + + private static final NavigableMap defaultLevelMap = new TreeMap(); + + static final ColorMap DEFAULT_COLOR_MAP = new ColorMap(defaultLevelMap); + + private final NavigableMap levelMap; + + private ColorMap(NavigableMap levelMap) { + this.levelMap = levelMap; + } + + private static final int SEVERE_NUM = Level.SEVERE.intValue(); + private static final int FATAL_NUM = Level.FATAL.intValue(); + private static final int ERROR_NUM = Level.ERROR.intValue(); + private static final int WARN_NUM = Level.WARN.intValue(); + private static final int INFO_NUM = Level.INFO.intValue(); + private static final int CONFIG_NUM = Level.CONFIG.intValue(); + private static final int DEBUG_NUM = Level.DEBUG.intValue(); + private static final int TRACE_NUM = Level.TRACE.intValue(); + private static final int FINE_NUM = Level.FINE.intValue(); + private static final int FINER_NUM = Level.FINER.intValue(); + private static final int FINEST_NUM = Level.FINEST.intValue(); + + static final String LEVEL_NAME = "level"; + static final String SEVERE_NAME = "severe"; + static final String FATAL_NAME = "fatal"; + static final String ERROR_NAME = "error"; + static final String WARN_NAME = "warn"; + static final String WARNING_NAME = "warning"; + static final String INFO_NAME = "info"; + static final String DEBUG_NAME = "debug"; + static final String TRACE_NAME = "trace"; + static final String CONFIG_NAME = "config"; + static final String FINE_NAME = "fine"; + static final String FINER_NAME = "finer"; + static final String FINEST_NAME = "finest"; + + static final String BLACK_NAME = "black"; + static final String GREEN_NAME = "green"; + static final String RED_NAME = "red"; + static final String YELLOW_NAME = "yellow"; + static final String BLUE_NAME = "blue"; + static final String MAGENTA_NAME = "magenta"; + static final String CYAN_NAME = "cyan"; + static final String WHITE_NAME = "white"; + + static final String BRIGHT_BLACK_NAME = "brightblack"; + static final String BRIGHT_RED_NAME = "brightred"; + static final String BRIGHT_GREEN_NAME = "brightgreen"; + static final String BRIGHT_BLUE_NAME = "brightblue"; + static final String BRIGHT_YELLOW_NAME = "brightyellow"; + static final String BRIGHT_MAGENTA_NAME = "brightmagenta"; + static final String BRIGHT_CYAN_NAME = "brightcyan"; + static final String BRIGHT_WHITE_NAME = "brightwhite"; + + static final String CLEAR_NAME = "clear"; + + static { + // Turn color on by default for everything but Windows, unless ansicon is used + String os = System.getProperty("os.name"); + final boolean dft = os != null && (!os.toLowerCase(Locale.ROOT).contains("win") || System.getenv("ANSICON") != null); + final String nocolor = System.getProperty("org.xbib.logging.nocolor"); + SUPPORTS_COLOR = (nocolor == null ? dft : "false".equalsIgnoreCase(nocolor)); + + levels.put(SEVERE_NAME, SEVERE_NUM); + levels.put(FATAL_NAME, FATAL_NUM); + levels.put(ERROR_NAME, ERROR_NUM); + levels.put(WARN_NAME, WARN_NUM); + levels.put(WARNING_NAME, WARN_NUM); + levels.put(INFO_NAME, INFO_NUM); + levels.put(CONFIG_NAME, CONFIG_NUM); + levels.put(DEBUG_NAME, DEBUG_NUM); + levels.put(TRACE_NAME, TRACE_NUM); + levels.put(FINE_NAME, FINE_NUM); + levels.put(FINER_NAME, FINER_NUM); + levels.put(FINEST_NAME, FINEST_NUM); + + reverseLevels.put(SEVERE_NUM, SEVERE_NAME); + reverseLevels.put(CONFIG_NUM, CONFIG_NAME); + reverseLevels.put(FINE_NUM, FINE_NAME); + reverseLevels.put(FINER_NUM, FINER_NAME); + reverseLevels.put(FINEST_NUM, FINEST_NAME); + reverseLevels.put(FATAL_NUM, FATAL_NAME); + reverseLevels.put(ERROR_NUM, ERROR_NAME); + reverseLevels.put(WARN_NUM, WARN_NAME); + reverseLevels.put(INFO_NUM, INFO_NAME); + reverseLevels.put(DEBUG_NUM, DEBUG_NAME); + reverseLevels.put(TRACE_NUM, TRACE_NAME); + + if (SUPPORTS_COLOR) { + codes = new HashMap(); + codes.put(BLACK_NAME, DARK_BLACK); + codes.put(RED_NAME, DARK_RED); + codes.put(GREEN_NAME, DARK_GREEN); + codes.put(YELLOW_NAME, DARK_YELLOW); + codes.put(BLUE_NAME, DARK_BLUE); + codes.put(MAGENTA_NAME, DARK_MAGENTA); + codes.put(CYAN_NAME, DARK_CYAN); + codes.put(WHITE_NAME, DARK_WHITE); + codes.put(BRIGHT_BLACK_NAME, BRIGHT_BLACK); + codes.put(BRIGHT_RED_NAME, BRIGHT_RED); + codes.put(BRIGHT_GREEN_NAME, BRIGHT_GREEN); + codes.put(BRIGHT_YELLOW_NAME, BRIGHT_YELLOW); + codes.put(BRIGHT_BLUE_NAME, BRIGHT_BLUE); + codes.put(BRIGHT_MAGENTA_NAME, BRIGHT_MAGENTA); + codes.put(BRIGHT_CYAN_NAME, BRIGHT_CYAN); + codes.put(BRIGHT_WHITE_NAME, BRIGHT_WHITE); + codes.put(CLEAR_NAME, CLEAR); + + reverseCodes = new HashMap(); + reverseCodes.put(DARK_BLACK, BLACK_NAME); + reverseCodes.put(DARK_RED, RED_NAME); + reverseCodes.put(DARK_GREEN, GREEN_NAME); + reverseCodes.put(DARK_YELLOW, YELLOW_NAME); + reverseCodes.put(DARK_BLUE, BLUE_NAME); + reverseCodes.put(DARK_MAGENTA, MAGENTA_NAME); + reverseCodes.put(DARK_CYAN, CYAN_NAME); + reverseCodes.put(DARK_WHITE, WHITE_NAME); + reverseCodes.put(BRIGHT_BLACK, BRIGHT_BLACK_NAME); + reverseCodes.put(BRIGHT_RED, BRIGHT_RED_NAME); + reverseCodes.put(BRIGHT_GREEN, BRIGHT_GREEN_NAME); + reverseCodes.put(BRIGHT_YELLOW, BRIGHT_YELLOW_NAME); + reverseCodes.put(BRIGHT_BLUE, BRIGHT_BLUE_NAME); + reverseCodes.put(BRIGHT_MAGENTA, BRIGHT_MAGENTA_NAME); + reverseCodes.put(BRIGHT_CYAN, BRIGHT_CYAN_NAME); + reverseCodes.put(BRIGHT_WHITE, BRIGHT_WHITE_NAME); + reverseCodes.put(CLEAR, CLEAR); + + defaultLevelMap.put(Level.ERROR.intValue(), DARK_RED); + defaultLevelMap.put(Level.WARN.intValue(), DARK_YELLOW); + defaultLevelMap.put(Level.INFO.intValue(), CLEAR); + defaultLevelMap.put(Level.DEBUG.intValue(), DARK_GREEN); + } else { + reverseCodes = codes = Collections.emptyMap(); + } + + } + + static ColorMap create(String expression) { + if (expression == null || expression.length() < 3) { + return DEFAULT_COLOR_MAP; + } + + NavigableMap levelMap = new TreeMap(); + + for (String pair : expression.split(",")) { + String[] parts = pair.split(":"); + if (parts.length != 2) { + continue; + } + + String color = codes.get(parts[1].toLowerCase(Locale.ROOT)); + if (color == null) { + continue; + } + + try { + int i = Integer.parseInt(parts[0]); + levelMap.put(i, color); + continue; + } catch (NumberFormatException e) { + // eat + } + + Integer i = levels.get(parts[0].toLowerCase(Locale.ROOT)); + if (i == null) { + continue; + } + + levelMap.put(i, color); + } + + return new ColorMap(levelMap); + } + + String getCode(String name, java.util.logging.Level level) { + if (name == null || !SUPPORTS_COLOR) { + return null; + } + + String lower = name.toLowerCase(Locale.ROOT); + if (lower.equals(LEVEL_NAME)) { + Map.Entry entry = levelMap.floorEntry(level.intValue()); + return entry != null ? entry.getValue() : null; + } + + return codes.get(lower); + } + + public String toString() { + StringBuilder builder = new StringBuilder(); + for (Map.Entry entry : levelMap.descendingMap().entrySet()) { + Integer num = entry.getKey(); + String level = reverseLevels.get(num); + builder.append(level == null ? num : level).append(":").append(reverseCodes.get(entry.getValue())) + .append(","); + } + if (builder.length() > 0) { + builder.setLength(builder.length() - 1); + } + + return builder.toString(); + } +} \ No newline at end of file diff --git a/logging/src/main/java/org/xbib/logging/formatters/ColorPatternFormatter.java b/logging/src/main/java/org/xbib/logging/formatters/ColorPatternFormatter.java new file mode 100644 index 0000000..aac7fd5 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/formatters/ColorPatternFormatter.java @@ -0,0 +1,183 @@ +package org.xbib.logging.formatters; + +import java.util.logging.Formatter; +import java.util.logging.LogRecord; +import org.xbib.logging.ExtLogRecord; +import org.xbib.logging.Level; +import org.xbib.logging.handlers.ConsoleHandler; +import static java.lang.Math.abs; + +/** + * A pattern formatter that colorizes the pattern in a fixed manner. + */ +public class ColorPatternFormatter extends PatternFormatter { + + private final Printf printf; + private final int darken; + + public ColorPatternFormatter() { + this(0); + } + + public ColorPatternFormatter(final String pattern) { + this(0, pattern); + } + + public ColorPatternFormatter(int darken) { + this.darken = darken; + printf = new ColorPrintf(darken); + } + + public ColorPatternFormatter(int darken, final String pattern) { + this(darken); + setPattern(pattern); + } + + public void setSteps(final FormatStep[] steps) { + FormatStep[] colorSteps = new FormatStep[steps.length]; + for (int i = 0; i < steps.length; i++) { + colorSteps[i] = colorize(steps[i]); + } + super.setSteps(colorSteps); + } + + private FormatStep colorize(final FormatStep step) { + switch (step.getItemType()) { + case LEVEL: + return new LevelColorStep(step, darken); + case SOURCE_CLASS_NAME: + return new ColorStep(step, 0xff, 0xff, 0x44, darken); + case DATE: + return new ColorStep(step, 0xc0, 0xc0, 0xc0, darken); + case SOURCE_FILE_NAME: + return new ColorStep(step, 0xff, 0xff, 0x44, darken); + case HOST_NAME: + return new ColorStep(step, 0x44, 0xff, 0x44, darken); + case SOURCE_LINE_NUMBER: + return new ColorStep(step, 0xff, 0xff, 0x44, darken); + case LINE_SEPARATOR: + return step; + case CATEGORY: + return new ColorStep(step, 0x44, 0x88, 0xff, darken); + case MDC: + return new ColorStep(step, 0x44, 0xff, 0xaa, darken); + case MESSAGE: + return new ColorStep(step, 0xff, 0xff, 0xff, darken); + case EXCEPTION_TRACE: + return new ColorStep(step, 0xff, 0x44, 0x44, darken); + case SOURCE_METHOD_NAME: + return new ColorStep(step, 0xff, 0xff, 0x44, darken); + case SOURCE_MODULE_NAME: + return new ColorStep(step, 0x88, 0xff, 0x44, darken); + case SOURCE_MODULE_VERSION: + return new ColorStep(step, 0x44, 0xff, 0x44, darken); + case NDC: + return new ColorStep(step, 0x44, 0xff, 0xaa, darken); + case PROCESS_ID: + return new ColorStep(step, 0xdd, 0xbb, 0x77, darken); + case PROCESS_NAME: + return new ColorStep(step, 0xdd, 0xdd, 0x77, darken); + case RELATIVE_TIME: + return new ColorStep(step, 0xc0, 0xc0, 0xc0, darken); + case RESOURCE_KEY: + return new ColorStep(step, 0x44, 0xff, 0x44, darken); + case SYSTEM_PROPERTY: + return new ColorStep(step, 0x88, 0x88, 0x00, darken); + case TEXT: + return new ColorStep(step, 0xd0, 0xd0, 0xd0, darken); + case THREAD_ID: + return new ColorStep(step, 0x44, 0xaa, 0x44, darken); + case THREAD_NAME: + return new ColorStep(step, 0x44, 0xaa, 0x44, darken); + case COMPOUND: + case GENERIC: + default: + return new ColorStep(step, 0xb0, 0xd0, 0xb0, darken); + } + } + + private String colorizePlain(final String str) { + return str; + } + + public String formatMessage(final LogRecord logRecord) { + if (logRecord instanceof ExtLogRecord record) { + if (record.getFormatStyle() != ExtLogRecord.FormatStyle.PRINTF || record.getParameters() == null + || record.getParameters().length == 0) { + return colorizePlain(super.formatMessage(record)); + } + return printf.format(record.getMessage(), record.getParameters()); + } else { + return colorizePlain(super.formatMessage(logRecord)); + } + } + + static final class ColorStep implements FormatStep { + private final int r, g, b; + private final FormatStep delegate; + private final boolean trueColor = ConsoleHandler.isTrueColor(); + + ColorStep(final FormatStep delegate, final int r, final int g, final int b, final int darken) { + this.r = r >>> darken; + this.g = g >>> darken; + this.b = b >>> darken; + this.delegate = delegate; + } + + public void render(final Formatter formatter, final StringBuilder builder, final ExtLogRecord record) { + ColorUtil.startFgColor(builder, trueColor, r, g, b); + delegate.render(formatter, builder, record); + ColorUtil.endFgColor(builder); + } + + public void render(final StringBuilder builder, final ExtLogRecord record) { + render(null, builder, record); + } + + public int estimateLength() { + return delegate.estimateLength() + 30; + } + + public boolean isCallerInformationRequired() { + return delegate.isCallerInformationRequired(); + } + + public ItemType getItemType() { + return delegate.getItemType(); + } + } + + static final class LevelColorStep implements FormatStep { + private static final int LARGEST_LEVEL = Level.ERROR.intValue(); + private static final int SMALLEST_LEVEL = Level.TRACE.intValue(); + private static final int SATURATION = 66; + private final FormatStep delegate; + private final int darken; + // capture current console state + private final boolean trueColor = ConsoleHandler.isTrueColor(); + + LevelColorStep(final FormatStep delegate, final int darken) { + this.delegate = delegate; + this.darken = darken; + } + + public void render(final Formatter formatter, final StringBuilder builder, final ExtLogRecord record) { + final int level = Math.max(Math.min(record.getLevel().intValue(), LARGEST_LEVEL), SMALLEST_LEVEL) - SMALLEST_LEVEL; + // really crappy linear interpolation + int r = ((level < 300 ? 0 : (level - 300) * (255 - SATURATION) / 300) + SATURATION) >>> darken; + int g = ((300 - abs(level - 300)) * (255 - SATURATION) / 300 + SATURATION) >>> darken; + int b = ((level > 300 ? 0 : level * (255 - SATURATION) / 300) + SATURATION) >>> darken; + ColorUtil.startFgColor(builder, trueColor, r, g, b); + delegate.render(formatter, builder, record); + ColorUtil.endFgColor(builder); + } + + public void render(final StringBuilder builder, final ExtLogRecord record) { + render(null, builder, record); + } + + public int estimateLength() { + return delegate.estimateLength() + 30; + } + } +} diff --git a/logging/src/main/java/org/xbib/logging/formatters/ColorPrintf.java b/logging/src/main/java/org/xbib/logging/formatters/ColorPrintf.java new file mode 100644 index 0000000..ad7900c --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/formatters/ColorPrintf.java @@ -0,0 +1,117 @@ +package org.xbib.logging.formatters; + +import java.lang.reflect.Executable; +import java.lang.reflect.Field; +import java.time.temporal.TemporalAccessor; +import java.time.temporal.TemporalField; +import java.util.Formattable; +import java.util.Locale; +import java.util.UUID; +import org.xbib.logging.handlers.ConsoleHandler; + +class ColorPrintf extends Printf { + private final int darken; + private final boolean trueColor = ConsoleHandler.isTrueColor(); + + ColorPrintf(final int darken) { + super(Locale.getDefault()); + this.darken = darken; + } + + public StringBuilder formatDirect(final StringBuilder destination, final String format, final Object... params) { + ColorUtil.endFgColor(destination); + super.formatDirect(destination, format, params); + return destination; + } + + protected void formatTimeTextField(final StringBuilder target, final TemporalAccessor ta, final TemporalField field, + final String[] symbols, final GeneralFlags genFlags, final int width) { + super.formatTimeTextField(target, ta, field, symbols, genFlags, width); + } + + protected void formatTimeZoneId(final StringBuilder target, final TemporalAccessor ta, final GeneralFlags genFlags, + final int width) { + super.formatTimeZoneId(target, ta, genFlags, width); + } + + protected void formatTimeZoneOffset(final StringBuilder target, final TemporalAccessor ta, final GeneralFlags genFlags, + final int width) { + super.formatTimeZoneOffset(target, ta, genFlags, width); + } + + protected void formatTimeField(final StringBuilder target, final TemporalAccessor ta, final TemporalField field, + final GeneralFlags genFlags, final int width, final int zeroPad) { + super.formatTimeField(target, ta, field, genFlags, width, zeroPad); + } + + protected void formatPercent(final StringBuilder target) { + super.formatPercent(target); + } + + protected void formatLineSeparator(final StringBuilder target) { + super.formatLineSeparator(target); + } + + protected void formatFormattableString(final StringBuilder target, final Formattable formattable, + final GeneralFlags genFlags, final int width, final int precision) { + super.formatFormattableString(target, formattable, genFlags, width, precision); + } + + protected void formatPlainString(final StringBuilder target, final Object item, final GeneralFlags genFlags, + final int width, final int precision) { + if (item instanceof Class || item instanceof Executable || item instanceof Field) { + ColorUtil.startFgColor(target, trueColor, 0xff >>> darken, 0xff >>> darken, 0xdd >>> darken); + } else if (item instanceof UUID) { + ColorUtil.startFgColor(target, trueColor, 0xdd >>> darken, 0xff >>> darken, 0xdd >>> darken); + } else { + ColorUtil.startFgColor(target, trueColor, 0xdd >>> darken, 0xdd >>> darken, 0xdd >>> darken); + } + super.formatPlainString(target, item, genFlags, width, precision); + ColorUtil.endFgColor(target); + } + + protected void formatBoolean(final StringBuilder target, final Object item, final GeneralFlags genFlags, final int width, + final int precision) { + super.formatBoolean(target, item, genFlags, width, precision); + } + + protected void formatHashCode(final StringBuilder target, final Object item, final GeneralFlags genFlags, final int width, + final int precision) { + super.formatHashCode(target, item, genFlags, width, precision); + } + + protected void formatCharacter(final StringBuilder target, final int codePoint, final GeneralFlags genFlags, + final int width, final int precision) { + super.formatCharacter(target, codePoint, genFlags, width, precision); + } + + protected void formatDecimalInteger(final StringBuilder target, final Number item, final GeneralFlags genFlags, + final NumericFlags numFlags, final int width) { + super.formatDecimalInteger(target, item, genFlags, numFlags, width); + } + + protected void formatOctalInteger(final StringBuilder target, final Number item, final GeneralFlags genFlags, + final NumericFlags numFlags, final int width) { + super.formatOctalInteger(target, item, genFlags, numFlags, width); + } + + protected void formatHexInteger(final StringBuilder target, final Number item, final GeneralFlags genFlags, + final NumericFlags numFlags, final int width) { + super.formatHexInteger(target, item, genFlags, numFlags, width); + } + + protected void formatFloatingPointSci(final StringBuilder target, final Number item, final GeneralFlags genFlags, + final NumericFlags numFlags, final int width, final int precision) { + super.formatFloatingPointSci(target, item, genFlags, numFlags, width, precision); + } + + protected void formatFloatingPointDecimal(final StringBuilder target, final Number item, final GeneralFlags genFlags, + final NumericFlags numFlags, final int width, final int precision) { + super.formatFloatingPointDecimal(target, item, genFlags, numFlags, width, precision); + } + + protected void formatFloatingPointGeneral(final StringBuilder target, final Number item, final GeneralFlags genFlags, + final NumericFlags numFlags, final int width, final int precision) { + super.formatFloatingPointGeneral(target, item, genFlags, numFlags, width, precision); + } +} diff --git a/logging/src/main/java/org/xbib/logging/formatters/ColorUtil.java b/logging/src/main/java/org/xbib/logging/formatters/ColorUtil.java new file mode 100644 index 0000000..ec2ff71 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/formatters/ColorUtil.java @@ -0,0 +1,48 @@ +package org.xbib.logging.formatters; + +/** + * This is a throwaway temp class. + */ +final class ColorUtil { + + private ColorUtil() { + } + + static StringBuilder startFgColor(StringBuilder target, boolean trueColor, int r, int g, int b) { + return startColor(target, 38, trueColor, r, g, b); + } + + static StringBuilder startBgColor(StringBuilder target, boolean trueColor, int r, int g, int b) { + return startColor(target, 48, trueColor, r, g, b); + } + + static StringBuilder startColor(StringBuilder target, int mode, boolean trueColor, int r, int g, int b) { + if (trueColor) { + return target.appendCodePoint(27).append('[').append(mode).append(';').append(2).append(';').append(clip(r)) + .append(';').append(clip(g)).append(';').append(clip(b)).append('m'); + } else { + int ar = (5 * clip(r)) / 255; + int ag = (5 * clip(g)) / 255; + int ab = (5 * clip(b)) / 255; + int col = 16 + 36 * ar + 6 * ag + ab; + return target.appendCodePoint(27).append('[').append(mode).append(';').append('5').append(';').append(col) + .append('m'); + } + } + + private static int clip(int color) { + return Math.min(Math.max(0, color), 255); + } + + static StringBuilder endFgColor(StringBuilder target) { + return endColor(target, 39); + } + + static StringBuilder endBgColor(StringBuilder target) { + return endColor(target, 49); + } + + static StringBuilder endColor(StringBuilder target, int mode) { + return target.appendCodePoint(27).append('[').append(mode).append('m'); + } +} diff --git a/logging/src/main/java/org/xbib/logging/formatters/FlagSet.java b/logging/src/main/java/org/xbib/logging/formatters/FlagSet.java new file mode 100644 index 0000000..15504a1 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/formatters/FlagSet.java @@ -0,0 +1,112 @@ +package org.xbib.logging.formatters; + +import java.util.AbstractSet; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.function.Consumer; +import java.util.function.IntFunction; + +abstract class FlagSet> extends AbstractSet { + final int value; + + FlagSet(final int value) { + this.value = value; + } + + abstract E[] values(); + + public Iterator iterator() { + return new Iterator() { + int bits = value; + + public boolean hasNext() { + return bits != 0; + } + + public E next() { + if (!hasNext()) + throw new NoSuchElementException(); + int lob = Integer.lowestOneBit(bits); + bits &= ~lob; + return values()[Integer.numberOfTrailingZeros(lob)]; + } + }; + } + + public int size() { + return Integer.bitCount(value); + } + + public void forEach(final Consumer action) { + int bits = value; + int lob; + while (bits != 0) { + lob = Integer.lowestOneBit(bits); + bits &= ~lob; + action.accept(values()[Integer.numberOfTrailingZeros(lob)]); + } + } + + @SuppressWarnings("unchecked") + public T[] toArray(final IntFunction generator) { + T[] array = generator.apply(size()); + int idx = 0, lob, bits = value; + while (bits != 0) { + lob = Integer.lowestOneBit(bits); + bits &= ~lob; + array[idx++] = (T) values()[Integer.numberOfTrailingZeros(lob)]; + } + return array; + } + + public boolean contains(final Object o) { + return o instanceof Enum && contains((Enum) o); + } + + public boolean contains(final Enum e) { + // override in subclass for type verification + return e != null && (value & 1 << e.ordinal()) != 0; + } + + public int hashCode() { + int hc = 0; + int bits = value; + int lob; + while (bits != 0) { + lob = Integer.lowestOneBit(bits); + bits &= ~lob; + hc += values()[Integer.numberOfTrailingZeros(lob)].hashCode(); + } + return hc; + } + + public boolean equals(final Object o) { + return o.getClass() == getClass() && ((FlagSet) o).value == value || super.equals(o); + } + + public void forbid(final E flag) { + if (contains(flag)) { + throw notAllowed(flag); + } + } + + public void forbidAll() { + if (!isEmpty()) { + throw notAllowed(this); + } + } + + public void forbidAllBut(final E flag) { + without(flag).forbidAll(); + } + + abstract FlagSet without(final E flag); + + private static IllegalArgumentException notAllowed(final FlagSet set) { + return new IllegalArgumentException("Flags " + set + " are not allowed here"); + } + + private static IllegalArgumentException notAllowed(final Enum flag) { + return new IllegalArgumentException("Flag " + flag + " is not allowed here"); + } +} diff --git a/logging/src/main/java/org/xbib/logging/formatters/FormatStep.java b/logging/src/main/java/org/xbib/logging/formatters/FormatStep.java new file mode 100644 index 0000000..ffabda0 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/formatters/FormatStep.java @@ -0,0 +1,87 @@ +package org.xbib.logging.formatters; + +import java.util.logging.Formatter; +import org.xbib.logging.ExtLogRecord; + +/** + * A single format step which handles some part of rendering a log record. + */ +public interface FormatStep { + + /** + * Render a part of the log record. + * + * @param builder the string builder to append to + * @param record the record being rendered + */ + void render(StringBuilder builder, ExtLogRecord record); + + /** + * Render a part of the log record to the given formatter. + * + * @param formatter the formatter to render to + * @param builder the string builder to append to + * @param record the record being rendered + */ + default void render(Formatter formatter, StringBuilder builder, ExtLogRecord record) { + render(builder, record); + } + + /** + * Emit an estimate of the length of data which this step will produce. The more accurate the estimate, the + * more likely the format operation will be performant. + * + * @return an estimate + */ + int estimateLength(); + + /** + * Indicates whether or not caller information is required for this format step. + * + * @return {@code true} if caller information is required, otherwise {@code false} + */ + default boolean isCallerInformationRequired() { + return false; + } + + /** + * Get the item type of this step. + * + * @return the item type + */ + default ItemType getItemType() { + return ItemType.GENERIC; + } + + /** + * An enumeration of the types of items that can be rendered. Note that this enumeration may be expanded + * in the future, so unknown values should be handled gracefully as if {@link #GENERIC} were used. + */ + enum ItemType { + GENERIC, + COMPOUND, + LEVEL, + SOURCE_CLASS_NAME, + DATE, + SOURCE_FILE_NAME, + HOST_NAME, + SOURCE_LINE_NUMBER, + LINE_SEPARATOR, + CATEGORY, + MDC, + MESSAGE, + EXCEPTION_TRACE, + SOURCE_METHOD_NAME, + SOURCE_MODULE_NAME, + SOURCE_MODULE_VERSION, + NDC, + PROCESS_ID, + PROCESS_NAME, + RELATIVE_TIME, + RESOURCE_KEY, + SYSTEM_PROPERTY, + TEXT, + THREAD_ID, + THREAD_NAME, + } +} diff --git a/logging/src/main/java/org/xbib/logging/formatters/FormatStringParser.java b/logging/src/main/java/org/xbib/logging/formatters/FormatStringParser.java new file mode 100644 index 0000000..8bc8ee6 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/formatters/FormatStringParser.java @@ -0,0 +1,219 @@ +package org.xbib.logging.formatters; + +import java.util.ArrayList; +import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A parser which can translate a log4j-style format string into a series of {@code FormatStep} instances. + */ +public final class FormatStringParser { + + /** + * The regular expression for format strings. Ain't regex grand? + */ + private static final Pattern pattern = Pattern.compile( + // greedily match all non-format characters + "([^%]++)" + + // match a format string... + "|(?:%" + + // optional minimum width plus justify flag + "(?:(-)?(\\d+))?" + + // optional maximum width + "(?:\\.(-)?(\\d+))?" + + // the actual format character + "(.)" + + // an optional argument string + "(?:\\{([^}]*)\\})?" + + // end format string + ")"); + + private FormatStringParser() { + } + + /** + * Compile a format string into a series of format steps. + * + * @param formatString the format string + * @return the format steps + */ + public static FormatStep[] getSteps(final String formatString, ColorMap colors) { + final long time = System.currentTimeMillis(); + final ArrayList stepList = new ArrayList(); + final Matcher matcher = pattern.matcher(formatString); + TimeZone timeZone = TimeZone.getDefault(); + + boolean colorUsed = false; + while (matcher.find()) { + final String otherText = matcher.group(1); + if (otherText != null) { + stepList.add(Formatters.textFormatStep(otherText)); + } else { + final String hyphen = matcher.group(2); + final String minWidthString = matcher.group(3); + final String widthHyphen = matcher.group(4); + final String maxWidthString = matcher.group(5); + final String formatCharString = matcher.group(6); + final String argument = matcher.group(7); + final int minimumWidth = minWidthString == null ? 0 : Integer.parseInt(minWidthString); + final boolean leftJustify = hyphen != null; + final boolean truncateBeginning = widthHyphen != null; + final int maximumWidth = maxWidthString == null ? 0 : Integer.parseInt(maxWidthString); + final char formatChar = formatCharString.charAt(0); + switch (formatChar) { + case 'c': { + stepList.add(Formatters.loggerNameFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth, + argument)); + break; + } + case 'C': { + stepList.add(Formatters.classNameFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth, + argument)); + break; + } + case 'd': { + stepList.add(Formatters.dateFormatStep(timeZone, argument, leftJustify, minimumWidth, truncateBeginning, + maximumWidth)); + break; + } + case 'D': { + stepList.add(Formatters.moduleNameFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth, + argument)); + break; + } + case 'e': { + stepList.add(Formatters.exceptionFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth, + argument, false)); + break; + } + case 'E': { + stepList.add(Formatters.exceptionFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth, + argument, true)); + break; + } + case 'F': { + stepList.add(Formatters.fileNameFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth)); + break; + } + case 'h': { + stepList.add(Formatters.hostnameFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth, + false)); + break; + } + case 'H': { + stepList.add(Formatters.hostnameFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth, + argument)); + break; + } + case 'i': { + stepList.add( + Formatters.processIdFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth)); + break; + } + case 'k': { + stepList.add( + Formatters.resourceKeyFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth)); + break; + } + case 'K': { + if (ColorMap.SUPPORTS_COLOR) { + colorUsed = true; + stepList.add(Formatters.formatColor(colors, argument)); + } + break; + } + case 'l': { + stepList.add(Formatters.locationInformationFormatStep(leftJustify, minimumWidth, truncateBeginning, + maximumWidth)); + break; + } + case 'L': { + stepList.add( + Formatters.lineNumberFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth)); + break; + } + case 'm': { + stepList.add(Formatters.messageFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth)); + break; + } + case 'M': { + stepList.add( + Formatters.methodNameFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth)); + break; + } + case 'n': { + stepList.add( + Formatters.lineSeparatorFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth)); + break; + } + case 'N': { + stepList.add( + Formatters.processNameFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth)); + break; + } + case 'p': { + stepList.add(Formatters.levelFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth)); + break; + } + case 'P': { + stepList.add(Formatters.localizedLevelFormatStep(leftJustify, minimumWidth, truncateBeginning, + maximumWidth)); + break; + } + case 'r': { + stepList.add(Formatters.relativeTimeFormatStep(time, leftJustify, minimumWidth, truncateBeginning, + maximumWidth)); + break; + } + case 's': { + stepList.add( + Formatters.simpleMessageFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth)); + break; + } + case 't': { + stepList.add(Formatters.threadFormatStep(argument, leftJustify, minimumWidth, truncateBeginning, + maximumWidth)); + break; + } + case 'v': { + stepList.add(Formatters.moduleVersionFormatStep(leftJustify, minimumWidth, maximumWidth, argument)); + break; + } + case 'x': { + final int count = argument == null ? 0 : Integer.parseInt(argument); + stepList.add( + Formatters.ndcFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth, count)); + break; + } + case 'X': { + stepList.add( + Formatters.mdcFormatStep(argument, leftJustify, minimumWidth, truncateBeginning, maximumWidth)); + break; + } + case 'z': { + timeZone = TimeZone.getTimeZone(argument); + break; + } + case '#': + case '$': { + stepList.add(Formatters.systemPropertyFormatStep(argument, leftJustify, minimumWidth, truncateBeginning, + maximumWidth)); + break; + } + case '%': { + stepList.add(Formatters.textFormatStep("%")); + break; + } + default: { + throw new IllegalArgumentException("Encountered an unknown format character"); + } + } + } + } + if (colorUsed) { + stepList.add(Formatters.formatColor(colors, ColorMap.CLEAR_NAME)); + } + return stepList.toArray(new FormatStep[stepList.size()]); + } +} diff --git a/logging/src/main/java/org/xbib/logging/formatters/Formatters.java b/logging/src/main/java/org/xbib/logging/formatters/Formatters.java new file mode 100644 index 0000000..20a4118 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/formatters/Formatters.java @@ -0,0 +1,1387 @@ +package org.xbib.logging.formatters; + +import java.io.PrintWriter; +import java.time.Duration; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.ArrayDeque; +import java.util.Collections; +import java.util.Deque; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; +import java.util.TreeMap; +import java.util.logging.Formatter; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.regex.Pattern; +import org.xbib.logging.ExtFormatter; +import org.xbib.logging.ExtLogRecord; +import org.xbib.logging.util.StackTraceFormatter; +import static java.lang.Math.max; +import static java.lang.Math.min; + +/** + * Formatter utility methods. + */ +@SuppressWarnings({"WeakerAccess", "unused"}) +public final class Formatters { + + public static final String THREAD_ID = "id"; + + private static final boolean DEFAULT_TRUNCATE_BEGINNING = false; + + private static final String NEW_LINE = String.format("%n"); + + private static final Pattern PRECISION_INT_PATTERN = Pattern.compile("\\d+"); + + private Formatters() { + } + + private static final Formatter NULL_FORMATTER = new Formatter() { + public String format(final LogRecord record) { + return ""; + } + }; + + /** + * Get the null formatter, which outputs nothing. + * + * @return the null formatter + */ + public static Formatter nullFormatter() { + return NULL_FORMATTER; + } + + /** + * Create a format step which simply emits the given string. + * + * @param string the string to emit + * @return a format step + */ + public static FormatStep textFormatStep(final String string) { + return new FormatStep() { + public void render(final StringBuilder builder, final ExtLogRecord record) { + builder.append(string); + } + + public int estimateLength() { + return string.length(); + } + + public ItemType getItemType() { + return ItemType.TEXT; + } + }; + } + + /** + * Apply up to {@code count} trailing segments of the given string to the given {@code builder}. + * + * @param count the maximum number of segments to include + * @param subject the subject string + * @return the substring + */ + private static String applySegments(final int count, final String subject) { + if (count == 0) { + return subject; + } + int idx = subject.length() + 1; + for (int i = 0; i < count; i++) { + idx = subject.lastIndexOf('.', idx - 1); + if (idx == -1) { + return subject; + } + } + return subject.substring(idx + 1); + } + + /** + * Apply up to {@code precision} trailing segments of the given string to the given {@code builder}. If the + * precision contains non-integer values + * + * @param precision the precision used to + * @param subject the subject string + * @return the substring + */ + private static String applySegments(final String precision, final String subject) { + if (precision == null || subject == null) { + return subject; + } + + // Check for dots + if (PRECISION_INT_PATTERN.matcher(precision).matches()) { + return applySegments(Integer.parseInt(precision), subject); + } + // %c{1.} would be o.j.l.f.FormatStringParser + // %c{1.~} would be o.~.~.~.FormatStringParser + // %c{.} ....FormatStringParser + final Map segments = parsePatternSegments(precision); + final Deque categorySegments = parseCategorySegments(subject); + final StringBuilder result = new StringBuilder(); + Segment segment = null; + int index = 0; + while (true) { + index++; + if (segments.containsKey(index)) { + segment = segments.get(index); + } + final String s = categorySegments.poll(); + // Always print the last part of the category segments + if (categorySegments.peek() == null) { + result.append(s); + break; + } + if (segment == null) { + result.append(s).append('.'); + } else { + if (segment.len > 0) { + if (segment.len > s.length()) { + result.append(s); + } else { + result.append(s, 0, segment.len); + } + } + if (segment.text != null) { + result.append(segment.text); + } + result.append('.'); + } + } + return result.toString(); + } + + private abstract static class JustifyingFormatStep implements FormatStep { + private final boolean leftJustify; + private final boolean truncateBeginning; + private final int minimumWidth; + private final int maximumWidth; + + protected JustifyingFormatStep(final boolean leftJustify, final int minimumWidth, final boolean truncateBeginning, + final int maximumWidth) { + if (maximumWidth != 0 && minimumWidth > maximumWidth) { + throw new IllegalArgumentException( + "Specified minimum width may not be greater than the specified maximum width"); + } + if (maximumWidth < 0 || minimumWidth < 0) { + throw new IllegalArgumentException("Minimum and maximum widths must not be less than zero"); + } + this.leftJustify = leftJustify; + this.truncateBeginning = truncateBeginning; + this.minimumWidth = minimumWidth; + this.maximumWidth = maximumWidth == 0 ? Integer.MAX_VALUE : maximumWidth; + } + + public void render(final StringBuilder builder, final ExtLogRecord record) { + render(null, builder, record); + } + + public void render(Formatter formatter, StringBuilder builder, ExtLogRecord record) { + final int minimumWidth = this.minimumWidth; + final int maximumWidth = this.maximumWidth; + final boolean leftJustify = this.leftJustify; + if (leftJustify) { + // no copy necessary for left justification + final int oldLen = builder.length(); + renderRaw(formatter, builder, record); + final int newLen = builder.length(); + // if we exceeded the max width, chop it off + final int writtenLen = newLen - oldLen; + final int overflow = writtenLen - maximumWidth; + if (overflow > 0) { + if (truncateBeginning) { + builder.delete(oldLen, oldLen + overflow); + } + builder.setLength(newLen - overflow); + } else { + final int spaces = minimumWidth - writtenLen; + for (int i = 0; i < spaces; i++) { + builder.append(' '); + } + } + } else { + // only copy the data if we're right justified + final StringBuilder subBuilder = new StringBuilder(); + renderRaw(formatter, subBuilder, record); + final int len = subBuilder.length(); + if (len > maximumWidth) { + if (truncateBeginning) { + final int overflow = len - maximumWidth; + subBuilder.delete(0, overflow); + } + subBuilder.setLength(maximumWidth); + } else if (len < minimumWidth) { + // right justify + int spaces = minimumWidth - len; + for (int i = 0; i < spaces; i++) { + builder.append(' '); + } + } + builder.append(subBuilder); + } + } + + public int estimateLength() { + final int maximumWidth = this.maximumWidth; + final int minimumWidth = this.minimumWidth; + if (maximumWidth != 0) { + return min(maximumWidth, minimumWidth * 3); + } else { + return max(32, minimumWidth); + } + } + + public abstract void renderRaw(Formatter formatter, final StringBuilder builder, final ExtLogRecord record); + } + + private abstract static class SegmentedFormatStep extends JustifyingFormatStep { + private final int count; + private final String precision; + + protected SegmentedFormatStep(final boolean leftJustify, final int minimumWidth, final boolean truncateBeginning, + final int maximumWidth, final int count) { + super(leftJustify, minimumWidth, truncateBeginning, maximumWidth); + this.count = count; + precision = null; + } + + protected SegmentedFormatStep(final boolean leftJustify, final int minimumWidth, final boolean truncateBeginning, + final int maximumWidth, final String precision) { + super(leftJustify, minimumWidth, truncateBeginning, maximumWidth); + this.count = 0; + this.precision = precision; + } + + public void renderRaw(Formatter formatter, final StringBuilder builder, final ExtLogRecord record) { + if (precision == null) { + builder.append(applySegments(count, getSegmentedSubject(record))); + } else { + builder.append(applySegments(precision, getSegmentedSubject(record))); + } + } + + public abstract String getSegmentedSubject(final ExtLogRecord record); + } + + /** + * Create a format step which emits the logger name with the given justification rules. + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @param precision the argument used for the logger name, may be {@code null} or contain dots to format the logger name + * @return the format + */ + public static FormatStep loggerNameFormatStep(final boolean leftJustify, final int minimumWidth, final int maximumWidth, + final String precision) { + return loggerNameFormatStep(leftJustify, minimumWidth, DEFAULT_TRUNCATE_BEGINNING, maximumWidth, precision); + } + + /** + * Create a format step which emits the logger name with the given justification rules. + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param truncateBeginning {@code true} to truncate the beginning, otherwise {@code false} to truncate the end + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @param precision the argument used for the logger name, may be {@code null} or contain dots to format the + * logger name + * @return the format + */ + public static FormatStep loggerNameFormatStep(final boolean leftJustify, final int minimumWidth, + final boolean truncateBeginning, final int maximumWidth, final String precision) { + return new SegmentedFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth, precision) { + public ItemType getItemType() { + return ItemType.CATEGORY; + } + + public String getSegmentedSubject(final ExtLogRecord record) { + return record.getLoggerName(); + } + }; + } + + /** + * Create a format step which emits the source class name with the given justification rules (NOTE: call stack + * introspection introduces a significant performance penalty). + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @param precision the argument used for the class name, may be {@code null} or contain dots to format the class name + * @return the format step + */ + public static FormatStep classNameFormatStep(final boolean leftJustify, final int minimumWidth, final int maximumWidth, + final String precision) { + return classNameFormatStep(leftJustify, minimumWidth, DEFAULT_TRUNCATE_BEGINNING, maximumWidth, precision); + } + + /** + * Create a format step which emits the source class name with the given justification rules (NOTE: call stack + * introspection introduces a significant performance penalty). + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param truncateBeginning {@code true} to truncate the beginning, otherwise {@code false} to truncate the end + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @param precision the argument used for the class name, may be {@code null} or contain dots to format the + * class name + * @return the format step + */ + public static FormatStep classNameFormatStep(final boolean leftJustify, final int minimumWidth, + final boolean truncateBeginning, final int maximumWidth, final String precision) { + return new SegmentedFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth, precision) { + public String getSegmentedSubject(final ExtLogRecord record) { + return record.getSourceClassName(); + } + + @Override + public boolean isCallerInformationRequired() { + return true; + } + + public ItemType getItemType() { + return ItemType.SOURCE_CLASS_NAME; + } + }; + } + + /** + * Create a format step which emits the source module name with the given justification rules (NOTE: call stack + * introspection introduces a significant performance penalty). + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @param precision the argument used for the class name, may be {@code null} or contain dots to format the class name + * @return the format step + */ + public static FormatStep moduleNameFormatStep(final boolean leftJustify, final int minimumWidth, final int maximumWidth, + final String precision) { + return moduleNameFormatStep(leftJustify, minimumWidth, DEFAULT_TRUNCATE_BEGINNING, maximumWidth, precision); + } + + /** + * Create a format step which emits the source module name with the given justification rules (NOTE: call stack + * introspection introduces a significant performance penalty). + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param truncateBeginning {@code true} to truncate the beginning, otherwise {@code false} to truncate the end + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @param precision the argument used for the class name, may be {@code null} or contain dots to format the + * class name + * @return the format step + */ + public static FormatStep moduleNameFormatStep(final boolean leftJustify, final int minimumWidth, + final boolean truncateBeginning, final int maximumWidth, final String precision) { + return new SegmentedFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth, precision) { + public String getSegmentedSubject(final ExtLogRecord record) { + return record.getSourceModuleName(); + } + + @Override + public boolean isCallerInformationRequired() { + return true; + } + + public ItemType getItemType() { + return ItemType.SOURCE_MODULE_NAME; + } + }; + } + + /** + * Create a format step which emits the source module version with the given justification rules (NOTE: call stack + * introspection introduces a significant performance penalty). + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @param precision the argument used for the class name, may be {@code null} or contain dots to format the + * class name + * @return the format step + */ + public static FormatStep moduleVersionFormatStep(final boolean leftJustify, final int minimumWidth, final int maximumWidth, + final String precision) { + return new SegmentedFormatStep(leftJustify, minimumWidth, DEFAULT_TRUNCATE_BEGINNING, maximumWidth, precision) { + public String getSegmentedSubject(final ExtLogRecord record) { + return record.getSourceModuleVersion(); + } + + @Override + public boolean isCallerInformationRequired() { + return true; + } + + public ItemType getItemType() { + return ItemType.SOURCE_MODULE_VERSION; + } + }; + } + + /** + * Create a format step which emits the date of the log record with the given justification rules. + * + * @param timeZone the time zone to format to + * @param formatString the date format string + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @return the format step + */ + public static FormatStep dateFormatStep(final TimeZone timeZone, final String formatString, final boolean leftJustify, + final int minimumWidth, final int maximumWidth) { + return dateFormatStep(timeZone, formatString, leftJustify, minimumWidth, DEFAULT_TRUNCATE_BEGINNING, maximumWidth); + } + + /** + * Create a format step which emits the date of the log record with the given justification rules. + * + * @param timeZone the time zone to format to + * @param formatString the date format string + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param truncateBeginning {@code true} to truncate the beginning, otherwise {@code false} to truncate the end + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @return the format step + */ + public static FormatStep dateFormatStep(final TimeZone timeZone, final String formatString, final boolean leftJustify, + final int minimumWidth, + final boolean truncateBeginning, final int maximumWidth) { + return new JustifyingFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth) { + final DateTimeFormatter dtf = DateTimeFormatter + .ofPattern(formatString == null ? "yyyy-MM-dd HH:mm:ss,SSS" : formatString); + + public ItemType getItemType() { + return ItemType.DATE; + } + + public void renderRaw(Formatter formatter, final StringBuilder builder, final ExtLogRecord record) { + dtf.formatTo(record.getInstant().atZone(timeZone.toZoneId()), builder); + } + }; + } + + /** + * Create a format step which emits the date of the log record with the given justification rules. + * + * @param formatString the date format string + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @return the format step + */ + public static FormatStep dateFormatStep(final String formatString, final boolean leftJustify, final int minimumWidth, + final int maximumWidth) { + return dateFormatStep(TimeZone.getDefault(), formatString, leftJustify, minimumWidth, maximumWidth); + } + + /** + * Create a format step which emits the source file name with the given justification rules (NOTE: call stack + * introspection introduces a significant performance penalty). + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @return the format step + */ + public static FormatStep fileNameFormatStep(final boolean leftJustify, final int minimumWidth, final int maximumWidth) { + return fileNameFormatStep(leftJustify, minimumWidth, DEFAULT_TRUNCATE_BEGINNING, maximumWidth); + } + + /** + * Create a format step which emits the source file name with the given justification rules (NOTE: call stack + * introspection introduces a significant performance penalty). + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param truncateBeginning {@code true} to truncate the beginning, otherwise {@code false} to truncate the end + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @return the format step + */ + public static FormatStep fileNameFormatStep(final boolean leftJustify, final int minimumWidth, + final boolean truncateBeginning, final int maximumWidth) { + return new JustifyingFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth) { + public void renderRaw(Formatter formatter, final StringBuilder builder, final ExtLogRecord record) { + builder.append(record.getSourceFileName()); + } + + @Override + public boolean isCallerInformationRequired() { + return true; + } + + public ItemType getItemType() { + return ItemType.SOURCE_FILE_NAME; + } + }; + } + + /** + * Create a format step which emits the source process name with the given justification rules. + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param truncateBeginning {@code true} to truncate the beginning, otherwise {@code false} to truncate the end + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @return the format step + */ + public static FormatStep processNameFormatStep(final boolean leftJustify, final int minimumWidth, + final boolean truncateBeginning, final int maximumWidth) { + return new JustifyingFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth) { + public ItemType getItemType() { + return ItemType.PROCESS_NAME; + } + + public void renderRaw(Formatter formatter, final StringBuilder builder, final ExtLogRecord record) { + builder.append(record.getProcessName()); + } + }; + } + + /** + * Create a format step which emits the source file line number with the given justification rules (NOTE: call stack + * introspection introduces a significant performance penalty). + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param truncateBeginning {@code true} to truncate the beginning, otherwise {@code false} to truncate the end + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @return the format step + */ + public static FormatStep processIdFormatStep(final boolean leftJustify, final int minimumWidth, + final boolean truncateBeginning, final int maximumWidth) { + return new JustifyingFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth) { + public ItemType getItemType() { + return ItemType.PROCESS_ID; + } + + public void renderRaw(Formatter formatter, final StringBuilder builder, final ExtLogRecord record) { + builder.append(record.getProcessId()); + } + }; + } + + /** + * Create a format step which emits the hostname. + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param truncateBeginning {@code true} to truncate the beginning, otherwise {@code false} to truncate the end + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @param qualified {@code true} to use the fully qualified host name, {@code false} to only use the + * @return the format step + */ + public static FormatStep hostnameFormatStep(final boolean leftJustify, final int minimumWidth, + final boolean truncateBeginning, final int maximumWidth, final boolean qualified) { + return qualified ? hostnameFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth, null) + : new SegmentedFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth, null) { + public ItemType getItemType() { + return ItemType.HOST_NAME; + } + + public String getSegmentedSubject(final ExtLogRecord record) { + final String hostName = record.getHostName(); + final int idx = hostName.indexOf('.'); + return idx == -1 ? hostName : hostName.substring(0, idx); + } + }; + } + + /** + * Create a format step which emits the hostname. + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param truncateBeginning {@code true} to truncate the beginning, otherwise {@code false} to truncate the end + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @param precision the argument used for the class name, may be {@code null} or contain dots to format the class + * name + * @return the format step + */ + public static FormatStep hostnameFormatStep(final boolean leftJustify, final int minimumWidth, + final boolean truncateBeginning, final int maximumWidth, final String precision) { + return new SegmentedFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth, null) { + public ItemType getItemType() { + return ItemType.HOST_NAME; + } + + public String getSegmentedSubject(final ExtLogRecord record) { + final String hostName = record.getHostName(); + // Check for a specified precision. This is not passed to the constructor because we want truncate + // segments from the right instead of the left. + if (precision != null && PRECISION_INT_PATTERN.matcher(precision).matches()) { + int count = Integer.parseInt(precision); + int end = 0; + for (int i = 0; i < hostName.length(); i++) { + // If we've got a dot we're at a new segment + if (hostName.charAt(i) == '.') { + count--; + end = i; + } + // We've reached the precision we want + if (count == 0) { + break; + } + } + if (end != 0 && count == 0) { + return hostName.substring(0, end); + } + } + return hostName; + } + }; + } + + /** + * Create a format step which emits the complete source location information with the given justification rules + * (NOTE: call stack introspection introduces a significant performance penalty). + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @return the format step + */ + public static FormatStep locationInformationFormatStep(final boolean leftJustify, final int minimumWidth, + final int maximumWidth) { + return locationInformationFormatStep(leftJustify, minimumWidth, DEFAULT_TRUNCATE_BEGINNING, maximumWidth); + } + + /** + * Create a format step which emits the complete source location information with the given justification rules + * (NOTE: call stack introspection introduces a significant performance penalty). + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param truncateBeginning {@code true} to truncate the beginning, otherwise {@code false} to truncate the end + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @return the format step + */ + public static FormatStep locationInformationFormatStep(final boolean leftJustify, final int minimumWidth, + final boolean truncateBeginning, final int maximumWidth) { + return new JustifyingFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth) { + public void renderRaw(Formatter formatter, final StringBuilder builder, final ExtLogRecord record) { + final String fileName = record.getSourceFileName(); + final int lineNumber = record.getSourceLineNumber(); + final String className = record.getSourceClassName(); + final String methodName = record.getSourceMethodName(); + builder.append(className).append('.').append(methodName); + builder.append('(').append(fileName); + if (lineNumber != -1) { + builder.append(':').append(lineNumber); + } + builder.append(')'); + } + + @Override + public boolean isCallerInformationRequired() { + return true; + } + }; + } + + /** + * Create a format step which emits the source file line number with the given justification rules (NOTE: call stack + * introspection introduces a significant performance penalty). + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @return the format step + */ + public static FormatStep lineNumberFormatStep(final boolean leftJustify, final int minimumWidth, final int maximumWidth) { + return lineNumberFormatStep(leftJustify, minimumWidth, DEFAULT_TRUNCATE_BEGINNING, maximumWidth); + } + + /** + * Create a format step which emits the source file line number with the given justification rules (NOTE: call stack + * introspection introduces a significant performance penalty). + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param truncateBeginning {@code true} to truncate the beginning, otherwise {@code false} to truncate the end + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @return the format step + */ + public static FormatStep lineNumberFormatStep(final boolean leftJustify, final int minimumWidth, + final boolean truncateBeginning, final int maximumWidth) { + return new JustifyingFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth) { + public void renderRaw(Formatter formatter, final StringBuilder builder, final ExtLogRecord record) { + builder.append(record.getSourceLineNumber()); + } + + @Override + public boolean isCallerInformationRequired() { + return true; + } + + public ItemType getItemType() { + return ItemType.SOURCE_LINE_NUMBER; + } + }; + } + + /** + * Create a format step which emits the formatted log message text with the given justification rules. + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @return the format step + */ + public static FormatStep messageFormatStep(final boolean leftJustify, final int minimumWidth, final int maximumWidth) { + return messageFormatStep(leftJustify, minimumWidth, DEFAULT_TRUNCATE_BEGINNING, maximumWidth); + } + + /** + * Create a format step which emits the formatted log message text with the given justification rules. + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param truncateBeginning {@code true} to truncate the beginning, otherwise {@code false} to truncate the end + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @return the format step + */ + public static FormatStep messageFormatStep(final boolean leftJustify, final int minimumWidth, + final boolean truncateBeginning, final int maximumWidth) { + return new JustifyingFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth) { + public void renderRaw(Formatter formatter, final StringBuilder builder, final ExtLogRecord record) { + String formatted; + if (formatter == null + || record.getFormatStyle() == ExtLogRecord.FormatStyle.PRINTF && !(formatter instanceof ExtFormatter)) { + formatted = record.getFormattedMessage(); + } else { + formatted = formatter.formatMessage(record); + } + builder.append(formatted); + final Throwable t = record.getThrown(); + if (t != null) { + builder.append(": "); + t.printStackTrace(new PrintWriter(new StringBuilderWriter(builder))); + } + } + + // not really correct but doesn't matter for now + public ItemType getItemType() { + return ItemType.MESSAGE; + } + }; + } + + /** + * Create a format step which emits the formatted log message text (simple version, no exception traces) with the given + * justification rules. + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @return the format step + */ + public static FormatStep simpleMessageFormatStep(final boolean leftJustify, final int minimumWidth, + final int maximumWidth) { + return simpleMessageFormatStep(leftJustify, minimumWidth, DEFAULT_TRUNCATE_BEGINNING, maximumWidth); + } + + /** + * Create a format step which emits the formatted log message text (simple version, no exception traces) with the given + * justification rules. + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param truncateBeginning {@code true} to truncate the beginning, otherwise {@code false} to truncate the end + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @return the format step + */ + public static FormatStep simpleMessageFormatStep(final boolean leftJustify, final int minimumWidth, + final boolean truncateBeginning, final int maximumWidth) { + return new JustifyingFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth) { + public void renderRaw(Formatter formatter, final StringBuilder builder, final ExtLogRecord record) { + String formatted; + if (formatter == null + || record.getFormatStyle() == ExtLogRecord.FormatStyle.PRINTF && !(formatter instanceof ExtFormatter)) { + formatted = record.getFormattedMessage(); + } else { + formatted = formatter.formatMessage(record); + } + builder.append(formatted); + } + }; + } + + /** + * Create a format step which emits the formatted log message text (simple version, no exception traces) with the given + * justification rules. + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @return the format step + */ + public static FormatStep simpleMessageFormatStep(final ExtFormatter formatter, final boolean leftJustify, + final int minimumWidth, final int maximumWidth) { + return simpleMessageFormatStep(formatter, leftJustify, minimumWidth, DEFAULT_TRUNCATE_BEGINNING, maximumWidth); + } + + /** + * Create a format step which emits the formatted log message text (simple version, no exception traces) with the given + * justification rules. + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param truncateBeginning {@code true} to truncate the beginning, otherwise {@code false} to truncate the end + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @return the format step + */ + public static FormatStep simpleMessageFormatStep(final ExtFormatter formatter, final boolean leftJustify, + final int minimumWidth, final boolean truncateBeginning, final int maximumWidth) { + return new JustifyingFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth) { + public void renderRaw(Formatter formatter, final StringBuilder builder, final ExtLogRecord record) { + builder.append(formatter.formatMessage(record)); + } + + public ItemType getItemType() { + return ItemType.MESSAGE; + } + }; + } + + /** + * Create a format step which emits the stack trace of an exception with the given justification rules. + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @param extended {@code true} if the stack trace should attempt to include extended JAR version information + * @return the format step + */ + public static FormatStep exceptionFormatStep(final boolean leftJustify, final int minimumWidth, final int maximumWidth, + final boolean extended) { + return exceptionFormatStep(leftJustify, minimumWidth, DEFAULT_TRUNCATE_BEGINNING, maximumWidth, null, extended); + } + + /** + * Create a format step which emits the stack trace of an exception with the given justification rules. + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param truncateBeginning {@code true} to truncate the beginning, otherwise {@code false} to truncate the end + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @param extended {@code true} if the stack trace should attempt to include extended JAR version information + * @return the format step + */ + public static FormatStep exceptionFormatStep(final boolean leftJustify, final int minimumWidth, + final boolean truncateBeginning, final int maximumWidth, final String argument, final boolean extended) { + return new JustifyingFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth) { + // not really correct but doesn't matter for now + public ItemType getItemType() { + return ItemType.EXCEPTION_TRACE; + } + + public void renderRaw(Formatter formatter, final StringBuilder builder, final ExtLogRecord record) { + doExceptionFormatStep(builder, record, argument, extended); + } + }; + } + + private static void doExceptionFormatStep(final StringBuilder builder, final ExtLogRecord record, final String argument, + final boolean extended) { + final Throwable t = record.getThrown(); + if (t != null) { + int depth = -1; + if (argument != null) { + try { + depth = Integer.parseInt(argument); + } catch (NumberFormatException ignore) { + } + } + StackTraceFormatter.renderStackTrace(builder, t, extended, depth); + } + } + + /** + * Create a format step which emits the log message resource key (if any) with the given justification rules. + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @return the format step + */ + public static FormatStep resourceKeyFormatStep(final boolean leftJustify, final int minimumWidth, final int maximumWidth) { + return resourceKeyFormatStep(leftJustify, minimumWidth, DEFAULT_TRUNCATE_BEGINNING, maximumWidth); + } + + /** + * Create a format step which emits the log message resource key (if any) with the given justification rules. + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param truncateBeginning {@code true} to truncate the beginning, otherwise {@code false} to truncate the end + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @return the format step + */ + public static FormatStep resourceKeyFormatStep(final boolean leftJustify, final int minimumWidth, + final boolean truncateBeginning, final int maximumWidth) { + return new JustifyingFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth) { + public ItemType getItemType() { + return ItemType.RESOURCE_KEY; + } + + public void renderRaw(Formatter formatter, final StringBuilder builder, final ExtLogRecord record) { + final String key = record.getResourceKey(); + if (key != null) + builder.append(key); + } + }; + } + + /** + * Create a format step which emits the source method name with the given justification rules (NOTE: call stack + * introspection introduces a significant performance penalty). + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @return the format step + */ + public static FormatStep methodNameFormatStep(final boolean leftJustify, final int minimumWidth, final int maximumWidth) { + return methodNameFormatStep(leftJustify, minimumWidth, DEFAULT_TRUNCATE_BEGINNING, maximumWidth); + } + + /** + * Create a format step which emits the source method name with the given justification rules (NOTE: call stack + * introspection introduces a significant performance penalty). + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param truncateBeginning {@code true} to truncate the beginning, otherwise {@code false} to truncate the end + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @return the format step + */ + public static FormatStep methodNameFormatStep(final boolean leftJustify, final int minimumWidth, + final boolean truncateBeginning, final int maximumWidth) { + return new JustifyingFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth) { + public void renderRaw(Formatter formatter, final StringBuilder builder, final ExtLogRecord record) { + builder.append(record.getSourceMethodName()); + } + + @Override + public boolean isCallerInformationRequired() { + return true; + } + + public ItemType getItemType() { + return ItemType.SOURCE_METHOD_NAME; + } + }; + } + + private static final String separatorString = System.lineSeparator(); + + /** + * Create a format step which emits the platform line separator. + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @return the format step + */ + public static FormatStep lineSeparatorFormatStep(final boolean leftJustify, final int minimumWidth, + final int maximumWidth) { + return lineSeparatorFormatStep(leftJustify, minimumWidth, DEFAULT_TRUNCATE_BEGINNING, maximumWidth); + } + + /** + * Create a format step which emits the platform line separator. + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param truncateBeginning {@code true} to truncate the beginning, otherwise {@code false} to truncate the end + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @return the format step + */ + public static FormatStep lineSeparatorFormatStep(final boolean leftJustify, final int minimumWidth, + final boolean truncateBeginning, final int maximumWidth) { + return new JustifyingFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth) { + public ItemType getItemType() { + return ItemType.SOURCE_LINE_NUMBER; + } + + public void renderRaw(Formatter formatter, final StringBuilder builder, final ExtLogRecord record) { + builder.append(separatorString); + } + }; + } + + /** + * Create a format step which emits the log level name. + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @return the format step + */ + public static FormatStep levelFormatStep(final boolean leftJustify, final int minimumWidth, final int maximumWidth) { + return levelFormatStep(leftJustify, minimumWidth, DEFAULT_TRUNCATE_BEGINNING, maximumWidth); + } + + /** + * Create a format step which emits the log level name. + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param truncateBeginning {@code true} to truncate the beginning, otherwise {@code false} to truncate the end + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @return the format step + */ + public static FormatStep levelFormatStep(final boolean leftJustify, final int minimumWidth, final boolean truncateBeginning, + final int maximumWidth) { + return new JustifyingFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth) { + public ItemType getItemType() { + return ItemType.LEVEL; + } + + public void renderRaw(Formatter formatter, final StringBuilder builder, final ExtLogRecord record) { + final Level level = record.getLevel(); + builder.append(level.getName()); + } + }; + } + + /** + * Create a format step which emits the localized log level name. + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @return the format step + */ + public static FormatStep localizedLevelFormatStep(final boolean leftJustify, final int minimumWidth, + final int maximumWidth) { + return localizedLevelFormatStep(leftJustify, minimumWidth, DEFAULT_TRUNCATE_BEGINNING, maximumWidth); + } + + /** + * Create a format step which emits the localized log level name. + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param truncateBeginning {@code true} to truncate the beginning, otherwise {@code false} to truncate the end + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @return the format step + */ + public static FormatStep localizedLevelFormatStep(final boolean leftJustify, final int minimumWidth, + final boolean truncateBeginning, final int maximumWidth) { + return new JustifyingFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth) { + public ItemType getItemType() { + return ItemType.LEVEL; + } + + public void renderRaw(Formatter formatter, final StringBuilder builder, final ExtLogRecord record) { + final Level level = record.getLevel(); + builder.append(level.getResourceBundleName() != null ? level.getLocalizedName() : level.getName()); + } + }; + } + + /** + * Create a format step which emits the number of milliseconds since the given base time. + * + * @param baseTime the base time as milliseconds as per {@link System#currentTimeMillis()} + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @return the format step + */ + public static FormatStep relativeTimeFormatStep(final long baseTime, final boolean leftJustify, final int minimumWidth, + final int maximumWidth) { + return relativeTimeFormatStep(baseTime, leftJustify, minimumWidth, DEFAULT_TRUNCATE_BEGINNING, maximumWidth); + } + + /** + * Create a format step which emits the number of milliseconds since the given base time. + * + * @param baseTime the base time as milliseconds as per {@link System#currentTimeMillis()} + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param truncateBeginning {@code true} to truncate the beginning, otherwise {@code false} to truncate the end + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @return the format step + */ + public static FormatStep relativeTimeFormatStep(final long baseTime, final boolean leftJustify, final int minimumWidth, + final boolean truncateBeginning, final int maximumWidth) { + return new JustifyingFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth) { + public ItemType getItemType() { + return ItemType.RELATIVE_TIME; + } + + public void renderRaw(Formatter formatter, final StringBuilder builder, final ExtLogRecord record) { + builder.append(Duration.between(Instant.ofEpochMilli(baseTime), record.getInstant()).toMillis()); + } + }; + } + + /** + * Create a format step which emits the id if {@code id} is passed as the argument, otherwise the the thread name + * is used. + * + * @param argument the argument which may be {@code id} to indicate the thread id or {@code null} to + * indicate the thread name + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param truncateBeginning {@code true} to truncate the beginning, otherwise {@code false} to truncate the end + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @return the format step + */ + public static FormatStep threadFormatStep(final String argument, final boolean leftJustify, final int minimumWidth, + final boolean truncateBeginning, final int maximumWidth) { + if (argument != null && THREAD_ID.equals(argument.toLowerCase(Locale.ROOT))) { + return threadIdFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth); + } + return threadNameFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth); + } + + /** + * Create a format step which emits the id of the thread which originated the log record. + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param truncateBeginning {@code true} to truncate the beginning, otherwise {@code false} to truncate the end + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @return the format step + */ + public static FormatStep threadIdFormatStep(final boolean leftJustify, final int minimumWidth, + final boolean truncateBeginning, final int maximumWidth) { + return new JustifyingFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth) { + public ItemType getItemType() { + return ItemType.THREAD_ID; + } + + public void renderRaw(Formatter formatter, final StringBuilder builder, final ExtLogRecord record) { + builder.append(record.getLongThreadID()); + } + }; + } + + /** + * Create a format step which emits the name of the thread which originated the log record. + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @return the format step + */ + public static FormatStep threadNameFormatStep(final boolean leftJustify, final int minimumWidth, final int maximumWidth) { + return threadNameFormatStep(leftJustify, minimumWidth, DEFAULT_TRUNCATE_BEGINNING, maximumWidth); + } + + /** + * Create a format step which emits the name of the thread which originated the log record. + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param truncateBeginning {@code true} to truncate the beginning, otherwise {@code false} to truncate the end + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @return the format step + */ + public static FormatStep threadNameFormatStep(final boolean leftJustify, final int minimumWidth, + final boolean truncateBeginning, final int maximumWidth) { + return new JustifyingFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth) { + public ItemType getItemType() { + return ItemType.THREAD_NAME; + } + + public void renderRaw(Formatter formatter, final StringBuilder builder, final ExtLogRecord record) { + builder.append(record.getThreadName()); + } + }; + } + + /** + * Create a format step which emits the NDC value of the log record. + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @return the format step + */ + public static FormatStep ndcFormatStep(final boolean leftJustify, final int minimumWidth, final int maximumWidth) { + return ndcFormatStep(leftJustify, minimumWidth, DEFAULT_TRUNCATE_BEGINNING, maximumWidth, 0); + } + + /** + * Create a format step which emits the NDC value of the log record. + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param truncateBeginning {@code true} to truncate the beginning, otherwise {@code false} to truncate the end + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @param count the limit to the number of segments to format + * @return the format step + */ + public static FormatStep ndcFormatStep(final boolean leftJustify, final int minimumWidth, final boolean truncateBeginning, + final int maximumWidth, final int count) { + return new SegmentedFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth, count) { + public ItemType getItemType() { + return ItemType.NDC; + } + + public String getSegmentedSubject(final ExtLogRecord record) { + return record.getNdc(); + } + }; + } + + /** + * Create a format step which emits the MDC value associated with the given key of the log record. + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @return the format step + */ + public static FormatStep mdcFormatStep(final String key, final boolean leftJustify, final int minimumWidth, + final int maximumWidth) { + return mdcFormatStep(key, leftJustify, minimumWidth, DEFAULT_TRUNCATE_BEGINNING, maximumWidth); + } + + /** + * Create a format step which emits the MDC value associated with the given key of the log record. + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param truncateBeginning {@code true} to truncate the beginning, otherwise {@code false} to truncate the end + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @return the format step + */ + public static FormatStep mdcFormatStep(final String key, final boolean leftJustify, final int minimumWidth, + final boolean truncateBeginning, final int maximumWidth) { + return new JustifyingFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth) { + public ItemType getItemType() { + return ItemType.MDC; + } + + public void renderRaw(Formatter formatter, final StringBuilder builder, final ExtLogRecord record) { + if (key == null) { + builder.append(new TreeMap<>(record.getMdcCopy())); + } else { + final String value = record.getMdc(key); + if (value != null) { + builder.append(value); + } + } + } + }; + } + + public static FormatStep formatColor(final ColorMap colors, final String color) { + return new FormatStep() { + public void render(final StringBuilder builder, final ExtLogRecord record) { + String code = colors.getCode(color, record.getLevel()); + if (code != null) { + builder.append(code); + } + } + + public int estimateLength() { + return 7; + } + + }; + } + + /** + * Create a format step which emits a system property value associated with the given key. + * + * @param argument the argument that may be a key or key with a default value separated by a colon, cannot + * be {@code null} + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param truncateBeginning {@code true} to truncate the beginning, otherwise {@code false} to truncate the end + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @return the format step + * @throws IllegalArgumentException if the {@code argument} is {@code null} + */ + public static FormatStep systemPropertyFormatStep(final String argument, final boolean leftJustify, final int minimumWidth, + final boolean truncateBeginning, final int maximumWidth) { + if (argument == null) { + throw new IllegalArgumentException("System property requires a key for the lookup"); + } + return new JustifyingFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth) { + public ItemType getItemType() { + return ItemType.SYSTEM_PROPERTY; + } + + public void renderRaw(Formatter formatter, final StringBuilder builder, final ExtLogRecord record) { + // Check for a default value + final String[] parts = argument.split("(? 1) { + value = parts[1]; + } + builder.append(value); + } + }; + } + + static Map parsePatternSegments(final String pattern) { + final Map segments = new HashMap(); + StringBuilder len = new StringBuilder(); + StringBuilder text = new StringBuilder(); + int pos = 0; + // Process each character + for (char c : pattern.toCharArray()) { + if (c >= '0' && c <= '9') { + len.append(c); + } else if (c == '.') { + pos++; + final int i = (len.length() > 0 ? Integer.parseInt(len.toString()) : 0); + segments.put(pos, new Segment(i, text.length() > 0 ? text.toString() : null)); + text = new StringBuilder(); + len = new StringBuilder(); + } else { + text.append(c); + } + } + if (len.length() > 0 || text.length() > 0) { + pos++; + final int i = (len.length() > 0 ? Integer.parseInt(len.toString()) : 0); + segments.put(pos, new Segment(i, text.length() > 0 ? text.toString() : null)); + } + return Collections.unmodifiableMap(segments); + } + + static Deque parseCategorySegments(final String category) { + // The category needs to be split into segments + final Deque categorySegments = new ArrayDeque(); + StringBuilder cat = new StringBuilder(); + for (char c : category.toCharArray()) { + if (c == '.') { + if (cat.length() > 0) { + categorySegments.add(cat.toString()); + cat = new StringBuilder(); + } else { + categorySegments.add(""); + } + } else { + cat.append(c); + } + } + if (cat.length() > 0) { + categorySegments.add(cat.toString()); + } + return categorySegments; + } + + static class Segment { + final int len; + final String text; + + Segment(final int len, final String text) { + this.len = len; + this.text = text; + } + } +} diff --git a/logging/src/main/java/org/xbib/logging/formatters/GeneralFlag.java b/logging/src/main/java/org/xbib/logging/formatters/GeneralFlag.java new file mode 100644 index 0000000..6572dc3 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/formatters/GeneralFlag.java @@ -0,0 +1,10 @@ +package org.xbib.logging.formatters; + +/** + * General formatting flags. + */ +enum GeneralFlag { + LEFT_JUSTIFY, + UPPERCASE, + ALTERNATE, +} diff --git a/logging/src/main/java/org/xbib/logging/formatters/GeneralFlags.java b/logging/src/main/java/org/xbib/logging/formatters/GeneralFlags.java new file mode 100644 index 0000000..5b67529 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/formatters/GeneralFlags.java @@ -0,0 +1,60 @@ +package org.xbib.logging.formatters; + +import java.util.concurrent.atomic.AtomicReferenceArray; + +/** + * A set of general format flags. + */ +final class GeneralFlags extends FlagSet { + static final GeneralFlag[] values = GeneralFlag.values(); + + private static final AtomicReferenceArray ALL_SETS = new AtomicReferenceArray<>(1 << values.length); + + public static final GeneralFlags NONE = getOrCreateSet(0); + + private GeneralFlags(final int value) { + super(value); + } + + GeneralFlag[] values() { + return values; + } + + public boolean contains(final Enum e) { + return e instanceof GeneralFlag && super.contains(e); + } + + public static GeneralFlags of(GeneralFlag flag) { + return flag == null ? NONE : getOrCreateSet(1 << flag.ordinal()); + } + + public static GeneralFlags of(GeneralFlag flag1, GeneralFlag flag2) { + return of(flag1).with(flag2); + } + + public static GeneralFlags of(GeneralFlag flag1, GeneralFlag flag2, GeneralFlag flag3) { + return of(flag1).with(flag2).with(flag3); + } + + public GeneralFlags with(final GeneralFlag flag) { + return flag == null ? this : getOrCreateSet(value | 1 << flag.ordinal()); + } + + public GeneralFlags without(final GeneralFlag flag) { + return flag == null ? this : getOrCreateSet(value & ~(1 << flag.ordinal())); + } + + private static GeneralFlags getOrCreateSet(final int bits) { + GeneralFlags set = ALL_SETS.get(bits); + if (set == null) { + set = new GeneralFlags(bits); + if (!ALL_SETS.compareAndSet(bits, null, set)) { + GeneralFlags appearing = ALL_SETS.get(bits); + if (appearing != null) { + set = appearing; + } + } + } + return set; + } +} diff --git a/logging/src/main/java/org/xbib/logging/formatters/IndentingXmlWriter.java b/logging/src/main/java/org/xbib/logging/formatters/IndentingXmlWriter.java new file mode 100644 index 0000000..70f06dd --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/formatters/IndentingXmlWriter.java @@ -0,0 +1,312 @@ +package org.xbib.logging.formatters; + +import java.util.Iterator; +import java.util.NoSuchElementException; +import javax.xml.namespace.NamespaceContext; +import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; + +/** + * An XML stream writer which pretty prints the XML. + */ +class IndentingXmlWriter implements XMLStreamWriter, XMLStreamConstants { + + private static final String SPACES = " "; + + private final XMLStreamWriter delegate; + private int index; + private int state = START_DOCUMENT; + private boolean indentEnd; + + IndentingXmlWriter(final XMLStreamWriter delegate) { + this.delegate = delegate; + index = 0; + indentEnd = false; + } + + private void indent() throws XMLStreamException { + final int index = this.index; + if (index > 0) { + for (int i = 0; i < index; i++) { + delegate.writeCharacters(SPACES); + } + } + + } + + private void newline() throws XMLStreamException { + delegate.writeCharacters("\n"); + } + + @Override + public void writeStartElement(final String localName) throws XMLStreamException { + newline(); + indent(); + delegate.writeStartElement(localName); + indentEnd = false; + state = START_ELEMENT; + index++; + } + + @Override + public void writeStartElement(final String namespaceURI, final String localName) throws XMLStreamException { + newline(); + indent(); + delegate.writeStartElement(namespaceURI, localName); + indentEnd = false; + state = START_ELEMENT; + index++; + } + + @Override + public void writeStartElement(final String prefix, final String localName, final String namespaceURI) + throws XMLStreamException { + newline(); + indent(); + delegate.writeStartElement(prefix, localName, namespaceURI); + indentEnd = false; + state = START_ELEMENT; + index++; + } + + @Override + public void writeEmptyElement(final String namespaceURI, final String localName) throws XMLStreamException { + newline(); + indent(); + delegate.writeEmptyElement(namespaceURI, localName); + state = END_ELEMENT; + } + + @Override + public void writeEmptyElement(final String prefix, final String localName, final String namespaceURI) + throws XMLStreamException { + newline(); + indent(); + delegate.writeEmptyElement(prefix, localName, namespaceURI); + state = END_ELEMENT; + } + + @Override + public void writeEmptyElement(final String localName) throws XMLStreamException { + newline(); + indent(); + delegate.writeEmptyElement(localName); + state = END_ELEMENT; + } + + @Override + public void writeEndElement() throws XMLStreamException { + index--; + if (state != CHARACTERS || indentEnd) { + newline(); + indent(); + indentEnd = false; + } + delegate.writeEndElement(); + state = END_ELEMENT; + } + + @Override + public void writeEndDocument() throws XMLStreamException { + delegate.writeEndDocument(); + state = END_DOCUMENT; + } + + @Override + public void close() throws XMLStreamException { + delegate.close(); + } + + @Override + public void flush() throws XMLStreamException { + delegate.flush(); + } + + @Override + public void writeAttribute(final String localName, final String value) throws XMLStreamException { + delegate.writeAttribute(localName, value); + } + + @Override + public void writeAttribute(final String prefix, final String namespaceURI, final String localName, final String value) + throws XMLStreamException { + delegate.writeAttribute(prefix, namespaceURI, localName, value); + } + + @Override + public void writeAttribute(final String namespaceURI, final String localName, final String value) + throws XMLStreamException { + delegate.writeAttribute(namespaceURI, localName, value); + } + + @Override + public void writeNamespace(final String prefix, final String namespaceURI) throws XMLStreamException { + delegate.writeNamespace(prefix, namespaceURI); + } + + @Override + public void writeDefaultNamespace(final String namespaceURI) throws XMLStreamException { + delegate.writeDefaultNamespace(namespaceURI); + } + + @Override + public void writeComment(final String data) throws XMLStreamException { + newline(); + indent(); + delegate.writeComment(data); + state = COMMENT; + } + + @Override + public void writeProcessingInstruction(final String target) throws XMLStreamException { + newline(); + indent(); + delegate.writeProcessingInstruction(target); + state = PROCESSING_INSTRUCTION; + } + + @Override + public void writeProcessingInstruction(final String target, final String data) throws XMLStreamException { + newline(); + indent(); + delegate.writeProcessingInstruction(target, data); + state = PROCESSING_INSTRUCTION; + } + + @Override + public void writeCData(final String data) throws XMLStreamException { + delegate.writeCData(data); + state = CDATA; + } + + @Override + public void writeDTD(final String dtd) throws XMLStreamException { + newline(); + indent(); + delegate.writeDTD(dtd); + state = DTD; + } + + @Override + public void writeEntityRef(final String name) throws XMLStreamException { + delegate.writeEntityRef(name); + state = ENTITY_REFERENCE; + } + + @Override + public void writeStartDocument() throws XMLStreamException { + delegate.writeStartDocument(); + newline(); + state = START_DOCUMENT; + } + + @Override + public void writeStartDocument(final String version) throws XMLStreamException { + delegate.writeStartDocument(version); + newline(); + state = START_DOCUMENT; + } + + @Override + public void writeStartDocument(final String encoding, final String version) throws XMLStreamException { + delegate.writeStartDocument(encoding, version); + newline(); + state = START_DOCUMENT; + } + + @Override + public void writeCharacters(final String text) throws XMLStreamException { + indentEnd = false; + boolean first = true; + final Iterator iterator = new SplitIterator(text, '\n'); + while (iterator.hasNext()) { + final String t = iterator.next(); + // On first iteration if more than one line is required, skip to a new line and indent + if (first && iterator.hasNext()) { + first = false; + newline(); + indent(); + } + delegate.writeCharacters(t); + if (iterator.hasNext()) { + newline(); + indent(); + indentEnd = true; + } + } + state = CHARACTERS; + } + + @Override + public void writeCharacters(final char[] text, final int start, final int len) throws XMLStreamException { + delegate.writeCharacters(text, start, len); + } + + @Override + public String getPrefix(final String uri) throws XMLStreamException { + return delegate.getPrefix(uri); + } + + @Override + public void setPrefix(final String prefix, final String uri) throws XMLStreamException { + delegate.setPrefix(prefix, uri); + } + + @Override + public void setDefaultNamespace(final String uri) throws XMLStreamException { + delegate.setDefaultNamespace(uri); + } + + @Override + public void setNamespaceContext(final NamespaceContext context) throws XMLStreamException { + delegate.setNamespaceContext(context); + } + + @Override + public NamespaceContext getNamespaceContext() { + return delegate.getNamespaceContext(); + } + + @Override + public Object getProperty(final String name) throws IllegalArgumentException { + return delegate.getProperty(name); + } + + private static class SplitIterator implements Iterator { + + private final String value; + private final char delimiter; + private int index; + + private SplitIterator(final String value, final char delimiter) { + this.value = value; + this.delimiter = delimiter; + index = 0; + } + + @Override + public boolean hasNext() { + return index != -1; + } + + @Override + public String next() { + final int index = this.index; + if (index == -1) { + throw new NoSuchElementException(); + } + int x = value.indexOf(delimiter, index); + try { + return x == -1 ? value.substring(index) : value.substring(index, x); + } finally { + this.index = (x == -1 ? -1 : x + 1); + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } +} diff --git a/logging/src/main/java/org/xbib/logging/formatters/MultistepFormatter.java b/logging/src/main/java/org/xbib/logging/formatters/MultistepFormatter.java new file mode 100644 index 0000000..e150739 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/formatters/MultistepFormatter.java @@ -0,0 +1,81 @@ +package org.xbib.logging.formatters; + +import org.xbib.logging.ExtFormatter; +import org.xbib.logging.ExtLogRecord; +import static java.lang.Math.max; + +/** + * A formatter which formats a record in a series of steps. + */ +public class MultistepFormatter extends ExtFormatter { + private volatile FormatStep[] steps; + private volatile int builderLength; + private volatile boolean callerCalculationRequired = false; + + private static final FormatStep[] EMPTY_STEPS = new FormatStep[0]; + + /** + * Construct a new instance. + * + * @param steps the steps to execute to format the record + */ + public MultistepFormatter(final FormatStep[] steps) { + this.steps = steps.clone(); + calculateBuilderLength(); + } + + private void calculateBuilderLength() { + boolean callerCalculatedRequired = false; + int builderLength = 0; + for (FormatStep step : steps) { + builderLength += step.estimateLength(); + if (step.isCallerInformationRequired()) { + callerCalculatedRequired = true; + } + } + this.builderLength = max(32, builderLength); + this.callerCalculationRequired = callerCalculatedRequired; + } + + /** + * Construct a new instance. + */ + public MultistepFormatter() { + steps = EMPTY_STEPS; + } + + /** + * Get a copy of the format steps. + * + * @return a copy of the format steps + */ + public FormatStep[] getSteps() { + return steps.clone(); + } + + /** + * Assign new format steps. + * + * @param steps the new format steps + */ + public void setSteps(final FormatStep[] steps) { + this.steps = steps == null || steps.length == 0 ? EMPTY_STEPS : steps.clone(); + calculateBuilderLength(); + } + + /** + * {@inheritDoc} + */ + public String format(final ExtLogRecord record) { + final StringBuilder builder = new StringBuilder(builderLength); + for (FormatStep step : steps) { + step.render(this, builder, record); + } + return builder.toString(); + } + + @Override + public boolean isCallerCalculationRequired() { + return callerCalculationRequired; + } +} diff --git a/logging/src/main/java/org/xbib/logging/formatters/NumericFlag.java b/logging/src/main/java/org/xbib/logging/formatters/NumericFlag.java new file mode 100644 index 0000000..ec17493 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/formatters/NumericFlag.java @@ -0,0 +1,9 @@ +package org.xbib.logging.formatters; + +enum NumericFlag { + SIGN, + SPACE_POSITIVE, + ZERO_PAD, + LOCALE_GROUPING_SEPARATORS, + NEGATIVE_PARENTHESES, +} diff --git a/logging/src/main/java/org/xbib/logging/formatters/NumericFlags.java b/logging/src/main/java/org/xbib/logging/formatters/NumericFlags.java new file mode 100644 index 0000000..d664d40 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/formatters/NumericFlags.java @@ -0,0 +1,60 @@ +package org.xbib.logging.formatters; + +import java.util.concurrent.atomic.AtomicReferenceArray; + +/** + * A set of numeric flags. + */ +final class NumericFlags extends FlagSet { + static final NumericFlag[] values = NumericFlag.values(); + + private static final AtomicReferenceArray ALL_SETS = new AtomicReferenceArray<>(1 << values.length); + + public static final NumericFlags NONE = getOrCreateSet(0); + + private NumericFlags(final int value) { + super(value); + } + + NumericFlag[] values() { + return values; + } + + public boolean contains(final Enum e) { + return e instanceof NumericFlag && super.contains(e); + } + + public static NumericFlags of(NumericFlag flag) { + return flag == null ? NONE : getOrCreateSet(1 << flag.ordinal()); + } + + public static NumericFlags of(NumericFlag flag1, NumericFlag flag2) { + return of(flag1).with(flag2); + } + + public static NumericFlags of(NumericFlag flag1, NumericFlag flag2, NumericFlag flag3) { + return of(flag1).with(flag2).with(flag3); + } + + public NumericFlags with(final NumericFlag flag) { + return flag == null ? this : getOrCreateSet(value | 1 << flag.ordinal()); + } + + public NumericFlags without(final NumericFlag flag) { + return flag == null ? this : getOrCreateSet(value & ~(1 << flag.ordinal())); + } + + private static NumericFlags getOrCreateSet(final int bits) { + NumericFlags set = ALL_SETS.get(bits); + if (set == null) { + set = new NumericFlags(bits); + if (!ALL_SETS.compareAndSet(bits, null, set)) { + NumericFlags appearing = ALL_SETS.get(bits); + if (appearing != null) { + set = appearing; + } + } + } + return set; + } +} diff --git a/logging/src/main/java/org/xbib/logging/formatters/PatternFormatter.java b/logging/src/main/java/org/xbib/logging/formatters/PatternFormatter.java new file mode 100644 index 0000000..e429c07 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/formatters/PatternFormatter.java @@ -0,0 +1,186 @@ +package org.xbib.logging.formatters; + +/** + * A formatter which uses a text pattern to format messages. + */ +public class PatternFormatter extends MultistepFormatter { + + private volatile String pattern; + + private volatile ColorMap colors; + + /** + * Construct a new instance. + */ + public PatternFormatter() { + this.colors = ColorMap.DEFAULT_COLOR_MAP; + } + + /** + * Construct a new instance. + * + * @param pattern the initial pattern + */ + public PatternFormatter(String pattern) { + super(FormatStringParser.getSteps(pattern, ColorMap.DEFAULT_COLOR_MAP)); + this.colors = ColorMap.DEFAULT_COLOR_MAP; + this.pattern = pattern; + } + + /** + * Construct a new instance. + * + * @param pattern the initial pattern + * @param colors the color map to use + */ + public PatternFormatter(String pattern, String colors) { + ColorMap colorMap = ColorMap.create(colors); + this.colors = colorMap; + this.pattern = pattern; + setSteps(FormatStringParser.getSteps(pattern, colorMap)); + } + + /** + * Get the current format pattern. + * + * @return the pattern + */ + public String getPattern() { + return pattern; + } + + /** + * Set the format pattern. + * + * @param pattern the pattern + */ + public void setPattern(final String pattern) { + if (pattern == null) { + setSteps(null); + } else { + setSteps(FormatStringParser.getSteps(pattern, colors)); + } + this.pattern = pattern; + } + + /** + * Set the color map to use for log levels when %K{level} is used. + * + *

+ * The format is level:color,level:color,... + * + *

+ * Where level is either a numerical value or one of the following constants: + *

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
fatal
error
severe
warn
warning
info
config
debug
trace
fine
finer
finest
+ * + *

+ * Color is one of the following constants: + *

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
clear
black
red
green
yellow
blue
magenta
cyan
white
brightblack
brightred
brightgreen
brightyellow
brightblue
brightmagenta
brightcyan
brightwhite
+ * + * @param colors a colormap expression string described above + */ + public void setColors(String colors) { + ColorMap colorMap = ColorMap.create(colors); + this.colors = colorMap; + if (pattern != null) { + setSteps(FormatStringParser.getSteps(pattern, colorMap)); + } + } + + public String getColors() { + return this.colors.toString(); + } +} diff --git a/logging/src/main/java/org/xbib/logging/formatters/Printf.java b/logging/src/main/java/org/xbib/logging/formatters/Printf.java new file mode 100644 index 0000000..1b392fc --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/formatters/Printf.java @@ -0,0 +1,1205 @@ +package org.xbib.logging.formatters; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.RoundingMode; +import java.text.AttributedCharacterIterator; +import java.text.DateFormatSymbols; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.NumberFormat; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoField; +import java.time.temporal.ChronoUnit; +import java.time.temporal.Temporal; +import java.time.temporal.TemporalAccessor; +import java.time.temporal.TemporalField; +import java.time.temporal.TemporalQueries; +import java.time.temporal.TemporalUnit; +import java.time.temporal.ValueRange; +import java.util.Calendar; +import java.util.Date; +import java.util.Formattable; +import java.util.FormattableFlags; +import java.util.Formatter; +import java.util.IllegalFormatConversionException; +import java.util.IllegalFormatFlagsException; +import java.util.IllegalFormatPrecisionException; +import java.util.Locale; +import java.util.MissingFormatArgumentException; +import java.util.Objects; +import java.util.TimeZone; +import java.util.UnknownFormatConversionException; +import static java.lang.Math.max; + +/** + * A string formatter which can be customized. + */ +class Printf { + + private static final String someSpaces = " "; //32 spaces + private static final String someZeroes = "00000000000000000000000000000000"; //32 zeros + + private final Locale locale; + private volatile DateFormatSymbols dfs; + + public static final Printf DEFAULT = new Printf(Locale.getDefault(Locale.Category.FORMAT)); + + public Printf(final Locale locale) { + this.locale = locale; + } + + public Printf() { + this(Locale.getDefault(Locale.Category.FORMAT)); + } + + public Locale getLocale() { + return locale; + } + + private static final int ST_INITIAL = 0; + private static final int ST_PCT = 1; + private static final int ST_TIME = 2; + private static final int ST_WIDTH = 3; + private static final int ST_DOT = 4; + private static final int ST_PREC = 5; + private static final int ST_DOLLAR = 6; + + public String format(String format, Object... params) { + return formatDirect(new StringBuilder(), format, params).toString(); + } + + public A formatBuffered(A destination, String format, Object... params) throws IOException { + destination.append(formatDirect(new StringBuilder(format.length() << 1), format, params)); + return destination; + } + + public StringBuilder formatDirect(StringBuilder destination, String format, Object... params) { + int cp; + int state = ST_INITIAL; + GeneralFlags genFlags = GeneralFlags.NONE; + NumericFlags numFlags = NumericFlags.NONE; + int precision = -1; + int width = -1; + int argIdx = -1; // selected argument + int lastArgIdx = -1; + int crs = 0; // current argument cursor + int start = -1; // for diagnostics + Object argVal = null; // argument value + for (int i = 0; i < format.length(); i = format.offsetByCodePoints(i, 1)) { + cp = format.codePointAt(i); + if (state == ST_INITIAL) { + if (cp == '%') { + start = i; + state = ST_PCT; + genFlags = GeneralFlags.NONE; + numFlags = NumericFlags.NONE; + precision = -1; + width = -1; + lastArgIdx = argIdx; + argIdx = -1; + continue; + } else { + destination.appendCodePoint(cp); + continue; + } + } else if (state == ST_PCT || state == ST_DOLLAR) { + if (state == ST_PCT && cp == '<') { + if (lastArgIdx == -1) + throw new IllegalFormatFlagsException("<"); + argIdx = lastArgIdx; + continue; + } + // select flags here + switch (cp) { + case '.': { + state = ST_DOT; + continue; + } + case ' ': { + numFlags.forbid(NumericFlag.SPACE_POSITIVE); + numFlags = numFlags.with(NumericFlag.SPACE_POSITIVE); + if (numFlags.contains(NumericFlag.SIGN)) + numFlags.forbid(NumericFlag.SPACE_POSITIVE); + continue; + } + case '+': { + numFlags.forbid(NumericFlag.SIGN); + numFlags = numFlags.with(NumericFlag.SIGN); + if (numFlags.contains(NumericFlag.SPACE_POSITIVE)) + numFlags.forbid(NumericFlag.SIGN); + continue; + } + case '0': { + numFlags.forbid(NumericFlag.ZERO_PAD); + numFlags = numFlags.with(NumericFlag.ZERO_PAD); + if (genFlags.contains(GeneralFlag.LEFT_JUSTIFY)) + numFlags.forbid(NumericFlag.ZERO_PAD); + continue; + } + case '-': { + genFlags.forbid(GeneralFlag.LEFT_JUSTIFY); + genFlags = genFlags.with(GeneralFlag.LEFT_JUSTIFY); + if (numFlags.contains(NumericFlag.ZERO_PAD)) + genFlags.forbid(GeneralFlag.LEFT_JUSTIFY); + continue; + } + case '(': { + numFlags.forbid(NumericFlag.NEGATIVE_PARENTHESES); + numFlags = numFlags.with(NumericFlag.NEGATIVE_PARENTHESES); + continue; + } + case ',': { + numFlags.forbid(NumericFlag.LOCALE_GROUPING_SEPARATORS); + numFlags = numFlags.with(NumericFlag.LOCALE_GROUPING_SEPARATORS); + continue; + } + case '#': { + genFlags.forbid(GeneralFlag.ALTERNATE); + genFlags = genFlags.with(GeneralFlag.ALTERNATE); + continue; + } + } + // otherwise, fall thru + } else if (state == ST_TIME) { + // time-specific format specifiers + numFlags.forbidAll(); + genFlags.forbid(GeneralFlag.ALTERNATE); + if (precision != -1) + throw precisionException(precision); + if (argVal == null) { + formatPlainString(destination, null, genFlags, width, -1); + continue; + } + TemporalAccessor ta; + if (argVal instanceof Long) { + ta = ZonedDateTime.ofInstant(Instant.ofEpochMilli(((Long) argVal).longValue()), ZoneId.systemDefault()); + } else if (argVal instanceof Date) { + ta = ZonedDateTime.ofInstant(Instant.ofEpochMilli(((Date) argVal).getTime()), ZoneId.systemDefault()); + } else if (argVal instanceof Calendar calendar) { + final TimeZone timeZone = calendar.getTimeZone(); + ZoneId zoneId = timeZone == null ? ZoneId.systemDefault() : timeZone.toZoneId(); + ta = ZonedDateTime.ofInstant(calendar.toInstant(), zoneId); + } else if (argVal instanceof TemporalAccessor) { + ta = (TemporalAccessor) argVal; + } else { + throw new IllegalFormatConversionException((char) cp, argVal.getClass()); + } + switch (cp) { + // locale-based names + case 'A': { + formatTimeTextField(destination, ta, ChronoField.DAY_OF_WEEK, getDateFormatSymbols().getWeekdays(), + genFlags, width); + break; + } + case 'a': { + formatTimeTextField(destination, ta, ChronoField.DAY_OF_WEEK, getDateFormatSymbols().getShortWeekdays(), + genFlags, width); + break; + } + case 'B': { + formatTimeTextField(destination, ta, ChronoField.MONTH_OF_YEAR, getDateFormatSymbols().getMonths(), + genFlags, width); + break; + } + case 'h': // synonym for 'b' + case 'b': { + formatTimeTextField(destination, ta, ChronoField.MONTH_OF_YEAR, getDateFormatSymbols().getShortMonths(), + genFlags, width); + break; + } + case 'p': { + formatTimeTextField(destination, ta, ChronoField.AMPM_OF_DAY, getDateFormatSymbols().getAmPmStrings(), + genFlags, width); + break; + } + + // chrono fields + case 'C': { + formatTimeField(destination, ta, CENTURY_OF_YEAR, genFlags, width, 2); + break; + } + case 'd': { + formatTimeField(destination, ta, ChronoField.DAY_OF_MONTH, genFlags, width, 2); + break; + } + case 'e': { + formatTimeField(destination, ta, ChronoField.DAY_OF_MONTH, genFlags, width, 1); + break; + } + case 'H': { + formatTimeField(destination, ta, ChronoField.HOUR_OF_DAY, genFlags, width, 2); + break; + } + case 'I': { + formatTimeField(destination, ta, ChronoField.CLOCK_HOUR_OF_AMPM, genFlags, width, 2); + break; + } + case 'j': { + formatTimeField(destination, ta, ChronoField.DAY_OF_YEAR, genFlags, width, 3); + break; + } + case 'k': { + formatTimeField(destination, ta, ChronoField.HOUR_OF_DAY, genFlags, width, 1); + break; + } + case 'L': { + formatTimeField(destination, ta, ChronoField.MILLI_OF_SECOND, genFlags, width, 3); + break; + } + case 'l': { + formatTimeField(destination, ta, ChronoField.CLOCK_HOUR_OF_AMPM, genFlags, width, 1); + break; + } + case 'M': { + formatTimeField(destination, ta, ChronoField.MINUTE_OF_HOUR, genFlags, width, 2); + break; + } + case 'm': { + formatTimeField(destination, ta, ChronoField.MONTH_OF_YEAR, genFlags, width, 2); + break; + } + case 'N': { + formatTimeField(destination, ta, ChronoField.NANO_OF_SECOND, genFlags, width, 9); + break; + } + case 'Q': { + formatTimeField(destination, ta, MILLIS_OF_INSTANT, genFlags, width, 1); + break; + } + case 'S': { + formatTimeField(destination, ta, ChronoField.SECOND_OF_MINUTE, genFlags, width, 2); + break; + } + case 's': { + formatTimeField(destination, ta, ChronoField.INSTANT_SECONDS, genFlags, width, 2); + break; + } + case 'Y': { + formatTimeField(destination, ta, ChronoField.YEAR_OF_ERA, genFlags, width, 4); + break; + } + case 'y': { + formatTimeField(destination, ta, YEAR_OF_CENTURY, genFlags, width, 2); + break; + } + + // zone strings + case 'Z': { + formatTimeZoneId(destination, ta, genFlags, width); + break; + } + case 'z': { + formatTimeZoneOffset(destination, ta, genFlags, width); + break; + } + + // compositions + case 'c': { + final StringBuilder b = new StringBuilder(); + formatTimeTextField(b, ta, ChronoField.DAY_OF_WEEK, getDateFormatSymbols().getShortWeekdays(), genFlags, + -1); + b.append(' '); + formatTimeTextField(b, ta, ChronoField.MONTH_OF_YEAR, getDateFormatSymbols().getShortMonths(), genFlags, + -1); + b.append(' '); + formatTimeField(b, ta, ChronoField.DAY_OF_MONTH, genFlags, -1, 2); + b.append(' '); + formatTimeField(b, ta, ChronoField.HOUR_OF_DAY, genFlags, -1, 2); + b.append(':'); + formatTimeField(b, ta, ChronoField.MINUTE_OF_HOUR, genFlags, -1, 2); + b.append(':'); + formatTimeField(b, ta, ChronoField.SECOND_OF_MINUTE, genFlags, -1, 2); + b.append(' '); + formatTimeZoneId(b, ta, genFlags.with(GeneralFlag.UPPERCASE), width); + b.append(' '); + formatTimeField(b, ta, ChronoField.YEAR_OF_ERA, genFlags, -1, 4); + appendStr(destination, genFlags, width, -1, b.toString()); + break; + } + case 'D': { + final StringBuilder b = new StringBuilder(); + formatTimeField(b, ta, ChronoField.MONTH_OF_YEAR, genFlags, -1, 2); + b.append('/'); + formatTimeField(b, ta, ChronoField.DAY_OF_MONTH, genFlags, -1, 2); + b.append('/'); + formatTimeField(b, ta, YEAR_OF_CENTURY, genFlags, -1, 2); + appendStr(destination, genFlags, width, -1, b.toString()); + break; + } + case 'F': { + final StringBuilder b = new StringBuilder(); + formatTimeField(b, ta, ChronoField.YEAR_OF_ERA, genFlags, -1, 4); + b.append('-'); + formatTimeField(b, ta, ChronoField.MONTH_OF_YEAR, genFlags, -1, 2); + b.append('-'); + formatTimeField(b, ta, ChronoField.DAY_OF_MONTH, genFlags, -1, 2); + appendStr(destination, genFlags, width, -1, b.toString()); + break; + } + case 'R': { + final StringBuilder b = new StringBuilder(); + formatTimeField(b, ta, ChronoField.HOUR_OF_DAY, genFlags, -1, 2); + b.append(':'); + formatTimeField(b, ta, ChronoField.MINUTE_OF_HOUR, genFlags, -1, 2); + b.append(':'); + formatTimeField(b, ta, ChronoField.SECOND_OF_MINUTE, genFlags, -1, 2); + appendStr(destination, genFlags, width, -1, b.toString()); + break; + } + case 'r': { + final StringBuilder b = new StringBuilder(); + formatTimeField(b, ta, ChronoField.HOUR_OF_DAY, genFlags, -1, 2); + b.append(':'); + formatTimeField(b, ta, ChronoField.MINUTE_OF_HOUR, genFlags, -1, 2); + b.append(':'); + formatTimeField(b, ta, ChronoField.SECOND_OF_MINUTE, genFlags, -1, 2); + b.append(' '); + formatTimeTextField(b, ta, ChronoField.AMPM_OF_DAY, getDateFormatSymbols().getAmPmStrings(), + genFlags.with(GeneralFlag.UPPERCASE), width); + appendStr(destination, genFlags, width, -1, b.toString()); + break; + } + case 'T': { + final StringBuilder b = new StringBuilder(); + formatTimeField(b, ta, ChronoField.HOUR_OF_DAY, genFlags, -1, 2); + b.append(':'); + formatTimeField(b, ta, ChronoField.MINUTE_OF_HOUR, genFlags, -1, 2); + b.append(':'); + formatTimeField(b, ta, ChronoField.SECOND_OF_MINUTE, genFlags, -1, 2); + appendStr(destination, genFlags, width, -1, b.toString()); + break; + } + default: { + throw unknownFormat(format, i); + } + } + state = ST_INITIAL; + continue; + } else if (state == ST_WIDTH) { + switch (cp) { + case '$': { + if (genFlags != GeneralFlags.NONE || numFlags != NumericFlags.NONE) { + throw unknownFormat(format, i); + } + argIdx = width; + width = -1; + state = ST_DOLLAR; + continue; + } + case '.': { + state = ST_DOT; + continue; + } + } + // otherwise, fall thru + } + if ('0' <= cp && cp <= '9') { + switch (state) { + case ST_DOLLAR: + case ST_PCT: { + state = ST_WIDTH; + width = cp - '0'; + continue; + } + case ST_WIDTH: { + width = Math.addExact(Math.multiplyExact(width, 10), cp - '0'); + continue; + } + case ST_DOT: { + state = ST_PREC; + precision = cp - '0'; + continue; + } + case ST_PREC: { + precision = Math.addExact(Math.multiplyExact(precision, 10), cp - '0'); + continue; + } + default: { + throw new UnsupportedOperationException(); + } + } + } + if (state == ST_DOT) { + throw unknownFormat(format, i); + } + // basic format specifiers + if (cp != 'n' && cp != '%') { + // capture argument + if (argIdx != -1) { + if (argIdx - 1 >= params.length) { + throw new MissingFormatArgumentException( + format.substring(start, cp == 't' || cp == 'T' ? i + 2 : i + 1)); + } + argVal = params[argIdx - 1]; + } else { + if (crs >= params.length) { + throw new MissingFormatArgumentException( + format.substring(start, cp == 't' || cp == 'T' ? i + 2 : i + 1)); + } + argVal = params[crs++]; + argIdx = crs; // crs is 0-based, argIdx & lastArgIdx are 1-based + } + } + switch (cp) { + case '%': { + genFlags.forbidAllBut(GeneralFlag.LEFT_JUSTIFY); // but it's ignored anyway + numFlags.forbidAll(); + if (precision != -1 || state == ST_PREC) + throw precisionException(precision); + formatPercent(destination); + break; + } + case 'A': + case 'a': { + // TODO support hex FP + throw unknownFormat(format, i); + } + case 'B': + case 'b': { + numFlags.forbidAll(); + genFlags.forbid(GeneralFlag.ALTERNATE); + if (Character.isUpperCase(cp)) + genFlags = genFlags.with(GeneralFlag.UPPERCASE); + if (argVal != null && !(argVal instanceof Boolean)) + throw new IllegalFormatConversionException((char) cp, argVal.getClass()); + formatBoolean(destination, checkType(cp, argVal, Boolean.class), genFlags, width, precision); + break; + } + case 'C': + case 'c': { + numFlags.forbidAll(); + genFlags.forbidAllBut(GeneralFlag.LEFT_JUSTIFY); + if (Character.isUpperCase(cp)) + genFlags = genFlags.with(GeneralFlag.UPPERCASE); + if (precision != -1 || state == ST_PREC) + throw precisionException(precision); + int cpa; + if (argVal == null) { + appendStr(destination, genFlags, width, precision, "null"); + break; + } else if (argVal instanceof Character) { + cpa = ((Character) argVal).charValue(); + } else if (argVal instanceof Integer) { + cpa = ((Integer) argVal).intValue(); + } else { + throw new IllegalFormatConversionException((char) cp, argVal.getClass()); + } + formatCharacter(destination, cpa, genFlags, width, precision); + break; + } + case 'd': { + genFlags.forbid(GeneralFlag.ALTERNATE); + if (precision != -1 || state == ST_PREC) + throw precisionException(precision); + formatDecimalInteger(destination, checkType(cp, argVal, Number.class, Byte.class, Short.class, + Integer.class, Long.class, BigInteger.class), genFlags, numFlags, width); + break; + } + case 'E': + case 'e': + case 'f': + case 'G': + case 'g': { + if (Character.isUpperCase(cp)) + genFlags = genFlags.with(GeneralFlag.UPPERCASE); + if (argVal != null && !(argVal instanceof Float) && !(argVal instanceof Double) + && !(argVal instanceof BigDecimal)) { + throw new IllegalFormatConversionException((char) cp, argVal.getClass()); + } + Number item = checkType(cp, argVal, Number.class, Float.class, Double.class, BigDecimal.class); + if (cp == 'e' || cp == 'E') { + formatFloatingPointSci(destination, item, genFlags, numFlags, width, precision); + break; + } else if (cp == 'f') { + formatFloatingPointDecimal(destination, item, genFlags, numFlags, width, precision); + break; + } else { + assert cp == 'g' || cp == 'G'; + formatFloatingPointGeneral(destination, item, genFlags, numFlags, width, precision); + break; + } + } + case 'H': + case 'h': { + genFlags.forbid(GeneralFlag.ALTERNATE); + numFlags.forbidAll(); + formatHashCode(destination, argVal, genFlags, width, precision); + break; + } + case 'n': { + numFlags.forbidAll(); + genFlags.forbidAll(); + formatLineSeparator(destination); + break; + } + case 'o': { + numFlags.forbidAllBut(NumericFlag.ZERO_PAD); + if (precision != -1 || state == ST_PREC) + throw precisionException(precision); + formatOctalInteger(destination, checkType(cp, argVal, Number.class, Byte.class, Short.class, Integer.class, + Long.class, BigInteger.class), genFlags, numFlags, width); + break; + } + case 's': + case 'S': { + numFlags.forbidAll(); + if (Character.isUpperCase(cp)) + genFlags = genFlags.with(GeneralFlag.UPPERCASE); + if (argVal instanceof Formattable) { + formatFormattableString(destination, (Formattable) argVal, genFlags, width, precision); + } else { + formatPlainString(destination, argVal, genFlags, width, precision); + } + break; + } + case 'T': + case 't': { + if (Character.isUpperCase(cp)) + genFlags = genFlags.with(GeneralFlag.UPPERCASE); + state = ST_TIME; + continue; + } + case 'X': + case 'x': { + numFlags.forbidAllBut(NumericFlag.ZERO_PAD); + if (Character.isUpperCase(cp)) + genFlags = genFlags.with(GeneralFlag.UPPERCASE); + if (precision != -1 || state == ST_PREC) + throw precisionException(precision); + formatHexInteger(destination, checkType(cp, argVal, Number.class, Byte.class, Short.class, Integer.class, + Long.class, BigInteger.class), genFlags, numFlags, width); + break; + } + default: { + throw unknownFormat(format, i); + } + } + state = ST_INITIAL; + //continue; + } + return destination; + } + + protected static void appendSpaces(StringBuilder target, int cnt) { + appendFiller(target, someSpaces, cnt); + } + + protected static void appendZeros(StringBuilder target, int cnt) { + appendFiller(target, someZeroes, cnt); + } + + protected DateFormatSymbols getDateFormatSymbols() { + DateFormatSymbols dfs = this.dfs; + if (dfs == null) { + synchronized (this) { + dfs = this.dfs; + if (dfs == null) { + this.dfs = dfs = DateFormatSymbols.getInstance(locale); + } + } + } + return dfs; + } + + protected void formatTimeTextField(final StringBuilder target, final TemporalAccessor ta, final TemporalField field, + final String[] symbols, final GeneralFlags genFlags, final int width) { + final int baseIdx = ta.get(field); + // fix offset fields + final int idx = field == ChronoField.MONTH_OF_YEAR ? baseIdx - 1 + : field == ChronoField.DAY_OF_WEEK ? (baseIdx + 1) % 7 : baseIdx; + appendStr(target, genFlags, width, -1, symbols[idx]); + } + + protected void formatTimeZoneId(final StringBuilder target, final TemporalAccessor ta, final GeneralFlags genFlags, + final int width) { + final boolean upper = genFlags.contains(GeneralFlag.UPPERCASE); + final ZoneId zoneId = ta.query(TemporalQueries.zone()); + if (zoneId == null) { + throw new IllegalFormatConversionException(upper ? 'T' : 't', ta.getClass()); + } + String output; + if (ta.isSupported(ChronoField.INSTANT_SECONDS)) { + final boolean dst = zoneId.getRules().isDaylightSavings(Instant.from(ta)); + output = TimeZone.getTimeZone(zoneId).getDisplayName(dst, 0, locale); + } else { + output = zoneId.getId(); + } + appendStr(target, genFlags, width, -1, output); + } + + protected void formatTimeZoneOffset(final StringBuilder target, final TemporalAccessor ta, final GeneralFlags genFlags, + final int width) { + final int offset = ta.get(ChronoField.OFFSET_SECONDS); + final int absOffset = Math.abs(offset); + final int minutes = (absOffset / 60) % 60; + final int hours = (absOffset / 3600); + final boolean lj = genFlags.contains(GeneralFlag.LEFT_JUSTIFY); + if (width > 5 && !lj) { + appendSpaces(target, width - 5); + } + target.append(offset > 0 ? '+' : '-'); + if (hours < 10) + target.append('0'); + target.append(hours); + if (minutes < 10) + target.append('0'); + target.append(minutes); + if (width > 5 && lj) { + appendSpaces(target, width - 5); + } + } + + protected void formatTimeField(final StringBuilder target, final TemporalAccessor ta, final TemporalField field, + final GeneralFlags genFlags, final int width, final int zeroPad) { + final long val = ta.getLong(field); + final String valStr = Long.toString(val); + final int length = valStr.length(); + final int extLen = max(zeroPad, length); + final boolean lj = genFlags.contains(GeneralFlag.LEFT_JUSTIFY); + if (width > extLen && !lj) { + appendSpaces(target, width - extLen); + } + if (zeroPad > length) { + appendZeros(target, zeroPad - length); + } + target.append(valStr); + if (width > extLen && lj) { + appendSpaces(target, width - extLen); + } + } + + protected void formatPercent(StringBuilder target) { + appendChar(target, GeneralFlags.NONE, 1, -1, '%'); + } + + protected void formatLineSeparator(StringBuilder target) { + target.append(System.lineSeparator()); + } + + protected void formatFormattableString(StringBuilder target, Formattable formattable, GeneralFlags genFlags, int width, + int precision) { + int fmtFlags = 0; + if (genFlags.contains(GeneralFlag.LEFT_JUSTIFY)) + fmtFlags |= FormattableFlags.LEFT_JUSTIFY; + if (genFlags.contains(GeneralFlag.UPPERCASE)) + fmtFlags |= FormattableFlags.UPPERCASE; + if (genFlags.contains(GeneralFlag.ALTERNATE)) + fmtFlags |= FormattableFlags.ALTERNATE; + // make a dummy Formatter to appease the constraints of the API + formattable.formatTo(new Formatter(target), fmtFlags, width, precision); + } + + protected void formatPlainString(StringBuilder target, Object item, GeneralFlags genFlags, int width, int precision) { + appendStr(target, genFlags, width, precision, String.valueOf(item)); + } + + protected void formatBoolean(StringBuilder target, Object item, GeneralFlags genFlags, int width, int precision) { + appendStr(target, genFlags, width, precision, + item instanceof Boolean ? item.toString() : Boolean.toString(item != null)); + } + + protected void formatHashCode(StringBuilder target, Object item, GeneralFlags genFlags, int width, int precision) { + appendStr(target, genFlags, width, precision, Integer.toHexString(Objects.hashCode(item))); + } + + protected void formatCharacter(StringBuilder target, int codePoint, GeneralFlags genFlags, int width, int precision) { + if (Character.isBmpCodePoint(codePoint)) { + appendChar(target, genFlags, width, precision, (char) codePoint); + } else { + appendStr(target, genFlags, width, precision, new String(new int[]{codePoint}, 0, 1)); + } + } + + protected void formatDecimalInteger(StringBuilder target, Number item, GeneralFlags genFlags, NumericFlags numFlags, + int width) { + if (item == null) { + appendStr(target, genFlags, width, -1, "null"); + } else { + DecimalFormat fmt = (DecimalFormat) NumberFormat.getIntegerInstance(locale); + if (numFlags.contains(NumericFlag.SIGN)) { + fmt.setPositivePrefix("+"); + } else if (numFlags.contains(NumericFlag.SPACE_POSITIVE)) { + fmt.setPositivePrefix(" "); + } else { + fmt.setPositivePrefix(""); + } + fmt.setPositiveSuffix(""); + if (numFlags.contains(NumericFlag.NEGATIVE_PARENTHESES)) { + fmt.setNegativePrefix("("); + fmt.setNegativeSuffix(")"); + } else { + fmt.setNegativePrefix("-"); + fmt.setNegativeSuffix(""); + } + fmt.setGroupingUsed(numFlags.contains(NumericFlag.LOCALE_GROUPING_SEPARATORS)); + if (numFlags.contains(NumericFlag.ZERO_PAD)) { + fmt.setMinimumIntegerDigits(width); + } + fmt.setDecimalSeparatorAlwaysShown(genFlags.contains(GeneralFlag.ALTERNATE)); + appendStr(target, genFlags, width, -1, fmt.format(item)); + } + } + + protected void formatOctalInteger(StringBuilder target, Number item, GeneralFlags genFlags, NumericFlags numFlags, + int width) { + if (item == null) { + appendStr(target, genFlags, width, -1, "null"); + } else { + final boolean addRadix = genFlags.contains(GeneralFlag.ALTERNATE); + final int fillCount = max(0, width - (bitLengthOf(item) + 2) / 3 - (addRadix ? 1 : 0)); + final boolean lj = genFlags.contains(GeneralFlag.LEFT_JUSTIFY); + if (numFlags.contains(NumericFlag.ZERO_PAD)) { + // write zeros first + if (addRadix) + target.append('0'); + appendZeros(target, fillCount); + } else if (lj) { + if (addRadix) + target.append('0'); + } else { + // ! LEFT_JUSTIFY + // write spaces first + appendSpaces(target, fillCount); + if (addRadix) + target.append('0'); + } + appendOctal(target, item); + if (lj) { + appendSpaces(target, fillCount); + } + } + } + + protected void formatHexInteger(StringBuilder target, Number item, GeneralFlags genFlags, NumericFlags numFlags, + int width) { + if (item == null) { + appendStr(target, genFlags, width, -1, "null"); + } else { + final boolean upper = genFlags.contains(GeneralFlag.UPPERCASE); + final boolean addRadix = genFlags.contains(GeneralFlag.ALTERNATE); + final int fillCount = max(0, width - (bitLengthOf(item) + 3) / 4 - (addRadix ? 2 : 0)); + final boolean lj = genFlags.contains(GeneralFlag.LEFT_JUSTIFY); + if (numFlags.contains(NumericFlag.ZERO_PAD)) { + // write zeros first + if (addRadix) + target.append(upper ? "0X" : "0x"); + appendZeros(target, fillCount); + } else if (lj) { + if (addRadix) + target.append(upper ? "0X" : "0x"); + } else { + // ! LEFT_JUSTIFY + // write spaces first + appendSpaces(target, fillCount); + if (addRadix) + target.append(upper ? "0X" : "0x"); + } + appendHex(target, item, upper); + if (lj) { + appendSpaces(target, fillCount); + } + } + } + + protected void formatFloatingPointSci(StringBuilder target, Number item, GeneralFlags genFlags, NumericFlags numFlags, + int width, int precision) { + if (item == null) { + appendStr(target, genFlags, width, precision, "null"); + } else { + final boolean upper = genFlags.contains(GeneralFlag.UPPERCASE); + final DecimalFormatSymbols sym = DecimalFormatSymbols.getInstance(locale); + if (negativeExp(item)) { + sym.setExponentSeparator(upper ? "E" : "e"); + } else { + sym.setExponentSeparator(upper ? "E+" : "e+"); + } + formatDFP(target, item, genFlags, numFlags, width, precision == -1 ? 6 : precision == 0 ? 1 : precision, true, sym, + "0.#E00"); + } + } + + protected void formatFloatingPointDecimal(StringBuilder target, Number item, GeneralFlags genFlags, NumericFlags numFlags, + int width, int precision) { + if (item == null) { + appendStr(target, genFlags, width, precision, "null"); + } else { + formatDFP(target, item, genFlags, numFlags, width, precision == 0 ? 1 : precision, false, + DecimalFormatSymbols.getInstance(locale), "0.#"); + } + } + + protected void formatFloatingPointGeneral(StringBuilder target, Number item, GeneralFlags genFlags, NumericFlags numFlags, + int width, int precision) { + if (item == null) { + appendStr(target, genFlags, width, precision, "null"); + } else { + boolean sci; + if (item instanceof BigDecimal) { + final BigDecimal mag = ((BigDecimal) item).abs(); + sci = mag.compareTo(NEG_TEN_EM4) < 0 || mag.compareTo(BigDecimal.valueOf(10, precision)) >= 0; + } else if (item instanceof Float) { + final float fv = Math.abs(item.floatValue()); + sci = Float.isFinite(fv) && (fv < 10e-4f || fv >= Math.pow(10, precision)); + } else { + assert item instanceof Double; + final double dv = Math.abs(item.doubleValue()); + sci = Double.isFinite(dv) && (dv < 10e-4f || dv >= Math.pow(10, precision)); + } + if (sci) { + formatFloatingPointSci(target, item, genFlags, numFlags, width, precision); + } else { + formatFloatingPointDecimal(target, item, genFlags, numFlags, width, precision); + } + } + } + + private void formatDFP(final StringBuilder target, final Number item, final GeneralFlags genFlags, + final NumericFlags numFlags, final int width, final int precision, final boolean oneIntDigit, + final DecimalFormatSymbols sym, final String s) { + if (!(item instanceof BigDecimal)) { + final double dv = item.doubleValue(); + if (!Double.isFinite(dv)) { + appendStr(target, genFlags, width, -1, Double.toString(dv)); + return; + } + } + DecimalFormat fmt = new DecimalFormat(s, sym); + if (numFlags.contains(NumericFlag.SIGN)) { + fmt.setPositivePrefix("+"); + } else if (numFlags.contains(NumericFlag.SPACE_POSITIVE)) { + fmt.setPositivePrefix(" "); + } else { + fmt.setPositivePrefix(""); + } + fmt.setPositiveSuffix(""); + if (numFlags.contains(NumericFlag.NEGATIVE_PARENTHESES)) { + fmt.setNegativePrefix("("); + fmt.setNegativeSuffix(")"); + } else { + fmt.setNegativePrefix("-"); + fmt.setNegativeSuffix(""); + } + fmt.setGroupingUsed(numFlags.contains(NumericFlag.LOCALE_GROUPING_SEPARATORS)); + fmt.setMinimumFractionDigits(precision == -1 ? 1 : precision); + fmt.setMaximumFractionDigits(precision == -1 ? Integer.MAX_VALUE : precision); + fmt.setMinimumIntegerDigits(1); + if (oneIntDigit) { + fmt.setMaximumIntegerDigits(1); + } + fmt.setRoundingMode(RoundingMode.HALF_UP); + final AttributedCharacterIterator iterator = fmt.formatToCharacterIterator(item); + final int end = iterator.getEndIndex(); + final boolean lj = genFlags.contains(GeneralFlag.LEFT_JUSTIFY); + final boolean zp = numFlags.contains(NumericFlag.ZERO_PAD); + if (!lj && !zp && width > end) { + appendSpaces(target, width - end); + } + while (iterator.getAttribute(NumberFormat.Field.SIGN) != null) { + target.append(iterator.current()); + iterator.next(); // move + } + assert iterator.getAttribute(NumberFormat.Field.INTEGER) != null; + if (zp && width > end) { + appendZeros(target, width - end); + } + // now continue to the end + while (iterator.getIndex() < end) { + target.append(iterator.current()); + iterator.next(); // move + } + if (lj && width > end) { + appendSpaces(target, width - end); + } + } + + private static final BigDecimal NEG_ONE = BigDecimal.ONE.negate(); + private static final BigDecimal NEG_TEN_EM4 = BigDecimal.valueOf(10, -4); + + private boolean negativeExp(final Number item) { + if (item instanceof BigDecimal bigDecimal) { + return bigDecimal.compareTo(BigDecimal.ONE) < 0 && bigDecimal.compareTo(NEG_ONE) > 0; + } else { + final double val = item.doubleValue(); + return -1 < val && val < 1; + } + } + + private static int bitLengthOf(final Number item) { + if (item instanceof Byte) { + return 32 - Integer.numberOfLeadingZeros(item.byteValue() & 0xff); + } else if (item instanceof Short) { + return 32 - Integer.numberOfLeadingZeros(item.shortValue() & 0xffff); + } else if (item instanceof Integer) { + return 32 - Integer.numberOfLeadingZeros(item.intValue()); + } else if (item instanceof Long) { + return 64 - Long.numberOfLeadingZeros(item.longValue()); + } else { + assert item instanceof BigInteger; + return ((BigInteger) item).bitLength(); + } + } + + private static void appendOctal(StringBuilder target, final Number item) { + if (item instanceof Byte) { + target.append(Integer.toOctalString(item.byteValue() & 0xff)); + } else if (item instanceof Short) { + target.append(Integer.toOctalString(item.shortValue() & 0xffff)); + } else if (item instanceof Integer) { + target.append(Integer.toOctalString(item.shortValue())); + } else if (item instanceof Long) { + target.append(Long.toOctalString(item.longValue())); + } else if (item instanceof BigInteger bi) { + final int bl = bi.bitLength(); + if (bl <= 64) { + target.append(Long.toOctalString(bi.longValue())); + } else { + int max = ((bl + 2) / 3) * 3; + for (int i = 0; i < max; i += 3) { + int val = 0; + if (bi.testBit(max - i)) + val |= 0b100; + if (bi.testBit(max - i - 1)) + val |= 0b010; + if (bi.testBit(max - i - 2)) + val |= 0b001; + target.append(val); + } + } + } + } + + private static void appendHex(final StringBuilder target, final Number item, final boolean upper) { + if (item instanceof Byte) { + final String str = Integer.toHexString(item.byteValue() & 0xff); + target.append(upper ? str.toUpperCase() : str); + } else if (item instanceof Short) { + final String str = Integer.toHexString(item.shortValue() & 0xffff); + target.append(upper ? str.toUpperCase() : str); + } else if (item instanceof Integer) { + final String str = Integer.toHexString(item.shortValue()); + target.append(upper ? str.toUpperCase() : str); + } else if (item instanceof Long) { + final String str = Long.toHexString(item.longValue()); + target.append(upper ? str.toUpperCase() : str); + } else if (item instanceof BigInteger bi) { + final int bl = bi.bitLength(); + if (bl <= 64) { + target.append(Long.toHexString(bi.longValue())); + } else { + int max = ((bl + 3) / 4) * 4; + for (int i = 0; i < max; i += 4) { + int val = 0; + if (bi.testBit(max - i)) + val |= 0b1000; + if (bi.testBit(max - i - 1)) + val |= 0b0100; + if (bi.testBit(max - i - 2)) + val |= 0b0010; + if (bi.testBit(max - i - 3)) + val |= 0b0001; + if (val > 9) + val += upper ? 'A' : 'a' - 10; + target.append((char) (val > 9 ? val - 10 + (upper ? 'A' : 'a') : val + '0')); + } + } + } + } + + private void appendChar(final StringBuilder target, GeneralFlags genFlags, final int width, final int precision, + final char c) { + if (genFlags.contains(GeneralFlag.UPPERCASE) && Character.isLowerCase(c)) { + appendStr(target, genFlags, width, precision, Character.toString(c)); + } else if (width <= 1) { + target.append(c); + } else if (genFlags.contains(GeneralFlag.LEFT_JUSTIFY)) { + target.append(c); + appendSpaces(target, width - 1); + } else { + appendSpaces(target, width - 1); + target.append(c); + } + } + + private void appendStr(final StringBuilder target, GeneralFlags genFlags, final int width, final int precision, + final String itemStr) { + String str = genFlags.contains(GeneralFlag.UPPERCASE) ? itemStr.toUpperCase(locale) : itemStr; + if (width == -1 && precision == -1) { + target.append(str); + } else { + final int length = str.codePointCount(0, str.length()); + if (precision != -1 && precision < length) { + str = str.substring(0, precision); + } + if (width != -1 && length < width) { + // fill + if (genFlags.contains(GeneralFlag.LEFT_JUSTIFY)) { + target.append(str); + appendSpaces(target, width - length); + } else { + appendSpaces(target, width - length); + target.append(str); + } + } else { + target.append(str); + } + } + } + + @SafeVarargs + private static T checkType(int convCp, Object arg, Class commonType, Class... allowedSubTypes) { + if (arg == null) + return null; + if (commonType.isInstance(arg)) { + if (allowedSubTypes.length == 0) + return commonType.cast(arg); + for (Class subType : allowedSubTypes) { + if (subType.isInstance(arg)) + return commonType.cast(arg); + } + } + throw new IllegalFormatConversionException((char) convCp, arg.getClass()); + } + + private static void appendFiller(final StringBuilder target, final String filler, int cnt) { + while (cnt > 32) { + target.append(filler); + cnt -= 32; + } + target.append(filler, 0, cnt); + } + + private static IllegalFormatPrecisionException precisionException(int prec) { + return new IllegalFormatPrecisionException(prec); + } + + private static UnknownFormatConversionException unknownFormat(final String format, final int i) { + return unknownFormat(format.substring(i, format.offsetByCodePoints(i, 1))); + } + + private static UnknownFormatConversionException unknownFormat(final String arg) { + return new UnknownFormatConversionException(arg); + } + + private static final TemporalField MILLIS_OF_INSTANT = new TemporalField() { + public TemporalUnit getBaseUnit() { + return ChronoUnit.MILLIS; + } + + public TemporalUnit getRangeUnit() { + return ChronoUnit.FOREVER; + } + + public ValueRange range() { + return ValueRange.of(Long.MIN_VALUE, Long.MAX_VALUE); + } + + public boolean isDateBased() { + return true; + } + + public boolean isTimeBased() { + return false; + } + + public boolean isSupportedBy(final TemporalAccessor temporal) { + return temporal.isSupported(ChronoField.INSTANT_SECONDS) && temporal.isSupported(ChronoField.MILLI_OF_SECOND); + } + + public ValueRange rangeRefinedBy(final TemporalAccessor temporal) { + return range(); + } + + public long getFrom(final TemporalAccessor temporal) { + return temporal.get(ChronoField.INSTANT_SECONDS) * 1000L + temporal.get(ChronoField.MILLI_OF_SECOND); + } + + @SuppressWarnings("unchecked") + public R adjustInto(final R temporal, final long newValue) { + final long millis = newValue % 1000L; + final long seconds = newValue / 1000L; + return (R) temporal.with(ChronoField.INSTANT_SECONDS, millis).with(ChronoField.MILLI_OF_SECOND, seconds); + } + }; + + private static final TemporalField YEAR_OF_CENTURY = new TemporalField() { + public TemporalUnit getBaseUnit() { + return ChronoUnit.YEARS; + } + + public TemporalUnit getRangeUnit() { + return ChronoUnit.CENTURIES; + } + + public ValueRange range() { + return ValueRange.of(0, 99); + } + + public boolean isDateBased() { + return false; + } + + public boolean isTimeBased() { + return false; + } + + public boolean isSupportedBy(final TemporalAccessor temporal) { + return temporal.isSupported(ChronoField.YEAR); + } + + public ValueRange rangeRefinedBy(final TemporalAccessor temporal) { + return range(); + } + + public long getFrom(final TemporalAccessor temporal) { + return temporal.get(ChronoField.YEAR) % 100; + } + + @SuppressWarnings("unchecked") + public R adjustInto(final R temporal, final long newValue) { + return (R) temporal.with(ChronoField.YEAR, (temporal.get(ChronoField.YEAR) / 100) * 100 + newValue); + } + }; + + private static final TemporalField CENTURY_OF_YEAR = new TemporalField() { + public TemporalUnit getBaseUnit() { + return ChronoUnit.YEARS; + } + + public TemporalUnit getRangeUnit() { + return ChronoUnit.CENTURIES; + } + + public ValueRange range() { + return ValueRange.of(Long.MIN_VALUE, Long.MAX_VALUE); + } + + public boolean isDateBased() { + return true; + } + + public boolean isTimeBased() { + return false; + } + + public boolean isSupportedBy(final TemporalAccessor temporal) { + return temporal.isSupported(ChronoField.YEAR); + } + + public ValueRange rangeRefinedBy(final TemporalAccessor temporal) { + return range(); + } + + public long getFrom(final TemporalAccessor temporal) { + return temporal.get(ChronoField.YEAR) / 100; + } + + @SuppressWarnings("unchecked") + public R adjustInto(final R temporal, final long newValue) { + return (R) temporal.with(ChronoField.YEAR, (temporal.get(ChronoField.YEAR) % 100) + 100 * newValue); + } + }; +} diff --git a/logging/src/main/java/org/xbib/logging/formatters/StringBuilderWriter.java b/logging/src/main/java/org/xbib/logging/formatters/StringBuilderWriter.java new file mode 100644 index 0000000..6f82da6 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/formatters/StringBuilderWriter.java @@ -0,0 +1,81 @@ +package org.xbib.logging.formatters; + +import java.io.Writer; + +public final class StringBuilderWriter extends Writer { + + private final StringBuilder builder; + + public StringBuilderWriter() { + this(new StringBuilder()); + } + + public StringBuilderWriter(final StringBuilder builder) { + this.builder = builder; + } + + /** + * Clears the builder used for the writer. + * + * @see StringBuilder#setLength(int) + */ + public void clear() { + builder.setLength(0); + } + + @Override + public void write(final char[] cbuf, final int off, final int len) { + builder.append(cbuf, off, len); + } + + @Override + public void write(final int c) { + builder.append((char) c); + } + + @Override + public void write(final char[] cbuf) { + builder.append(cbuf); + } + + @Override + public void write(final String str) { + builder.append(str); + } + + @Override + public void write(final String str, final int off, final int len) { + builder.append(str, off, len); + } + + @Override + public Writer append(final CharSequence csq) { + builder.append(csq); + return this; + } + + @Override + public Writer append(final CharSequence csq, final int start, final int end) { + builder.append(csq, start, end); + return this; + } + + @Override + public Writer append(final char c) { + builder.append(c); + return this; + } + + @Override + public void flush() { + } + + @Override + public void close() { + } + + @Override + public String toString() { + return builder.toString(); + } +} diff --git a/logging/src/main/java/org/xbib/logging/formatters/StructuredFormatter.java b/logging/src/main/java/org/xbib/logging/formatters/StructuredFormatter.java new file mode 100644 index 0000000..4ea1177 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/formatters/StructuredFormatter.java @@ -0,0 +1,693 @@ +package org.xbib.logging.formatters; + +import java.io.Writer; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Collections; +import java.util.EnumMap; +import java.util.IdentityHashMap; +import java.util.Map; +import org.xbib.logging.ExtFormatter; +import org.xbib.logging.ExtLogRecord; +import org.xbib.logging.util.PropertyValues; +import org.xbib.logging.util.StackTraceFormatter; + +/** + * An abstract class that uses a generator to help generate structured data from a {@link + * ExtLogRecord record}. + *

+ * Note that including details can be expensive in terms of calculating the caller. + *

+ *

+ * By default the {@linkplain #setRecordDelimiter(String) record delimiter} is set to {@code \n}. + *

+ */ +@SuppressWarnings({"unused", "WeakerAccess"}) +public abstract class StructuredFormatter extends ExtFormatter { + + /** + * The key used for the structured log record data. + */ + public enum Key { + EXCEPTION("exception"), + EXCEPTION_CAUSED_BY("causedBy"), + EXCEPTION_CIRCULAR_REFERENCE("circularReference"), + EXCEPTION_TYPE("exceptionType"), + EXCEPTION_FRAME("frame"), + EXCEPTION_FRAME_CLASS("class"), + EXCEPTION_FRAME_LINE("line"), + EXCEPTION_FRAME_METHOD("method"), + EXCEPTION_FRAMES("frames"), + EXCEPTION_MESSAGE("message"), + EXCEPTION_REFERENCE_ID("refId"), + EXCEPTION_SUPPRESSED("suppressed"), + HOST_NAME("hostName"), + LEVEL("level"), + LOGGER_CLASS_NAME("loggerClassName"), + LOGGER_NAME("loggerName"), + MDC("mdc"), + MESSAGE("message"), + NDC("ndc"), + PROCESS_ID("processId"), + PROCESS_NAME("processName"), + RECORD("record"), + SEQUENCE("sequence"), + SOURCE_CLASS_NAME("sourceClassName"), + SOURCE_FILE_NAME("sourceFileName"), + SOURCE_LINE_NUMBER("sourceLineNumber"), + SOURCE_METHOD_NAME("sourceMethodName"), + SOURCE_MODULE_NAME("sourceModuleName"), + SOURCE_MODULE_VERSION("sourceModuleVersion"), + STACK_TRACE("stackTrace"), + THREAD_ID("threadId"), + THREAD_NAME("threadName"), + TIMESTAMP("timestamp"); + + private final String key; + + Key(final String key) { + this.key = key; + } + + /** + * Returns the name of the key for the structure. + * + * @return the name of they key + */ + public String getKey() { + return key; + } + } + + /** + * Defines the way a cause will be formatted. + */ + public enum ExceptionOutputType { + /** + * The cause, if present, will be an array of stack trace elements. This will include suppressed exceptions and + * the {@linkplain Throwable#getCause() cause} of the exception. + */ + DETAILED, + /** + * The cause, if present, will be a string representation of the stack trace in a {@code stackTrace} property. + * The property value is a string created by {@link Throwable#printStackTrace()}. + */ + FORMATTED, + /** + * Adds both the {@link #DETAILED} and {@link #FORMATTED} + */ + DETAILED_AND_FORMATTED + } + + private final Map keyOverrides; + private final String keyOverridesValue; + // Guarded by this + private String metaData; + // Guarded by this + private Map metaDataMap; + private volatile boolean printDetails; + private volatile String eorDelimiter = "\n"; + // Guarded by this + private DateTimeFormatter dateTimeFormatter; + // Guarded by this + private ZoneId zoneId; + private volatile ExceptionOutputType exceptionOutputType; + private final StringBuilderWriter writer = new StringBuilderWriter(); + // Guarded by this + private int refId; + + protected StructuredFormatter() { + this(null, null); + } + + protected StructuredFormatter(final Map keyOverrides) { + this(keyOverrides, PropertyValues.mapToString(keyOverrides)); + } + + protected StructuredFormatter(final String keyOverrides) { + this(PropertyValues.stringToEnumMap(Key.class, keyOverrides), keyOverrides); + } + + private StructuredFormatter(final Map keyOverrides, final String keyOverridesValue) { + this.keyOverridesValue = keyOverridesValue; + this.printDetails = false; + zoneId = ZoneId.systemDefault(); + dateTimeFormatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(zoneId); + this.keyOverrides = (keyOverrides == null ? Collections.emptyMap() : new EnumMap<>(keyOverrides)); + exceptionOutputType = ExceptionOutputType.DETAILED; + } + + /** + * Creates the generator used to create the structured data. + * + * @return the generator to use + * @throws Exception if an error occurs creating the generator + */ + protected abstract Generator createGenerator(Writer writer) throws Exception; + + /** + * Invoked before the structured data is added to the generator. + * + * @param generator the generator to use + * @param record the log record + */ + protected void before(final Generator generator, final ExtLogRecord record) throws Exception { + // do nothing + } + + /** + * Invoked after the structured data has been added to the generator. + * + * @param generator the generator to use + * @param record the log record + */ + protected void after(final Generator generator, final ExtLogRecord record) throws Exception { + // do nothing + } + + /** + * Checks to see if the key should be overridden. + * + * @param defaultKey the default key + * @return the overridden key or the default key if no override exists + */ + protected final String getKey(final Key defaultKey) { + if (keyOverrides.containsKey(defaultKey)) { + return keyOverrides.get(defaultKey); + } + return defaultKey.getKey(); + } + + @Override + public final synchronized String format(final ExtLogRecord record) { + final boolean details = printDetails; + try { + final Generator generator = createGenerator(writer).begin(); + before(generator, record); + + // Add the default structure + generator.add(getKey(Key.TIMESTAMP), dateTimeFormatter.format(record.getInstant())) + .add(getKey(Key.SEQUENCE), record.getSequenceNumber()) + .add(getKey(Key.LOGGER_CLASS_NAME), record.getLoggerClassName()) + .add(getKey(Key.LOGGER_NAME), record.getLoggerName()) + .add(getKey(Key.LEVEL), record.getLevel().getName()) + .add(getKey(Key.MESSAGE), formatMessage(record)) + .add(getKey(Key.THREAD_NAME), record.getThreadName()) + .add(getKey(Key.THREAD_ID), record.getThreadID()) + .add(getKey(Key.MDC), record.getMdcCopy()) + .add(getKey(Key.NDC), record.getNdc()); + + if (isNotNullOrEmpty(record.getHostName())) { + generator.add(getKey(Key.HOST_NAME), record.getHostName()); + } + + if (isNotNullOrEmpty(record.getProcessName())) { + generator.add(getKey(Key.PROCESS_NAME), record.getProcessName()); + } + final long processId = record.getProcessId(); + if (processId >= 0) { + generator.add(getKey(Key.PROCESS_ID), record.getProcessId()); + } + + // Add the cause of the log message if applicable + final Throwable thrown = record.getThrown(); + if (thrown != null) { + if (isDetailedExceptionOutputType()) { + refId = 0; + final Map seen = new IdentityHashMap<>(); + generator.startObject(getKey(Key.EXCEPTION)); + addException(generator, thrown, seen); + generator.endObject(); + } + + if (isFormattedExceptionOutputType()) { + final StringBuilder sb = new StringBuilder(); + StackTraceFormatter.renderStackTrace(sb, thrown, -1); + generator.add(getKey(Key.STACK_TRACE), sb.toString()); + } + } + if (details) { + generator.add(getKey(Key.SOURCE_CLASS_NAME), record.getSourceClassName()) + .add(getKey(Key.SOURCE_FILE_NAME), record.getSourceFileName()) + .add(getKey(Key.SOURCE_METHOD_NAME), record.getSourceMethodName()) + .add(getKey(Key.SOURCE_LINE_NUMBER), record.getSourceLineNumber()) + .add(getKey(Key.SOURCE_MODULE_NAME), record.getSourceModuleName()) + .add(getKey(Key.SOURCE_MODULE_VERSION), record.getSourceModuleVersion()); + } + + if (isNotNullOrEmpty(metaData)) { + generator.addMetaData(metaDataMap); + } + + after(generator, record); + generator.end(); + + // Append an EOL character if desired + if (getRecordDelimiter() != null) { + writer.append(getRecordDelimiter()); + } + return writer.toString(); + } catch (Exception e) { + // Wrap and rethrow + throw new RuntimeException(e); + } finally { + // Clear the writer for the next format + writer.clear(); + } + } + + @Override + public boolean isCallerCalculationRequired() { + return isPrintDetails(); + } + + /** + * A string representation of the key overrides. The default is {@code null}. + * + * @return a string representation of the key overrides or {@code null} if no overrides were configured + */ + public String getKeyOverrides() { + return keyOverridesValue; + } + + /** + * Returns the character used to indicate the record has is complete. This defaults to {@code \n} and may be + * {@code null} if no end of record character is desired. + * + * @return the end of record delimiter or {@code null} if no delimiter is desired + */ + public String getRecordDelimiter() { + return eorDelimiter; + } + + /** + * Sets the value to be used to indicate the end of a record. If set to {@code null} no delimiter will be used at + * the end of the record. + * + * @param eorDelimiter the delimiter to be used or {@code null} to not use a delimiter + */ + public void setRecordDelimiter(final String eorDelimiter) { + this.eorDelimiter = eorDelimiter; + } + + /** + * Returns the value set for meta data. + *

+ * The value is a string where key/value pairs are separated by commas. The key and value are separated by an + * equal sign. + *

+ * + * @return the meta data string or {@code null} if one was not set + * @see PropertyValues#stringToMap(String) + */ + public String getMetaData() { + return metaData; + } + + /** + * Sets the meta data to use in the structured format. + *

+ * The value is a string where key/value pairs are separated by commas. The key and value are separated by an + * equal sign. + *

+ * + * @param metaData the meta data to set or {@code null} to not format any meta data + * @see PropertyValues#stringToMap(String) + */ + public synchronized void setMetaData(final String metaData) { + this.metaData = metaData; + metaDataMap = PropertyValues.stringToMap(metaData); + } + + /** + * Returns the current formatter used to format a records date and time. + * + * @return the current formatter + */ + public synchronized DateTimeFormatter getDateTimeFormatter() { + return dateTimeFormatter; + } + + /** + * Sets the pattern to use when formatting the date. The pattern must be a valid + * {@link DateTimeFormatter#ofPattern(String)} pattern. + *

+ * If the pattern is {@code null} a default {@linkplain DateTimeFormatter#ISO_OFFSET_DATE_TIME formatter} will be + * used. The {@linkplain #setZoneId(String) zone id} will always be appended to the formatter. By default the zone + * id will default to the {@linkplain ZoneId#systemDefault() systems zone id}. + *

+ * + * @param pattern the pattern to use or {@code null} to use a default pattern + */ + public synchronized void setDateFormat(final String pattern) { + if (pattern == null) { + dateTimeFormatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(zoneId); + } else { + dateTimeFormatter = DateTimeFormatter.ofPattern(pattern).withZone(zoneId); + } + } + + /** + * Returns the current zone id used for the {@linkplain #getDateTimeFormatter() date and time formatter}. + * + * @return the current zone id + */ + public synchronized ZoneId getZoneId() { + return zoneId; + } + + /** + * Sets the {@link ZoneId} to use when formatting the date and time from the {@link java.util.logging.LogRecord}. + *

+ * The rules of the id must conform to the rules specified on {@link ZoneId#of(String)}. + *

+ * + * @param zoneId the zone id or {@code null} to use the {@linkplain ZoneId#systemDefault() system default} + * @see ZoneId#of(String) + */ + public void setZoneId(final String zoneId) { + final ZoneId changed; + if (zoneId == null) { + changed = ZoneId.systemDefault(); + } else { + changed = ZoneId.of(zoneId); + } + synchronized (this) { + this.zoneId = changed; + dateTimeFormatter = dateTimeFormatter.withZone(changed); + } + } + + /** + * Indicates whether or not details should be printed. + * + * @return {@code true} if details should be printed, otherwise {@code false} + */ + public boolean isPrintDetails() { + return printDetails; + } + + /** + * Sets whether or not details should be printed. + *

+ * Printing the details can be expensive as the values are retrieved from the caller. The details include the + * source class name, source file name, source method name and source line number. + *

+ * + * @param printDetails {@code true} if details should be printed + */ + public void setPrintDetails(@SuppressWarnings("SameParameterValue") final boolean printDetails) { + this.printDetails = printDetails; + } + + /** + * Get the current output type for exceptions. + * + * @return the output type for exceptions + */ + public ExceptionOutputType getExceptionOutputType() { + return exceptionOutputType; + } + + /** + * Set the output type for exceptions. The default is {@link ExceptionOutputType#DETAILED DETAILED}. + * + * @param exceptionOutputType the desired output type, if {@code null} {@link ExceptionOutputType#DETAILED} is used + */ + public void setExceptionOutputType(final ExceptionOutputType exceptionOutputType) { + if (exceptionOutputType == null) { + this.exceptionOutputType = ExceptionOutputType.DETAILED; + } else { + this.exceptionOutputType = exceptionOutputType; + } + } + + /** + * Checks the exception output type and determines if detailed output should be written. + * + * @return {@code true} if detailed output should be written, otherwise {@code false} + */ + protected boolean isDetailedExceptionOutputType() { + final ExceptionOutputType exceptionOutputType = this.exceptionOutputType; + return exceptionOutputType == ExceptionOutputType.DETAILED || + exceptionOutputType == ExceptionOutputType.DETAILED_AND_FORMATTED; + } + + /** + * Checks the exception output type and determines if formatted output should be written. The formatted output is + * equivalent to {@link Throwable#printStackTrace()}. + * + * @return {@code true} if formatted exception output should be written, otherwise {@code false} + */ + protected boolean isFormattedExceptionOutputType() { + final ExceptionOutputType exceptionOutputType = this.exceptionOutputType; + return exceptionOutputType == ExceptionOutputType.FORMATTED || + exceptionOutputType == ExceptionOutputType.DETAILED_AND_FORMATTED; + } + + private void addException(final Generator generator, final Throwable throwable, final Map seen) + throws Exception { + if (throwable == null) { + return; + } + if (seen.containsKey(throwable)) { + generator.addAttribute(getKey(Key.EXCEPTION_REFERENCE_ID), seen.get(throwable)); + generator.startObject(getKey(Key.EXCEPTION_CIRCULAR_REFERENCE)); + generator.add(getKey(Key.EXCEPTION_MESSAGE), throwable.getMessage()); + generator.endObject(); // end circular reference + } else { + final int id = ++refId; + seen.put(throwable, id); + generator.addAttribute(getKey(Key.EXCEPTION_REFERENCE_ID), id); + generator.add(getKey(Key.EXCEPTION_TYPE), throwable.getClass().getName()); + generator.add(getKey(Key.EXCEPTION_MESSAGE), throwable.getMessage()); + + final StackTraceElement[] elements = throwable.getStackTrace(); + addStackTraceElements(generator, elements); + + // Render the suppressed messages + final Throwable[] suppressed = throwable.getSuppressed(); + if (suppressed != null && suppressed.length > 0) { + generator.startArray(getKey(Key.EXCEPTION_SUPPRESSED)); + for (Throwable s : suppressed) { + if (generator.wrapArrays()) { + generator.startObject(getKey(Key.EXCEPTION)); + } else { + generator.startObject(null); + } + addException(generator, s, seen); + generator.endObject(); // end exception + } + generator.endArray(); + } + + // Render the cause + final Throwable cause = throwable.getCause(); + if (cause != null) { + generator.startObject(getKey(Key.EXCEPTION_CAUSED_BY)); + generator.startObject(getKey(Key.EXCEPTION)); + addException(generator, cause, seen); + generator.endObject(); + generator.endObject(); // end exception + } + } + } + + private void addStackTraceElements(final Generator generator, final StackTraceElement[] elements) throws Exception { + generator.startArray(getKey(Key.EXCEPTION_FRAMES)); + for (StackTraceElement e : elements) { + if (generator.wrapArrays()) { + generator.startObject(getKey(Key.EXCEPTION_FRAME)); + } else { + generator.startObject(null); + } + generator.add(getKey(Key.EXCEPTION_FRAME_CLASS), e.getClassName()); + generator.add(getKey(Key.EXCEPTION_FRAME_METHOD), e.getMethodName()); + final int line = e.getLineNumber(); + if (line >= 0) { + generator.add(getKey(Key.EXCEPTION_FRAME_LINE), e.getLineNumber()); + } + generator.endObject(); // end exception object + } + generator.endArray(); // end array + } + + private static boolean isNotNullOrEmpty(final String value) { + return value != null && !value.isEmpty(); + } + + /** + * A generator used to create the structured output. + */ + @SuppressWarnings("UnusedReturnValue") + protected interface Generator { + + /** + * Initial method invoked at the start of the generation. + * + * @return the generator + * @throws Exception if an error occurs while adding the data + */ + default Generator begin() throws Exception { + return this; + } + + /** + * Writes an integer value. + * + * @param key they key + * @param value the value + * @return the generator + * @throws Exception if an error occurs while adding the data + */ + default Generator add(final String key, final int value) throws Exception { + add(key, Integer.toString(value)); + return this; + } + + /** + * Writes a long value. + * + * @param key they key + * @param value the value + * @return the generator + * @throws Exception if an error occurs while adding the data + */ + default Generator add(final String key, final long value) throws Exception { + add(key, Long.toString(value)); + return this; + } + + /** + * Writes a map value + * + * @param key the key for the map + * @param value the map + * @return the generator + * @throws Exception if an error occurs while adding the data + */ + Generator add(String key, Map value) throws Exception; + + /** + * Writes a string value. + * + * @param key the key for the value + * @param value the string value + * @return the generator + * @throws Exception if an error occurs while adding the data + */ + Generator add(String key, String value) throws Exception; + + /** + * Adds the meta data to the structured format. + *

+ * By default this processes the map and uses {@link #add(String, String)} to add entries. + *

+ * + * @param metaData the map of the meta data, cannot be {@code null} + * @return the generator + * @throws Exception if an error occurs while adding the data + */ + default Generator addMetaData(final Map metaData) throws Exception { + for (Map.Entry entry : metaData.entrySet()) { + add(entry.getKey(), entry.getValue()); + } + return this; + } + + /** + * Writes the start of an object. + *

+ * If the {@link #wrapArrays()} returns {@code false} the key may be {@code null} and implementations should + * handle this. + *

+ * + * @param key they key for the object, or {@code null} if this object was + * {@linkplain #startArray(String) started in an array} and the {@link #wrapArrays()} is + * {@code false} + * @return the generator + * @throws Exception if an error occurs while adding the data + */ + Generator startObject(String key) throws Exception; + + /** + * Writes an end to the object. + * + * @return the generator + * @throws Exception if an error occurs while adding the data + */ + Generator endObject() throws Exception; + + /** + * Writes the start of an array. This defaults to {@link #startObject(String)} for convenience of generators + * that don't have a specific type for arrays. + * + * @param key they key for the array + * @return the generator + * @throws Exception if an error occurs while adding the data + */ + default Generator startArray(String key) throws Exception { + return startObject(key); + } + + /** + * Writes an end for an array. This defaults to {@link #endObject()} for convenience of generators that don't + * have a specific type for arrays. + * + * @return the generator + * @throws Exception if an error occurs while adding the data + */ + default Generator endArray() throws Exception { + return endObject(); + } + + /** + * Writes an attribute. + *

+ * By default this uses the {@link #add(String, int)} method to add the attribute. If a formatter requires + * special handling for attributes, for example an attribute on an XML element, this method can be overridden. + *

+ * + * @param name the name of the attribute + * @param value the value of the attribute + * @return the generator + * @throws Exception if an error occurs while adding the data + */ + default Generator addAttribute(final String name, final int value) throws Exception { + return add(name, value); + } + + /** + * Writes an attribute. + *

+ * By default this uses the {@link #add(String, String)} method to add the attribute. If a formatter requires + * special handling for attributes, for example an attribute on an XML element, this method can be overridden. + *

+ * + * @param name the name of the attribute + * @param value the value of the attribute + * @return the generator + * @throws Exception if an error occurs while adding the data + */ + default Generator addAttribute(final String name, final String value) throws Exception { + return add(name, value); + } + + /** + * Writes any trailing data that's needed. + * + * @return the generator + * @throws Exception if an error occurs while adding the data during the build + */ + Generator end() throws Exception; + + /** + * Indicates whether or not elements in an array should be wrapped or not. The default is {@code false}. + * + * @return {@code true} if elements should be wrapped, otherwise {@code false} + */ + default boolean wrapArrays() { + return false; + } + } +} diff --git a/logging/src/main/java/org/xbib/logging/formatters/TextBannerFormatter.java b/logging/src/main/java/org/xbib/logging/formatters/TextBannerFormatter.java new file mode 100644 index 0000000..cfa2d74 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/formatters/TextBannerFormatter.java @@ -0,0 +1,129 @@ +package org.xbib.logging.formatters; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Objects; +import java.util.function.Supplier; +import java.util.logging.Handler; +import org.xbib.logging.ExtFormatter; + +/** + * A formatter which prints a text banner ahead of the normal formatter header. + * The text banner is acquired from a {@link Supplier} which is passed in to the constructor. + * Several utility methods are also present which allow easy creation of {@code Supplier} instances. + */ +public final class TextBannerFormatter extends ExtFormatter.Delegating { + private final Supplier bannerSupplier; + + /** + * Construct a new instance. + * + * @param bannerSupplier the supplier for the banner (must not be {@code null}) + * @param delegate the delegate formatter (must not be {@code null}) + */ + public TextBannerFormatter(final Supplier bannerSupplier, final ExtFormatter delegate) { + super(delegate); + this.bannerSupplier = Objects.requireNonNull(bannerSupplier, "bannerSupplier"); + } + + // doc inherited + public String getHead(final Handler h) { + final String dh = Objects.requireNonNullElse(delegate.getHead(h), ""); + final String banner = Objects.requireNonNullElse(bannerSupplier.get(), ""); + return banner + dh; + } + + /** + * Get the empty supplier which always returns an empty string. + * + * @return the empty supplier (not {@code null}) + */ + public static Supplier getEmptySupplier() { + return EMPTY; + } + + /** + * Create a supplier which always returns the given string. + * + * @param string the string (must not be {@code null}) + * @return a supplier which returns the given string (not {@code null}) + */ + public static Supplier createStringSupplier(String string) { + Objects.requireNonNull(string, "string"); + return () -> string; + } + + /** + * Create a supplier which loads the banner from the given file path, + * falling back to the given fallback supplier on error. + * + * @param path the path to load from (must not be {@code null}) + * @param fallback the fallback supplier (must not be {@code null}) + * @return the supplier (not {@code null}) + */ + public static Supplier createFileSupplier(Path path, Supplier fallback) { + Objects.requireNonNull(path, "path"); + Objects.requireNonNull(fallback, "fallback"); + return () -> { + try { + return Files.readString(path, StandardCharsets.UTF_8); + } catch (IOException ignored) { + return fallback.get(); + } + }; + } + + /** + * Create a supplier which loads the banner from the given URL, + * falling back to the given fallback supplier on error. + * + * @param url the URL to load from (must not be {@code null}) + * @param fallback the fallback supplier (must not be {@code null}) + * @return the supplier (not {@code null}) + */ + public static Supplier createUrlSupplier(URL url, Supplier fallback) { + Objects.requireNonNull(url, "url"); + Objects.requireNonNull(fallback, "fallback"); + return () -> { + try { + final InputStream is = url.openStream(); + return is == null ? fallback.get() : loadStringFromStream(is); + } catch (IOException ignored) { + return fallback.get(); + } + }; + } + + /** + * Create a supplier which loads the banner from a resource in the given class loader, + * falling back to the given fallback supplier on error. + * + * @param resource the resource name (must not be {@code null}) + * @param fallback the fallback supplier (must not be {@code null}) + * @return the supplier (not {@code null}) + */ + public static Supplier createResourceSupplier(String resource, Supplier fallback) { + Objects.requireNonNull(resource, "resource"); + Objects.requireNonNull(fallback, "fallback"); + return () -> { + try { + final InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream(resource); + return is == null ? fallback.get() : loadStringFromStream(is); + } catch (IOException ignored) { + return fallback.get(); + } + }; + } + + private static final Supplier EMPTY = createStringSupplier(""); + + private static String loadStringFromStream(final InputStream is) throws IOException { + try (is) { + return new String(is.readAllBytes(), StandardCharsets.UTF_8); + } + } +} diff --git a/logging/src/main/java/org/xbib/logging/formatters/ThreadLoggingFormatter.java b/logging/src/main/java/org/xbib/logging/formatters/ThreadLoggingFormatter.java new file mode 100644 index 0000000..df3ad3f --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/formatters/ThreadLoggingFormatter.java @@ -0,0 +1,80 @@ +package org.xbib.logging.formatters; + +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadMXBean; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.function.Function; +import java.util.logging.Formatter; +import java.util.logging.LogManager; +import java.util.logging.LogRecord; +import org.xbib.logging.util.StackTraceFormatter; + +public class ThreadLoggingFormatter extends Formatter { + + private static final String PROPERTY_KEY = "org.xbib.interlibrary.jul.ThreadLoggingFormatter.format"; + + private static final String DEFAULT_FORMAT = + "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] [%5$s] %6$s %7$s%n"; + + private final ThreadMXBean threadMXBean; + + public ThreadLoggingFormatter() { + this.threadMXBean = ManagementFactory.getThreadMXBean(); + } + + @Override + public String format(LogRecord record) { + ZonedDateTime zdt = ZonedDateTime.ofInstant(record.getInstant(), ZoneId.systemDefault()); + String source; + if (record.getSourceClassName() != null) { + source = record.getSourceClassName(); + if (record.getSourceMethodName() != null) { + source += " " + record.getSourceMethodName(); + } + } else { + source = record.getLoggerName(); + } + String message = formatMessage(record); + String throwable = ""; + if (record.getThrown() != null) { + StringBuilder sb = new StringBuilder(); + StackTraceFormatter.renderStackTrace(sb, record.getThrown(), -1); + throwable = sb.toString(); + } + return String.format(DEFAULT_FORMAT_STRING, + zdt, + source, + record.getLoggerName(), + record.getLevel().getName(), + threadMXBean.getThreadInfo(record.getLongThreadID()).getThreadName(), + message, + throwable); + } + + private static String getLoggingProperty(String name) { + return LogManager.getLogManager().getProperty(name); + } + + private static final String DEFAULT_FORMAT_STRING = getFormat(ThreadLoggingFormatter::getLoggingProperty); + + private static String getFormat(Function defaultPropertyGetter) { + String format = System.getProperty(PROPERTY_KEY); + if (format == null && defaultPropertyGetter != null) { + format = defaultPropertyGetter.apply(PROPERTY_KEY); + } + if (format != null) { + try { + // validate the user-defined format string + String.format(format, ZonedDateTime.now(), "", "", "", "", ""); + } catch (IllegalArgumentException e) { + // illegal syntax; fall back to the default format + format = DEFAULT_FORMAT; + } + } else { + format = DEFAULT_FORMAT; + } + return format; + } + +} diff --git a/logging/src/main/java/org/xbib/logging/formatters/XmlFormatter.java b/logging/src/main/java/org/xbib/logging/formatters/XmlFormatter.java new file mode 100644 index 0000000..785f193 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/formatters/XmlFormatter.java @@ -0,0 +1,268 @@ +package org.xbib.logging.formatters; + +import java.io.Writer; +import java.util.Map; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; +import org.xbib.logging.ExtLogRecord; +import org.xbib.logging.util.PropertyValues; + +/** + * A formatter that outputs the record in XML format. + *

+ * The details include; + *

+ *
    + *
  • {@link ExtLogRecord#getSourceClassName() source class name}
  • + *
  • {@link ExtLogRecord#getSourceFileName() source file name}
  • + *
  • {@link ExtLogRecord#getSourceMethodName() source method name}
  • + *
  • {@link ExtLogRecord#getSourceLineNumber() source line number}
  • + *
+ */ +@SuppressWarnings({"WeakerAccess", "unused", "SameParameterValue"}) +public class XmlFormatter extends StructuredFormatter { + + public static final String DEFAULT_NAMESPACE = "urn:xbib:logging:formatter:1.0"; + + private final XMLOutputFactory factory = XMLOutputFactory.newFactory(); + + private volatile boolean prettyPrint = false; + private volatile boolean printNamespace = false; + private volatile String namespaceUri; + + /** + * Creates a new XML formatter. + */ + public XmlFormatter() { + namespaceUri = DEFAULT_NAMESPACE; + } + + /** + * Creates a new XML formatter. + *

+ * If the {@code keyOverrides} is empty the default {@linkplain #DEFAULT_NAMESPACE namespace} will be used. + *

+ * + * @param keyOverrides a string representation of a map to override keys + * @see PropertyValues#stringToEnumMap(Class, String) + */ + public XmlFormatter(final String keyOverrides) { + super(keyOverrides); + if (keyOverrides == null || keyOverrides.isEmpty()) { + namespaceUri = DEFAULT_NAMESPACE; + } else { + namespaceUri = null; + } + } + + /** + * Creates a new XML formatter. + *

+ * If the {@code keyOverrides} is empty the default {@linkplain #DEFAULT_NAMESPACE namespace} will be used. + *

+ * + * @param keyOverrides a map of overrides for the default keys + */ + public XmlFormatter(final Map keyOverrides) { + super(keyOverrides); + if (keyOverrides == null || keyOverrides.isEmpty()) { + namespaceUri = DEFAULT_NAMESPACE; + } else { + namespaceUri = null; + } + } + + /** + * Indicates whether or not pretty printing is enabled. + * + * @return {@code true} if pretty printing is enabled, otherwise {@code false} + */ + public boolean isPrettyPrint() { + return prettyPrint; + } + + /** + * Turns on or off pretty printing. + * + * @param prettyPrint {@code true} to turn on pretty printing or {@code false} to turn it off + */ + public void setPrettyPrint(final boolean prettyPrint) { + this.prettyPrint = prettyPrint; + } + + /** + * Indicates whether or not the name space should be written on the {@literal }. + * + * @return {@code true} if the name space should be written for each record + */ + public boolean isPrintNamespace() { + return printNamespace; + } + + /** + * Turns on or off the printing of the namespace for each {@literal }. This is set to + * {@code false} by default. + * + * @param printNamespace {@code true} if the name space should be written for each record + */ + public void setPrintNamespace(final boolean printNamespace) { + this.printNamespace = printNamespace; + } + + /** + * Returns the namespace URI used for each record if {@link #isPrintNamespace()} is {@code true}. + * + * @return the namespace URI, may be {@code null} if explicitly set to {@code null} + */ + public String getNamespaceUri() { + return namespaceUri; + } + + /** + * Sets the namespace URI used for each record if {@link #isPrintNamespace()} is {@code true}. + * + * @param namespaceUri the namespace to use or {@code null} if no namespace URI should be used regardless of the + * {@link #isPrintNamespace()} value + */ + public void setNamespaceUri(final String namespaceUri) { + this.namespaceUri = namespaceUri; + } + + @Override + protected Generator createGenerator(final Writer writer) throws Exception { + final XMLStreamWriter xmlWriter; + if (prettyPrint) { + xmlWriter = new IndentingXmlWriter(factory.createXMLStreamWriter(writer)); + } else { + xmlWriter = factory.createXMLStreamWriter(writer); + } + return new XmlGenerator(xmlWriter); + } + + private class XmlGenerator implements Generator { + private final XMLStreamWriter xmlWriter; + + private XmlGenerator(final XMLStreamWriter xmlWriter) { + this.xmlWriter = xmlWriter; + } + + @Override + public Generator begin() throws Exception { + writeStart(getKey(Key.RECORD)); + if (printNamespace && namespaceUri != null) { + xmlWriter.writeDefaultNamespace(namespaceUri); + } + return this; + } + + @Override + public Generator add(final String key, final Map value) throws Exception { + if (value == null) { + writeEmpty(key); + } else { + writeStart(key); + for (Map.Entry entry : value.entrySet()) { + final String k = entry.getKey(); + final Object v = entry.getValue(); + if (v == null) { + writeEmpty(k); + } else { + add(k, String.valueOf(v)); + } + } + writeEnd(); + } + return this; + } + + @Override + public Generator add(final String key, final String value) throws Exception { + if (value == null) { + writeEmpty(key); + } else { + writeStart(key); + xmlWriter.writeCharacters(value); + writeEnd(); + } + return this; + } + + @Override + public Generator addMetaData(final Map metaData) throws Exception { + for (Map.Entry entry : metaData.entrySet()) { + writeStart("metaData"); + xmlWriter.writeAttribute("key", entry.getKey()); + if (entry.getValue() != null) { + xmlWriter.writeCharacters(entry.getValue()); + } + writeEnd(); + } + return this; + } + + @Override + public Generator startObject(final String key) throws Exception { + writeStart(key); + return this; + } + + @Override + public Generator endObject() throws Exception { + writeEnd(); + return this; + } + + @Override + public Generator addAttribute(final String name, final int value) throws Exception { + return addAttribute(name, Integer.toString(value)); + } + + @Override + public Generator addAttribute(final String name, final String value) throws Exception { + xmlWriter.writeAttribute(name, value); + return this; + } + + @Override + public Generator end() throws Exception { + writeEnd(); // end record + safeFlush(xmlWriter); + safeClose(xmlWriter); + return this; + } + + @Override + public boolean wrapArrays() { + return true; + } + + private void writeEmpty(final String name) throws XMLStreamException { + xmlWriter.writeEmptyElement(name); + } + + private void writeStart(final String name) throws XMLStreamException { + xmlWriter.writeStartElement(name); + } + + private void writeEnd() throws XMLStreamException { + xmlWriter.writeEndElement(); + } + + private void safeFlush(final XMLStreamWriter flushable) { + if (flushable != null) + try { + flushable.flush(); + } catch (Throwable ignore) { + } + } + + private void safeClose(final XMLStreamWriter closeable) { + if (closeable != null) + try { + closeable.close(); + } catch (Throwable ignore) { + } + } + } +} diff --git a/logging/src/main/java/org/xbib/logging/handlers/AsyncHandler.java b/logging/src/main/java/org/xbib/logging/handlers/AsyncHandler.java new file mode 100644 index 0000000..52ba559 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/handlers/AsyncHandler.java @@ -0,0 +1,202 @@ +package org.xbib.logging.handlers; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.logging.Handler; +import org.xbib.logging.ExtHandler; +import org.xbib.logging.ExtLogRecord; + +/** + * An asynchronous log handler which is used to write to a handler or group of handlers which are "slow" or introduce + * some degree of latency. + */ +public class AsyncHandler extends ExtHandler { + + private final BlockingQueue recordQueue; + private final int queueLength; + private final Thread thread; + private volatile OverflowAction overflowAction = OverflowAction.BLOCK; + + @SuppressWarnings("unused") + private volatile int state; + + private static final AtomicIntegerFieldUpdater stateUpdater = AtomicIntegerFieldUpdater + .newUpdater(AsyncHandler.class, "state"); + + private static final int DEFAULT_QUEUE_LENGTH = 512; + + /** + * Construct a new instance. + * + * @param queueLength the queue length + * @param threadFactory the thread factory to use to construct the handler thread + */ + public AsyncHandler(final int queueLength, final ThreadFactory threadFactory) { + recordQueue = new ArrayBlockingQueue(queueLength); + thread = threadFactory.newThread(new AsyncTask()); + if (thread == null) { + throw new IllegalArgumentException("Thread factory did not create a thread"); + } + thread.setDaemon(true); + this.queueLength = queueLength; + } + + /** + * Construct a new instance. + * + * @param threadFactory the thread factory to use to construct the handler thread + */ + public AsyncHandler(final ThreadFactory threadFactory) { + this(DEFAULT_QUEUE_LENGTH, threadFactory); + } + + /** + * Construct a new instance. + * + * @param queueLength the queue length + */ + public AsyncHandler(final int queueLength) { + this(queueLength, Executors.defaultThreadFactory()); + } + + /** + * Construct a new instance. + */ + public AsyncHandler() { + this(DEFAULT_QUEUE_LENGTH); + } + + /** + * The full size of the queue. + * + * @return the full size of the queue. + */ + public int getQueueLength() { + return queueLength; + } + + /** + * Get the overflow action. + * + * @return the overflow action + */ + public OverflowAction getOverflowAction() { + return overflowAction; + } + + /** + * Set the overflow action. + * + * @param overflowAction the overflow action + */ + public void setOverflowAction(final OverflowAction overflowAction) { + if (overflowAction == null) { + throw new NullPointerException("overflowAction is null"); + } + this.overflowAction = overflowAction; + } + + /** + * {@inheritDoc} + */ + public void doPublish(final ExtLogRecord record) { + switch (state) { + case 0: { + if (stateUpdater.compareAndSet(this, 0, 1)) { + thread.start(); + } + } + case 1: { + break; + } + default: { + return; + } + } + final BlockingQueue recordQueue = this.recordQueue; + // Determine if we need to calculate the caller information before we queue the record + if (isCallerCalculationRequired()) { + // prepare record to move to another thread + record.copyAll(); + } else { + // Disable the caller calculation since it's been determined we won't be using it + record.disableCallerCalculation(); + // Copy the MDC over + record.copyMdc(); + } + if (Thread.currentThread() == thread) { + publishToNestedHandlers(record); + return; + } + if (overflowAction == OverflowAction.DISCARD) { + recordQueue.offer(record); + } else { + try { + recordQueue.put(record); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + + /** + * {@inheritDoc} + */ + public void close() { + if (stateUpdater.getAndSet(this, 2) != 2) { + thread.interrupt(); + super.close(); + } + } + + private final class AsyncTask implements Runnable { + public void run() { + final BlockingQueue recordQueue = AsyncHandler.this.recordQueue; + final Handler[] handlers = AsyncHandler.this.handlers; + + boolean intr = false; + try { + for (; ; ) { + ExtLogRecord rec = null; + try { + if (state == 2) { + rec = recordQueue.poll(); + if (rec == null) { + return; + } + } else { + // auto-flush will flush on an empty queue + if (isAutoFlush()) { + rec = recordQueue.poll(); + if (rec == null) { + // flush all handlers + flush(); + rec = recordQueue.take(); + } + } else { + rec = recordQueue.take(); + } + } + } catch (InterruptedException e) { + intr = true; + continue; + } + publishToNestedHandlers(rec); + } + } finally { + if (intr) { + Thread.currentThread().interrupt(); + } + clearHandlers(); + } + } + } + + public enum OverflowAction { + BLOCK, + DISCARD, + } +} diff --git a/logging/src/main/java/org/xbib/logging/handlers/ConsoleHandler.java b/logging/src/main/java/org/xbib/logging/handlers/ConsoleHandler.java new file mode 100644 index 0000000..f9f0d8b --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/handlers/ConsoleHandler.java @@ -0,0 +1,287 @@ +package org.xbib.logging.handlers; + +import java.io.Console; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Objects; +import java.util.logging.ErrorManager; +import java.util.logging.Formatter; +import org.xbib.logging.errormanager.HandlerErrorManager; +import org.xbib.logging.formatters.Formatters; +import org.xbib.logging.io.UncloseableOutputStream; +import org.xbib.logging.io.UncloseableWriter; +import org.xbib.logging.util.CharsetUtil; + +/** + * A console handler which writes to {@code System.out} by default. + */ +public class ConsoleHandler extends OutputStreamHandler { + + private static final OutputStream out = System.out; + + private static final OutputStream err = System.err; + + /** + * The target stream type. + */ + public enum Target { + + /** + * The target for {@link System#out}. + */ + SYSTEM_OUT, + /** + * The target for {@link System#err}. + */ + SYSTEM_ERR, + /** + * The target for {@link System#console()}. + */ + CONSOLE, + } + + private static final PrintWriter console; + + private final ErrorManager localErrorManager = new HandlerErrorManager(this); + + static { + final Console con = System.console(); + console = con == null ? null : con.writer(); + } + + /** + * Construct a new instance. + */ + public ConsoleHandler() { + this(Formatters.nullFormatter()); + } + + /** + * Construct a new instance. + * + * @param formatter the formatter to use + */ + public ConsoleHandler(final Formatter formatter) { + this(console == null ? Target.SYSTEM_OUT : Target.CONSOLE, formatter); + } + + /** + * Construct a new instance. + * + * @param target the target to write to, or {@code null} to start with an uninitialized target + */ + public ConsoleHandler(final Target target) { + this(target, Formatters.nullFormatter()); + } + + /** + * Construct a new instance. + * + * @param target the target to write to, or {@code null} to start with an uninitialized target + * @param formatter the formatter to use + */ + public ConsoleHandler(final Target target, final Formatter formatter) { + super(formatter); + setCharset(CharsetUtil.consoleCharset()); + switch (target) { + case SYSTEM_OUT: + setOutputStream(wrap(out)); + break; + case SYSTEM_ERR: + setOutputStream(wrap(err)); + break; + case CONSOLE: + setWriter(wrap(console)); + break; + default: + throw new IllegalArgumentException(); + } + } + + /** + * Set the target for this console handler. + * + * @param target the target to write to, or {@code null} to clear the target + */ + public void setTarget(Target target) { + final Target t = (target == null ? console == null ? Target.SYSTEM_OUT : Target.CONSOLE : target); + switch (t) { + case SYSTEM_OUT: + setOutputStream(wrap(out)); + break; + case SYSTEM_ERR: + setOutputStream(wrap(err)); + break; + case CONSOLE: + setWriter(wrap(console)); + break; + default: + throw new IllegalArgumentException(); + } + } + + public void setErrorManager(final ErrorManager em) { + if (em == localErrorManager) { + // ignore to avoid loops + super.setErrorManager(new ErrorManager()); + return; + } + super.setErrorManager(em); + } + + private static final String ESC = Character.toString(27); + + /** + * Write a PNG image to the console log, if it is supported. + * The image data stream must be closed by the caller. + * + * @param imageData the PNG image data stream to write (must not be {@code null}) + * @param columns the number of text columns to occupy (0 for automatic) + * @param rows the number of text rows to occupy (0 for automatic) + * @return {@code true} if the image was written, or {@code false} if image support isn't found + * @throws IOException if the stream failed while writing the image + */ + public boolean writeImagePng(InputStream imageData, int columns, int rows) throws IOException { + Objects.requireNonNull(imageData, "imageData"); + columns = Math.max(0, columns); + rows = Math.max(0, rows); + if (!isGraphicsSupportPassivelyDetected()) { + // no graphics + return false; + } + lock.lock(); + try { + // clear out any pending stuff + final Writer writer = getWriter(); + if (writer == null) + return false; + // start with the header + try (OutputStream os = Base64.getEncoder().wrap(new OutputStream() { + final byte[] buffer = new byte[2048]; + int pos = 0; + + public void write(final int b) throws IOException { + if (pos == buffer.length) + more(); + buffer[pos++] = (byte) b; + } + + public void write(final byte[] b, int off, int len) throws IOException { + while (len > 0) { + if (pos == buffer.length) { + more(); + } + final int cnt = Math.min(len, buffer.length - pos); + System.arraycopy(b, off, buffer, pos, cnt); + pos += cnt; + off += cnt; + len -= cnt; + } + } + + void more() throws IOException { + writer.write("m=1;"); + writer.write(new String(buffer, 0, pos, StandardCharsets.US_ASCII)); + writer.write(ESC + "\\"); + // set up next segment + writer.write(ESC + "_G"); + pos = 0; + } + + public void close() throws IOException { + writer.write("m=0;"); + writer.write(new String(buffer, 0, pos, StandardCharsets.US_ASCII)); + writer.write(ESC + "\\\n"); + writer.flush(); + pos = 0; + } + })) { + // set the header + writer.write(String.format(ESC + "_Gf=100,a=T,c=%d,r=%d,", Integer.valueOf(columns), Integer.valueOf(rows))); + // write the data in encoded chunks + imageData.transferTo(os); + } + // OK + return true; + } finally { + lock.unlock(); + } + } + + /** + * Get the local error manager. This is an error manager that will publish errors to this console handler. + * The console handler itself should not use this error manager. + * + * @return the local error manager + */ + public ErrorManager getLocalErrorManager() { + return localErrorManager; + } + + private static OutputStream wrap(final OutputStream outputStream) { + return outputStream == null ? null + : outputStream instanceof UncloseableOutputStream ? outputStream : new UncloseableOutputStream(outputStream); + } + + private static Writer wrap(final Writer writer) { + return writer == null ? null : writer instanceof UncloseableWriter ? writer : new UncloseableWriter(writer); + } + + /** + * {@inheritDoc} + */ + public void setOutputStream(final OutputStream outputStream) { + super.setOutputStream(wrap(outputStream)); + } + + /** + * Determine whether the console exists. + * If the console does not exist, then the standard output stream will be used when {@link Target#CONSOLE} is + * selected as {@linkplain #setTarget(Target) the output target}. + * + * @return {@code true} if there is a console, {@code false} otherwise + */ + public static boolean hasConsole() { + return console != null; + } + + /** + * Determine whether the console supports truecolor output. + * This call may be expensive, so the result should be captured for the lifetime of any formatter making use of + * this information. + * + * @return {@code true} if the console exists and supports truecolor output; {@code false} otherwise + */ + public static boolean isTrueColor() { + if (!hasConsole()) { + return false; + } + final String colorterm = System.getenv("COLORTERM"); + return colorterm != null && (colorterm.contains("truecolor") || colorterm.contains("24bit")); + } + + /** + * Determine whether the console can be passively detected to support graphical output. + * This call may be expensive, so the result should be captured for the lifetime of any formatter making use of + * this information. + * + * @return {@code true} if the console exists and supports graphical output; {@code false} otherwise or if + * graphical support cannot be passively detected + */ + public static boolean isGraphicsSupportPassivelyDetected() { + if (!hasConsole()) { + return false; + } + final String term = System.getenv("TERM"); + final String termProgram = System.getenv("TERM_PROGRAM"); + return term != null && (term.equalsIgnoreCase("kitty") + || term.equalsIgnoreCase("xterm-kitty") + || term.equalsIgnoreCase("wezterm") + || term.equalsIgnoreCase("konsole")) || termProgram != null && termProgram.equalsIgnoreCase("wezterm"); + } +} diff --git a/logging/src/main/java/org/xbib/logging/handlers/DelayedHandler.java b/logging/src/main/java/org/xbib/logging/handlers/DelayedHandler.java new file mode 100644 index 0000000..5680da8 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/handlers/DelayedHandler.java @@ -0,0 +1,359 @@ +package org.xbib.logging.handlers; + +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Deque; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; +import java.util.logging.ErrorManager; +import java.util.logging.Formatter; +import java.util.logging.Handler; +import java.util.logging.Level; +import org.xbib.logging.ExtHandler; +import org.xbib.logging.ExtLogRecord; +import org.xbib.logging.LogContext; +import org.xbib.logging.formatters.PatternFormatter; +import org.xbib.logging.util.StandardOutputStreams; + +/** + * A handler that queues messages until it's at least one child handler is {@linkplain #addHandler(Handler) added} or + * {@linkplain #setHandlers(Handler[]) set}. If the children handlers are {@linkplain #clearHandlers() cleared} then + * the handler is no longer considered activated and messages will once again be queued. + */ +@SuppressWarnings({"unused", "WeakerAccess"}) +public class DelayedHandler extends ExtHandler { + + private final Map> queues = new HashMap<>(); + + private volatile boolean activated = false; + private volatile boolean callerCalculationRequired = false; + + private final LogContext logContext; + private final int queueLimit; + private final Level warnThreshold; + + /** + * Construct a new instance. + */ + public DelayedHandler() { + this(null); + } + + /** + * Construct a new instance, with the given log context used to recheck log levels on replay. + * + * @param logContext the log context to use for level checks on replay, or {@code null} for none + */ + public DelayedHandler(LogContext logContext) { + this(logContext, 200); + } + + /** + * Construct a new instance. + * The given queue limit value is used to limit the length of each level queue. + * + * @param queueLimit the queue limit + */ + public DelayedHandler(int queueLimit) { + this(null, queueLimit); + } + + /** + * Construct a new instance, with the given log context used to recheck log levels on replay. + * The given queue limit value is used to limit the length of each level queue. + * + * @param logContext the log context to use for level checks on replay, or {@code null} for none + * @param queueLimit the queue limit + */ + public DelayedHandler(LogContext logContext, int queueLimit) { + this(logContext, queueLimit, Level.INFO); + } + + /** + * Construct a new instance. + * The given queue limit value is used to limit the length of each level queue. + * The warning threshold specifies that only queues with the threshold level or higher will report overrun errors. + * + * @param queueLimit the queue limit + * @param warnThreshold the threshold level to report queue overruns for + */ + public DelayedHandler(int queueLimit, Level warnThreshold) { + this(null, queueLimit, warnThreshold); + } + + /** + * Construct a new instance, with the given log context used to recheck log levels on replay. + * The given queue limit value is used to limit the length of each level queue. + * The warning threshold specifies that only queues with the threshold level or higher will report overrun errors. + * + * @param logContext the log context to use for level checks on replay, or {@code null} for none + * @param queueLimit the queue limit + * @param warnThreshold the threshold level to report queue overruns for + */ + public DelayedHandler(LogContext logContext, int queueLimit, Level warnThreshold) { + this.logContext = logContext; + this.queueLimit = queueLimit; + this.warnThreshold = warnThreshold; + } + + private static Deque newDeque(Object ignored) { + return new ArrayDeque<>(); + } + + @Override + protected void doPublish(final ExtLogRecord record) { + // If activated just delegate + if (activated) { + publishToNestedHandlers(record); + super.doPublish(record); + } else { + lock.lock(); + try { + // Check one more time to see if we've been activated before queuing the messages + if (activated) { + publishToNestedHandlers(record); + super.doPublish(record); + } else { + // Determine if we need to calculate the caller information before we queue the record + if (isCallerCalculationRequired()) { + // prepare record to move to another thread + record.copyAll(); + } else { + // Disable the caller calculation since it's been determined we won't be using it + record.disableCallerCalculation(); + // Copy the MDC over + record.copyMdc(); + } + Level level = record.getLevel(); + Deque q = queues.computeIfAbsent(level, DelayedHandler::newDeque); + if (q.size() >= queueLimit && level.intValue() >= warnThreshold.intValue()) { + reportError( + "The delayed handler's queue was overrun and log record(s) were lost. Did you forget to configure logging?", + null, ErrorManager.WRITE_FAILURE); + } + enqueueOrdered(q, record); + } + } finally { + lock.unlock(); + } + } + } + + /** + * Enqueue the log record such that the queue's order (by sequence number) is maintained. + * + * @param q the queue + * @param record the record + */ + private void enqueueOrdered(Deque q, ExtLogRecord record) { + assert lock.isHeldByCurrentThread(); + ExtLogRecord last = q.peekLast(); + if (last != null) { + // check the ordering + if (Long.compareUnsigned(last.getSequenceNumber(), record.getSequenceNumber()) > 0) { + // out of order; we have to re-sort.. typically, it's only going to be out of order by a couple though + q.pollLast(); + try { + enqueueOrdered(q, record); + } finally { + q.addLast(last); + } + return; + } + } + // order is OK + q.addLast(record); + } + + private Supplier drain() { + assert lock.isHeldByCurrentThread(); + if (queues.isEmpty()) { + return () -> null; + } + List> values = List.copyOf(queues.values()); + queues.clear(); + int size = values.size(); + List current = Arrays.asList(new ExtLogRecord[size]); + // every queue must have at least one item in it + int i = 0; + for (Deque value : values) { + current.set(i++, value.removeFirst()); + } + return new Supplier() { + @Override + public ExtLogRecord get() { + ExtLogRecord min = null; + int minIdx = 0; + for (int i = 0; i < size; i++) { + ExtLogRecord item = current.get(i); + if (compareSeq(min, item) > 0) { + min = item; + minIdx = i; + } + } + if (min == null) { + return null; + } + current.set(minIdx, values.get(minIdx).pollFirst()); + return min; + } + + private int compareSeq(ExtLogRecord min, ExtLogRecord testItem) { + if (min == null) { + // null is greater than everything + return testItem == null ? 0 : 1; + } else if (testItem == null) { + return -1; + } else { + return Long.compareUnsigned(min.getSequenceNumber(), testItem.getSequenceNumber()); + } + } + }; + } + + @Override + public final void close() { + lock.lock(); + try { + if (!queues.isEmpty()) { + Formatter formatter = getFormatter(); + if (formatter == null) { + formatter = new PatternFormatter("%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c] (%t) %s%e%n"); + } + StandardOutputStreams.printError("The DelayedHandler was closed before any children handlers were " + + "configured. Messages will be written to stderr."); + // Always attempt to drain the queue + Supplier drain = drain(); + ExtLogRecord record; + while ((record = drain.get()) != null) { + StandardOutputStreams.printError(formatter.format(record)); + } + } + } finally { + lock.unlock(); + } + activated = false; + super.close(); + } + + /** + * {@inheritDoc} + *

+ * Note that once this is invoked the handler will be activated and the messages will no longer be queued. If more + * than one child handler is required the {@link #setHandlers(Handler[])} should be used. + *

+ * + * @see #setHandlers(Handler[]) + */ + @Override + public void addHandler(final Handler handler) { + super.addHandler(handler); + activate(); + } + + /** + * {@inheritDoc} + *

+ * Note that once this is invoked the handler will be activated and the messages will no longer be queued. + *

+ */ + @Override + public Handler[] setHandlers(final Handler[] newHandlers) { + final Handler[] result = super.setHandlers(newHandlers); + activate(); + return result; + } + + /** + * {@inheritDoc} + *

+ * Note that if the last child handler is removed the handler will no longer be activated and the messages will + * again be queued. + *

+ * + * @see #clearHandlers() + */ + @Override + public void removeHandler(final Handler handler) { + super.removeHandler(handler); + activated = handlers.length != 0; + } + + /** + * {@inheritDoc} + *

+ * Note that once this is invoked the handler will no longer be activated and messages will again be queued. + *

+ * + * @see #removeHandler(Handler) + */ + @Override + public Handler[] clearHandlers() { + activated = false; + return super.clearHandlers(); + } + + /** + * {@inheritDoc} + *

+ * This can be overridden to always require the caller calculation by setting the + * {@link #setCallerCalculationRequired(boolean)} value to {@code true}. + *

+ * + * @see #setCallerCalculationRequired(boolean) + */ + @Override + public boolean isCallerCalculationRequired() { + return callerCalculationRequired || super.isCallerCalculationRequired(); + } + + /** + * Sets whether or not {@linkplain ExtLogRecord#copyAll() caller information} will be required when formatting + * records. + *

+ * If set to {@code true} the {@linkplain ExtLogRecord#copyAll() caller information} will be calculated for each + * record that is placed in the queue. A value of {@code false} means the + * {@code super.isCallerCalculationRequired()} will be used. + *

+ *

+ * Note that the caller information is only attempted to be calculated when the handler has not been activated. Once + * activated it's up to the {@linkplain #getHandlers() children handlers} to determine how the record is processed. + *

+ * + * @param callerCalculationRequired {@code true} if the {@linkplain ExtLogRecord#copyAll() caller information} + * should always be calculated before the record is being placed in the queue + */ + public void setCallerCalculationRequired(final boolean callerCalculationRequired) { + this.callerCalculationRequired = callerCalculationRequired; + } + + /** + * Indicates whether or not this handler has been activated. + * + * @return {@code true} if the handler has been activated, otherwise {@code false} + */ + public final boolean isActivated() { + return activated; + } + + private void activate() { + lock.lock(); + try { + // Always attempt to drain the queue + ExtLogRecord record; + final LogContext logContext = this.logContext; + Supplier drain = drain(); + while ((record = drain.get()) != null) { + if (isEnabled() && isLoggable(record) + && (logContext == null || logContext.getLogger(record.getLoggerName()).isLoggable(record.getLevel()))) { + publishToNestedHandlers(record); + } + } + activated = true; + } finally { + lock.unlock(); + } + } +} \ No newline at end of file diff --git a/logging/src/main/java/org/xbib/logging/handlers/FileHandler.java b/logging/src/main/java/org/xbib/logging/handlers/FileHandler.java new file mode 100644 index 0000000..d008aa9 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/handlers/FileHandler.java @@ -0,0 +1,181 @@ +package org.xbib.logging.handlers; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.util.logging.Formatter; + +/** + * A simple file handler. + */ +public class FileHandler extends OutputStreamHandler { + + private File file; + private boolean append; + + /** + * Construct a new instance with no formatter and no output file. + */ + public FileHandler() { + } + + /** + * Construct a new instance with the given formatter and no output file. + * + * @param formatter the formatter + */ + public FileHandler(final Formatter formatter) { + super(formatter); + } + + /** + * Construct a new instance with the given formatter and output file. + * + * @param formatter the formatter + * @param file the file + * @throws FileNotFoundException if the file could not be found on open + */ + public FileHandler(final Formatter formatter, final File file) throws FileNotFoundException { + super(formatter); + setFile(file); + } + + /** + * Construct a new instance with the given formatter, output file, and append setting. + * + * @param formatter the formatter + * @param file the file + * @param append {@code true} to append, {@code false} to overwrite + * @throws FileNotFoundException if the file could not be found on open + */ + public FileHandler(final Formatter formatter, final File file, final boolean append) throws FileNotFoundException { + super(formatter); + this.append = append; + setFile(file); + } + + /** + * Construct a new instance with the given output file. + * + * @param file the file + * @throws FileNotFoundException if the file could not be found on open + */ + public FileHandler(final File file) throws FileNotFoundException { + setFile(file); + } + + /** + * Construct a new instance with the given output file and append setting. + * + * @param file the file + * @param append {@code true} to append, {@code false} to overwrite + * @throws FileNotFoundException if the file could not be found on open + */ + public FileHandler(final File file, final boolean append) throws FileNotFoundException { + this.append = append; + setFile(file); + } + + /** + * Construct a new instance with the given output file. + * + * @param fileName the file name + * @throws FileNotFoundException if the file could not be found on open + */ + public FileHandler(final String fileName) throws FileNotFoundException { + setFileName(fileName); + } + + /** + * Construct a new instance with the given output file and append setting. + * + * @param fileName the file name + * @param append {@code true} to append, {@code false} to overwrite + * @throws FileNotFoundException if the file could not be found on open + */ + public FileHandler(final String fileName, final boolean append) throws FileNotFoundException { + this.append = append; + setFileName(fileName); + } + + /** + * Specify whether to append to the target file. + * + * @param append {@code true} to append, {@code false} to overwrite + */ + public void setAppend(final boolean append) { + lock.lock(); + try { + this.append = append; + } finally { + lock.unlock(); + } + } + + /** + * Set the output file. + * + * @param file the file + * @throws FileNotFoundException if an error occurs opening the file + */ + public void setFile(File file) throws FileNotFoundException { + lock.lock(); + try { + if (file == null) { + this.file = null; + setOutputStream(null); + return; + } + final File parentFile = file.getParentFile(); + if (parentFile != null) { + parentFile.mkdirs(); + } + boolean ok = false; + final FileOutputStream fos = new FileOutputStream(file, append); + try { + final OutputStream bos = new BufferedOutputStream(fos); + try { + setOutputStream(bos); + this.file = file; + ok = true; + } finally { + if (!ok) { + safeClose(bos); + } + } + } finally { + if (!ok) { + safeClose(fos); + } + } + } finally { + lock.unlock(); + } + } + + /** + * Get the current output file. + * + * @return the file + */ + public File getFile() { + lock.lock(); + try { + return file; + } finally { + lock.unlock(); + } + } + + /** + * Set the output file by name. + * + * @param fileName the file name + * @throws FileNotFoundException if an error occurs opening the file + */ + public void setFileName(String fileName) throws FileNotFoundException { + setFile(fileName == null ? null : new File(fileName)); + } +} diff --git a/logging/src/main/java/org/xbib/logging/handlers/OutputStreamHandler.java b/logging/src/main/java/org/xbib/logging/handlers/OutputStreamHandler.java new file mode 100644 index 0000000..cd4ee80 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/handlers/OutputStreamHandler.java @@ -0,0 +1,135 @@ +package org.xbib.logging.handlers; + +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.Charset; +import java.util.logging.ErrorManager; +import java.util.logging.Formatter; +import org.xbib.logging.formatters.Formatters; +import org.xbib.logging.io.UncloseableOutputStream; +import org.xbib.logging.io.UninterruptibleOutputStream; + +/** + * An output stream handler which supports any {@code OutputStream}, using the specified encoding. If no encoding is + * specified, the platform default is used. + */ +public class OutputStreamHandler extends WriterHandler { + + private OutputStream outputStream; + + /** + * Construct a new instance with no formatter. + */ + public OutputStreamHandler() { + setFormatter(Formatters.nullFormatter()); + } + + /** + * Construct a new instance. + * + * @param formatter the formatter to use + */ + public OutputStreamHandler(final Formatter formatter) { + setFormatter(formatter); + } + + /** + * Construct a new instance. + * + * @param outputStream the output stream to use + * @param formatter the formatter to use + */ + public OutputStreamHandler(final OutputStream outputStream, final Formatter formatter) { + setFormatter(formatter); + setOutputStream(outputStream); + } + + @Override + protected void setCharsetPrivate(Charset charset) { + lock.lock(); + try { + super.setCharsetPrivate(charset); + // we only want to change the writer, not the output stream + final OutputStream outputStream = this.outputStream; + if (outputStream != null) { + super.setWriter(getNewWriter(outputStream)); + } + } finally { + lock.unlock(); + } + } + + public Charset getCharset() { + lock.lock(); + try { + return super.getCharset(); + } finally { + lock.unlock(); + } + } + + /** + * {@inheritDoc} Setting a writer will replace any target output stream. + */ + public void setWriter(final Writer writer) { + lock.lock(); + try { + super.setWriter(writer); + final OutputStream oldStream = this.outputStream; + outputStream = null; + safeFlush(oldStream); + safeClose(oldStream); + } finally { + lock.unlock(); + } + } + + /** + * Set the output stream to write to. The output stream will then belong to this handler; when the handler is + * closed or a new writer or output stream is set, this output stream will be closed. + * + * @param outputStream the new output stream or {@code null} for none + */ + public void setOutputStream(final OutputStream outputStream) { + if (outputStream == null) { + // call ours, not the superclass one + this.setWriter(null); + return; + } + // Close the writer, then close the old stream, then establish the new stream with a new writer. + try { + lock.lock(); + try { + final OutputStream oldStream = this.outputStream; + // do not close the old stream if creating the writer fails + final Writer writer = getNewWriter(outputStream); + try { + this.outputStream = outputStream; + super.setWriter(writer); + } finally { + safeFlush(oldStream); + safeClose(oldStream); + } + } finally { + lock.unlock(); + } + } catch (Exception e) { + reportError("Error opening output stream", e, ErrorManager.OPEN_FAILURE); + } + } + + OutputStream getOutputStream() { + assert lock.isHeldByCurrentThread(); + return outputStream; + } + + private Writer getNewWriter(OutputStream newOutputStream) { + if (newOutputStream == null) { + return null; + } + final UninterruptibleOutputStream outputStream = new UninterruptibleOutputStream( + new UncloseableOutputStream(newOutputStream)); + return new OutputStreamWriter(outputStream, getCharset()); + } +} diff --git a/logging/src/main/java/org/xbib/logging/handlers/PeriodicRotatingFileHandler.java b/logging/src/main/java/org/xbib/logging/handlers/PeriodicRotatingFileHandler.java new file mode 100644 index 0000000..6a9e22a --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/handlers/PeriodicRotatingFileHandler.java @@ -0,0 +1,293 @@ +package org.xbib.logging.handlers; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.time.Instant; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoField; +import java.time.temporal.ChronoUnit; +import java.util.TimeZone; +import java.util.logging.ErrorManager; +import org.xbib.logging.ExtLogRecord; +import org.xbib.logging.util.SuffixRotator; + +/** + * A file handler which rotates the log at a preset time interval. The interval is determined by the content of the + * suffix string which is passed in to {@link #setSuffix(String)}. + */ +public class PeriodicRotatingFileHandler extends FileHandler { + + private DateTimeFormatter format; + private String nextSuffix; + private Period period = Period.NEVER; + private Instant nextRollover = Instant.MAX; + private TimeZone timeZone = TimeZone.getDefault(); + private SuffixRotator suffixRotator = SuffixRotator.EMPTY; + + /** + * Construct a new instance with no formatter and no output file. + */ + public PeriodicRotatingFileHandler() { + } + + /** + * Construct a new instance with the given output file. + * + * @param fileName the file name + * @throws FileNotFoundException if the file could not be found on open + */ + public PeriodicRotatingFileHandler(final String fileName) throws FileNotFoundException { + super(fileName); + } + + /** + * Construct a new instance with the given output file and append setting. + * + * @param fileName the file name + * @param append {@code true} to append, {@code false} to overwrite + * @throws FileNotFoundException if the file could not be found on open + */ + public PeriodicRotatingFileHandler(final String fileName, final boolean append) throws FileNotFoundException { + super(fileName, append); + } + + /** + * Construct a new instance with the given output file. + * + * @param file the file + * @param suffix the format suffix to use + * @throws FileNotFoundException if the file could not be found on open + */ + public PeriodicRotatingFileHandler(final File file, final String suffix) throws FileNotFoundException { + super(file); + setSuffix(suffix); + } + + /** + * Construct a new instance with the given output file and append setting. + * + * @param file the file + * @param suffix the format suffix to use + * @param append {@code true} to append, {@code false} to overwrite + * @throws FileNotFoundException if the file could not be found on open + */ + public PeriodicRotatingFileHandler(final File file, final String suffix, final boolean append) + throws FileNotFoundException { + super(file, append); + setSuffix(suffix); + } + + @Override + public void setFile(final File file) throws FileNotFoundException { + lock.lock(); + try { + super.setFile(file); + if (format != null && file != null && file.lastModified() > 0) { + calcNextRollover(Instant.ofEpochMilli(file.lastModified())); + } + } finally { + lock.unlock(); + } + } + + /** + * {@inheritDoc} This implementation checks to see if the scheduled rollover time has yet occurred. + */ + protected void preWrite(final ExtLogRecord record) { + Instant recordInstant = record.getInstant(); + if (!recordInstant.isBefore(nextRollover)) { + rollOver(); + calcNextRollover(recordInstant); + } + } + + /** + * Set the suffix string. The string is in a format which can be understood by {@link DateTimeFormatter}. + * The period of the rotation is automatically calculated based on the suffix. + *

+ * If the suffix ends with {@code .gz} or {@code .zip} the file will be compressed on rotation. + *

+ * + * @param suffix the suffix + * @throws IllegalArgumentException if the suffix is not valid + */ + public void setSuffix(String suffix) throws IllegalArgumentException { + final SuffixRotator suffixRotator = SuffixRotator.parse(suffix); + final String dateSuffix = suffixRotator.getDatePattern(); + final DateTimeFormatter format = DateTimeFormatter.ofPattern(dateSuffix).withZone(timeZone.toZoneId()); + final int len = dateSuffix.length(); + Period period = Period.NEVER; + for (int i = 0; i < len; i++) { + switch (dateSuffix.charAt(i)) { + case 'y': + period = min(period, Period.YEAR); + break; + case 'M': + period = min(period, Period.MONTH); + break; + case 'w': + case 'W': + period = min(period, Period.WEEK); + break; + case 'D': + case 'd': + case 'F': + case 'E': + period = min(period, Period.DAY); + break; + case 'a': + period = min(period, Period.HALF_DAY); + break; + case 'H': + case 'k': + case 'K': + case 'h': + period = min(period, Period.HOUR); + break; + case 'm': + period = min(period, Period.MINUTE); + break; + case '\'': + while (dateSuffix.charAt(++i) != '\'') + ; + break; + case 's': + case 'S': + throw new IllegalArgumentException("Rotating by second or millisecond is not supported"); + } + } + lock.lock(); + try { + this.format = format; + this.period = period; + this.suffixRotator = suffixRotator; + final Instant now; + final File file = getFile(); + if (file != null && file.lastModified() > 0) { + now = Instant.ofEpochMilli(file.lastModified()); + } else { + now = Instant.now(); + } + calcNextRollover(now); + } finally { + lock.unlock(); + } + } + + /** + * Returns the suffix to be used. + * + * @return the suffix to be used + */ + protected final String getNextSuffix() { + return nextSuffix; + } + + /** + * Returns the file rotator for this handler. + * + * @return the file rotator + */ + SuffixRotator getSuffixRotator() { + return suffixRotator; + } + + private void rollOver() { + try { + final File file = getFile(); + if (file == null) { + // no file is set; a direct output stream or writer was specified + return; + } + // first, close the original file (some OSes won't let you move/rename a file that is open) + setFileInternal(null); + // next, rotate it + suffixRotator.rotate(getErrorManager(), file.toPath(), nextSuffix); + // start new file + setFileInternal(file); + } catch (IOException e) { + reportError("Unable to rotate log file", e, ErrorManager.OPEN_FAILURE); + } + } + + private void calcNextRollover(final Instant fromTime) { + if (period == Period.NEVER) { + nextRollover = Instant.MAX; + return; + } + ZonedDateTime zdt = ZonedDateTime.ofInstant(fromTime, timeZone.toZoneId()); + nextSuffix = zdt.format(format); + final Period period = this.period; + // round up to the next field depending on the period + switch (period) { + default: + case YEAR: + zdt = zdt.withDayOfYear(1).truncatedTo(ChronoUnit.DAYS).plusYears(1); + break; + case MONTH: + zdt = zdt.withDayOfMonth(1).truncatedTo(ChronoUnit.DAYS).plusMonths(1); + break; + case WEEK: + zdt = zdt.with(ChronoField.DAY_OF_WEEK, 1).truncatedTo(ChronoUnit.DAYS).plusWeeks(1); + break; + case DAY: + zdt = zdt.truncatedTo(ChronoUnit.DAYS).plusDays(1); + break; + case HALF_DAY: + zdt = zdt.truncatedTo(ChronoUnit.HALF_DAYS).plusHours(12); + break; + case HOUR: + zdt = zdt.truncatedTo(ChronoUnit.HOURS).plusHours(1); + break; + case MINUTE: + zdt = zdt.truncatedTo(ChronoUnit.MINUTES).plusMinutes(1); + break; + } + nextRollover = zdt.toInstant(); + } + + /** + * Get the configured time zone for this handler. + * + * @return the configured time zone + */ + public TimeZone getTimeZone() { + return timeZone; + } + + /** + * Set the configured time zone for this handler. + * + * @param timeZone the configured time zone + */ + public void setTimeZone(final TimeZone timeZone) { + if (timeZone == null) { + throw new NullPointerException("timeZone is null"); + } + this.timeZone = timeZone; + } + + private void setFileInternal(final File file) throws FileNotFoundException { + super.setFile(file); + } + + private static > T min(T a, T b) { + return a.compareTo(b) <= 0 ? a : b; + } + + /** + * Possible period values. Keep in strictly ascending order of magnitude. + */ + public enum Period { + MINUTE, + HOUR, + HALF_DAY, + DAY, + WEEK, + MONTH, + YEAR, + NEVER, + } +} diff --git a/logging/src/main/java/org/xbib/logging/handlers/PeriodicSizeRotatingFileHandler.java b/logging/src/main/java/org/xbib/logging/handlers/PeriodicSizeRotatingFileHandler.java new file mode 100644 index 0000000..cb56196 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/handlers/PeriodicSizeRotatingFileHandler.java @@ -0,0 +1,235 @@ +package org.xbib.logging.handlers; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStream; +import java.util.logging.ErrorManager; +import org.xbib.logging.ExtLogRecord; +import org.xbib.logging.io.CountingOutputStream; +import org.xbib.logging.util.SuffixRotator; + +/** + * A file handler which rotates the log at a preset time interval or the size of the log. + *

+ * The time interval is determined by the content of the suffix string which is passed in to {@link + * #setSuffix(String)}. + *

+ * The size interval is determined by the value passed in the {@link #setRotateSize(long)}. + */ +public class PeriodicSizeRotatingFileHandler extends PeriodicRotatingFileHandler { + // by default, rotate at 10MB + private long rotateSize = 0xa00000L; + private int maxBackupIndex = 1; + private CountingOutputStream outputStream; + private boolean rotateOnBoot; + + /** + * Default constructor. + */ + public PeriodicSizeRotatingFileHandler() { + super(); + } + + /** + * Construct a new instance with the given output file. + * + * @param fileName the file name + * @throws FileNotFoundException if the file could not be found on open + */ + public PeriodicSizeRotatingFileHandler(final String fileName) throws FileNotFoundException { + super(fileName); + } + + /** + * Construct a new instance with the given output file and append setting. + * + * @param fileName the file name + * @param append {@code true} to append, {@code false} to overwrite + * @throws FileNotFoundException if the file could not be found on open + */ + public PeriodicSizeRotatingFileHandler(final String fileName, final boolean append) throws FileNotFoundException { + super(fileName, append); + } + + /** + * Construct a new instance with the given output file. + * + * @param file the file + * @param suffix the format suffix to use + * @throws FileNotFoundException if the file could not be found on open + */ + public PeriodicSizeRotatingFileHandler(final File file, final String suffix) throws FileNotFoundException { + super(file, suffix); + } + + /** + * Construct a new instance with the given output file and append setting. + * + * @param file the file + * @param suffix the format suffix to use + * @param append {@code true} to append, {@code false} to overwrite + * @throws FileNotFoundException if the file could not be found on open + */ + public PeriodicSizeRotatingFileHandler(final File file, final String suffix, final boolean append) + throws FileNotFoundException { + super(file, suffix, append); + } + + /** + * Construct a new instance with the given output file. + * + * @param file the file + * @param suffix the format suffix to use + * @param rotateSize the size the file should rotate at + * @param maxBackupIndex the maximum number of files to backup + * @throws FileNotFoundException if the file could not be found on open + */ + public PeriodicSizeRotatingFileHandler(final File file, final String suffix, final long rotateSize, + final int maxBackupIndex) throws FileNotFoundException { + super(file, suffix); + this.rotateSize = rotateSize; + this.maxBackupIndex = maxBackupIndex; + } + + /** + * Construct a new instance with the given output file. + * + * @param file the file + * @param suffix the format suffix to use + * @param rotateSize the size the file should rotate at + * @param maxBackupIndex the maximum number of files to backup + * @param append {@code true} to append, {@code false} to overwrite + * @throws FileNotFoundException if the file could not be found on open + */ + public PeriodicSizeRotatingFileHandler(final File file, final String suffix, final long rotateSize, + final int maxBackupIndex, final boolean append) throws FileNotFoundException { + super(file, suffix, append); + this.rotateSize = rotateSize; + this.maxBackupIndex = maxBackupIndex; + } + + @Override + public void setOutputStream(final OutputStream outputStream) { + lock.lock(); + try { + this.outputStream = outputStream == null ? null : new CountingOutputStream(outputStream); + super.setOutputStream(this.outputStream); + } finally { + lock.unlock(); + } + } + + /** + * {@inheritDoc} + * + * @throws RuntimeException if there is an attempt to rotate file and the rotation fails + */ + @Override + public void setFile(final File file) throws FileNotFoundException { + lock.lock(); + try { + // Check for a rotate + if (rotateOnBoot && maxBackupIndex > 0 && file != null && file.exists() && file.length() > 0L) { + final String suffix = getNextSuffix(); + final SuffixRotator suffixRotator = getSuffixRotator(); + if (suffixRotator != SuffixRotator.EMPTY && suffix != null) { + // Make sure any previous files are closed before we attempt to rotate + setFileInternal(null); + suffixRotator.rotate(getErrorManager(), file.toPath(), suffix, maxBackupIndex); + } + } + setFileInternal(file); + } finally { + lock.unlock(); + } + } + + /** + * Indicates whether or a not the handler should rotate the file before the first log record is written. + * + * @return {@code true} if file should rotate on boot, otherwise {@code false}/ + */ + public boolean isRotateOnBoot() { + lock.lock(); + try { + return rotateOnBoot; + } finally { + lock.unlock(); + } + } + + /** + * Set to a value of {@code true} if the file should be rotated before the a new file is set. The rotation only + * happens if the file names are the same and the file has a {@link File#length() length} greater than 0. + * + * @param rotateOnBoot {@code true} to rotate on boot, otherwise {@code false} + */ + public void setRotateOnBoot(final boolean rotateOnBoot) { + lock.lock(); + try { + this.rotateOnBoot = rotateOnBoot; + } finally { + lock.unlock(); + } + } + + /** + * Set the rotation size, in bytes. + * + * @param rotateSize the number of bytes before the log is rotated + */ + public void setRotateSize(final long rotateSize) { + lock.lock(); + try { + this.rotateSize = rotateSize; + } finally { + lock.unlock(); + } + } + + /** + * Set the maximum backup index (the number of log files to keep around). + * + * @param maxBackupIndex the maximum backup index + */ + public void setMaxBackupIndex(final int maxBackupIndex) { + lock.lock(); + try { + this.maxBackupIndex = maxBackupIndex; + } finally { + lock.unlock(); + } + } + + @Override + protected void preWrite(final ExtLogRecord record) { + super.preWrite(record); + final int maxBackupIndex = this.maxBackupIndex; + final long currentSize = (outputStream == null ? Long.MIN_VALUE : outputStream.currentSize); + if (currentSize > rotateSize && maxBackupIndex > 0) { + try { + final File file = getFile(); + if (file == null) { + // no file is set; a direct output stream or writer was specified + return; + } + // close the old file. + setFileInternal(null); + getSuffixRotator().rotate(getErrorManager(), file.toPath(), getNextSuffix(), + maxBackupIndex); + // start with new file. + setFileInternal(file); + } catch (IOException e) { + reportError("Unable to rotate log file", e, ErrorManager.OPEN_FAILURE); + } + } + } + + private void setFileInternal(final File file) throws FileNotFoundException { + super.setFile(file); + if (outputStream != null) { + outputStream.currentSize = file == null ? 0L : file.length(); + } + } +} diff --git a/logging/src/main/java/org/xbib/logging/handlers/QueueHandler.java b/logging/src/main/java/org/xbib/logging/handlers/QueueHandler.java new file mode 100644 index 0000000..0bf1b60 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/handlers/QueueHandler.java @@ -0,0 +1,191 @@ +package org.xbib.logging.handlers; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.logging.ErrorManager; +import java.util.logging.Formatter; +import java.util.logging.Handler; +import java.util.logging.LogRecord; +import org.xbib.logging.ExtHandler; +import org.xbib.logging.ExtLogRecord; + +/** + * A queue handler which retains the last few messages logged. The handler can be used as-is to remember recent + * messages, or one or more handlers may be nested, which allows this handler to "replay" messages to the child + * handler(s) upon request. + */ +public class QueueHandler extends ExtHandler { + private final Deque buffer = new ArrayDeque<>(); + private int limit = 10; + + /** + * Construct a new instance with a default queue length. + */ + public QueueHandler() { + } + + /** + * Construct a new instance. + * + * @param limit the queue length to use + */ + public QueueHandler(final int limit) { + if (limit < 1) { + throw badQueueLength(); + } + this.limit = limit; + } + + public void publish(final ExtLogRecord record) { + if (isEnabled() && record != null) { + doPublish(record); + } + } + + public void publish(final LogRecord record) { + if (isEnabled() && record != null) { + doPublish(ExtLogRecord.wrap(record)); + } + } + + protected void doPublish(final ExtLogRecord record) { + lock.lock(); + try { + if (isLoggable(record)) { + // Determine if we need to calculate the caller information before we queue the record + if (isCallerCalculationRequired()) { + // prepare record to move to another thread + record.copyAll(); + } else { + // Disable the caller calculation since it's been determined we won't be using it + record.disableCallerCalculation(); + // Copy the MDC over + record.copyMdc(); + } + if (buffer.size() == limit) { + buffer.removeFirst(); + } + buffer.addLast(record); + } + publishToNestedHandlers(record); + } finally { + lock.unlock(); + } + } + + /** + * Get the queue length limit. This is the number of messages that will be saved before old messages roll off + * of the queue. + * + * @return the queue length limit + */ + public int getLimit() { + lock.lock(); + try { + return limit; + } finally { + lock.unlock(); + } + } + + /** + * Set the queue length limit. This is the number of messages that will be saved before old messages roll off + * of the queue. + * + * @param limit the queue length limit + */ + public void setLimit(final int limit) { + if (limit < 1) { + throw badQueueLength(); + } + lock.lock(); + try { + this.limit = limit; + } finally { + lock.unlock(); + } + } + + @Override + public void addHandler(Handler handler) { + addHandler(handler, false); + } + + /** + * Add the given handler, optionally atomically replaying the queue, allowing the delegate handler to receive + * all queued messages as well as all subsequent messages with no loss or reorder in between. + * + * @param handler the handler to add (must not be {@code null}) + * @param replay {@code true} to replay the prior messages, or {@code false} to add the handler without replaying + */ + public void addHandler(Handler handler, boolean replay) { + if (replay) { + lock.lock(); + try { + super.addHandler(handler); + for (ExtLogRecord record : buffer) { + handler.publish(record); + } + } finally { + lock.unlock(); + } + } else { + super.addHandler(handler); + } + } + + /** + * Get a copy of the queue as it is at an exact moment in time. + * + * @return the copy of the queue + */ + public ExtLogRecord[] getQueue() { + lock.lock(); + try { + return buffer.toArray(ExtLogRecord[]::new); + } finally { + lock.unlock(); + } + } + + /** + * Get a copy of the queue, rendering each record as a string. + * + * @return the copy of the queue rendered as strings + */ + public String[] getQueueAsStrings() { + final ExtLogRecord[] queue = getQueue(); + final int length = queue.length; + final String[] strings = new String[length]; + final Formatter formatter = getFormatter(); + for (int i = 0, j = 0; j < length; j++) { + final String formatted; + try { + formatted = formatter.format(queue[j]); + if (!formatted.isEmpty()) { + strings[i++] = getFormatter().format(queue[j]); + } + } catch (Exception ex) { + reportError("Formatting error", ex, ErrorManager.FORMAT_FAILURE); + } + } + return strings; + } + + /** + * Replay the stored queue to the nested handlers. + */ + public void replay() { + final Handler[] handlers = getHandlers(); + if (handlers.length > 0) + for (ExtLogRecord record : getQueue()) { + for (Handler handler : handlers) { + handler.publish(record); + } + } + } + + private static IllegalArgumentException badQueueLength() { + return new IllegalArgumentException("Queue length must be at least 1"); + } +} diff --git a/logging/src/main/java/org/xbib/logging/handlers/SizeRotatingFileHandler.java b/logging/src/main/java/org/xbib/logging/handlers/SizeRotatingFileHandler.java new file mode 100644 index 0000000..48d00cc --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/handlers/SizeRotatingFileHandler.java @@ -0,0 +1,263 @@ +package org.xbib.logging.handlers; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStream; +import java.util.logging.ErrorManager; +import org.xbib.logging.ExtLogRecord; +import org.xbib.logging.io.CountingOutputStream; +import org.xbib.logging.util.SuffixRotator; + +public class SizeRotatingFileHandler extends FileHandler { + + // by default, rotate at 10MB + private long rotateSize = 0xa00000L; + private int maxBackupIndex = 1; + private CountingOutputStream outputStream; + private boolean rotateOnBoot; + private SuffixRotator suffixRotator = SuffixRotator.EMPTY; + + /** + * Construct a new instance with no formatter and no output file. + */ + public SizeRotatingFileHandler() { + } + + /** + * Construct a new instance with the given output file. + * + * @param file the file + * @throws FileNotFoundException if the file could not be found on open + */ + public SizeRotatingFileHandler(final File file) throws FileNotFoundException { + super(file); + } + + /** + * Construct a new instance with the given output file and append setting. + * + * @param file the file + * @param append {@code true} to append, {@code false} to overwrite + * @throws FileNotFoundException if the file could not be found on open + */ + public SizeRotatingFileHandler(final File file, final boolean append) throws FileNotFoundException { + super(file, append); + } + + /** + * Construct a new instance with the given output file. + * + * @param fileName the file name + * @throws FileNotFoundException if the file could not be found on open + */ + public SizeRotatingFileHandler(final String fileName) throws FileNotFoundException { + super(fileName); + } + + /** + * Construct a new instance with the given output file and append setting. + * + * @param fileName the file name + * @param append {@code true} to append, {@code false} to overwrite + * @throws FileNotFoundException if the file could not be found on open + */ + public SizeRotatingFileHandler(final String fileName, final boolean append) throws FileNotFoundException { + super(fileName, append); + } + + /** + * Construct a new instance with no formatter and no output file. + */ + public SizeRotatingFileHandler(final long rotateSize, final int maxBackupIndex) { + this.rotateSize = rotateSize; + this.maxBackupIndex = maxBackupIndex; + } + + /** + * Construct a new instance with the given output file. + * + * @param file the file + * @throws FileNotFoundException if the file could not be found on open + */ + public SizeRotatingFileHandler(final File file, final long rotateSize, final int maxBackupIndex) + throws FileNotFoundException { + super(file); + this.rotateSize = rotateSize; + this.maxBackupIndex = maxBackupIndex; + } + + /** + * Construct a new instance with the given output file and append setting. + * + * @param file the file + * @param append {@code true} to append, {@code false} to overwrite + * @throws FileNotFoundException if the file could not be found on open + */ + public SizeRotatingFileHandler(final File file, final boolean append, final long rotateSize, final int maxBackupIndex) + throws FileNotFoundException { + super(file, append); + this.rotateSize = rotateSize; + this.maxBackupIndex = maxBackupIndex; + } + + /** + * {@inheritDoc} + */ + public void setOutputStream(final OutputStream outputStream) { + lock.lock(); + try { + this.outputStream = outputStream == null ? null : new CountingOutputStream(outputStream); + super.setOutputStream(this.outputStream); + } finally { + lock.unlock(); + } + } + + /** + * {@inheritDoc} + * + * @throws RuntimeException if there is an attempt to rotate file and the rotation fails + */ + public void setFile(final File file) throws FileNotFoundException { + lock.lock(); + try { + // Check for a rotate + if (rotateOnBoot && maxBackupIndex > 0 && file != null && file.exists() && file.length() > 0L) { + // Make sure any previous files are closed before we attempt to rotate + setFileInternal(null); + suffixRotator.rotate(getErrorManager(), file.toPath(), maxBackupIndex); + } + setFileInternal(file); + } finally { + lock.unlock(); + } + } + + /** + * Indicates whether or a not the handler should rotate the file before the first log record is written. + * + * @return {@code true} if file should rotate on boot, otherwise {@code false}/ + */ + public boolean isRotateOnBoot() { + lock.lock(); + try { + return rotateOnBoot; + } finally { + lock.unlock(); + } + } + + /** + * Set to a value of {@code true} if the file should be rotated before the a new file is set. The rotation only + * happens if the file names are the same and the file has a {@link File#length() length} greater than 0. + * + * @param rotateOnBoot {@code true} to rotate on boot, otherwise {@code false} + */ + public void setRotateOnBoot(final boolean rotateOnBoot) { + lock.lock(); + try { + this.rotateOnBoot = rotateOnBoot; + } finally { + lock.unlock(); + } + } + + /** + * Set the rotation size, in bytes. + * + * @param rotateSize the number of bytes before the log is rotated + */ + public void setRotateSize(final long rotateSize) { + lock.lock(); + try { + this.rotateSize = rotateSize; + } finally { + lock.unlock(); + } + } + + /** + * Set the maximum backup index (the number of log files to keep around). + * + * @param maxBackupIndex the maximum backup index + */ + public void setMaxBackupIndex(final int maxBackupIndex) { + lock.lock(); + try { + this.maxBackupIndex = maxBackupIndex; + } finally { + lock.unlock(); + } + } + + /** + * Returns the suffix set to be appended to files during rotation. + * + * @return the suffix or {@code null} if no suffix should be used + */ + public String getSuffix() { + if (suffixRotator == SuffixRotator.EMPTY) { + return null; + } + return suffixRotator.toString(); + } + + /** + * Sets the suffix to be appended to the file name during the file rotation. The suffix does not play a role in + * determining when the file should be rotated. + *

+ * The suffix must be a string understood by the {@link java.text.SimpleDateFormat}. Optionally the suffix can end + * with {@code .gz} or {@code .zip} which will compress the file on rotation. + *

+ *

+ * If the suffix ends with {@code .gz} or {@code .zip} the file will be compressed on rotation. + *

+ *

+ * Note: The {@link #setMaxBackupIndex(int) maxBackupIndex} only takes into account files rotated with same + * suffix. For example if the suffix pattern is {@code .yyyy-MM-dd} and the size rotation is reached only files + * with the same date suffix will be purged. A file the day before or after will not be purged. + *

+ * + * @param suffix the suffix to place after the filename when the file is rotated + */ + public void setSuffix(final String suffix) { + lock.lock(); + try { + this.suffixRotator = SuffixRotator.parse(suffix); + } finally { + lock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + protected void preWrite(final ExtLogRecord record) { + final int maxBackupIndex = this.maxBackupIndex; + final long currentSize = (outputStream == null ? Long.MIN_VALUE : outputStream.currentSize); + if (currentSize > rotateSize && maxBackupIndex > 0) { + try { + final File file = getFile(); + if (file == null) { + // no file is set; a direct output stream or writer was specified + return; + } + // close the old file. + setFileInternal(null); + suffixRotator.rotate(getErrorManager(), file.toPath(), maxBackupIndex); + // start with new file. + setFileInternal(file); + } catch (IOException e) { + reportError("Unable to rotate log file", e, ErrorManager.OPEN_FAILURE); + } + } + } + + private void setFileInternal(final File file) throws FileNotFoundException { + super.setFile(file); + if (outputStream != null) { + outputStream.currentSize = file == null ? 0L : file.length(); + } + } +} diff --git a/logging/src/main/java/org/xbib/logging/handlers/SocketHandler.java b/logging/src/main/java/org/xbib/logging/handlers/SocketHandler.java new file mode 100644 index 0000000..fce6175 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/handlers/SocketHandler.java @@ -0,0 +1,514 @@ +package org.xbib.logging.handlers; + +import java.io.Closeable; +import java.io.Flushable; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.logging.ErrorManager; +import java.util.logging.Formatter; +import javax.net.SocketFactory; +import javax.net.ssl.SSLSocketFactory; +import org.xbib.logging.ExtHandler; +import org.xbib.logging.ExtLogRecord; +import org.xbib.logging.io.TcpOutputStream; +import org.xbib.logging.io.UdpOutputStream; +import org.xbib.logging.io.UninterruptibleOutputStream; +import org.xbib.logging.net.ClientSocketFactory; + +/** + * A handler used to communicate over a socket. + */ +@SuppressWarnings({"unused", "WeakerAccess"}) +public class SocketHandler extends ExtHandler { + + /** + * The type of socket + */ + public enum Protocol { + TCP, + UDP, + SSL_TCP, + } + + @SuppressWarnings("WeakerAccess") + public static final int DEFAULT_PORT = 4560; + + // All the following fields are guarded by outputLock + private ClientSocketFactory clientSocketFactory; + private SocketFactory socketFactory; + private InetAddress address; + private int port; + private Protocol protocol; + private boolean blockOnReconnect; + private Writer writer; + private boolean initialize; + + /** + * Creates a socket handler with an address of {@linkplain InetAddress#getLocalHost() localhost} and port + * of {@linkplain #DEFAULT_PORT 4560}. + * + * @throws UnknownHostException if an error occurs attempting to retrieve the localhost + */ + public SocketHandler() throws UnknownHostException { + this(InetAddress.getLocalHost(), DEFAULT_PORT); + } + + /** + * Creates a socket handler. + * + * @param hostname the hostname to connect to + * @param port the port to connect to + * @throws UnknownHostException if an error occurs resolving the address + */ + public SocketHandler(final String hostname, final int port) throws UnknownHostException { + this(InetAddress.getByName(hostname), port); + } + + /** + * Creates a socket handler. + * + * @param address the address to connect to + * @param port the port to connect to + */ + public SocketHandler(final InetAddress address, final int port) { + this(Protocol.TCP, address, port); + } + + /** + * Creates a socket handler. + * + * @param protocol the protocol to connect with + * @param hostname the hostname to connect to + * @param port the port to connect to + * @throws UnknownHostException if an error occurs resolving the hostname + */ + public SocketHandler(final Protocol protocol, final String hostname, final int port) throws UnknownHostException { + this(protocol, InetAddress.getByName(hostname), port); + } + + /** + * Creates a socket handler. + * + * @param protocol the protocol to connect with + * @param address the address to connect to + * @param port the port to connect to + */ + public SocketHandler(final Protocol protocol, final InetAddress address, final int port) { + this(null, protocol, address, port); + } + + /** + * Creates a socket handler. + * + * @param socketFactory the socket factory to use for creating {@linkplain Protocol#TCP TCP} or + * {@linkplain Protocol#SSL_TCP SSL TCP} connections, if {@code null} a default factory will + * be used + * @param protocol the protocol to connect with + * @param hostname the hostname to connect to + * @param port the port to connect to + * @throws UnknownHostException if an error occurs resolving the hostname + * @see #SocketHandler(ClientSocketFactory, Protocol) + */ + public SocketHandler(final SocketFactory socketFactory, final Protocol protocol, final String hostname, final int port) + throws UnknownHostException { + this(socketFactory, protocol, InetAddress.getByName(hostname), port); + } + + /** + * Creates a socket handler. + * + * @param socketFactory the socket factory to use for creating {@linkplain Protocol#TCP TCP} or + * {@linkplain Protocol#SSL_TCP SSL TCP} connections, if {@code null} a default factory will + * be used + * @param protocol the protocol to connect with + * @param address the address to connect to + * @param port the port to connect to + * @see #SocketHandler(ClientSocketFactory, Protocol) + */ + public SocketHandler(final SocketFactory socketFactory, final Protocol protocol, final InetAddress address, + final int port) { + this.socketFactory = socketFactory; + this.clientSocketFactory = null; + this.address = address; + this.port = port; + this.protocol = (protocol == null ? Protocol.TCP : protocol); + initialize = true; + writer = null; + blockOnReconnect = false; + } + + /** + * Creates a socket handler. + * + * @param clientSocketFactory the client socket factory used to create sockets + * @param protocol the protocol to connect with + */ + public SocketHandler(final ClientSocketFactory clientSocketFactory, final Protocol protocol) { + this.clientSocketFactory = clientSocketFactory; + if (clientSocketFactory != null) { + address = clientSocketFactory.getAddress(); + port = clientSocketFactory.getPort(); + } + this.protocol = (protocol == null ? Protocol.TCP : protocol); + initialize = true; + writer = null; + blockOnReconnect = false; + } + + @Override + protected void doPublish(final ExtLogRecord record) { + final String formatted; + final Formatter formatter = getFormatter(); + try { + formatted = formatter.format(record); + } catch (Exception e) { + reportError("Could not format message", e, ErrorManager.FORMAT_FAILURE); + return; + } + if (formatted.isEmpty()) { + // nothing to write; move along + return; + } + try { + lock.lock(); + try { + if (initialize) { + initialize(); + initialize = false; + } + if (writer == null) { + return; + } + writer.write(formatted); + super.doPublish(record); + } finally { + lock.unlock(); + } + } catch (Exception e) { + reportError("Error writing log message", e, ErrorManager.WRITE_FAILURE); + } + } + + @Override + public void flush() { + lock.lock(); + try { + safeFlush(writer); + } finally { + lock.unlock(); + } + super.flush(); + } + + @Override + public void close() { + lock.lock(); + try { + safeClose(writer); + writer = null; + initialize = true; + } finally { + lock.unlock(); + } + super.close(); + } + + /** + * Returns the address being used. + * + * @return the address + */ + public InetAddress getAddress() { + return address; + } + + /** + * Sets the address to connect to. + *

+ * Note that is resets the {@linkplain #setClientSocketFactory(ClientSocketFactory) client socket factory}. + *

+ * + * @param address the address + */ + public void setAddress(final InetAddress address) { + lock.lock(); + try { + if (!this.address.equals(address)) { + initialize = true; + clientSocketFactory = null; + } + this.address = address; + } finally { + lock.unlock(); + } + } + + /** + * Sets the address to connect to by doing a lookup on the hostname. + *

+ * Note that is resets the {@linkplain #setClientSocketFactory(ClientSocketFactory) client socket factory}. + *

+ * + * @param hostname the host name used to resolve the address + * @throws UnknownHostException if an error occurs resolving the address + */ + public void setHostname(final String hostname) throws UnknownHostException { + setAddress(InetAddress.getByName(hostname)); + } + + /** + * Indicates whether or not the output stream is set to block when attempting to reconnect a TCP connection. + * + * @return {@code true} if blocking is enabled, otherwise {@code false} + */ + public boolean isBlockOnReconnect() { + lock.lock(); + try { + return blockOnReconnect; + } finally { + lock.unlock(); + } + } + + /** + * Enables or disables blocking when attempting to reconnect the socket when using a {@linkplain Protocol#TCP TCP} + * or {@linkplain Protocol#SSL_TCP SSL TCP} connections. + *

+ * If set to {@code true} the {@code write} methods will block when attempting to reconnect. This is only advisable + * to be set to {@code true} if using an asynchronous handler. + * + * @param blockOnReconnect {@code true} to block when reconnecting or {@code false} to reconnect asynchronously + * discarding any new messages coming in + */ + public void setBlockOnReconnect(final boolean blockOnReconnect) { + lock.lock(); + try { + this.blockOnReconnect = blockOnReconnect; + initialize = true; + } finally { + lock.unlock(); + } + } + + /** + * Returns the protocol being used. + * + * @return the protocol + */ + public Protocol getProtocol() { + return protocol; + } + + /** + * Sets the protocol to use. If the value is {@code null} the protocol will be set to + * {@linkplain Protocol#TCP TCP}. + *

+ * Note that is resets the {@linkplain #setSocketFactory(SocketFactory) socket factory} if it was previously set. + *

+ * + * @param protocol the protocol to use + */ + public void setProtocol(final Protocol protocol) { + lock.lock(); + try { + if (protocol == null) { + this.protocol = Protocol.TCP; + } + if (this.protocol != protocol) { + socketFactory = null; + initialize = true; + } + this.protocol = protocol; + } finally { + lock.unlock(); + } + } + + /** + * Returns the port being used. + * + * @return the port + */ + public int getPort() { + return port; + } + + /** + * Sets the port to connect to. + *

+ * Note that is resets the {@linkplain #setClientSocketFactory(ClientSocketFactory) client socket factory}. + *

+ * + * @param port the port + */ + public void setPort(final int port) { + lock.lock(); + try { + if (this.port != port) { + initialize = true; + clientSocketFactory = null; + } + this.port = port; + } finally { + lock.unlock(); + } + } + + /** + * Sets the socket factory to use for creating {@linkplain Protocol#TCP TCP} or {@linkplain Protocol#SSL_TCP SSL} + * connections. + *

+ * Note that if the {@linkplain #setProtocol(Protocol) protocol} is set the socket factory will be set to + * {@code null} and reset. Setting a value here also resets the + * {@linkplain #setClientSocketFactory(ClientSocketFactory) client socket factory}. + *

+ * + * @param socketFactory the socket factory + * @see #setClientSocketFactory(ClientSocketFactory) + */ + public void setSocketFactory(final SocketFactory socketFactory) { + lock.lock(); + try { + this.socketFactory = socketFactory; + this.clientSocketFactory = null; + initialize = true; + } finally { + lock.unlock(); + } + } + + /** + * Sets the client socket factory used to create sockets. If {@code null} the + * {@linkplain #setAddress(InetAddress) address} and {@linkplain #setPort(int) port} are required to be set. + * + * @param clientSocketFactory the client socket factory to use + */ + public void setClientSocketFactory(final ClientSocketFactory clientSocketFactory) { + lock.lock(); + try { + this.clientSocketFactory = clientSocketFactory; + initialize = true; + } finally { + lock.unlock(); + } + } + + private void initialize() { + final Writer current = this.writer; + boolean okay = false; + try { + if (current != null) { + writeTail(current); + safeFlush(current); + } + // Close the current writer before we attempt to create a new connection + safeClose(current); + final OutputStream out = createOutputStream(); + if (out == null) { + return; + } + final String encoding = getEncoding(); + final UninterruptibleOutputStream outputStream = new UninterruptibleOutputStream(out); + if (encoding == null) { + writer = new OutputStreamWriter(outputStream); + } else { + writer = new OutputStreamWriter(outputStream, encoding); + } + writeHead(writer); + okay = true; + } catch (UnsupportedEncodingException e) { + reportError("Error opening", e, ErrorManager.OPEN_FAILURE); + } finally { + safeClose(current); + if (!okay) { + safeClose(writer); + } + } + + } + + private OutputStream createOutputStream() { + if (address != null || port >= 0) { + try { + final ClientSocketFactory socketFactory = getClientSocketFactory(); + if (protocol == Protocol.UDP) { + return new UdpOutputStream(socketFactory); + } + return new TcpOutputStream(socketFactory, blockOnReconnect); + } catch (IOException e) { + reportError("Failed to create socket output stream", e, ErrorManager.OPEN_FAILURE); + } + } + return null; + } + + private ClientSocketFactory getClientSocketFactory() { + lock.lock(); + try { + if (clientSocketFactory != null) { + return clientSocketFactory; + } + if (address == null || port <= 0) { + throw new IllegalStateException("An address and port greater than 0 is required."); + } + final ClientSocketFactory clientSocketFactory; + if (socketFactory == null) { + if (protocol == Protocol.SSL_TCP) { + clientSocketFactory = ClientSocketFactory.of(SSLSocketFactory.getDefault(), address, port); + } else { + clientSocketFactory = ClientSocketFactory.of(address, port); + } + } else { + clientSocketFactory = ClientSocketFactory.of(socketFactory, address, port); + } + return clientSocketFactory; + } finally { + lock.unlock(); + } + } + + private void writeHead(final Writer writer) { + try { + final Formatter formatter = getFormatter(); + if (formatter != null) + writer.write(formatter.getHead(this)); + } catch (Exception e) { + reportError("Error writing section header", e, ErrorManager.WRITE_FAILURE); + } + } + + private void writeTail(final Writer writer) { + try { + final Formatter formatter = getFormatter(); + if (formatter != null) + writer.write(formatter.getTail(this)); + } catch (Exception ex) { + reportError("Error writing section tail", ex, ErrorManager.WRITE_FAILURE); + } + } + + private void safeClose(Closeable c) { + try { + if (c != null) + c.close(); + } catch (Exception e) { + reportError("Error closing resource", e, ErrorManager.CLOSE_FAILURE); + } catch (Throwable ignored) { + } + } + + private void safeFlush(Flushable f) { + try { + if (f != null) + f.flush(); + } catch (Exception e) { + reportError("Error on flush", e, ErrorManager.FLUSH_FAILURE); + } catch (Throwable ignored) { + } + } +} diff --git a/logging/src/main/java/org/xbib/logging/handlers/SyslogHandler.java b/logging/src/main/java/org/xbib/logging/handlers/SyslogHandler.java new file mode 100644 index 0000000..e573273 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/handlers/SyslogHandler.java @@ -0,0 +1,1288 @@ +package org.xbib.logging.handlers; + +import java.io.Closeable; +import java.io.Flushable; +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.text.Normalizer; +import java.text.Normalizer.Form; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.format.TextStyle; +import java.util.Collection; +import java.util.Locale; +import java.util.logging.ErrorManager; +import java.util.logging.Formatter; +import java.util.logging.Level; +import java.util.regex.Pattern; +import javax.net.SocketFactory; +import javax.net.ssl.SSLSocketFactory; +import org.xbib.logging.ExtHandler; +import org.xbib.logging.ExtLogRecord; +import org.xbib.logging.io.TcpOutputStream; +import org.xbib.logging.io.UdpOutputStream; +import org.xbib.logging.net.ClientSocketFactory; +import org.xbib.logging.util.ByteStringBuilder; +import static java.time.temporal.ChronoField.DAY_OF_MONTH; +import static java.time.temporal.ChronoField.HOUR_OF_DAY; +import static java.time.temporal.ChronoField.MILLI_OF_SECOND; +import static java.time.temporal.ChronoField.MINUTE_OF_HOUR; +import static java.time.temporal.ChronoField.MONTH_OF_YEAR; +import static java.time.temporal.ChronoField.SECOND_OF_MINUTE; +import static java.time.temporal.ChronoField.YEAR; + +/** + * A syslog handler for logging to syslogd. + *

+ * This handler can write to syslog servers that accept the RFC3164 + * and RFC5424 formats. Writes can be done via TCP, SSL over TCP or + * UDP protocols. You can also override the {@link #setOutputStream(OutputStream) output stream} if a custom + * protocol is needed. + *

+ * + *

+ * 
+ *  
+ *      
+ *          
+ *      
+ *  
+ *  
+ *      
+ *          
+ *          
+ *          
+ *          
+ *      
+ *      
+ *          
+ *          
+ *          
+ *          
+ *      
+ *      
+ *          
+ *          
+ *          
+ *          
+ *      
+ *      
+ *          
+ *          
+ *          
+ *          
+ *      
+ *      
+ *          
+ *          
+ *          
+ *          
+ *      
+ *      
+ *          
+ *          
+ *          
+ *          
+ *      
+ *      
+ *          
+ *          
+ *          
+ *          
+ *      
+ *      
+ *          
+ *          
+ *          
+ *          
+ *      
+ *      
+ *          
+ *          
+ *          
+ *          
+ *      
+ *      
+ *          
+ *          
+ *          
+ *          
+ *      
+ *      
+ *          
+ *          
+ *          
+ *          
+ *      
+ *      
+ *          
+ *          
+ *          
+ *          
+ *      
+ *      
+ *          
+ *          
+ *          
+ *          
+ *      
+ *  
+ * 
Configuration Properties:
PropertyDescriptionTypeDefault
serverHostnameThe address of the syslog server{@link String String}localhost
portThe port of the syslog serverint514
facilityThe facility used to calculate the priority of the log message{@link Facility Facility}{@link Facility#USER_LEVEL USER_LEVEL}
appNameThe name of the application that is logging{@link String String}java
hostnameThe name of the host the messages are being sent from. See {@link #setHostname(String)} for more + * details{@link String String}{@code + * null + * }
syslogTypeThe type of the syslog used to format the message{@link SyslogType SyslogType}{@link SyslogType#RFC5424 RFC5424}
protocolThe protocol to send the message over{@link Protocol Protocol}{@link Protocol#UDP UDP}
delimiterThe delimiter to use at the end of the message if {@link #setUseMessageDelimiter(boolean) useDelimiter} + * is set to {@code + * true + * }{@link String String}For {@link Protocol#UDP UDP} {@code + * null + * } - For {@link Protocol#TCP TCP} or {@link Protocol#SSL_TCP + * SSL_TCP} {@code \n}
useDelimiterWhether or not the message should be appended with a {@link #setMessageDelimiter(String) + * delimiter}{@code boolean}For {@link Protocol#UDP UDP} {@code + * false + * } - For {@link Protocol#TCP TCP} or {@link Protocol#SSL_TCP + * SSL_TCP} {@code + * true + * }
useCountingFramingPrefixes the size of the message, mainly used for {@link Protocol#TCP TCP} or {@link Protocol#SSL_TCP + * SSL_TCP}, connections to the message being sent to the syslog server. See http://tools.ietf.org/html/rfc6587 + * for more details on framing types.{@code boolean}{@code + * false + * }
truncateWhether or not a message, including the header, should truncate the message if the length in bytes is + * greater than the {@link #setMaxLength(int) maximum length}. If set to {@code + * false + * } messages will be split and sent + * with the same header values.{@code boolean}{@code + * true + * }
maxLengthThe maximum length a log message, including the header, is allowed to be.{@code int}For {@link SyslogType#RFC3164 RFC3164} 1024 (1k) - For {@link SyslogType#RFC5424 RFC5424} 2048 + * (2k)
+ *
+ *

+ */ +@SuppressWarnings({"WeakerAccess", "unused"}) +public class SyslogHandler extends ExtHandler { + + /** + * The type of socket the syslog should write to + */ + public enum Protocol { + TCP, + UDP, + SSL_TCP, + } + + /** + * Severity as defined by RFC-5424 (http://tools.ietf.org/html/rfc5424) + * and RFC-3164 (http://tools.ietf.org/html/rfc3164). + */ + public enum Severity { + EMERGENCY(0, "Emergency: system is unusable"), + ALERT(1, "Alert: action must be taken immediately"), + CRITICAL(2, "Critical: critical conditions"), + ERROR(3, "Error: error conditions"), + WARNING(4, "Warning: warning conditions"), + NOTICE(5, "Notice: normal but significant condition"), + INFORMATIONAL(6, "Informational: informational messages"), + DEBUG(7, "Debug: debug-level messages"); + + final int code; + final String desc; + + Severity(final int code, final String desc) { + this.code = code; + this.desc = desc; + } + + @Override + public String toString() { + return String.format("%s[%d,%s]", name(), code, desc); + } + + /** + * Maps a {@link Level level} to a {@link Severity severity}. By default returns {@link + * Severity#INFORMATIONAL}. + * + * @param level the level to map + * @return the severity + */ + // TODO (jrp) allow for a custom mapping + public static Severity fromLevel(final Level level) { + if (level == null) { + throw new IllegalArgumentException("Level cannot be null"); + } + final int levelValue = level.intValue(); + if (levelValue >= org.xbib.logging.Level.FATAL.intValue()) { + return Severity.EMERGENCY; + } else if (levelValue >= org.xbib.logging.Level.SEVERE.intValue() + || levelValue >= org.xbib.logging.Level.ERROR.intValue()) { + return Severity.ERROR; + } else if (levelValue >= org.xbib.logging.Level.WARN.intValue() || levelValue >= Level.WARNING.intValue()) { + return Severity.WARNING; + } else if (levelValue >= org.xbib.logging.Level.INFO.intValue()) { + return Severity.INFORMATIONAL; + } else if (levelValue >= org.xbib.logging.Level.TRACE.intValue() || levelValue >= Level.FINEST.intValue()) { + // DEBUG for all TRACE, DEBUG, FINE, FINER, FINEST + return Severity.DEBUG; + } + return Severity.INFORMATIONAL; + } + } + + /** + * Facility as defined by RFC-5424 (http://tools.ietf.org/html/rfc5424) + * and RFC-3164 (http://tools.ietf.org/html/rfc3164). + */ + public enum Facility { + KERNEL(0, "kernel messages"), + USER_LEVEL(1, "user-level messages"), + MAIL_SYSTEM(2, "mail system"), + SYSTEM_DAEMONS(3, "system daemons"), + SECURITY(4, "security/authorization messages"), + SYSLOGD(5, "messages generated internally by syslogd"), + LINE_PRINTER(6, "line printer subsystem"), + NETWORK_NEWS(7, "network news subsystem"), + UUCP(8, "UUCP subsystem"), + CLOCK_DAEMON(9, "clock daemon"), + SECURITY2(10, "security/authorization messages"), + FTP_DAEMON(11, "FTP daemon"), + NTP(12, "NTP subsystem"), + LOG_AUDIT(13, "log audit"), + LOG_ALERT(14, "log alert"), + CLOCK_DAEMON2(15, "clock daemon (note 2)"), + LOCAL_USE_0(16, "local use 0 (local0)"), + LOCAL_USE_1(17, "local use 1 (local1)"), + LOCAL_USE_2(18, "local use 2 (local2)"), + LOCAL_USE_3(19, "local use 3 (local3)"), + LOCAL_USE_4(20, "local use 4 (local4)"), + LOCAL_USE_5(21, "local use 5 (local5)"), + LOCAL_USE_6(22, "local use 6 (local6)"), + LOCAL_USE_7(23, "local use 7 (local7)"); + + final int code; + final String desc; + final int octal; + + Facility(final int code, final String desc) { + this.code = code; + this.desc = desc; + octal = code * 8; + } + + @Override + public String toString() { + return String.format("%s[%d,%s]", name(), code, desc); + } + } + + /** + * The syslog type used for formatting the message. + */ + public enum SyslogType { + /** + * Formats the message according the the RFC-5424 specification + * (http://tools.ietf.org/html/rfc5424#section-6 + */ + RFC5424, + + /** + * Formats the message according the the RFC-3164 specification + * (http://tools.ietf.org/html/rfc3164#section-4.1 + */ + RFC3164, + } + + public static final InetAddress DEFAULT_ADDRESS; + public static final int DEFAULT_PORT = 514; + public static final int DEFAULT_SECURE_PORT = 6514; + public static final String DEFAULT_ENCODING = "UTF-8"; + public static final Facility DEFAULT_FACILITY = Facility.USER_LEVEL; + public static final String NILVALUE_SP = "- "; + private static final Pattern PRINTABLE_ASCII_PATTERN = Pattern.compile("[\\P{Print} ]"); + + static { + try { + DEFAULT_ADDRESS = InetAddress.getByName("localhost"); + } catch (UnknownHostException e) { + throw new IllegalStateException("Could not create address to localhost"); + } + } + + private InetAddress serverAddress; + private int port; + private String appName; + private String hostname; + private Facility facility; + private SyslogType syslogType; + private OutputStream out; + private Protocol protocol; + private boolean useCountingFraming; + private boolean initializeConnection; + private boolean outputStreamSet; + private String delimiter; + private boolean useDelimiter; + private boolean truncate; + private int maxLen; + private boolean blockOnReconnect; + private ClientSocketFactory clientSocketFactory; + + /** + * The default class constructor. + * + * @throws IOException if an error occurs creating the UDP socket + */ + public SyslogHandler() throws IOException { + this(DEFAULT_ADDRESS, DEFAULT_PORT); + } + + /** + * Creates a new syslog handler that sends the messages to the server represented by the {@code serverHostname} + * parameter on the port represented by the {@code port} parameter. + * + * @param serverHostname the server to send the messages to + * @param port the port the syslogd is listening on + * @throws IOException if an error occurs creating the UDP socket + */ + public SyslogHandler(final String serverHostname, final int port) throws IOException { + this(serverHostname, port, DEFAULT_FACILITY, null); + } + + /** + * Creates a new syslog handler that sends the messages to the server represented by the {@code serverAddress} + * parameter on the port represented by the {@code port} parameter. + * + * @param serverAddress the server to send the messages to + * @param port the port the syslogd is listening on + * @throws IOException if an error occurs creating the UDP socket + */ + public SyslogHandler(final InetAddress serverAddress, final int port) throws IOException { + this(serverAddress, port, DEFAULT_FACILITY, null); + } + + /** + * Creates a new syslog handler that sends the messages to the server represented by the {@code serverAddress} + * parameter on the port represented by the {@code port} parameter. + * + * @param serverHostname the server to send the messages to + * @param port the port the syslogd is listening on + * @param facility the facility to use when calculating priority + * @param hostname the name of the host the messages are being sent from see {@link #setHostname(String)} for + * details on the hostname + * @throws IOException if an error occurs creating the UDP socket + */ + public SyslogHandler(final String serverHostname, final int port, final Facility facility, final String hostname) + throws IOException { + this(serverHostname, port, facility, null, hostname); + } + + /** + * Creates a new syslog handler that sends the messages to the server represented by the {@code serverAddress} + * parameter on the port represented by the {@code port} parameter. + * + * @param serverAddress the server to send the messages to + * @param port the port the syslogd is listening on + * @param facility the facility to use when calculating priority + * @param hostname the name of the host the messages are being sent from see {@link #setHostname(String)} for + * details on the hostname + * @throws IOException if an error occurs creating the UDP socket + */ + public SyslogHandler(final InetAddress serverAddress, final int port, final Facility facility, final String hostname) + throws IOException { + this(serverAddress, port, facility, null, hostname); + } + + /** + * Creates a new syslog handler that sends the messages to the server represented by the {@code serverAddress} + * parameter on the port represented by the {@code port} parameter. + * + * @param serverHostname the server to send the messages to + * @param port the port the syslogd is listening on + * @param facility the facility to use when calculating priority + * @param syslogType the type of the syslog used to format the message + * @param hostname the name of the host the messages are being sent from see {@link #setHostname(String)} for + * details on the hostname + * @throws IOException if an error occurs creating the UDP socket + */ + public SyslogHandler(final String serverHostname, final int port, final Facility facility, final SyslogType syslogType, + final String hostname) throws IOException { + this(InetAddress.getByName(serverHostname), port, facility, syslogType, hostname); + } + + /** + * Creates a new syslog handler that sends the messages to the server represented by the {@code serverAddress} + * parameter on the port represented by the {@code port} parameter. + * + * @param serverAddress the server to send the messages to + * @param port the port the syslogd is listening on + * @param facility the facility to use when calculating priority + * @param syslogType the type of the syslog used to format the message + * @param hostname the name of the host the messages are being sent from see {@link #setHostname(String)} for + * details on the hostname + * @throws IOException if an error occurs creating the UDP socket + */ + public SyslogHandler(final InetAddress serverAddress, final int port, final Facility facility, final SyslogType syslogType, + final String hostname) throws IOException { + this(serverAddress, port, facility, syslogType, null, hostname); + } + + /** + * Creates a new syslog handler that sends the messages to the server represented by the {@code serverAddress} + * parameter on the port represented by the {@code port} parameter. + * + * @param serverHostname the server to send the messages to + * @param port the port the syslogd is listening on + * @param facility the facility to use when calculating priority + * @param syslogType the type of the syslog used to format the message + * @param protocol the socket type used to the connect to the syslog server + * @param hostname the name of the host the messages are being sent from see {@link #setHostname(String)} for + * details on the hostname + * @throws IOException if an error occurs creating the UDP socket + */ + public SyslogHandler(final String serverHostname, final int port, final Facility facility, final SyslogType syslogType, + final Protocol protocol, final String hostname) throws IOException { + this(InetAddress.getByName(serverHostname), port, facility, syslogType, protocol, hostname); + } + + /** + * Creates a new syslog handler that sends the messages to the server represented by the {@code serverAddress} + * parameter on the port represented by the {@code port} parameter. + * + * @param serverAddress the server to send the messages to + * @param port the port the syslogd is listening on + * @param facility the facility to use when calculating priority + * @param syslogType the type of the syslog used to format the message + * @param protocol the socket type used to the connect to the syslog server + * @param hostname the name of the host the messages are being sent from see {@link #setHostname(String)} for + * details on the hostname + * @throws IOException if an error occurs creating the UDP socket + */ + public SyslogHandler(final InetAddress serverAddress, final int port, final Facility facility, final SyslogType syslogType, + final Protocol protocol, final String hostname) throws IOException { + setCharsetPrivate(StandardCharsets.UTF_8); + this.serverAddress = serverAddress; + this.port = port; + this.facility = facility; + this.appName = "java"; + this.hostname = checkPrintableAscii("host name", hostname); + this.syslogType = (syslogType == null ? SyslogType.RFC5424 : syslogType); + if (protocol == null) { + this.protocol = Protocol.UDP; + delimiter = null; + useDelimiter = false; + } else { + this.protocol = protocol; + if (protocol == Protocol.UDP) { + delimiter = null; + useDelimiter = false; + } else if (protocol == Protocol.TCP || protocol == Protocol.SSL_TCP) { + delimiter = "\n"; + useDelimiter = true; + } + } + useCountingFraming = false; + initializeConnection = true; + outputStreamSet = false; + truncate = true; + if (this.syslogType == SyslogType.RFC3164) { + maxLen = 1024; + } else if (this.syslogType == SyslogType.RFC5424) { + maxLen = 2048; + } + blockOnReconnect = false; + } + + @Override + public final void doPublish(final ExtLogRecord record) { + lock.lock(); + try { + init(); + if (out == null) { + throw new IllegalStateException("The syslog handler has been closed."); + } + try { + // Create the header + final byte[] header; + if (syslogType == SyslogType.RFC3164) { + header = createRFC3164Header(record); + } else if (syslogType == SyslogType.RFC5424) { + header = createRFC5424Header(record); + } else { + throw new IllegalStateException("The syslog type of '" + syslogType + "' is invalid."); + } + + // Trailer in bytes + final byte[] trailer = delimiter == null ? new byte[]{0x00} : delimiter.getBytes(StandardCharsets.UTF_8); + + // Buffer currently only has the header + final int maxMsgLen = maxLen - (header.length + (useDelimiter ? trailer.length : 0)); + // Can't write the message if the header and trailer are bigger than the allowed length + if (maxMsgLen < 1) { + throw new IOException(String.format( + "The header and delimiter length, %d, is greater than the message length, %d, allows.", + (header.length + (useDelimiter ? trailer.length : 0)), maxLen)); + } + + // Get the message + final Formatter formatter = getFormatter(); + String logMsg; + if (formatter != null) { + logMsg = formatter.format(record); + } else { + logMsg = record.getFormattedMessage(); + } + if (!Normalizer.isNormalized(logMsg, Form.NFKC)) { + logMsg = Normalizer.normalize(logMsg, Form.NFKC); + } + // Create a message buffer + ByteStringBuilder message = new ByteStringBuilder(maxMsgLen); + // Write the message to the buffer, the len is the number of characters written + int len = message.write(logMsg, maxMsgLen); + sendMessage(header, message, trailer); + // If not truncating, chunk the message and send separately + if (!truncate && len < logMsg.length()) { + while (len > 0) { + // Get the next part of the message to write + logMsg = logMsg.substring(len + 1); + if (logMsg.isEmpty()) { + break; + } + message = new ByteStringBuilder(maxMsgLen); + // Write the message to the buffer, the len is the number of characters written + len = message.write(logMsg, maxMsgLen); + sendMessage(header, message, trailer); + } + } + } catch (IOException e) { + reportError("Could not write to syslog", e, ErrorManager.WRITE_FAILURE); + } + } finally { + lock.unlock(); + } + super.doPublish(record); + } + + /** + * Writes the message to the output stream. The message buffer is cleared after it's written. + * + * @param message the message to write + * @throws IOException if there is an error writing the message + */ + private void sendMessage(final byte[] header, final ByteStringBuilder message, final byte[] trailer) throws IOException { + final ByteStringBuilder payload = new ByteStringBuilder(header.length + message.length()); + // Prefix the size of the message if counting framing is being used + if (useCountingFraming) { + int len = header.length + message.length() + (useDelimiter ? trailer.length : 0); + payload.append(len).append(' '); + } + payload.append(header); + payload.append(message); + if (useDelimiter) + payload.append(trailer); + out.write(payload.toArray()); + // If this is a TcpOutputStream print any errors that may have occurred + if (out instanceof TcpOutputStream) { + final Collection errors = ((TcpOutputStream) out).getErrors(); + for (Exception error : errors) { + reportError("Error writing to TCP stream", error, ErrorManager.WRITE_FAILURE); + } + } + } + + @Override + public void close() { + lock.lock(); + try { + safeClose(out); + out = null; + } finally { + lock.unlock(); + } + super.close(); + } + + @Override + public void flush() { + lock.lock(); + try { + safeFlush(out); + } finally { + lock.unlock(); + } + super.flush(); + } + + /** + * Gets app name used when formatting the message in RFC5424 format. By default the app name is "java". + * + * @return the app name being used + */ + public String getAppName() { + lock.lock(); + try { + return appName; + } finally { + lock.unlock(); + } + } + + /** + * Sets app name used when formatting the message in RFC5424 format. By default the app name is "java". If + * set to {@code null} the {@link ExtLogRecord#getProcessName()} will be used. + * + * @param appName the app name to use + */ + public void setAppName(final String appName) { + lock.lock(); + try { + this.appName = checkPrintableAscii("app name", appName); + } finally { + lock.unlock(); + } + } + + /** + * Indicates whether or not a {@link Protocol#TCP TCP} or {@link + * Protocol#SSL_TCP SSL TCP} connection should block when attempting to + * reconnect. + * + * @return {@code true} if blocking is enabled, otherwise {@code false} + */ + public boolean isBlockOnReconnect() { + lock.lock(); + try { + return blockOnReconnect; + } finally { + lock.unlock(); + } + } + + /** + * Enables or disables blocking when attempting to reconnect a {@link Protocol#TCP + * TCP} or {@link Protocol#SSL_TCP SSL TCP} protocol. + *

+ * If set to {@code true} the {@code publish} methods will block when attempting to reconnect. This is only + * advisable to be set to {@code true} if using an asynchronous handler. + * + * @param blockOnReconnect {@code true} to block when reconnecting or {@code false} to reconnect asynchronously + * discarding any new messages coming in + */ + public void setBlockOnReconnect(final boolean blockOnReconnect) { + lock.lock(); + try { + this.blockOnReconnect = blockOnReconnect; + if (out instanceof TcpOutputStream) { + ((TcpOutputStream) out).setBlockOnReconnect(blockOnReconnect); + } + } finally { + lock.unlock(); + } + } + + /** + * Sets the client socket factory used to create sockets. + * + * @param clientSocketFactory the client socket factory to use + */ + public void setClientSocketFactory(final ClientSocketFactory clientSocketFactory) { + lock.lock(); + try { + this.clientSocketFactory = clientSocketFactory; + initializeConnection = true; + } finally { + lock.unlock(); + } + } + + /** + * Returns the port the syslogd is listening on. + * + * @return the port + */ + public int getPort() { + lock.lock(); + try { + return port; + } finally { + lock.unlock(); + } + } + + /** + * Sets the port the syslogd server is listening on. + * + * @param port the port + */ + public void setPort(final int port) { + lock.lock(); + try { + this.port = port; + initializeConnection = true; + } finally { + lock.unlock(); + } + } + + /** + * Returns the facility used for calculating the priority of the message. + * + * @return the facility + */ + public Facility getFacility() { + lock.lock(); + try { + return facility; + } finally { + lock.unlock(); + } + } + + /** + * Sets the facility used when calculating the priority of the message. + * + * @param facility the facility + */ + public void setFacility(final Facility facility) { + lock.lock(); + try { + this.facility = facility; + } finally { + lock.unlock(); + } + } + + /** + * Returns the host name which is used when sending the message to the syslog. + * + * @return the host name + */ + public String getHostname() { + lock.lock(); + try { + return hostname; + } finally { + lock.unlock(); + } + } + + /** + * Returns the maximum length, in bytes, of the message allowed to be sent. The length includes the header and the + * message. + * + * @return the maximum length, in bytes, of the message allowed to be sent + */ + public int getMaxLength() { + lock.lock(); + try { + return maxLen; + } finally { + lock.unlock(); + } + } + + /** + * Sets the maximum length, in bytes, of the message allowed to tbe sent. Note that the message length includes the + * header and the message itself. + * + * @param maxLen the maximum length, in bytes, allowed to be sent to the syslog server + */ + public void setMaxLength(final int maxLen) { + lock.lock(); + try { + this.maxLen = maxLen; + } finally { + lock.unlock(); + } + } + + /** + * Returns the delimiter being used for the message if {@link #setUseMessageDelimiter(boolean) use message + * delimiter} is set to {@code true}. + * + * @return the delimiter being used for the message + */ + public String getMessageDelimiter() { + lock.lock(); + try { + return delimiter; + } finally { + lock.unlock(); + } + } + + /** + * Sets the message delimiter to be used if {@link #setUseMessageDelimiter(boolean) use message + * delimiter} is set to {@code true}. + * + * @param delimiter the delimiter to use for the message + */ + public void setMessageDelimiter(final String delimiter) { + lock.lock(); + try { + this.delimiter = delimiter; + } finally { + lock.unlock(); + } + } + + /** + * Checks whether to append the message with a delimiter or not. + * + * @return {@code true} to append the message with a delimiter, otherwise {@code false} + */ + public boolean isUseMessageDelimiter() { + lock.lock(); + try { + return useDelimiter; + } finally { + lock.unlock(); + } + } + + /** + * Whether to append the message with a delimiter or not. + * + * @param useDelimiter {@code true} to append the message with a delimiter, otherwise {@code false} + */ + public void setUseMessageDelimiter(final boolean useDelimiter) { + lock.lock(); + try { + this.useDelimiter = useDelimiter; + } finally { + lock.unlock(); + } + } + + /** + * Sets the host name which is used when sending the message to the syslog. + *

+ * This should be the name of the host sending the log messages, Note that the name cannot contain any whitespace. + *

+ * The hostname should be the most specific available value first. The order of preference for the contents of the + * hostname is as follows: + *

    + *
  1. FQDN
  2. + *
  3. Static IP address
  4. + *
  5. hostname
  6. + *
  7. Dynamic IP address
  8. + *
  9. {@code null}
  10. + *
+ * + * @param hostname the host name + */ + public void setHostname(final String hostname) { + lock.lock(); + try { + this.hostname = checkPrintableAscii("host name", hostname); + } finally { + lock.unlock(); + } + } + + /** + * Returns {@code true} if the message size should be prefixed to the message being sent. + *

+ * See http://tools.ietf.org/html/rfc6587 + * for more details on framing types. + * + * @return the message transfer type + */ + public boolean isUseCountingFraming() { + lock.lock(); + try { + return useCountingFraming; + } finally { + lock.unlock(); + } + } + + /** + * Set to {@code true} if the message being sent should be prefixed with the size of the message. + *

+ * See http://tools.ietf.org/html/rfc6587 + * for more details on framing types. + * + * @param useCountingFraming {@code true} if the message being sent should be prefixed with the size of the message + */ + public void setUseCountingFraming(final boolean useCountingFraming) { + lock.lock(); + try { + this.useCountingFraming = useCountingFraming; + } finally { + lock.unlock(); + } + } + + /** + * Sets the server address the messages should be sent to. + * + * @param hostname the hostname used to created the connection + * @throws UnknownHostException if no IP address for the host could be found, or if a scope_id was specified for a + * global IPv6 address. + * @see InetAddress#getByName(String) + */ + public void setServerHostname(final String hostname) throws UnknownHostException { + setServerAddress(InetAddress.getByName(hostname)); + } + + /** + * Returns the server address the messages are being sent to. + * + * @return the server address + */ + public InetAddress getServerAddress() { + lock.lock(); + try { + return serverAddress; + } finally { + lock.unlock(); + } + } + + /** + * Sets the server address the messages should be sent to. + * + * @param serverAddress the server address + */ + public void setServerAddress(final InetAddress serverAddress) { + lock.lock(); + try { + this.serverAddress = serverAddress; + initializeConnection = true; + } finally { + lock.unlock(); + } + } + + /** + * Returns the {@link SyslogType syslog type} this handler is using to format the message sent. + * + * @return the syslog type + */ + public SyslogType getSyslogType() { + lock.lock(); + try { + return syslogType; + } finally { + lock.unlock(); + } + } + + /** + * Set the {@link SyslogType syslog type} this handler should use to format the message sent. + * + * @param syslogType the syslog type + */ + public void setSyslogType(final SyslogType syslogType) { + lock.lock(); + try { + this.syslogType = syslogType; + } finally { + lock.unlock(); + } + } + + /** + * The protocol used to connect to the syslog server + * + * @return the protocol + */ + public Protocol getProtocol() { + lock.lock(); + try { + return protocol; + } finally { + lock.unlock(); + } + } + + /** + * Sets the protocol used to connect to the syslog server + * + * @param type the protocol + */ + public void setProtocol(final Protocol type) { + lock.lock(); + try { + this.protocol = type; + initializeConnection = true; + } finally { + lock.unlock(); + } + } + + /** + * Sets the output stream for the syslog handler to write to. + *

+ * Setting the output stream closes any already established connections or open output streams and will not open + * any new connections until the output stream is set to {@code null}. + * The {@link #setProtocol(Protocol) protocol}, {@link #setServerAddress(InetAddress) server address}, + * {@link #setServerHostname(String) server hostname} or + * {@link #setPort(int) port} have no effect when the output stream is set. + * + * @param out the output stream to write to + */ + public void setOutputStream(final OutputStream out) { + setOutputStream(out, true); + } + + /** + * Checks if the message should truncated if the total length exceeds the {@link #getMaxLength() maximum length}. + * + * @return {@code true} if the message should be truncated if too large, otherwise {@code false} + */ + public boolean isTruncate() { + lock.lock(); + try { + return truncate; + } finally { + lock.unlock(); + } + } + + /** + * Set to {@code true} if the message should be truncated if the total length the {@link #getMaxLength() maximum + * length}. + *

+ * Set to {@code false} if the message should be split and sent as multiple messages. The header will remain the + * same for each message sent. The wrapping is not a word based wrap and could split words between log messages. + * + * @param truncate {@code true} to truncate, otherwise {@code false} to send multiple messages + */ + public void setTruncate(final boolean truncate) { + lock.lock(); + try { + this.truncate = truncate; + } finally { + lock.unlock(); + } + } + + private void setOutputStream(final OutputStream out, final boolean outputStreamSet) { + OutputStream oldOut = null; + boolean ok = false; + try { + lock.lock(); + try { + initializeConnection = false; + oldOut = this.out; + if (oldOut != null) { + safeFlush(oldOut); + } + this.out = out; + ok = true; + this.outputStreamSet = (out != null && outputStreamSet); + } finally { + lock.unlock(); + } + } finally { + safeClose(oldOut); + if (!ok) + safeClose(out); + } + } + + static void safeClose(final Closeable closeable) { + if (closeable != null) + try { + closeable.close(); + } catch (Exception ignore) { + // ignore + } + } + + static void safeFlush(final Flushable flushable) { + if (flushable != null) + try { + flushable.flush(); + } catch (Exception ignore) { + // ignore + } + } + + private void init() { + if (initializeConnection && !outputStreamSet) { + if (serverAddress == null || port < 0 || protocol == null) { + throw new IllegalStateException( + "Invalid connection parameters. The port, server address and protocol must be set."); + } + initializeConnection = false; + final OutputStream out; + // Check the sockets + try { + final ClientSocketFactory clientSocketFactory = getClientSocketFactory(); + if (protocol == Protocol.UDP) { + out = new UdpOutputStream(clientSocketFactory); + } else { + out = new TcpOutputStream(clientSocketFactory, blockOnReconnect); + } + setOutputStream(out, false); + } catch (IOException e) { + throw new IllegalStateException("Could not set " + protocol + " output stream.", e); + } + } + } + + protected int calculatePriority(final Level level, final Facility facility) { + final Severity severity = Severity.fromLevel(level); + // facility * 8 + severity + return facility.octal | severity.code; + } + + private static final DateTimeFormatter RFC5424_DATE = new DateTimeFormatterBuilder() + .appendValue(YEAR, 4) + .appendLiteral('-') + .appendValue(MONTH_OF_YEAR, 2) + .appendLiteral('-') + .appendValue(DAY_OF_MONTH, 2) + .appendLiteral('T') + .appendValue(HOUR_OF_DAY, 2) + .appendLiteral(':') + .appendValue(MINUTE_OF_HOUR, 2) + .appendLiteral(':') + .appendValue(SECOND_OF_MINUTE, 2) + .appendLiteral('.') + .appendValue(MILLI_OF_SECOND, 3) + .appendOffset("+HH:MM", "+00:00") + .toFormatter() + .withZone(ZoneId.of("UTC")) + .withLocale(Locale.ROOT); + + protected byte[] createRFC5424Header(final ExtLogRecord record) throws IOException { + final ByteStringBuilder buffer = new ByteStringBuilder(256); + // Set the property + buffer.append('<').append(calculatePriority(record.getLevel(), facility)).append('>'); + // Set the version + buffer.appendUSASCII("1 "); + // Set the time + RFC5424_DATE.formatTo(ZonedDateTime.ofInstant(record.getInstant(), ZoneId.systemDefault()), buffer); + buffer.append(' '); + // Set the host name + final String recordHostName = record.getHostName(); + if (hostname != null) { + buffer.appendPrintUSASCII(hostname, 255).append(' '); + } else if (recordHostName != null) { + buffer.appendPrintUSASCII(recordHostName, 255).append(' '); + } else { + buffer.append(NILVALUE_SP); + } + // Set the app name + final String recordProcName = record.getProcessName(); + if (appName != null) { + buffer.appendPrintUSASCII(appName, 48); + buffer.append(' '); + } else if (recordProcName != null) { + buffer.appendPrintUSASCII(recordProcName, 48); + buffer.append(' '); + } else { + buffer.appendUSASCII(NILVALUE_SP); + } + // Set the procid + final long recordProcId = record.getProcessId(); + if (recordProcId != -1) { + buffer.append(recordProcId); + buffer.append(' '); + } else { + buffer.appendUSASCII(NILVALUE_SP); + } + // Set the msgid + final String msgid = record.getLoggerName(); + if (msgid == null) { + buffer.appendUSASCII(NILVALUE_SP); + } else if (msgid.isEmpty()) { + buffer.appendUSASCII("root-logger"); + buffer.append(' '); + } else { + buffer.appendPrintUSASCII(msgid, 32); + buffer.append(' '); + } + // Set the structured data + buffer.appendUSASCII(NILVALUE_SP); + // TODO (jrp) review structured data http://tools.ietf.org/html/rfc5424#section-6.3 + final String encoding = getEncoding(); + if (encoding == null || DEFAULT_ENCODING.equalsIgnoreCase(encoding)) { + buffer.appendUtf8Raw(0xFEFF); + } + return buffer.toArray(); + } + + private static final DateTimeFormatter RFC3164_DATE = new DateTimeFormatterBuilder() + .parseCaseInsensitive() + .appendText(MONTH_OF_YEAR, TextStyle.SHORT) + .appendLiteral(' ') + .padNext(2) + .appendValue(DAY_OF_MONTH) + .appendLiteral(' ') + .appendValue(HOUR_OF_DAY, 2) + .appendLiteral(':') + .appendValue(MINUTE_OF_HOUR, 2) + .appendLiteral(':') + .appendValue(SECOND_OF_MINUTE, 2) + .toFormatter() + .withZone(ZoneId.of("UTC")) + .withLocale(Locale.ROOT); + + protected byte[] createRFC3164Header(final ExtLogRecord record) throws IOException { + final ByteStringBuilder buffer = new ByteStringBuilder(256); + // Set the property + buffer.append('<').append(calculatePriority(record.getLevel(), facility)).append('>'); + + // Set the time + RFC3164_DATE.formatTo(ZonedDateTime.ofInstant(record.getInstant(), ZoneId.systemDefault()), buffer); + buffer.append(' '); + + // Set the host name + final String recordHostName = record.getHostName(); + if (hostname != null) { + buffer.appendUSASCII(hostname).append(' '); + } else if (recordHostName != null) { + buffer.appendUSASCII(recordHostName).append(' '); + } else { + buffer.appendUSASCII("UNKNOWN_HOSTNAME").append(' '); + } + // Set the app name and the proc id + final String recordProcName = record.getProcessName(); + boolean colon = false; + if (appName != null) { + buffer.appendUSASCII(appName); + colon = true; + } else if (recordProcName != null) { + buffer.appendUSASCII(recordProcName); + colon = true; + } + final long recordProcId = record.getProcessId(); + if (recordProcId != -1) { + buffer.append('[').append(recordProcId).append(']'); + colon = true; + } + if (colon) { + buffer.append(':').append(' '); + } + return buffer.toArray(); + } + + private ClientSocketFactory getClientSocketFactory() { + lock.lock(); + try { + if (clientSocketFactory != null) { + return clientSocketFactory; + } + final SocketFactory socketFactory = (protocol == Protocol.SSL_TCP ? SSLSocketFactory.getDefault() + : SocketFactory.getDefault()); + return ClientSocketFactory.of(socketFactory, serverAddress, port); + } finally { + lock.unlock(); + } + } + + private static String checkPrintableAscii(final String name, final String value) { + if (value != null && PRINTABLE_ASCII_PATTERN.matcher(value).find()) { + final String upper = Character.toUpperCase(name.charAt(0)) + name.substring(1); + throw new IllegalArgumentException(String.format( + "%s '%s' is invalid. The %s must be printable ASCII characters with no spaces.", upper, value, name)); + } + return value; + } +} diff --git a/logging/src/main/java/org/xbib/logging/handlers/ThreadLoggingHandler.java b/logging/src/main/java/org/xbib/logging/handlers/ThreadLoggingHandler.java new file mode 100644 index 0000000..0a4672a --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/handlers/ThreadLoggingHandler.java @@ -0,0 +1,25 @@ +package org.xbib.logging.handlers; + +import java.util.logging.LogRecord; + +/** + * Replacement for java.util.logging.ConsoleHandler. + */ +public class ThreadLoggingHandler extends ThreadLoggingStreamHandler { + + public ThreadLoggingHandler() { + super(); + this.setOutputStream(System.out); + } + + @Override + public void publish(LogRecord record) { + super.publish(record); + this.flush(); + } + + @Override + public void close() { + this.flush(); + } +} diff --git a/logging/src/main/java/org/xbib/logging/handlers/ThreadLoggingStreamHandler.java b/logging/src/main/java/org/xbib/logging/handlers/ThreadLoggingStreamHandler.java new file mode 100644 index 0000000..a57c4e7 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/handlers/ThreadLoggingStreamHandler.java @@ -0,0 +1,220 @@ +package org.xbib.logging.handlers; + +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.util.concurrent.locks.ReentrantLock; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import org.xbib.logging.formatters.ThreadLoggingFormatter; + +/** + * Replacement for java.util.logging.StreamHandler that is wired to the ThreadLoggingFormatter + * and does not use any fall-back formatter + */ +public class ThreadLoggingStreamHandler extends Handler { + + private static final ThreadLoggingFormatter formatter = new ThreadLoggingFormatter(); + + private final ReentrantLock lock; + + private OutputStream output; + + private boolean doneHeader; + + private volatile Writer writer; + + public ThreadLoggingStreamHandler() { + super(); + this.lock = this.initLocking(); + setLevel(Level.ALL); + setFilter(null); + setFormatter(formatter); + try { + setEncoding("UTF-8"); + } catch (UnsupportedEncodingException e) { + // can't happen + } + } + + protected void setOutputStream(OutputStream out) { + if (this.tryUseLock()) { + try { + this.setOutputStream0(out); + } finally { + this.unlock(); + } + } else { + synchronized (this) { + this.setOutputStream0(out); + } + } + } + + private void setOutputStream0(OutputStream out) { + if (out == null) { + throw new NullPointerException(); + } else { + this.flushAndClose(); + this.output = out; + this.doneHeader = false; + String encoding = this.getEncoding(); + if (encoding == null) { + this.writer = new OutputStreamWriter(this.output); + } else { + try { + this.writer = new OutputStreamWriter(this.output, encoding); + } catch (UnsupportedEncodingException e) { + throw new Error("Unexpected exception " + e); + } + } + } + } + + @Override + public void setEncoding(String encoding) throws UnsupportedEncodingException { + if (this.tryUseLock()) { + try { + this.setEncoding0(encoding); + } finally { + this.unlock(); + } + } else { + synchronized (this) { + this.setEncoding0(encoding); + } + } + } + + private void setEncoding0(String encoding) throws UnsupportedEncodingException { + super.setEncoding(encoding); + if (this.output != null) { + this.flush(); + if (encoding == null) { + this.writer = new OutputStreamWriter(this.output); + } else { + this.writer = new OutputStreamWriter(this.output, encoding); + } + } + } + + @Override + public void publish(LogRecord record) { + if (this.tryUseLock()) { + try { + this.publish0(record); + } finally { + this.unlock(); + } + } else { + synchronized (this) { + this.publish0(record); + } + } + } + + private void publish0(LogRecord record) { + if (this.isLoggable(record)) { + String msg; + try { + msg = this.getFormatter().format(record); + } catch (Exception e) { + this.reportError(null, e, 5); + return; + } + try { + if (!this.doneHeader) { + this.writer.write(this.getFormatter().getHead(this)); + this.doneHeader = true; + } + this.writer.write(msg); + } catch (Exception e) { + this.reportError(null, e, 1); + } + } + } + + @Override + public boolean isLoggable(LogRecord record) { + return this.writer != null && record != null && super.isLoggable(record); + } + + @Override + public void flush() { + if (this.tryUseLock()) { + try { + this.flush0(); + } finally { + this.unlock(); + } + } else { + synchronized (this) { + this.flush0(); + } + } + } + + private void flush0() { + if (this.writer != null) { + try { + this.writer.flush(); + } catch (Exception e) { + this.reportError(null, e, 2); + } + } + } + + private void flushAndClose() { + if (this.writer != null) { + try { + if (!this.doneHeader) { + this.writer.write(this.getFormatter().getHead(this)); + this.doneHeader = true; + } + this.writer.write(this.getFormatter().getTail(this)); + this.writer.flush(); + this.writer.close(); + } catch (Exception e) { + this.reportError(null, e, 3); + } + this.writer = null; + this.output = null; + } + } + + @Override + public void close() { + if (this.tryUseLock()) { + try { + this.flushAndClose(); + } finally { + this.unlock(); + } + } else { + synchronized (this) { + this.flushAndClose(); + } + } + } + + private ReentrantLock initLocking() { + Class clazz = this.getClass(); + ClassLoader loader = clazz.getClassLoader(); + return loader != null && loader != ClassLoader.getPlatformClassLoader() ? null : new ReentrantLock(); + } + + private boolean tryUseLock() { + if (this.lock == null) { + return false; + } else { + this.lock.lock(); + return true; + } + } + + private void unlock() { + this.lock.unlock(); + } +} diff --git a/logging/src/main/java/org/xbib/logging/handlers/WriterHandler.java b/logging/src/main/java/org/xbib/logging/handlers/WriterHandler.java new file mode 100644 index 0000000..a6f2c47 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/handlers/WriterHandler.java @@ -0,0 +1,239 @@ +package org.xbib.logging.handlers; + +import java.io.BufferedWriter; +import java.io.Closeable; +import java.io.Flushable; +import java.io.Writer; +import java.util.logging.ErrorManager; +import java.util.logging.Formatter; +import org.xbib.logging.ExtHandler; +import org.xbib.logging.ExtLogRecord; + +/** + * A handler which writes to any {@code Writer}. + */ +public class WriterHandler extends ExtHandler { + + private volatile boolean checkHeadEncoding = true; + + private volatile boolean checkTailEncoding = true; + + private Writer writer; + + /** + * Construct a new instance. + */ + public WriterHandler() { + } + + /** + * {@inheritDoc} + */ + protected void doPublish(final ExtLogRecord record) { + final String formatted; + final Formatter formatter = getFormatter(); + try { + formatted = formatter.format(record); + } catch (Exception ex) { + reportError("Formatting error", ex, ErrorManager.FORMAT_FAILURE); + return; + } + if (formatted.isEmpty()) { + // nothing to write; don't bother + return; + } + try { + lock.lock(); + try { + if (writer == null) { + return; + } + preWrite(record); + final Writer writer = this.writer; + if (writer == null) { + return; + } + writer.write(formatted); + // only flush if something was written + super.doPublish(record); + } finally { + lock.unlock(); + } + } catch (Exception ex) { + reportError("Error writing log message", ex, ErrorManager.WRITE_FAILURE); + } + } + + /** + * Execute any pre-write policy, such as file rotation. The write lock is held during this method, so make + * it quick. The default implementation does nothing. + * + * @param record the record about to be logged + */ + protected void preWrite(final ExtLogRecord record) { + // do nothing by default + } + + /** + * Set the writer. The writer will then belong to this handler; when the handler is closed or a new writer is set, + * this writer will be closed. + * + * @param writer the new writer, or {@code null} to disable logging + */ + public void setWriter(final Writer writer) { + Writer oldWriter = null; + boolean ok = false; + try { + lock.lock(); + try { + oldWriter = this.writer; + if (oldWriter != null) { + writeTail(oldWriter); + safeFlush(oldWriter); + } + if (writer != null) { + writeHead(this.writer = new BufferedWriter(writer)); + } else { + this.writer = null; + } + ok = true; + } finally { + lock.unlock(); + } + } finally { + safeClose(oldWriter); + if (!ok) + safeClose(writer); + } + } + + Writer getWriter() { + assert lock.isHeldByCurrentThread(); + return writer; + } + + /** + * Determine whether head encoding checking is turned on. + * + * @return {@code true} to check and report head encoding problems, or {@code false} to ignore them + */ + public boolean isCheckHeadEncoding() { + return checkHeadEncoding; + } + + /** + * Establish whether head encoding checking is turned on. + * + * @param checkHeadEncoding {@code true} to check and report head encoding problems, or {@code false} to ignore them + * @return this handler + */ + public WriterHandler setCheckHeadEncoding(boolean checkHeadEncoding) { + this.checkHeadEncoding = checkHeadEncoding; + return this; + } + + /** + * Determine whether tail encoding checking is turned on. + * + * @return {@code true} to check and report tail encoding problems, or {@code false} to ignore them + */ + public boolean isCheckTailEncoding() { + return checkTailEncoding; + } + + /** + * Establish whether tail encoding checking is turned on. + * + * @param checkTailEncoding {@code true} to check and report tail encoding problems, or {@code false} to ignore them + * @return this handler + */ + public WriterHandler setCheckTailEncoding(boolean checkTailEncoding) { + this.checkTailEncoding = checkTailEncoding; + return this; + } + + private void writeHead(final Writer writer) { + try { + final Formatter formatter = getFormatter(); + if (formatter != null) { + final String head = formatter.getHead(this); + if (checkHeadEncoding) { + if (!getCharset().newEncoder().canEncode(head)) { + reportError("Section header cannot be encoded into charset \"" + getCharset().name() + "\"", null, + ErrorManager.GENERIC_FAILURE); + return; + } + } + writer.write(head); + } + } catch (Exception e) { + reportError("Error writing section header", e, ErrorManager.WRITE_FAILURE); + } + } + + private void writeTail(final Writer writer) { + try { + final Formatter formatter = getFormatter(); + if (formatter != null) { + final String tail = formatter.getTail(this); + if (checkTailEncoding) { + if (!getCharset().newEncoder().canEncode(tail)) { + reportError("Section tail cannot be encoded into charset \"" + getCharset().name() + "\"", null, + ErrorManager.GENERIC_FAILURE); + return; + } + } + writer.write(tail); + } + } catch (Exception ex) { + reportError("Error writing section tail", ex, ErrorManager.WRITE_FAILURE); + } + } + + /** + * Flush this logger. + */ + public void flush() { + // todo - maybe this synch is not really needed... if there's a perf detriment, drop it + lock.lock(); + try { + safeFlush(writer); + } finally { + lock.unlock(); + } + super.flush(); + } + + /** + * Close this logger. + */ + public void close() { + setWriter(null); + super.close(); + } + + /** + * Safely close the resource, reporting an error if the close fails. + * + * @param c the resource + */ + protected void safeClose(Closeable c) { + try { + if (c != null) + c.close(); + } catch (Exception e) { + reportError("Error closing resource", e, ErrorManager.CLOSE_FAILURE); + } catch (Throwable ignored) { + } + } + + void safeFlush(Flushable f) { + try { + if (f != null) + f.flush(); + } catch (Exception e) { + reportError("Error on flush", e, ErrorManager.FLUSH_FAILURE); + } catch (Throwable ignored) { + } + } +} diff --git a/logging/src/main/java/org/xbib/logging/io/CountingOutputStream.java b/logging/src/main/java/org/xbib/logging/io/CountingOutputStream.java new file mode 100644 index 0000000..592b7f3 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/io/CountingOutputStream.java @@ -0,0 +1,39 @@ +package org.xbib.logging.io; + +import java.io.IOException; +import java.io.OutputStream; + +public final class CountingOutputStream extends OutputStream { + + private final OutputStream delegate; + + public long currentSize; + + public CountingOutputStream(final OutputStream delegate) { + this.delegate = delegate; + currentSize = 0; + } + + public void write(final int b) throws IOException { + delegate.write(b); + currentSize++; + } + + public void write(final byte[] b) throws IOException { + delegate.write(b); + currentSize += b.length; + } + + public void write(final byte[] b, final int off, final int len) throws IOException { + delegate.write(b, off, len); + currentSize += len; + } + + public void flush() throws IOException { + delegate.flush(); + } + + public void close() throws IOException { + delegate.close(); + } +} diff --git a/logging/src/main/java/org/xbib/logging/io/SslTcpOutputStream.java b/logging/src/main/java/org/xbib/logging/io/SslTcpOutputStream.java new file mode 100644 index 0000000..5a4b9aa --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/io/SslTcpOutputStream.java @@ -0,0 +1,74 @@ +package org.xbib.logging.io; + +import java.io.Flushable; +import java.io.IOException; +import java.net.InetAddress; +import javax.net.SocketFactory; +import javax.net.ssl.SSLSocketFactory; + +/** + * An output stream that writes data to a {@link java.net.Socket socket}. Uses {@link + * SSLSocketFactory#getDefault()} to create the socket. + */ +@SuppressWarnings({"unused", "WeakerAccess"}) +public class SslTcpOutputStream extends TcpOutputStream implements AutoCloseable, Flushable { + + /** + * Creates a SSL TCP output stream. + *

+ * Uses the {@link SSLSocketFactory#getDefault() default socket factory} to create the socket. + * + * @param address the address to connect to + * @param port the port to connect to + * @throws IOException if an I/O error occurs when creating the socket + */ + public SslTcpOutputStream(final InetAddress address, final int port) throws IOException { + super(SSLSocketFactory.getDefault(), address, port); + } + + /** + * Creates a SSL TCP output stream. + *

+ * Uses the {@link SSLSocketFactory#getDefault() default socket factory} to create the socket. + * + * @param socketFactory the factory used to create the socket + * @param address the address to connect to + * @param port the port to connect to + * @throws IOException if an I/O error occurs when creating the socket + */ + public SslTcpOutputStream(final SocketFactory socketFactory, final InetAddress address, final int port) throws IOException { + super(socketFactory, address, port); + } + + /** + * Creates a SSL TCP output stream. + *

+ * Uses the {@link SSLSocketFactory#getDefault() default socket factory} to create the socket. + * + * @param address the address to connect to + * @param port the port to connect to + * @param blockOnReconnect {@code true} to block when attempting to reconnect the socket or {@code false} to + * reconnect asynchronously + * @throws IOException if an I/O error occurs when creating the socket + */ + public SslTcpOutputStream(final InetAddress address, final int port, final boolean blockOnReconnect) throws IOException { + super(SSLSocketFactory.getDefault(), address, port, blockOnReconnect); + } + + /** + * Creates a SSL TCP output stream. + *

+ * Uses the {@link SSLSocketFactory#getDefault() default socket factory} to create the socket. + * + * @param socketFactory the factory used to create the socket + * @param address the address to connect to + * @param port the port to connect to + * @param blockOnReconnect {@code true} to block when attempting to reconnect the socket or {@code false} to + * reconnect asynchronously + * @throws IOException if an I/O error occurs when creating the socket + */ + public SslTcpOutputStream(final SocketFactory socketFactory, final InetAddress address, final int port, + final boolean blockOnReconnect) throws IOException { + super(socketFactory, address, port, blockOnReconnect); + } +} diff --git a/logging/src/main/java/org/xbib/logging/io/TcpOutputStream.java b/logging/src/main/java/org/xbib/logging/io/TcpOutputStream.java new file mode 100644 index 0000000..7efb571 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/io/TcpOutputStream.java @@ -0,0 +1,379 @@ +package org.xbib.logging.io; + +import java.io.Closeable; +import java.io.Flushable; +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.Socket; +import java.net.SocketException; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; +import javax.net.SocketFactory; +import org.xbib.logging.net.ClientSocketFactory; + +/** + * An output stream that writes data to a {@link Socket socket}. + *

+ * If an {@link IOException IOException} occurs during a {@link #write(byte[], int, int)} and a {@link + * SocketFactory socket factory} was defined the stream will attempt to reconnect indefinitely. By default + * additional writes are discarded when reconnecting. If you set the {@link #setBlockOnReconnect(boolean) block on + * reconnect} to {@code true}, then the reconnect will indefinitely block until the TCP stream is reconnected. + *

+ * You can optionally get a collection of the errors that occurred during a write or reconnect. + */ +@SuppressWarnings({"unused", "WeakerAccess"}) +public class TcpOutputStream extends OutputStream implements AutoCloseable, Flushable { + private static final long retryTimeout = 5L; + private static final long maxRetryTimeout = 40L; + private static final int maxErrors = 10; + + protected final ReentrantLock outputLock = new ReentrantLock(); + + private final ClientSocketFactory socketFactory; + private final Deque errors = new ArrayDeque(maxErrors); + + // Guarded by outputLock + private Thread reconnectThread; + // Guarded by outputLock + private boolean blockOnReconnect; + // Guarded by outputLock + private Socket socket; + // Guarded by outputLock + private boolean connected; + + /** + * Creates a TCP output stream. + *

+ * Uses the {@link SocketFactory#getDefault() default socket factory} to create the socket. + * + * @param address the address to connect to + * @param port the port to connect to + * @throws IOException no longer throws an exception. If an exception occurs while attempting to connect the socket + * a reconnect will be attempted on the next write. + */ + public TcpOutputStream(final InetAddress address, final int port) throws IOException { + this(SocketFactory.getDefault(), address, port); + } + + /** + * Creates a TCP output stream. + *

+ * Uses the {@link SocketFactory#getDefault() default socket factory} to create the socket. + *

+ * + * @param address the address to connect to + * @param port the port to connect to + * @param blockOnReconnect {@code true} to block when attempting to reconnect the socket or {@code false} to + * reconnect asynchronously + * @throws IOException no longer throws an exception. If an exception occurs while attempting to connect the socket + * a reconnect will be attempted on the next write. + */ + public TcpOutputStream(final InetAddress address, final int port, final boolean blockOnReconnect) throws IOException { + this(SocketFactory.getDefault(), address, port, blockOnReconnect); + } + + /** + * Creates a new TCP output stream. + * Creates a {@link Socket socket} from the {@code socketFactory} argument. + * + * @param socketFactory the factory used to create the socket + * @param address the address to connect to + * @param port the port to connect to + * @throws IOException no longer throws an exception. If an exception occurs while attempting to connect the socket + * a reconnect will be attempted on the next write. + */ + protected TcpOutputStream(final SocketFactory socketFactory, final InetAddress address, final int port) throws IOException { + this(socketFactory, address, port, false); + } + + /** + * Creates a new TCP output stream. + *

+ * Creates a {@link Socket socket} from the {@code socketFactory} argument. + *

+ * + * @param socketFactory the factory used to create the socket + * @param address the address to connect to + * @param port the port to connect to + * @param blockOnReconnect {@code true} to block when attempting to reconnect the socket or {@code false} to + * reconnect asynchronously + * @throws IOException no longer throws an exception. If an exception occurs while attempting to connect the socket + * a reconnect will be attempted on the next write. + */ + protected TcpOutputStream(final SocketFactory socketFactory, final InetAddress address, final int port, + final boolean blockOnReconnect) throws IOException { + this(ClientSocketFactory.of(socketFactory, address, port), blockOnReconnect); + } + + /** + * Creates a new TCP stream which uses the {@link ClientSocketFactory#createSocket()} to create the socket. + * + * @param socketFactory the socket factory used to create TCP sockets + * @param blockOnReconnect {@code true} to block when attempting to reconnect the socket or {@code false} to + * reconnect asynchronously + */ + public TcpOutputStream(final ClientSocketFactory socketFactory, final boolean blockOnReconnect) { + this.socketFactory = socketFactory; + this.blockOnReconnect = blockOnReconnect; + try { + socket = this.socketFactory.createSocket(); + connected = true; + } catch (IOException e) { + connected = false; + } + } + + @Override + public void write(final int b) throws IOException { + write(new byte[]{(byte) b}, 0, 1); + } + + @Override + public void write(final byte[] b) throws IOException { + write(b, 0, b.length); + } + + @Override + public void write(final byte[] b, final int off, final int len) throws IOException { + outputLock.lock(); + try { + checkReconnect(); + if (connected) { + socket.getOutputStream().write(b, off, len); + } + } catch (SocketException e) { + if (isReconnectAllowed()) { + // Close the previous socket + safeClose(socket); + connected = false; + addError(e); + // Handle the reconnection + reconnectThread = createThread(); + if (blockOnReconnect) { + reconnectThread.run(); + // We should be reconnected, try to write again + write(b, off, len); + } else { + reconnectThread.start(); + } + } else { + throw e; + } + } finally { + outputLock.unlock(); + } + } + + @Override + public void flush() throws IOException { + outputLock.lock(); + try { + if (socket != null) { + socket.getOutputStream().flush(); + } + } catch (SocketException e) { + // This should likely never be hit, but should attempt to reconnect if it does happen + if (isReconnectAllowed()) { + // Close the previous socket + safeClose(socket); + // Reconnection should be attempted on the next write if allowed + connected = false; + addError(e); + } else { + throw e; + } + } finally { + outputLock.unlock(); + } + } + + @Override + public void close() throws IOException { + outputLock.lock(); + try { + if (reconnectThread != null) { + reconnectThread.interrupt(); + } + if (socket != null) { + socket.close(); + } + } finally { + outputLock.unlock(); + } + } + + /** + * Indicates whether or not the output stream is set to block when attempting to reconnect a TCP connection. + * + * @return {@code true} if blocking is enabled, otherwise {@code false} + */ + public boolean isBlockOnReconnect() { + outputLock.lock(); + try { + return blockOnReconnect; + } finally { + outputLock.unlock(); + } + } + + /** + * Enables or disables blocking when attempting to reconnect the socket. + *

+ * If set to {@code true} the {@code write} methods will block when attempting to reconnect. This is only advisable + * to be set to {@code true} if using an asynchronous handler. + * + * @param blockOnReconnect {@code true} to block when reconnecting or {@code false} to reconnect asynchronously + * discarding any new messages coming in + */ + public void setBlockOnReconnect(final boolean blockOnReconnect) { + outputLock.lock(); + try { + this.blockOnReconnect = blockOnReconnect; + } finally { + outputLock.unlock(); + } + } + + /** + * Returns the connected state of the TCP stream. + *

+ * The stream is said to be disconnected when an {@link IOException} occurs during a write. Otherwise a + * stream is considered connected. + * + * @return {@code true} if the stream is connected, otherwise {@code false} + */ + public boolean isConnected() { + outputLock.lock(); + try { + return connected; + } finally { + outputLock.unlock(); + } + } + + /** + * Retrieves the errors occurred, if any, during a write or reconnect. + * + * @return a collection of errors or an empty list + */ + public Collection getErrors() { + synchronized (errors) { + if (!errors.isEmpty()) { + // drain the errors and return a list + final List result = new ArrayList(errors); + errors.clear(); + return result; + } + } + return Collections.emptyList(); + } + + private void addError(final Exception e) { + synchronized (errors) { + if (errors.size() < maxErrors) { + errors.addLast(e); + } + // TODO (jrp) should we do something with these errors + } + } + + /** + * Invocations of this method must be locked by the {@link #outputLock}. + */ + private boolean isReconnectAllowed() { + return socketFactory != null && reconnectThread == null; + } + + /** + * Attempts to reconnect the socket if required and allowed. Invocations of this method must be locked by the + * {@link #outputLock}. + */ + private void checkReconnect() { + if (!connected && isReconnectAllowed()) { + reconnectThread = createThread(); + if (blockOnReconnect) { + reconnectThread.run(); + } else { + reconnectThread.start(); + } + } + } + + private Thread createThread() { + final Thread thread = new Thread(new RetryConnector()); + thread.setDaemon(true); + thread.setName("LogManager Socket Reconnect Thread"); + return thread; + } + + private static void safeClose(final Closeable closeable) { + if (closeable != null) + try { + closeable.close(); + } catch (Exception ignore) { + } + } + + private class RetryConnector implements Runnable { + private int attempts = 0; + + @Override + public void run() { + boolean connected = false; + while (socketFactory != null && !connected) { + Socket socket = null; + try { + socket = socketFactory.createSocket(); + outputLock.lock(); + try { + // Unlikely but if we've been interrupted due to a close, we should shutdown + if (Thread.currentThread().isInterrupted()) { + safeClose(socket); + break; + } else { + TcpOutputStream.this.socket = socket; + TcpOutputStream.this.connected = true; + TcpOutputStream.this.reconnectThread = null; + connected = true; + } + } finally { + outputLock.unlock(); + } + } catch (IOException e) { + connected = false; + addError(e); + final long timeout; + if (attempts++ > 0L) { + timeout = (10L * attempts); + } else { + timeout = retryTimeout; + } + // Wait for a bit, then try to reconnect + try { + TimeUnit.SECONDS.sleep(Math.min(timeout, maxRetryTimeout)); + } catch (InterruptedException ignore) { + outputLock.lock(); + try { + TcpOutputStream.this.connected = false; + } finally { + outputLock.unlock(); + } + break; + } + } finally { + // It's possible the thread was interrupted, if we're not connected we should clean up the socket + if (!connected) { + safeClose(socket); + } + } + } + } + } +} diff --git a/logging/src/main/java/org/xbib/logging/io/UdpOutputStream.java b/logging/src/main/java/org/xbib/logging/io/UdpOutputStream.java new file mode 100644 index 0000000..5c39032 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/io/UdpOutputStream.java @@ -0,0 +1,57 @@ +package org.xbib.logging.io; + +import java.io.Flushable; +import java.io.IOException; +import java.io.OutputStream; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.SocketAddress; +import java.net.SocketException; +import org.xbib.logging.net.ClientSocketFactory; + +/** + * An output stream that writes data to a {@link DatagramSocket DatagramSocket}. + */ +@SuppressWarnings("WeakerAccess") +public class UdpOutputStream extends OutputStream implements AutoCloseable, Flushable { + private final DatagramSocket socket; + private final SocketAddress socketAddress; + + public UdpOutputStream(final InetAddress address, final int port) throws IOException { + this(ClientSocketFactory.of(address, port)); + } + + public UdpOutputStream(final ClientSocketFactory socketManager) throws SocketException { + socket = socketManager.createDatagramSocket(); + socketAddress = socketManager.getSocketAddress(); + } + + @Override + public void write(final int b) throws IOException { + final byte[] msg = new byte[]{(byte) b}; + final DatagramPacket packet = new DatagramPacket(msg, 1, socketAddress); + socket.send(packet); + } + + @Override + public void write(final byte[] b) throws IOException { + if (b != null) { + final DatagramPacket packet = new DatagramPacket(b, b.length, socketAddress); + socket.send(packet); + } + } + + @Override + public void write(final byte[] b, final int off, final int len) throws IOException { + if (b != null) { + final DatagramPacket packet = new DatagramPacket(b, off, len, socketAddress); + socket.send(packet); + } + } + + @Override + public void close() throws IOException { + socket.close(); + } +} diff --git a/logging/src/main/java/org/xbib/logging/io/UncloseableOutputStream.java b/logging/src/main/java/org/xbib/logging/io/UncloseableOutputStream.java new file mode 100644 index 0000000..04cdedc --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/io/UncloseableOutputStream.java @@ -0,0 +1,33 @@ +package org.xbib.logging.io; + +import java.io.IOException; +import java.io.OutputStream; + +public final class UncloseableOutputStream extends OutputStream { + + private final OutputStream delegate; + + public UncloseableOutputStream(final OutputStream delegate) { + this.delegate = delegate; + } + + public void write(final int b) throws IOException { + delegate.write(b); + } + + public void write(final byte[] b) throws IOException { + delegate.write(b); + } + + public void write(final byte[] b, final int off, final int len) throws IOException { + delegate.write(b, off, len); + } + + public void flush() throws IOException { + delegate.flush(); + } + + public void close() { + // ignore + } +} diff --git a/logging/src/main/java/org/xbib/logging/io/UncloseableWriter.java b/logging/src/main/java/org/xbib/logging/io/UncloseableWriter.java new file mode 100644 index 0000000..a6b6c07 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/io/UncloseableWriter.java @@ -0,0 +1,55 @@ +package org.xbib.logging.io; + +import java.io.IOException; +import java.io.Writer; + +/** + * An output stream wrapper which drops calls to the {@code close()} method. + */ +public final class UncloseableWriter extends Writer { + private final Writer delegate; + + public UncloseableWriter(final Writer delegate) { + this.delegate = delegate; + } + + public void write(final int c) throws IOException { + delegate.write(c); + } + + public void write(final char[] cbuf) throws IOException { + delegate.write(cbuf); + } + + public void write(final char[] cbuf, final int off, final int len) throws IOException { + delegate.write(cbuf, off, len); + } + + public void write(final String str) throws IOException { + delegate.write(str); + } + + public void write(final String str, final int off, final int len) throws IOException { + delegate.write(str, off, len); + } + + public Writer append(final CharSequence csq) throws IOException { + return delegate.append(csq); + } + + public Writer append(final CharSequence csq, final int start, final int end) throws IOException { + return delegate.append(csq, start, end); + } + + public Writer append(final char c) throws IOException { + return delegate.append(c); + } + + public void flush() throws IOException { + delegate.flush(); + } + + public void close() { + // ignore + } +} diff --git a/logging/src/main/java/org/xbib/logging/io/UninterruptibleOutputStream.java b/logging/src/main/java/org/xbib/logging/io/UninterruptibleOutputStream.java new file mode 100644 index 0000000..015cb62 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/io/UninterruptibleOutputStream.java @@ -0,0 +1,133 @@ +package org.xbib.logging.io; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.io.OutputStream; +import static java.lang.Thread.currentThread; +import static java.lang.Thread.interrupted; + +/** + * An output stream which is not interruptible. + */ +public final class UninterruptibleOutputStream extends OutputStream { + private final OutputStream out; + + /** + * Construct a new instance. + * + * @param out the delegate stream + */ + public UninterruptibleOutputStream(final OutputStream out) { + this.out = out; + } + + /** + * Write the given byte uninterruptibly. + * + * @param b the byte to write + * @throws IOException if an error occurs + */ + public void write(final int b) throws IOException { + boolean intr = false; + try { + for (; ; ) + try { + out.write(b); + return; + } catch (InterruptedIOException e) { + final int transferred = e.bytesTransferred; + if (transferred == 1) { + return; + } + intr |= interrupted(); + } + } finally { + if (intr) { + currentThread().interrupt(); + } + } + } + + /** + * Write the given bytes uninterruptibly. + * + * @param b the bytes to write + * @param off the offset into the array + * @param len the length of the array to write + * @throws IOException if an error occurs + */ + public void write(final byte[] b, int off, int len) throws IOException { + boolean intr = false; + try { + while (len > 0) + try { + out.write(b, off, len); + return; + } catch (InterruptedIOException e) { + final int transferred = e.bytesTransferred; + if (transferred > 0) { + off += transferred; + len -= transferred; + } + intr |= interrupted(); + } + } finally { + if (intr) { + currentThread().interrupt(); + } + } + } + + /** + * Flush the stream uninterruptibly. + * + * @throws IOException if an error occurs + */ + public void flush() throws IOException { + boolean intr = false; + try { + for (; ; ) + try { + out.flush(); + return; + } catch (InterruptedIOException e) { + intr |= interrupted(); + } + } finally { + if (intr) { + currentThread().interrupt(); + } + } + } + + /** + * Close the stream uninterruptibly. + * + * @throws IOException if an error occurs + */ + public void close() throws IOException { + boolean intr = false; + try { + for (; ; ) + try { + out.close(); + return; + } catch (InterruptedIOException e) { + intr |= interrupted(); + } + } finally { + if (intr) { + currentThread().interrupt(); + } + } + } + + /** + * Get the string representation of this stream. + * + * @return the string + */ + public String toString() { + return "uninterruptible " + out.toString(); + } +} diff --git a/logging/src/main/java/org/xbib/logging/net/ClientSocketFactory.java b/logging/src/main/java/org/xbib/logging/net/ClientSocketFactory.java new file mode 100644 index 0000000..5c3a5ed --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/net/ClientSocketFactory.java @@ -0,0 +1,110 @@ +package org.xbib.logging.net; + +import java.io.IOException; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.SocketException; +import javax.net.SocketFactory; + +/** + * A factory used to create writable sockets. + */ +public interface ClientSocketFactory { + + /** + * Creates a datagram socket for UDP communication. + * + * @return the newly created socket + * @throws SocketException if binding the socket fails + */ + DatagramSocket createDatagramSocket() throws SocketException; + + /** + * Creates a TCP socket. + * + * @return the newly created socket + * @throws IOException if an error occurs creating the socket + */ + Socket createSocket() throws IOException; + + /** + * Returns the address being used to create sockets. + * + * @return the address being used + */ + InetAddress getAddress(); + + /** + * Returns the port being used to create sockets. + * + * @return the port being used + */ + int getPort(); + + /** + * A convenience method to return the socket address. + *

+ * The default implementation simply returns {@code new InetSocketAddress(getAddress(), getPort())}. + *

+ * + * @return a socket address + */ + default SocketAddress getSocketAddress() { + return new InetSocketAddress(getAddress(), getPort()); + } + + /** + * Creates a new default implementation of the factory which uses {@link SocketFactory#getDefault()} for TCP + * sockets and {@code new DatagramSocket()} for UDP sockets. + * + * @param address the address to bind to + * @param port the port to bind to + * @return the client socket factory + */ + static ClientSocketFactory of(final InetAddress address, final int port) { + return of(SocketFactory.getDefault(), address, port); + } + + /** + * Creates a new default implementation of the factory which uses the provided + * {@linkplain SocketFactory#createSocket(InetAddress, int) socket factory} to create TCP connections and + * {@code new DatagramSocket()} for UDP sockets. + * + * @param socketFactory the socket factory used for TCP connections, if {@code null} the + * {@linkplain SocketFactory#getDefault() default} socket factory will be used + * @param address the address to bind to + * @param port the port to bind to + * @return the client socket factory + */ + static ClientSocketFactory of(final SocketFactory socketFactory, final InetAddress address, final int port) { + if (address == null || port < 0) { + throw new IllegalArgumentException(String + .format("The address cannot be null (%s) and the port must be a positive integer (%d)", address, port)); + } + final SocketFactory factory = (socketFactory == null ? SocketFactory.getDefault() : socketFactory); + return new ClientSocketFactory() { + @Override + public DatagramSocket createDatagramSocket() throws SocketException { + return new DatagramSocket(); + } + + @Override + public Socket createSocket() throws IOException { + return factory.createSocket(address, port); + } + + @Override + public InetAddress getAddress() { + return address; + } + + @Override + public int getPort() { + return port; + } + }; + } +} diff --git a/logging/src/main/java/org/xbib/logging/os/GetHostInfoAction.java b/logging/src/main/java/org/xbib/logging/os/GetHostInfoAction.java new file mode 100644 index 0000000..43a24e2 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/os/GetHostInfoAction.java @@ -0,0 +1,60 @@ +package org.xbib.logging.os; + +import java.net.UnknownHostException; +import java.util.regex.Pattern; + +final class GetHostInfoAction { + + GetHostInfoAction() { + } + + public String[] run() { + // allow host name to be overridden + String qualifiedHostName = System.getProperty("xbib.host.name"); + String simpleHostName = System.getProperty("xbib.simple.host.name"); + String providedNodeName = System.getProperty("xbib.node.name"); + if (qualifiedHostName == null) { + // if host name is specified, don't pick a qualified host name that isn't related to it + qualifiedHostName = simpleHostName; + if (qualifiedHostName == null) { + // POSIX-like OSes including Mac should have this set + qualifiedHostName = System.getenv("HOSTNAME"); + } + if (qualifiedHostName == null) { + // Certain versions of Windows + qualifiedHostName = System.getenv("COMPUTERNAME"); + } + if (qualifiedHostName == null) { + try { + qualifiedHostName = HostName.getLocalHost().getHostName(); + } catch (UnknownHostException e) { + // ignore + } + } + if (qualifiedHostName != null + && Pattern.compile("^\\d+\\.\\d+\\.\\d+\\.\\d+$|:").matcher(qualifiedHostName).find()) { + // IP address is not acceptable + qualifiedHostName = null; + } + if (qualifiedHostName == null) { + // Give up + qualifiedHostName = "unknown-host.unknown-domain"; + } else { + qualifiedHostName = qualifiedHostName.trim().toLowerCase(); + } + } + if (simpleHostName == null) { + // Use the host part of the qualified host name + final int idx = qualifiedHostName.indexOf('.'); + simpleHostName = idx == -1 ? qualifiedHostName : qualifiedHostName.substring(0, idx); + } + if (providedNodeName == null) { + providedNodeName = simpleHostName; + } + return new String[]{ + simpleHostName, + qualifiedHostName, + providedNodeName + }; + } +} diff --git a/logging/src/main/java/org/xbib/logging/os/HostName.java b/logging/src/main/java/org/xbib/logging/os/HostName.java new file mode 100644 index 0000000..6398378 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/os/HostName.java @@ -0,0 +1,88 @@ +package org.xbib.logging.os; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * Methods for getting the system host name. The host name is detected from the environment, but may be overridden by + * use of the {@code xbib.host.name} system property. + */ +public final class HostName { + + private static final Object lock = new Object(); + private static volatile String hostName; + private static volatile String qualifiedHostName; + private static volatile String nodeName; + + static { + GetHostInfoAction action = new GetHostInfoAction(); + String[] names = action.run(); + hostName = names[0]; + qualifiedHostName = names[1]; + nodeName = names[2]; + } + + private HostName() { + } + + static InetAddress getLocalHost() throws UnknownHostException { + InetAddress addr; + try { + addr = InetAddress.getLocalHost(); + } catch (ArrayIndexOutOfBoundsException e) { //this is workaround for mac osx bug see AS7-3223 and JGRP-1404 + addr = InetAddress.getByName(null); + } + return addr; + } + + /** + * Get the detected host name. + * + * @return the detected host name + */ + public static String getHostName() { + return hostName; + } + + /** + * Get the detected qualified host name. + * + * @return the detected qualified host name + */ + public static String getQualifiedHostName() { + return qualifiedHostName; + } + + /** + * Get the node name. + * + * @return the node name + */ + public static String getNodeName() { + return nodeName; + } + + /** + * Set the host name. The qualified host name is set directly from the given value; the unqualified host name + * is then re-derived from that value. The node name is not changed by this method. + * + * @param qualifiedHostName the host name + */ + public static void setQualifiedHostName(final String qualifiedHostName) { + synchronized (lock) { + HostName.qualifiedHostName = qualifiedHostName; + // Use the host part of the qualified host name + final int idx = qualifiedHostName.indexOf('.'); + HostName.hostName = idx == -1 ? qualifiedHostName : qualifiedHostName.substring(0, idx); + } + } + + /** + * Set the node name. + * + * @param nodeName the node name + */ + public static void setNodeName(final String nodeName) { + HostName.nodeName = nodeName; + } +} diff --git a/logging/src/main/java/org/xbib/logging/os/Process.java b/logging/src/main/java/org/xbib/logging/os/Process.java new file mode 100644 index 0000000..c909b30 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/os/Process.java @@ -0,0 +1,31 @@ +package org.xbib.logging.os; + +/** + * Utilities for getting information about the current process. + */ +public final class Process { + private Process() { + } + + /** + * Get the name of this process. If the process name is not known, then "<unknown>" is returned. + * The process name may be overridden by setting the {@code xbib.process.name} property. + * + * @return the process name (not {@code null}) + */ + public static String getProcessName() { + return computeProcessName(); + } + + private static String computeProcessName() { + final ProcessHandle processHandle = ProcessHandle.current(); + String processName = System.getProperty("xbib.process.name"); + if (processName == null) { + processName = processHandle.info().command().orElse(null); + } + if (processName == null) { + processName = ""; + } + return processName; + } +} diff --git a/logging/src/main/java/org/xbib/logging/ref/CleanerReference.java b/logging/src/main/java/org/xbib/logging/ref/CleanerReference.java new file mode 100644 index 0000000..3344bd0 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/ref/CleanerReference.java @@ -0,0 +1,36 @@ +package org.xbib.logging.ref; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * A special version of {@link PhantomReference} that is strongly retained until it is reaped by the collection thread. + */ +public class CleanerReference extends PhantomReference { + private static final Set> set = Collections.newSetFromMap(new ConcurrentHashMap<>()); + + /** + * Construct a new instance with a reaper. + * + * @param referent the referent + * @param attachment the attachment + * @param reaper the reaper to use + */ + public CleanerReference(final T referent, final A attachment, final Reaper reaper) { + super(referent, attachment, reaper); + set.add(this); + } + + void clean() { + set.remove(this); + } + + public final int hashCode() { + return super.hashCode(); + } + + public final boolean equals(final Object obj) { + return super.equals(obj); + } +} diff --git a/logging/src/main/java/org/xbib/logging/ref/PhantomReference.java b/logging/src/main/java/org/xbib/logging/ref/PhantomReference.java new file mode 100644 index 0000000..aee4ae9 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/ref/PhantomReference.java @@ -0,0 +1,58 @@ +package org.xbib.logging.ref; + +import java.lang.ref.ReferenceQueue; + +/** + * A reapable phantom reference with an attachment. If a {@link Reaper} is given, then it will be used to asynchronously + * clean up the referent. + * + * @param the reference value type + * @param the attachment type + * @see java.lang.ref.PhantomReference + */ +public class PhantomReference extends java.lang.ref.PhantomReference implements Reference, Reapable { + private final A attachment; + private final Reaper reaper; + + /** + * Construct a new instance with an explicit reference queue. + * + * @param referent the referent + * @param attachment the attachment + * @param q the reference queue to use + */ + public PhantomReference(final T referent, final A attachment, final ReferenceQueue q) { + super(referent, q); + this.attachment = attachment; + reaper = null; + } + + /** + * Construct a new instance with a reaper. + * + * @param referent the referent + * @param attachment the attachment + * @param reaper the reaper to use + */ + public PhantomReference(final T referent, final A attachment, final Reaper reaper) { + super(referent, References.ReaperThread.getReaperQueue()); + this.reaper = reaper; + this.attachment = attachment; + } + + public A getAttachment() { + return attachment; + } + + public Type getType() { + return Type.PHANTOM; + } + + public Reaper getReaper() { + return reaper; + } + + public String toString() { + return "phantom reference"; + } +} diff --git a/logging/src/main/java/org/xbib/logging/ref/Reapable.java b/logging/src/main/java/org/xbib/logging/ref/Reapable.java new file mode 100644 index 0000000..bacba4f --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/ref/Reapable.java @@ -0,0 +1,17 @@ +package org.xbib.logging.ref; + +/** + * A reference which is reapable (can be automatically collected). + * + * @param the reference type + * @param the reference attachment type + */ +interface Reapable { + + /** + * Get the associated reaper. + * + * @return the reaper + */ + Reaper getReaper(); +} diff --git a/logging/src/main/java/org/xbib/logging/ref/Reaper.java b/logging/src/main/java/org/xbib/logging/ref/Reaper.java new file mode 100644 index 0000000..e54788a --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/ref/Reaper.java @@ -0,0 +1,17 @@ +package org.xbib.logging.ref; + +/** + * A cleaner for a dead object. + * + * @param the reference type + * @param the reference attachment type + */ +public interface Reaper { + + /** + * Perform the cleanup action for a reference. + * + * @param reference the reference + */ + void reap(Reference reference); +} diff --git a/logging/src/main/java/org/xbib/logging/ref/Reference.java b/logging/src/main/java/org/xbib/logging/ref/Reference.java new file mode 100644 index 0000000..12a378c --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/ref/Reference.java @@ -0,0 +1,127 @@ +package org.xbib.logging.ref; + +import java.util.EnumSet; + +/** + * An enhanced reference type with a type-safe attachment. + * + * @param the reference value type + * @param the attachment type + * @see java.lang.ref.Reference + */ +public interface Reference { + + /** + * Get the value, or {@code null} if the reference has been cleared. + * + * @return the value + */ + T get(); + + /** + * Get the attachment, if any. + * + * @return the attachment + */ + A getAttachment(); + + /** + * Clear the reference. + */ + void clear(); + + /** + * Get the type of the reference. + * + * @return the type + */ + Type getType(); + + /** + * A reference type. + */ + enum Type { + + /** + * A strong reference. + */ + STRONG, + /** + * A weak reference. + */ + WEAK, + /** + * A phantom reference. + */ + PHANTOM, + /** + * A soft reference. + */ + SOFT, + /** + * A {@code null} reference. + */ + NULL, + ; + + private static final int fullSize = values().length; + + /** + * Determine whether the given set is fully populated (or "full"), meaning it contains all possible values. + * + * @param set the set + * @return {@code true} if the set is full, {@code false} otherwise + */ + public static boolean isFull(final EnumSet set) { + return set != null && set.size() == fullSize; + } + + /** + * Determine whether this instance is equal to one of the given instances. + * + * @param v1 the first instance + * @return {@code true} if one of the instances matches this one, {@code false} otherwise + */ + public boolean in(final Type v1) { + return this == v1; + } + + /** + * Determine whether this instance is equal to one of the given instances. + * + * @param v1 the first instance + * @param v2 the second instance + * @return {@code true} if one of the instances matches this one, {@code false} otherwise + */ + public boolean in(final Type v1, final Type v2) { + return this == v1 || this == v2; + } + + /** + * Determine whether this instance is equal to one of the given instances. + * + * @param v1 the first instance + * @param v2 the second instance + * @param v3 the third instance + * @return {@code true} if one of the instances matches this one, {@code false} otherwise + */ + public boolean in(final Type v1, final Type v2, final Type v3) { + return this == v1 || this == v2 || this == v3; + } + + /** + * Determine whether this instance is equal to one of the given instances. + * + * @param values the possible values + * @return {@code true} if one of the instances matches this one, {@code false} otherwise + */ + public boolean in(final Type... values) { + if (values != null) + for (Type value : values) { + if (this == value) + return true; + } + return false; + } + } +} diff --git a/logging/src/main/java/org/xbib/logging/ref/References.java b/logging/src/main/java/org/xbib/logging/ref/References.java new file mode 100644 index 0000000..7a0fd1b --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/ref/References.java @@ -0,0 +1,151 @@ +package org.xbib.logging.ref; + +import java.lang.ref.ReferenceQueue; + +/** + * A set of utility methods for reference types. + */ +public final class References { + private References() { + } + + private static final Reference NULL = new StrongReference<>(null); + + static final class BuildTimeHolder { + static final ReferenceQueue REAPER_QUEUE = new ReferenceQueue(); + } + + static final class ReaperThread extends Thread { + static ReferenceQueue getReaperQueue() { + return BuildTimeHolder.REAPER_QUEUE; + } + + static { + startThreadAction(1); + } + + + private static Void startThreadAction(int id) { + final ReaperThread thr = new ReaperThread(); + thr.setName("Reference Reaper #" + id); + thr.setDaemon(true); + thr.start(); + return null; + } + + public void run() { + for (; ; ) + try { + final java.lang.ref.Reference ref = ReaperThread.getReaperQueue().remove(); + if (ref instanceof CleanerReference) { + ((CleanerReference) ref).clean(); + } + if (ref instanceof Reapable) { + reap((Reapable) ref); + } + } catch (InterruptedException ignored) { + // we consume interrupts. + } catch (Throwable cause) { + // ignore failures. + } + } + + @SuppressWarnings({"unchecked"}) + private static void reap(final Reapable reapable) { + reapable.getReaper().reap((Reference) reapable); + } + } + + /** + * Create a reference of a given type with the provided value and attachment. If the reference type is + * {@link Reference.Type#STRONG} or {@link Reference.Type#NULL} then the reaper argument is ignored. If + * the reference type is {@link Reference.Type#NULL} then the value and attachment arguments are ignored. + * + * @param type the reference type + * @param value the reference value + * @param attachment the attachment value + * @param reaper the reaper to use, if any + * @param the reference value type + * @param the reference attachment type + * @return the reference + */ + public static Reference create(Reference.Type type, T value, A attachment, Reaper reaper) { + if (value == null) { + type = Reference.Type.NULL; + } + return switch (type) { + case STRONG -> new StrongReference(value, attachment); + case WEAK -> new WeakReference(value, attachment, reaper); + case PHANTOM -> new PhantomReference(value, attachment, reaper); + case SOFT -> new SoftReference(value, attachment, reaper); + case NULL -> attachment == null ? getNullReference() : new StrongReference<>(null, attachment); + }; + } + + /** + * Create a reference of a given type with the provided value and attachment. If the reference type is + * {@link Reference.Type#STRONG} or {@link Reference.Type#NULL} then the reference queue argument is ignored. If + * the reference type is {@link Reference.Type#NULL} then the value and attachment arguments are ignored. + * + * @param type the reference type + * @param value the reference value + * @param attachment the attachment value + * @param referenceQueue the reference queue to use, if any + * @param the reference value type + * @param the reference attachment type + * @return the reference + */ + public static Reference create(Reference.Type type, T value, A attachment, + ReferenceQueue referenceQueue) { + if (referenceQueue == null) + return create(type, value, attachment); + if (value == null) { + type = Reference.Type.NULL; + } + return switch (type) { + case STRONG -> new StrongReference(value, attachment); + case WEAK -> new WeakReference(value, attachment, referenceQueue); + case PHANTOM -> new PhantomReference(value, attachment, referenceQueue); + case SOFT -> new SoftReference(value, attachment, referenceQueue); + case NULL -> attachment == null ? getNullReference() : new StrongReference<>(null, attachment); + }; + } + + /** + * Create a reference of a given type with the provided value and attachment. If the reference type is + * {@link Reference.Type#PHANTOM} then this method will return a {@code null} reference because + * such references are not constructable without a queue or reaper. If the reference type is + * {@link Reference.Type#NULL} then the value and attachment arguments are ignored. + * + * @param type the reference type + * @param value the reference value + * @param attachment the attachment value + * @param the reference value type + * @param the reference attachment type + * @return the reference + */ + public static Reference create(Reference.Type type, T value, A attachment) { + if (value == null) { + type = Reference.Type.NULL; + } + return switch (type) { + case STRONG -> new StrongReference(value, attachment); + case WEAK -> new WeakReference(value, attachment); + case SOFT -> new SoftReference(value, attachment); + case PHANTOM, NULL -> attachment == null ? getNullReference() : new StrongReference<>(null, attachment); + }; + } + + /** + * Get a {@code null} reference. This reference type is always cleared and does not retain an attachment; as such + * there is only one single instance of it. + * + * @param the reference value type + * @param the attachment value type + * @return the {@code null} reference + */ + @SuppressWarnings({"unchecked"}) + public static Reference getNullReference() { + return (Reference) NULL; + } +} diff --git a/logging/src/main/java/org/xbib/logging/ref/SoftReference.java b/logging/src/main/java/org/xbib/logging/ref/SoftReference.java new file mode 100644 index 0000000..0347114 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/ref/SoftReference.java @@ -0,0 +1,77 @@ +package org.xbib.logging.ref; + +import java.lang.ref.ReferenceQueue; + +/** + * A reapable soft reference with an attachment. If a {@link Reaper} is given, then it will be used to asynchronously + * clean up the referent. + * + * @param the reference value type + * @param the attachment type + * @see java.lang.ref.SoftReference + */ +public class SoftReference extends java.lang.ref.SoftReference implements Reference, Reapable { + private final A attachment; + private final Reaper reaper; + + /** + * Construct a new instance. + * + * @param referent the referent + */ + public SoftReference(final T referent) { + this(referent, null, (ReferenceQueue) null); + } + + /** + * Construct a new instance. + * + * @param referent the referent + * @param attachment the attachment + */ + public SoftReference(final T referent, final A attachment) { + this(referent, attachment, (ReferenceQueue) null); + } + + /** + * Construct a new instance with an explicit reference queue. + * + * @param referent the referent + * @param attachment the attachment + * @param q the reference queue to use + */ + public SoftReference(final T referent, final A attachment, final ReferenceQueue q) { + super(referent, q); + reaper = null; + this.attachment = attachment; + } + + /** + * Construct a new instance with a reaper. + * + * @param referent the referent + * @param attachment the attachment + * @param reaper the reaper to use + */ + public SoftReference(final T referent, final A attachment, final Reaper reaper) { + super(referent, References.ReaperThread.getReaperQueue()); + this.reaper = reaper; + this.attachment = attachment; + } + + public Reaper getReaper() { + return reaper; + } + + public A getAttachment() { + return attachment; + } + + public Type getType() { + return Type.SOFT; + } + + public String toString() { + return "soft reference to " + get(); + } +} diff --git a/logging/src/main/java/org/xbib/logging/ref/StrongReference.java b/logging/src/main/java/org/xbib/logging/ref/StrongReference.java new file mode 100644 index 0000000..9b120d8 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/ref/StrongReference.java @@ -0,0 +1,53 @@ +package org.xbib.logging.ref; + +/** + * A strong reference with an attachment. Since strong references are always reachable, a reaper may not be used. + * + * @param the reference value type + * @param the attachment type + */ +public class StrongReference implements Reference { + + private volatile T referent; + private final A attachment; + + /** + * Construct a new instance. + * + * @param referent the referent + * @param attachment the attachment + */ + public StrongReference(final T referent, final A attachment) { + this.referent = referent; + this.attachment = attachment; + } + + /** + * Construct a new instance. + * + * @param referent the referent + */ + public StrongReference(final T referent) { + this(referent, null); + } + + public T get() { + return referent; + } + + public void clear() { + referent = null; + } + + public A getAttachment() { + return attachment; + } + + public Type getType() { + return Type.STRONG; + } + + public String toString() { + return "strong reference to " + get(); + } +} diff --git a/logging/src/main/java/org/xbib/logging/ref/WeakReference.java b/logging/src/main/java/org/xbib/logging/ref/WeakReference.java new file mode 100644 index 0000000..09ed3e9 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/ref/WeakReference.java @@ -0,0 +1,77 @@ +package org.xbib.logging.ref; + +import java.lang.ref.ReferenceQueue; + +/** + * A reapable weak reference with an attachment. If a {@link Reaper} is given, then it will be used to asynchronously + * clean up the referent. + * + * @param the reference value type + * @param the attachment type + * @see java.lang.ref.WeakReference + */ +public class WeakReference extends java.lang.ref.WeakReference implements Reference, Reapable { + private final A attachment; + private final Reaper reaper; + + /** + * Construct a new instance. + * + * @param referent the referent + */ + public WeakReference(final T referent) { + this(referent, null, (Reaper) null); + } + + /** + * Construct a new instance. + * + * @param referent the referent + * @param attachment the attachment + */ + public WeakReference(final T referent, final A attachment) { + this(referent, attachment, (Reaper) null); + } + + /** + * Construct a new instance with an explicit reference queue. + * + * @param referent the referent + * @param attachment the attachment + * @param q the reference queue to use + */ + public WeakReference(final T referent, final A attachment, final ReferenceQueue q) { + super(referent, q); + this.attachment = attachment; + reaper = null; + } + + /** + * Construct a new instance with a reaper. + * + * @param referent the referent + * @param attachment the attachment + * @param reaper the reaper to use + */ + public WeakReference(final T referent, final A attachment, final Reaper reaper) { + super(referent, References.ReaperThread.getReaperQueue()); + this.attachment = attachment; + this.reaper = reaper; + } + + public A getAttachment() { + return attachment; + } + + public Type getType() { + return Type.WEAK; + } + + public Reaper getReaper() { + return reaper; + } + + public String toString() { + return "weak reference to " + get(); + } +} diff --git a/logging/src/main/java/org/xbib/logging/util/AtomicArray.java b/logging/src/main/java/org/xbib/logging/util/AtomicArray.java new file mode 100644 index 0000000..6f8d4f9 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/util/AtomicArray.java @@ -0,0 +1,372 @@ +package org.xbib.logging.util; + +import java.lang.reflect.Array; +import java.util.Arrays; +import java.util.Comparator; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.logging.Handler; + +/** + * Utility for snapshot/copy-on-write arrays. To use these methods, two things are required: an immutable array + * stored on a volatile field, and an instance of + * {@link AtomicReferenceFieldUpdater AtomicReferenceFieldUpdater} + * which corresponds to that field. Some of these methods perform multi-step operations; if the array field value is + * changed in the middle of such an operation, the operation is retried. To avoid spinning, in some situations it + * may be advisable to hold a write lock to prevent multiple concurrent updates. + * + * @param the type which contains the target field + * @param the array value type + */ +public final class AtomicArray { + + private final AtomicReferenceFieldUpdater updater; + private final Class componentType; + private final V[] emptyArray; + + /** + * Construct an instance. + * + * @param updater the field updater + * @param componentType the component class + */ + public AtomicArray(AtomicReferenceFieldUpdater updater, Class componentType) { + this.updater = updater; + this.componentType = componentType; + emptyArray = newInstance(componentType, 0); + } + + /** + * Convenience method to create an instance. + * + * @param updater the field updater + * @param componentType the component class + * @param the type which contains the target field + * @param the array value type + * @return the new instance + */ + public static AtomicArray create(AtomicReferenceFieldUpdater updater, Class componentType) { + return new AtomicArray(updater, componentType); + } + + /** + * Convenience method to set the field value to the empty array. Empty array instances are shared. + * + * @param instance the instance holding the field + */ + public void clear(T instance) { + updater.set(instance, emptyArray); + } + + /** + * Update the value of this array. + * + * @param instance the instance holding the field + * @param value the new value + */ + public void set(T instance, V[] value) { + updater.set(instance, value); + } + + /** + * Atomically get and update the value of this array. + * + * @param instance the instance holding the field + * @param value the new value + */ + public V[] getAndSet(T instance, V[] value) { + return updater.getAndSet(instance, value); + } + + @SuppressWarnings({"unchecked"}) + private static V[] copyOf(final Class componentType, V[] old, int newLen) { + final V[] target = newInstance(componentType, newLen); + System.arraycopy(old, 0, target, 0, Math.min(old.length, newLen)); + return target; + } + + /** + * Atomically replace the array with a new array which is one element longer, and which includes the given value. + * + * @param instance the instance holding the field + * @param value the updated value + */ + public void add(T instance, V value) { + final AtomicReferenceFieldUpdater updater = this.updater; + for (; ; ) { + final V[] oldVal = updater.get(instance); + final int oldLen = oldVal.length; + final V[] newVal = copyOf(componentType, oldVal, oldLen + 1); + newVal[oldLen] = value; + if (updater.compareAndSet(instance, oldVal, newVal)) { + return; + } + } + } + + /** + * Atomically replace the array with a new array which is one element longer, and which includes the given value, + * if the value is not already present within the array. This method does a linear search for the target value. + * + * @param instance the instance holding the field + * @param value the updated value + * @param identity {@code true} if comparisons should be done using reference identity, or {@code false} to use the + * {@code equals()} method + * @return {@code true} if the value was added, or {@code false} if it was already present + */ + public boolean addIfAbsent(T instance, V value, boolean identity) { + final AtomicReferenceFieldUpdater updater = this.updater; + for (; ; ) { + final V[] oldVal = updater.get(instance); + final int oldLen = oldVal.length; + if (identity || value == null) { + for (int i = 0; i < oldLen; i++) { + if (oldVal[i] == value) { + return false; + } + } + } else { + for (int i = 0; i < oldLen; i++) { + if (value.equals(oldVal[i])) { + return false; + } + } + } + final V[] newVal = copyOf(componentType, oldVal, oldLen + 1); + newVal[oldLen] = value; + if (updater.compareAndSet(instance, oldVal, newVal)) { + return true; + } + } + } + + /** + * Atomically replace the array with a new array which does not include the first occurrence of the given value, if + * the value is present in the array. + * + * @param instance the instance holding the field + * @param value the updated value + * @param identity {@code true} if comparisons should be done using reference identity, or {@code false} to use the + * {@code equals()} method + * @return {@code true} if the value was removed, or {@code false} if it was not present + */ + public boolean remove(T instance, V value, boolean identity) { + final AtomicReferenceFieldUpdater updater = this.updater; + for (; ; ) { + final V[] oldVal = updater.get(instance); + final int oldLen = oldVal.length; + if (oldLen == 0) { + return false; + } else { + int index = -1; + if (identity || value == null) { + for (int i = 0; i < oldLen; i++) { + if (oldVal[i] == value) { + index = i; + break; + } + } + } else { + for (int i = 0; i < oldLen; i++) { + if (value.equals(oldVal[i])) { + index = i; + break; + } + } + } + if (index == -1) { + return false; + } + final V[] newVal = newInstance(componentType, oldLen - 1); + System.arraycopy(oldVal, 0, newVal, 0, index); + System.arraycopy(oldVal, index + 1, newVal, index, oldLen - index - 1); + if (updater.compareAndSet(instance, oldVal, newVal)) { + return true; + } + } + } + } + + /** + * Atomically replace the array with a new array which does not include any occurrences of the given value, if + * the value is present in the array. + * + * @param instance the instance holding the field + * @param value the updated value + * @param identity {@code true} if comparisons should be done using reference identity, or {@code false} to use the + * {@code equals()} method + * @return the number of values removed + */ + public int removeAll(T instance, V value, boolean identity) { + final AtomicReferenceFieldUpdater updater = this.updater; + for (; ; ) { + final V[] oldVal = updater.get(instance); + final int oldLen = oldVal.length; + if (oldLen == 0) { + return 0; + } else { + final boolean[] removeSlots = new boolean[oldLen]; + int removeCount = 0; + if (identity || value == null) { + for (int i = 0; i < oldLen; i++) { + if (oldVal[i] == value) { + removeSlots[i] = true; + removeCount++; + } + } + } else { + for (int i = 0; i < oldLen; i++) { + if (value.equals(oldVal[i])) { + removeSlots[i] = true; + removeCount++; + } + } + } + if (removeCount == 0) { + return 0; + } + final int newLen = oldLen - removeCount; + final V[] newVal; + if (newLen == 0) { + newVal = emptyArray; + } else { + newVal = newInstance(componentType, newLen); + for (int i = 0, j = 0; i < oldLen; i++) { + if (!removeSlots[i]) { + newVal[j++] = oldVal[i]; + } + } + } + if (updater.compareAndSet(instance, oldVal, newVal)) { + return removeCount; + } + } + } + } + + /** + * Add a value to a sorted array. Does not check for duplicates. + * + * @param instance the instance holding the field + * @param value the value to add + * @param comparator a comparator, or {@code null} to use natural ordering + */ + public void add(T instance, V value, Comparator comparator) { + final AtomicReferenceFieldUpdater updater = this.updater; + for (; ; ) { + final V[] oldVal = updater.get(instance); + final int oldLen = oldVal.length; + final int pos = insertionPoint(Arrays.binarySearch(oldVal, value, comparator)); + final V[] newVal = newInstance(componentType, oldLen + 1); + System.arraycopy(oldVal, 0, newVal, 0, pos); + newVal[pos] = value; + System.arraycopy(oldVal, pos, newVal, pos + 1, oldLen - pos); + if (updater.compareAndSet(instance, oldVal, newVal)) { + return; + } + } + } + + /** + * Add a value to a sorted array if it is not already present. Does not check for duplicates. + * + * @param instance the instance holding the field + * @param value the value to add + * @param comparator a comparator, or {@code null} to use natural ordering + */ + public boolean addIfAbsent(T instance, V value, Comparator comparator) { + final AtomicReferenceFieldUpdater updater = this.updater; + for (; ; ) { + final V[] oldVal = updater.get(instance); + final int oldLen = oldVal.length; + final int pos = Arrays.binarySearch(oldVal, value, comparator); + if (pos < 0) { + return false; + } + final V[] newVal = newInstance(componentType, oldLen + 1); + System.arraycopy(oldVal, 0, newVal, 0, pos); + newVal[pos] = value; + System.arraycopy(oldVal, pos, newVal, pos + 1, oldLen - pos); + if (updater.compareAndSet(instance, oldVal, newVal)) { + return true; + } + } + } + + /** + * Remove a value to a sorted array. Does not check for duplicates. If there are multiple occurrences of a value, + * there is no guarantee as to which one is removed. + * + * @param instance the instance holding the field + * @param value the value to remove + * @param comparator a comparator, or {@code null} to use natural ordering + */ + public boolean remove(T instance, V value, Comparator comparator) { + final AtomicReferenceFieldUpdater updater = this.updater; + for (; ; ) { + final V[] oldVal = updater.get(instance); + final int oldLen = oldVal.length; + if (oldLen == 0) { + return false; + } else { + final int pos = Arrays.binarySearch(oldVal, value, comparator); + if (pos < 0) { + return false; + } + final V[] newVal = newInstance(componentType, oldLen - 1); + System.arraycopy(oldVal, 0, newVal, 0, pos); + System.arraycopy(oldVal, pos + 1, newVal, pos, oldLen - pos - 1); + if (updater.compareAndSet(instance, oldVal, newVal)) { + return true; + } + } + } + } + + /** + * Sort an array. + * + * @param instance the instance holding the field + * @param comparator a comparator, or {@code null} to use natural ordering + */ + public void sort(T instance, Comparator comparator) { + final AtomicReferenceFieldUpdater updater = this.updater; + for (; ; ) { + final V[] oldVal = updater.get(instance); + if (oldVal.length == 0) { + return; + } + final V[] newVal = oldVal.clone(); + Arrays.sort(newVal, comparator); + if (updater.compareAndSet(instance, oldVal, newVal)) { + return; + } + } + } + + private static int insertionPoint(int searchResult) { + return searchResult > 0 ? searchResult : -(searchResult + 1); + } + + @SuppressWarnings({"unchecked"}) + private static V[] newInstance(Class componentType, int length) { + if (componentType == Handler.class) { + return (V[]) new Handler[length]; + } else if (componentType == Object.class) { + return (V[]) new Object[length]; + } else { + return (V[]) Array.newInstance(componentType, length); + } + } + + /** + * Compare and set the array. + * + * @param instance the instance holding the field + * @param expect the expected value + * @param update the update value + * @return {@code true} if the value was updated or {@code false} if the expected value did not match + */ + public boolean compareAndSet(final T instance, final V[] expect, final V[] update) { + return updater.compareAndSet(instance, expect, update); + } +} diff --git a/logging/src/main/java/org/xbib/logging/util/ByteStringBuilder.java b/logging/src/main/java/org/xbib/logging/util/ByteStringBuilder.java new file mode 100644 index 0000000..128c36b --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/util/ByteStringBuilder.java @@ -0,0 +1,309 @@ +package org.xbib.logging.util; + +import java.util.Arrays; + +/** + * This builder is not thread-safe. + */ +@SuppressWarnings({"unused", "WeakerAccess"}) +public final class ByteStringBuilder implements Appendable { + + private static final int INVALID_US_ASCII_CODE_POINT = 0x3f; + private static final int INVALID_UTF_8_CODE_POINT = 0xfffd; + private byte[] content; + private int length; + + public ByteStringBuilder(final int len) { + this.content = new byte[len]; + } + + public ByteStringBuilder append(final boolean b) { + appendLatin1(Boolean.toString(b)); + return this; + } + + @Override + public Appendable append(CharSequence csq) { + return append(csq, 0, csq.length()); + } + + @Override + public Appendable append(CharSequence csq, int start, int end) { + int c; + int i = start; + while (i < end - start) { + c = csq.charAt(start + i++); + if (Character.isHighSurrogate((char) c)) { + if (i < end - start) { + char t = csq.charAt(start + i++); + if (!Character.isLowSurrogate(t)) { + c = INVALID_UTF_8_CODE_POINT; + } else { + c = Character.toCodePoint((char) c, t); + } + } else { + c = INVALID_UTF_8_CODE_POINT; + } + } + appendUtf8Raw(c); + } + return this; + } + + public ByteStringBuilder append(final char c) { + return appendUtf8Raw((byte) c); + } + + public static int getUtf8LengthOf(final int c) { + if (c < 0x80) { + return 1; + } else if (c < 0x800) { + return 2; + } else if (c < 0x10000) { + return 3; + } else if (c < 0x110000) { + return 4; + } + return 1; + } + + public ByteStringBuilder appendUtf8Raw(final int codePoint) { + if (codePoint < 0) { + appendUtf8Raw(INVALID_UTF_8_CODE_POINT); + } else if (codePoint < 0x80) { + doAppend((byte) codePoint); + } else if (codePoint < 0x800) { + doAppend((byte) (0xC0 | 0x1F & codePoint >>> 6)); + doAppend((byte) (0x80 | 0x3F & codePoint)); + } else if (codePoint < 0x10000) { + doAppend((byte) (0xE0 | 0x0F & codePoint >>> 12)); + doAppend((byte) (0x80 | 0x3F & codePoint >>> 6)); + doAppend((byte) (0x80 | 0x3F & codePoint)); + } else if (codePoint < 0x110000) { + doAppend((byte) (0xF0 | 0x07 & codePoint >>> 18)); + doAppend((byte) (0x80 | 0x3F & codePoint >>> 12)); + doAppend((byte) (0x80 | 0x3F & codePoint >>> 6)); + doAppend((byte) (0x80 | 0x3F & codePoint)); + } else { + appendUtf8Raw(INVALID_UTF_8_CODE_POINT); + } + return this; + } + + public ByteStringBuilder append(final byte[] bytes) { + int length = this.length; + int bl = bytes.length; + reserve(bl, false); + System.arraycopy(bytes, 0, content, length, bl); + this.length = length + bl; + return this; + } + + public ByteStringBuilder append(final byte[] bytes, final int offs, final int len) { + reserve(len, false); + int length = this.length; + System.arraycopy(bytes, offs, content, length, len); + this.length = length + len; + return this; + } + + public ByteStringBuilder appendUSASCII(final String s) { + return appendUSASCII(s, 0, s.length()); + } + + public ByteStringBuilder appendUSASCII(final String s, final int maxLen) { + return appendASCII(128, s, 0, s.length(), maxLen); + } + + public ByteStringBuilder appendUSASCII(final String s, final int offs, final int len) { + return appendASCII(128, s, offs, len, Integer.MAX_VALUE); + } + + public ByteStringBuilder appendPrintUSASCII(final String s) { + return appendPrintUSASCII(s, 0, s.length()); + } + + public ByteStringBuilder appendPrintUSASCII(final String s, final int maxLen) { + return appendASCII(33, 126, s, 0, s.length(), maxLen); + } + + public ByteStringBuilder appendPrintUSASCII(final String s, final int offs, final int len) { + return appendASCII(33, 126, s, offs, len, Integer.MAX_VALUE); + } + + public ByteStringBuilder appendLatin1(final String s) { + return appendLatin1(s, 0, s.length()); + } + + public ByteStringBuilder appendLatin1(final String s, final int offs, final int len) { + return appendASCII(256, s, offs, len, Integer.MAX_VALUE); + } + + public ByteStringBuilder append(final String s) { + return append(s, 0, s.length()); + } + + public ByteStringBuilder append(final String s, final int offs, final int len) { + int c; + int i = offs; + while (i < len) { + c = s.charAt(offs + i++); + if (Character.isHighSurrogate((char) c)) { + if (i < len) { + char t = s.charAt(offs + i++); + if (!Character.isLowSurrogate(t)) { + c = INVALID_UTF_8_CODE_POINT; + } else { + c = Character.toCodePoint((char) c, t); + } + } else { + c = INVALID_UTF_8_CODE_POINT; + } + } + appendUtf8Raw(c); + } + return this; + } + + public int write(final String s, final int limit) { + int result = 0; + int c; + final int len = s.length(); + for (int i = 0; i < len; i++) { + c = s.charAt(i); + if (Character.isHighSurrogate((char) c)) { + if (i < len) { + char t = s.charAt(++i); + if (!Character.isLowSurrogate(t)) { + c = INVALID_UTF_8_CODE_POINT; + } else { + c = Character.toCodePoint((char) c, t); + } + } else { + c = INVALID_UTF_8_CODE_POINT; + } + } + final int byteLen = getUtf8LengthOf(c); + if (length + byteLen > limit) { + break; + } + result = i; + appendUtf8Raw(c); + } + return result; + } + + public ByteStringBuilder append(final int i) { + appendLatin1(Integer.toString(i)); + return this; + } + + public ByteStringBuilder append(final long l) { + appendLatin1(Long.toString(l)); + return this; + } + + public ByteStringBuilder append(final ByteStringBuilder other) { + append(other.content, 0, other.length); + return this; + } + + public byte[] toArray() { + return Arrays.copyOf(content, length); + } + + public byte byteAt(final int index) { + if (index < 0 || index > length) + throw new IndexOutOfBoundsException(); + return content[index]; + } + + public int capacity() { + return content.length; + } + + public int length() { + return length; + } + + public void setLength(final int newLength) { + if (newLength > length) { + // grow + reserve(newLength - length, true); + } + length = newLength; + } + + public boolean contentEquals(final byte[] other) { + return contentEquals(other, 0, other.length); + } + + public boolean contentEquals(final byte[] other, final int offs, final int length) { + if (length != this.length) + return false; + for (int i = 0; i < length; i++) { + if (content[i] != other[offs + i]) { + return false; + } + } + return true; + } + + private ByteStringBuilder appendASCII(final int asciiLen, final String s, final int offs, final int len, final int maxLen) { + return appendASCII(0, asciiLen, s, offs, len, maxLen); + } + + private ByteStringBuilder appendASCII(final int minChar, final int maxChar, final String s, final int offs, final int len, + final int maxLen) { + reserve(len, false); + char c; + for (int i = 0; i < len; i++) { + if (i >= maxLen) { + break; + } + c = s.charAt(i + offs); + if (c < minChar || c > maxChar) { + doAppendNoCheck((byte) INVALID_US_ASCII_CODE_POINT); + } else { + doAppendNoCheck((byte) c); + } + } + return this; + } + + private void reserve(final int count, final boolean clear) { + final int length = this.length; + final byte[] content = this.content; + int cl = content.length; + if (cl - length >= count) { + if (clear) + Arrays.fill(content, length, length + count, (byte) 0); + return; + } + // clear remainder + if (clear) + Arrays.fill(content, length, cl, (byte) 0); + do { + // not enough space... grow by 1.5x + cl = cl + (cl + 1 >> 1); + if (cl < 0) + throw new IllegalStateException("Too large"); + } while (cl - length < count); + this.content = Arrays.copyOf(content, cl); + } + + private void doAppend(final byte b) { + byte[] content = this.content; + final int cl = content.length; + final int length = this.length; + if (length == cl) { + content = this.content = Arrays.copyOf(content, cl + (cl + 1 >> 1)); + } + content[length] = b; + this.length = length + 1; + } + + private void doAppendNoCheck(final byte b) { + content[length++] = b; + } +} \ No newline at end of file diff --git a/logging/src/main/java/org/xbib/logging/util/CharsetUtil.java b/logging/src/main/java/org/xbib/logging/util/CharsetUtil.java new file mode 100644 index 0000000..991b936 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/util/CharsetUtil.java @@ -0,0 +1,34 @@ +package org.xbib.logging.util; + +import java.io.Console; +import java.nio.charset.Charset; + +/** + * JDK-specific code relating to {@link Console}. + */ +public final class CharsetUtil { + private CharsetUtil() { + } + + private static final Charset CONSOLE_CHARSET; + + static { + Console console = System.console(); + Charset charset; + if (console != null) { + charset = console.charset(); + } else { + // Make various guesses as to what the encoding of the console is + String encodingName = System.getProperty("stdout.encoding"); + if (encodingName == null) { + encodingName = System.getProperty("native.encoding"); + } + charset = encodingName == null ? Charset.defaultCharset() : Charset.forName(encodingName); + } + CONSOLE_CHARSET = charset; + } + + public static Charset consoleCharset() { + return CONSOLE_CHARSET; + } +} diff --git a/logging/src/main/java/org/xbib/logging/util/CopyOnWriteMap.java b/logging/src/main/java/org/xbib/logging/util/CopyOnWriteMap.java new file mode 100644 index 0000000..65b8cf0 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/util/CopyOnWriteMap.java @@ -0,0 +1,149 @@ +package org.xbib.logging.util; + +import java.util.AbstractMap; +import java.util.Collection; +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; + +public final class CopyOnWriteMap extends AbstractMap implements ConcurrentMap, Cloneable { + + private volatile FastCopyHashMap map = new FastCopyHashMap<>(32, 0.25f); + + @SuppressWarnings("rawtypes") + private static final AtomicReferenceFieldUpdater mapUpdater = + AtomicReferenceFieldUpdater.newUpdater(CopyOnWriteMap.class, FastCopyHashMap.class, "map"); + + public CopyOnWriteMap() { + super(); + } + + public V get(final Object key) { + return map.get(key); + } + + public boolean containsKey(final Object key) { + return map.containsKey(key); + } + + public int size() { + return map.size(); + } + + public boolean containsValue(final Object value) { + return map.containsValue(value); + } + + @SuppressWarnings("unchecked") + public void clear() { + map = new FastCopyHashMap<>(32, 0.25f); + } + + public V put(final K key, final V value) { + FastCopyHashMap oldVal, newVal; + V result; + do { + oldVal = map; + newVal = oldVal.clone(); + result = newVal.put(key, value); + } while (!mapUpdater.compareAndSet(this, oldVal, newVal)); + return result; + } + + public V remove(final Object key) { + FastCopyHashMap oldVal, newVal; + V result; + do { + oldVal = map; + if (!oldVal.containsKey(key)) { + return null; + } + newVal = oldVal.clone(); + result = newVal.remove(key); + } while (!mapUpdater.compareAndSet(this, oldVal, newVal)); + return result; + } + + @SuppressWarnings("unchecked") + public CopyOnWriteMap clone() { + try { + return (CopyOnWriteMap) super.clone(); + } catch (CloneNotSupportedException e) { + throw new IllegalStateException(); + } + } + + public V putIfAbsent(final K key, final V value) { + FastCopyHashMap oldVal, newVal; + do { + oldVal = map; + if (oldVal.containsKey(key)) { + return oldVal.get(key); + } + newVal = oldVal.clone(); + newVal.put(key, value); + } while (!mapUpdater.compareAndSet(this, oldVal, newVal)); + return null; + } + + @SuppressWarnings("unchecked") + public boolean remove(final Object key, final Object value) { + FastCopyHashMap oldVal, newVal; + do { + oldVal = map; + if (value == oldVal.get(key) && (value != null || oldVal.containsKey(key))) { + if (oldVal.size() == 1) { + newVal = new FastCopyHashMap<>(32, 0.25f); + } else { + newVal = oldVal.clone(); + newVal.remove(key); + } + } else { + return false; + } + } while (!mapUpdater.compareAndSet(this, oldVal, newVal)); + return true; + } + + public boolean replace(final K key, final V oldValue, final V newValue) { + FastCopyHashMap oldVal, newVal; + do { + oldVal = map; + if (oldValue == oldVal.get(key) && (oldValue != null || oldVal.containsKey(key))) { + newVal = oldVal.clone(); + newVal.put(key, newValue); + } else { + return false; + } + } while (!mapUpdater.compareAndSet(this, oldVal, newVal)); + return true; + } + + public V replace(final K key, final V value) { + FastCopyHashMap oldVal, newVal; + V result; + do { + oldVal = map; + if (value == oldVal.get(key) && (value != null || oldVal.containsKey(key))) { + newVal = oldVal.clone(); + result = newVal.put(key, value); + } else { + return null; + } + } while (!mapUpdater.compareAndSet(this, oldVal, newVal)); + return result; + } + + public Set keySet() { + return Collections.unmodifiableSet(map.keySet()); + } + + public Collection values() { + return Collections.unmodifiableCollection(map.values()); + } + + public Set> entrySet() { + return Collections.unmodifiableSet(map.entrySet()); + } +} diff --git a/logging/src/main/java/org/xbib/logging/util/CopyOnWriteWeakMap.java b/logging/src/main/java/org/xbib/logging/util/CopyOnWriteWeakMap.java new file mode 100644 index 0000000..3f8f6ae --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/util/CopyOnWriteWeakMap.java @@ -0,0 +1,243 @@ +package org.xbib.logging.util; + +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.util.AbstractMap; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; + +public final class CopyOnWriteWeakMap extends AbstractMap implements ConcurrentMap { + + private final Queue queue = new Queue(); + + private static final FastCopyHashMap EMPTY = new FastCopyHashMap<>(32, 0.25f); + + public CopyOnWriteWeakMap() { + } + + @SuppressWarnings({"unchecked"}) + private FastCopyHashMap> empty() { + return (FastCopyHashMap>) EMPTY; + } + + private volatile FastCopyHashMap> map = empty(); + + private FastCopyHashMap> cleanCopyForRemove() { + assert Thread.holdsLock(this); + final Map> oldMap = map; + final Queue queue = this.queue; + if (oldMap.isEmpty()) { + queue.clear(); + return empty(); + } + final FastCopyHashMap> newMap = new FastCopyHashMap>(oldMap); + Node ref; + while ((ref = queue.poll()) != null) { + final Object key = ref.getKey(); + if (newMap.get(key) == ref) { + newMap.remove(key); + if (newMap.isEmpty()) { + queue.clear(); + return empty(); + } + } + } + return newMap; + } + + private FastCopyHashMap> cleanCopyForMod() { + assert Thread.holdsLock(this); + final Map> oldMap = map; + final Queue queue = this.queue; + if (oldMap.isEmpty()) { + queue.clear(); + return empty().clone(); + } + final FastCopyHashMap> newMap = new FastCopyHashMap>(oldMap); + Node ref; + while ((ref = queue.poll()) != null) { + final Object key = ref.getKey(); + if (newMap.get(key) == ref) { + newMap.remove(key); + if (newMap.isEmpty()) { + queue.clear(); + return empty().clone(); + } + } + } + return newMap; + } + + public V putIfAbsent(final K key, final V value) { + if (key == null) { + throw new IllegalArgumentException("key is null"); + } + if (value == null) { + throw new IllegalArgumentException("value is null"); + } + synchronized (this) { + final Node oldNode = map.get(key); + if (oldNode != null) { + final V val = oldNode.get(); + if (val != null) + return val; + } + final FastCopyHashMap> newMap = cleanCopyForMod(); + newMap.put(key, new Node(key, value, queue)); + map = newMap; + return null; + } + } + + public boolean remove(final Object key, final Object value) { + synchronized (this) { + final Node oldNode = map.get(key); + final V existing = oldNode.get(); + if (existing != null && existing.equals(value)) { + final FastCopyHashMap> newMap = cleanCopyForRemove(); + newMap.remove(key); + map = newMap; + return true; + } + } + return false; + } + + public boolean replace(final K key, final V oldValue, final V newValue) { + if (newValue == null) { + throw new IllegalArgumentException("newValue is null"); + } + if (oldValue == null) { + return false; + } + synchronized (this) { + final Node oldNode = map.get(key); + final V existing = oldNode.get(); + if (existing != null && existing.equals(oldValue)) { + final FastCopyHashMap> newMap = cleanCopyForMod(); + map.put(key, new Node(key, newValue, queue)); + map = newMap; + return true; + } + } + return false; + } + + public V replace(final K key, final V value) { + if (value == null) { + throw new IllegalArgumentException("value is null"); + } + synchronized (this) { + final Node oldNode = map.get(key); + final V existing = oldNode.get(); + if (existing != null) { + final FastCopyHashMap> newMap = cleanCopyForMod(); + map.put(key, new Node(key, value, queue)); + map = newMap; + } + return existing; + } + } + + public int size() { + return map.size(); + } + + public boolean isEmpty() { + return map.isEmpty(); + } + + public boolean containsKey(final Object key) { + return map.containsKey(key); + } + + public boolean containsValue(final Object value) { + if (value == null) + return false; + for (Node node : map.values()) { + if (value.equals(node.get())) { + return true; + } + } + return false; + } + + public V get(final Object key) { + final Node node = map.get(key); + return node == null ? null : node.get(); + } + + public V put(final K key, final V value) { + if (key == null) { + throw new IllegalArgumentException("key is null"); + } + if (value == null) { + throw new IllegalArgumentException("value is null"); + } + synchronized (this) { + final FastCopyHashMap> newMap = cleanCopyForMod(); + final Node old = newMap.put(key, new Node(key, value, queue)); + map = newMap; + return old == null ? null : old.get(); + } + } + + public V remove(final Object key) { + if (key == null) + return null; + synchronized (this) { + final FastCopyHashMap> newMap = cleanCopyForRemove(); + final Node old = newMap.remove(key); + map = newMap; + return old == null ? null : old.get(); + } + } + + public void clear() { + synchronized (this) { + map = empty(); + } + } + + public Set> entrySet() { + final FastCopyHashMap> snapshot = map; + final Map copyMap = new HashMap(); + for (Node node : snapshot.values()) { + final V value = node.get(); + if (value == null) + continue; + final K key = node.getKey(); + copyMap.put(key, value); + } + return Collections.unmodifiableMap(copyMap).entrySet(); + } + + private static final class Node extends WeakReference { + private final K key; + + Node(final K key, final V value, final ReferenceQueue queue) { + super(value, queue); + this.key = key; + } + + K getKey() { + return key; + } + } + + @SuppressWarnings("unchecked") + private static final class Queue extends ReferenceQueue { + + public Node poll() { + return (Node) super.poll(); + } + + void clear() { + while (poll() != null) + ; + } + } +} diff --git a/logging/src/main/java/org/xbib/logging/util/FastCopyHashMap.java b/logging/src/main/java/org/xbib/logging/util/FastCopyHashMap.java new file mode 100644 index 0000000..41a972d --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/util/FastCopyHashMap.java @@ -0,0 +1,723 @@ +package org.xbib.logging.util; + +import java.io.IOException; +import java.util.AbstractCollection; +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.Collection; +import java.util.ConcurrentModificationException; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Set; + +/** + * A HashMap that is optimized for fast shallow copies. If the copy-ctor is + * passed another FastCopyHashMap, or clone is called on this map, the shallow + * copy can be performed using little more than a single array copy. In order to + * accomplish this, immutable objects must be used internally, so update + * operations result in slightly more object churn than HashMap. + * Note: It is very important to use a smaller load factor than you normally + * would for HashMap, since the implementation is open-addressed with linear + * probing. With a 50% load-factor a get is expected to return in only 2 probes. + * However, a 90% load-factor is expected to return in around 50 probes. + */ +public class FastCopyHashMap extends AbstractMap implements Map, Cloneable { + /** + * Marks null keys. + */ + private static final Object NULL = new Object(); + + /** + * Same default as HashMap, must be a power of 2 + */ + private static final int DEFAULT_CAPACITY = 8; + + /** + * MAX_INT - 1 + */ + private static final int MAXIMUM_CAPACITY = 1 << 30; + + /** + * 67%, just like IdentityHashMap + */ + private static final float DEFAULT_LOAD_FACTOR = 0.67f; + + /** + * The open-addressed table + */ + private transient Entry[] table; + + /** + * The current number of key-value pairs + */ + private transient int size; + + /** + * The next resize + */ + private transient int threshold; + + /** + * The user defined load factor which defines when to resize + */ + private final float loadFactor; + + /** + * Counter used to detect changes made outside of an iterator + */ + private transient int modCount; + + // Cached views + private transient KeySet keySet; + private transient Values values; + private transient EntrySet entrySet; + + public FastCopyHashMap(int initialCapacity, float loadFactor) { + if (initialCapacity < 0) + throw new IllegalArgumentException("Can not have a negative size table!"); + + if (initialCapacity > MAXIMUM_CAPACITY) + initialCapacity = MAXIMUM_CAPACITY; + + if (!(loadFactor > 0F && loadFactor <= 1F)) + throw new IllegalArgumentException("Load factor must be greater than 0 and less than or equal to 1"); + + this.loadFactor = loadFactor; + init(initialCapacity, loadFactor); + } + + @SuppressWarnings("unchecked") + public FastCopyHashMap(Map map) { + if (map instanceof FastCopyHashMap fast) { + this.table = (Entry[]) fast.table.clone(); + this.loadFactor = fast.loadFactor; + this.size = fast.size; + this.threshold = fast.threshold; + } else { + this.loadFactor = DEFAULT_LOAD_FACTOR; + init(map.size(), this.loadFactor); + putAll(map); + } + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private void init(int initialCapacity, float loadFactor) { + int c = 1; + for (; c < initialCapacity; c <<= 1) + ; + threshold = (int) (c * loadFactor); + // Include the load factor when sizing the table for the first time + if (initialCapacity > threshold && c < MAXIMUM_CAPACITY) { + c <<= 1; + threshold = (int) (c * loadFactor); + } + this.table = (Entry[]) new Entry[c]; + } + + public FastCopyHashMap(int initialCapacity) { + this(initialCapacity, DEFAULT_LOAD_FACTOR); + } + + public FastCopyHashMap() { + this(DEFAULT_CAPACITY); + } + + // The normal bit spreader... + private static int hash(Object key) { + int h = key.hashCode(); + h ^= (h >>> 20) ^ (h >>> 12); + return h ^ (h >>> 7) ^ (h >>> 4); + } + + @SuppressWarnings("unchecked") + private static K maskNull(K key) { + return key == null ? (K) NULL : key; + } + + private static K unmaskNull(K key) { + return key == NULL ? null : key; + } + + private int nextIndex(int index, int length) { + index = (index >= length - 1) ? 0 : index + 1; + return index; + } + + private static boolean eq(Object o1, Object o2) { + return Objects.equals(o1, o2); + } + + private static int index(int hashCode, int length) { + return hashCode & (length - 1); + } + + public int size() { + return size; + } + + public boolean isEmpty() { + return size == 0; + } + + public V get(Object key) { + key = maskNull(key); + + int hash = hash(key); + int length = table.length; + int index = index(hash, length); + + for (int start = index; ; ) { + Entry e = table[index]; + if (e == null) + return null; + + if (e.hash == hash && eq(key, e.key)) + return e.value; + + index = nextIndex(index, length); + if (index == start) // Full table + return null; + } + } + + public boolean containsKey(Object key) { + key = maskNull(key); + + int hash = hash(key); + int length = table.length; + int index = index(hash, length); + + for (int start = index; ; ) { + Entry e = table[index]; + if (e == null) + return false; + + if (e.hash == hash && eq(key, e.key)) + return true; + + index = nextIndex(index, length); + if (index == start) // Full table + return false; + } + } + + public boolean containsValue(Object value) { + for (Entry e : table) + if (e != null && eq(value, e.value)) + return true; + + return false; + } + + public V put(K key, V value) { + key = maskNull(key); + + Entry[] table = this.table; + int hash = hash(key); + int length = table.length; + int index = index(hash, length); + + for (int start = index; ; ) { + Entry e = table[index]; + if (e == null) + break; + + if (e.hash == hash && eq(key, e.key)) { + table[index] = new Entry(e.key, e.hash, value); + return e.value; + } + + index = nextIndex(index, length); + if (index == start) + throw new IllegalStateException("Table is full!"); + } + + modCount++; + table[index] = new Entry(key, hash, value); + if (++size >= threshold) + resize(length); + + return null; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private void resize(int from) { + int newLength = from << 1; + // Can't get any bigger + if (newLength > MAXIMUM_CAPACITY || newLength <= from) { + return; + } + Entry[] newTable = new Entry[newLength]; + Entry[] old = table; + for (Entry e : old) { + if (e == null) + continue; + + int index = index(e.hash, newLength); + while (newTable[index] != null) + index = nextIndex(index, newLength); + + newTable[index] = e; + } + + threshold = (int) (loadFactor * newLength); + table = newTable; + } + + public void putAll(Map map) { + int size = map.size(); + if (size == 0) + return; + + if (size > threshold) { + if (size > MAXIMUM_CAPACITY) + size = MAXIMUM_CAPACITY; + + int length = table.length; + for (; length < size; length <<= 1) + ; + + resize(length); + } + + for (Map.Entry e : map.entrySet()) + put(e.getKey(), e.getValue()); + } + + public V remove(Object key) { + key = maskNull(key); + + Entry[] table = this.table; + int length = table.length; + int hash = hash(key); + int start = index(hash, length); + + for (int index = start; ; ) { + Entry e = table[index]; + if (e == null) + return null; + + if (e.hash == hash && eq(key, e.key)) { + table[index] = null; + relocate(index); + modCount++; + size--; + return e.value; + } + + index = nextIndex(index, length); + if (index == start) + return null; + } + + } + + private void relocate(int start) { + Entry[] table = this.table; + int length = table.length; + int current = nextIndex(start, length); + + for (; ; ) { + Entry e = table[current]; + if (e == null) + return; + + // A Doug Lea variant of Knuth's Section 6.4 Algorithm R. + // This provides a non-recursive method of relocating + // entries to their optimal positions once a gap is created. + int prefer = index(e.hash, length); + if ((current < prefer && (prefer <= start || start <= current)) + || (prefer <= start && start <= current)) { + table[start] = e; + table[current] = null; + start = current; + } + + current = nextIndex(current, length); + } + } + + public void clear() { + modCount++; + Entry[] table = this.table; + for (int i = 0; i < table.length; i++) + table[i] = null; + + size = 0; + } + + @SuppressWarnings("unchecked") + public FastCopyHashMap clone() { + try { + FastCopyHashMap clone = (FastCopyHashMap) super.clone(); + clone.table = table.clone(); + clone.entrySet = null; + clone.values = null; + clone.keySet = null; + return clone; + } catch (CloneNotSupportedException e) { + // should never happen + throw new IllegalStateException(e); + } + } + + public void printDebugStats() { + int optimal = 0; + int total = 0; + int totalSkew = 0; + int maxSkew = 0; + for (int i = 0; i < table.length; i++) { + Entry e = table[i]; + if (e != null) { + + total++; + int target = index(e.hash, table.length); + if (i == target) + optimal++; + else { + int skew = Math.abs(i - target); + if (skew > maxSkew) + maxSkew = skew; + totalSkew += skew; + } + + } + } + + System.out.println(" Size: " + size); + System.out.println(" Real Size: " + total); + System.out.println(" Optimal: " + optimal + " (" + (float) optimal * 100 / total + "%)"); + System.out.println(" Average Distance:" + ((float) totalSkew / (total - optimal))); + System.out.println(" Max Distance: " + maxSkew); + } + + public Set> entrySet() { + if (entrySet == null) + entrySet = new EntrySet(); + + return entrySet; + } + + public Set keySet() { + if (keySet == null) + keySet = new KeySet(); + + return keySet; + } + + public Collection values() { + if (values == null) + values = new Values(); + + return values; + } + + public static FastCopyHashMap of(Map map) { + if (map instanceof FastCopyHashMap) { + return (FastCopyHashMap) map; + } else { + return new FastCopyHashMap<>(map); + } + } + + @SuppressWarnings("unchecked") + private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { + s.defaultReadObject(); + + int size = s.readInt(); + + init(size, loadFactor); + + for (int i = 0; i < size; i++) { + K key = (K) s.readObject(); + V value = (V) s.readObject(); + putForCreate(key, value); + } + + this.size = size; + } + + @SuppressWarnings("unchecked") + private void putForCreate(K key, V value) { + key = maskNull(key); + + Entry[] table = this.table; + int hash = hash(key); + int length = table.length; + int index = index(hash, length); + + Entry e = table[index]; + while (e != null) { + index = nextIndex(index, length); + e = table[index]; + } + + table[index] = new Entry(key, hash, value); + } + + private void writeObject(java.io.ObjectOutputStream s) throws IOException { + s.defaultWriteObject(); + s.writeInt(size); + + for (Entry e : table) { + if (e != null) { + s.writeObject(unmaskNull(e.key)); + s.writeObject(e.value); + } + } + } + + private static final class Entry { + final K key; + final int hash; + final V value; + + Entry(K key, int hash, V value) { + this.key = key; + this.hash = hash; + this.value = value; + } + } + + private abstract class FastCopyHashMapIterator implements Iterator { + private int next = 0; + private int expectedCount = modCount; + private int current = -1; + private boolean hasNext; + Entry[] table = FastCopyHashMap.this.table; + + public boolean hasNext() { + if (hasNext) + return true; + + Entry[] table = this.table; + for (int i = next; i < table.length; i++) { + if (table[i] != null) { + next = i; + return hasNext = true; + } + } + + next = table.length; + return false; + } + + protected Entry nextEntry() { + if (modCount != expectedCount) + throw new ConcurrentModificationException(); + + if (!hasNext && !hasNext()) + throw new NoSuchElementException(); + + current = next++; + hasNext = false; + + return table[current]; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + public void remove() { + if (modCount != expectedCount) + throw new ConcurrentModificationException(); + + int current = this.current; + int delete = current; + + if (current == -1) { + throw new IllegalStateException(); + } + // Invalidate current (prevents multiple remove) + this.current = -1; + + // Start were we relocate + next = delete; + + Entry[] table = this.table; + if (table != FastCopyHashMap.this.table) { + FastCopyHashMap.this.remove(table[delete].key); + table[delete] = null; + expectedCount = modCount; + return; + } + + int length = table.length; + int i = delete; + + table[delete] = null; + size--; + + for (; ; ) { + i = nextIndex(i, length); + Entry e = table[i]; + if (e == null) + break; + + int prefer = index(e.hash, length); + if ((i < prefer && (prefer <= delete || delete <= i)) + || (prefer <= delete && delete <= i)) { + // Snapshot the unseen portion of the table if we have + // to relocate an entry that was already seen by this iterator + if (i < current && current <= delete && table == FastCopyHashMap.this.table) { + int remaining = length - current; + Entry[] newTable = (Entry[]) new Entry[remaining]; + System.arraycopy(table, current, newTable, 0, remaining); + // Replace iterator's table. + // Leave table local var pointing to the real table + this.table = newTable; + next = 0; + } + // Do the swap on the real table + table[delete] = e; + table[i] = null; + delete = i; + } + } + } + } + + private class KeyIterator extends FastCopyHashMapIterator { + public K next() { + return unmaskNull(nextEntry().key); + } + } + + private class ValueIterator extends FastCopyHashMapIterator { + public V next() { + return nextEntry().value; + } + } + + private class EntryIterator extends FastCopyHashMapIterator> { + private class WriteThroughEntry extends SimpleEntry { + WriteThroughEntry(K key, V value) { + super(key, value); + } + + public V setValue(V value) { + if (table != FastCopyHashMap.this.table) + FastCopyHashMap.this.put(getKey(), value); + + return super.setValue(value); + } + } + + public Map.Entry next() { + Entry e = nextEntry(); + return new WriteThroughEntry(unmaskNull(e.key), e.value); + } + + } + + private class KeySet extends AbstractSet { + public Iterator iterator() { + return new KeyIterator(); + } + + public void clear() { + FastCopyHashMap.this.clear(); + } + + public boolean contains(Object o) { + return containsKey(o); + } + + public boolean remove(Object o) { + int size = size(); + FastCopyHashMap.this.remove(o); + return size() < size; + } + + public int size() { + return FastCopyHashMap.this.size(); + } + } + + private class Values extends AbstractCollection { + public Iterator iterator() { + return new ValueIterator(); + } + + public void clear() { + FastCopyHashMap.this.clear(); + } + + public int size() { + return FastCopyHashMap.this.size(); + } + } + + private class EntrySet extends AbstractSet> { + public Iterator> iterator() { + return new EntryIterator(); + } + + public boolean contains(Object o) { + if (!(o instanceof Map.Entry entry)) + return false; + + Object value = get(entry.getKey()); + return eq(entry.getValue(), value); + } + + public void clear() { + FastCopyHashMap.this.clear(); + } + + public boolean isEmpty() { + return FastCopyHashMap.this.isEmpty(); + } + + public int size() { + return FastCopyHashMap.this.size(); + } + } + + protected static class SimpleEntry implements Map.Entry { + private final K key; + private V value; + + SimpleEntry(K key, V value) { + this.key = key; + this.value = value; + } + + SimpleEntry(Map.Entry entry) { + this.key = entry.getKey(); + this.value = entry.getValue(); + } + + public K getKey() { + return key; + } + + public V getValue() { + return value; + } + + public V setValue(V value) { + V old = this.value; + this.value = value; + return old; + } + + public boolean equals(Object o) { + if (this == o) + return true; + + if (!(o instanceof Map.Entry e)) + return false; + return eq(key, e.getKey()) && eq(value, e.getValue()); + } + + public int hashCode() { + return (key == null ? 0 : hash(key)) ^ + (value == null ? 0 : hash(value)); + } + + public String toString() { + return getKey() + "=" + getValue(); + } + } +} diff --git a/logging/src/main/java/org/xbib/logging/util/PropertyValues.java b/logging/src/main/java/org/xbib/logging/util/PropertyValues.java new file mode 100644 index 0000000..b795b8f --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/util/PropertyValues.java @@ -0,0 +1,405 @@ +package org.xbib.logging.util; + +import java.util.Collections; +import java.util.EnumMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * A utility for converting objects into strings and strings into objects for storage in logging configurations. + */ +@SuppressWarnings("WeakerAccess") +public class PropertyValues { + + private static final int KEY = 0; + + private static final int VALUE = 1; + + public PropertyValues() { + } + + /** + * Parses a string of key/value pairs into a map. + *

+ * The key/value pairs are separated by a comma ({@code ,}). The key and value are separated by an equals + * ({@code =}). + *

+ *

+ * If a key contains a {@code \} or an {@code =} it must be escaped by a preceding {@code \}. Example: {@code + * key\==value,\\key=value}. + *

+ *

+ * If a value contains a {@code \} or a {@code ,} it must be escaped by a preceding {@code \}. Example: {@code + * key=part1\,part2,key2=value\\other}. + *

+ * + *

+ * If the value for a key is empty there is no trailing {@code =} after a key the will be {@code null}. + *

+ * + * @param s the string to parse + * @return a map of the key value pairs or an empty map if the string is {@code null} or empty + */ + public static Map stringToMap(final String s) { + if (s == null || s.isEmpty()) + return Collections.emptyMap(); + + final Map map = new LinkedHashMap<>(); + + final StringBuilder key = new StringBuilder(); + final StringBuilder value = new StringBuilder(); + final char[] chars = s.toCharArray(); + int state = 0; + for (int i = 0; i < chars.length; i++) { + final char c = chars[i]; + switch (state) { + case KEY: { + switch (c) { + case '\\': { + // Handle escapes + if (chars.length > ++i) { + final char next = chars[i]; + if (next == '=' || next == '\\') { + key.append(next); + continue; + } + } + throw new IllegalStateException("Escape character found at invalid position " + i + + ". Only characters '=' and '\\' need to be escaped for a key."); + } + case '=': { + state = VALUE; + continue; + } + default: { + key.append(c); + continue; + } + } + } + case VALUE: { + switch (c) { + case '\\': { + // Handle escapes + if (chars.length > ++i) { + final char next = chars[i]; + if (next == ',' || next == '\\') { + value.append(next); + continue; + } + } + throw new IllegalStateException("Escape character found at invalid position " + i + + ". Only characters ',' and '\\' need to be escaped for a value."); + } + case ',': { + // Only add if the key isn't empty + if (!key.isEmpty()) { + // Add the entry + if (value.isEmpty()) { + map.put(key.toString(), null); + } else { + map.put(key.toString(), value.toString()); + } + // Clear the key + key.setLength(0); + } + // Clear the value + value.setLength(0); + state = KEY; + continue; + } + default: { + value.append(c); + continue; + } + } + } + default: + // not reachable + throw new IllegalStateException(); + } + } + // Add the last entry + if (key.length() > 0) { + // Add the entry + if (value.length() == 0) { + map.put(key.toString(), null); + } else { + map.put(key.toString(), value.toString()); + } + } + return Collections.unmodifiableMap(map); + } + + /** + * Parses a string of key/value pairs into an {@linkplain EnumMap enum map}. + *

+ * The key/value pairs are separated by a comma ({@code ,}). The key and value are separated by an equals + * ({@code =}). The key must be a valid {@linkplain Enum#valueOf(Class, String) enum value}. For convenience the + * case of each character will be converted to uppercase and any dashes ({@code -}) will be converted to + * underscores ({@code _}). + *

+ *

+ * If a value contains a {@code \} or a {@code ,} it must be escaped by a preceding {@code \}. Example: {@code + * key=part1\,part2,key2=value\\other}. + *

+ * + *

+ * If the value for a key is empty there is no trailing {@code =} after a key the value will be {@code null}. + *

+ * + * @param enumType the enum type + * @param s the string to parse + * @return a map of the key value pairs or an empty map if the string is {@code null} or empty + */ + public static > EnumMap stringToEnumMap(final Class enumType, final String s) { + return stringToEnumMap(enumType, s, true); + } + + /** + * Parses a string of key/value pairs into an {@linkplain EnumMap enum map}. + *

+ * The key/value pairs are separated by a comma ({@code ,}). The key and value are separated by an equals + * ({@code =}). The key must be a valid {@linkplain Enum#valueOf(Class, String) enum value}. For convenience any + * dashes ({@code -}) will be converted to underscores ({@code _}). If {@code convertKeyCase} is set to + * {@code true} the case will also be converted to uppercase for each key character. + *

+ *

+ * If a value contains a {@code \} or a {@code ,} it must be escaped by a preceding {@code \}. Example: {@code + * key=part1\,part2,key2=value\\other}. + *

+ * + *

+ * If the value for a key is empty there is no trailing {@code =} after a key the value will be {@code null}. + *

+ * + * @param enumType the enum type + * @param s the string to parse + * @param convertKeyCase {@code true} if the each character from the key should be converted to uppercase, + * otherwise {@code false} to keep the case as is + * @return a map of the key value pairs or an empty map if the string is {@code null} or empty + */ + @SuppressWarnings("SameParameterValue") + public static > EnumMap stringToEnumMap(final Class enumType, final String s, + final boolean convertKeyCase) { + final EnumMap result = new EnumMap<>(enumType); + if (s == null || s.isEmpty()) + return result; + + final StringBuilder key = new StringBuilder(); + final StringBuilder value = new StringBuilder(); + final char[] chars = s.toCharArray(); + int state = 0; + for (int i = 0; i < chars.length; i++) { + final char c = chars[i]; + switch (state) { + case KEY: { + switch (c) { + case '=': { + state = VALUE; + continue; + } + case '-': { + key.append('_'); + continue; + } + default: { + if (convertKeyCase) { + key.append(Character.toUpperCase(c)); + } else { + key.append(c); + } + continue; + } + } + } + case VALUE: { + switch (c) { + case '\\': { + // Handle escapes + if (chars.length > ++i) { + final char next = chars[i]; + if (next == ',' || next == '\\') { + value.append(next); + continue; + } + } + throw new IllegalStateException("Escape character found at invalid position " + i + + ". Only characters ',' and '\\' need to be escaped for a value."); + } + case ',': { + // Only add if the key isn't empty + if (key.length() > 0) { + // Add the value + if (value.length() == 0) { + result.put(E.valueOf(enumType, key.toString()), null); + } else { + result.put(E.valueOf(enumType, key.toString()), value.toString()); + } + // Clear the key + key.setLength(0); + } + // Clear the value + value.setLength(0); + state = KEY; + continue; + } + default: { + value.append(c); + continue; + } + } + } + default: + // not reachable + throw new IllegalStateException(); + } + } + // Add the last entry + if (key.length() > 0) { + // Add the value + if (value.length() == 0) { + result.put(E.valueOf(enumType, key.toString()), null); + } else { + result.put(E.valueOf(enumType, key.toString()), value.toString()); + } + } + return result; + } + + /** + * Converts a map into a string that can be parsed by {@link #stringToMap(String)}. Note that if this is an + * {@link EnumMap} the {@link #mapToString(EnumMap)} will be used and the key will be the + * {@linkplain Enum#name() enum name}. + * + * @param map the map to convert to a string + * @param the type of the key + * @return a string value for that map that can be used for configuration properties + * @see #escapeKey(StringBuilder, String) + * @see #escapeValue(StringBuilder, String) + */ + @SuppressWarnings("unchecked") + public static String mapToString(final Map map) { + if (map == null || map.isEmpty()) { + return null; + } + if (map instanceof EnumMap) { + return mapToString((EnumMap) map); + } + final StringBuilder sb = new StringBuilder(map.size() * 32); + final Iterator> iterator = map.entrySet().iterator(); + while (iterator.hasNext()) { + final Map.Entry entry = iterator.next(); + escapeKey(sb, String.valueOf(entry.getKey())); + sb.append('='); + escapeValue(sb, entry.getValue()); + if (iterator.hasNext()) { + sb.append(','); + } + } + return sb.toString(); + } + + /** + * Converts a map into a string that can be parsed by {@link #stringToMap(String)}. The kwy will be the + * {@linkplain Enum#name() enum name}. + * + * @param map the map to convert to a string + * @param the type of the key + * @return a string value for that map that can be used for configuration properties + * @see #escapeKey(StringBuilder, String) + * @see #escapeValue(StringBuilder, String) + */ + public static > String mapToString(final EnumMap map) { + if (map == null || map.isEmpty()) { + return null; + } + final StringBuilder sb = new StringBuilder(map.size() * 32); + final Iterator> iterator = map.entrySet().iterator(); + while (iterator.hasNext()) { + final Map.Entry entry = iterator.next(); + sb.append(entry.getKey().name()); + sb.append('='); + escapeValue(sb, entry.getValue()); + if (iterator.hasNext()) { + sb.append(','); + } + } + return sb.toString(); + } + + /** + * Escapes a maps key value for marshalling to a string. If the key contains a {@code \} or an {@code =} it will + * be escaped by a preceding {@code \}. Example: {@code key\=} or {@code \\key}. + * + * @param sb the string builder to append the escaped key to + * @param key the key + */ + public static void escapeKey(final StringBuilder sb, final String key) { + final char[] chars = key.toCharArray(); + for (int i = 0; i < chars.length; i++) { + final char c = chars[i]; + // Ensure that \ and = are escaped + if (c == '\\') { + final int n = i + 1; + if (n >= chars.length) { + sb.append('\\').append('\\'); + } else { + final char next = chars[n]; + if (next == '\\' || next == '=') { + // Nothing to do, already properly escaped + sb.append(c); + sb.append(next); + i = n; + } else { + // Now we need to escape the \ + sb.append('\\').append('\\'); + } + } + } else if (c == '=') { + sb.append('\\').append(c); + } else { + sb.append(c); + } + } + } + + /** + * Escapes a maps value for marshalling to a string. If a value contains a {@code \} or a {@code ,} it will be + * escaped by a preceding {@code \}. Example: {@code part1\,part2} or {@code value\\other}. + * + * @param sb the string builder to append the escaped value to + * @param value the value + */ + public static void escapeValue(final StringBuilder sb, final String value) { + if (value != null) { + final char[] chars = value.toCharArray(); + for (int i = 0; i < chars.length; i++) { + final char c = chars[i]; + // Ensure that \ and , are escaped + if (c == '\\') { + final int n = i + 1; + if (n >= chars.length) { + sb.append('\\').append('\\'); + } else { + final char next = chars[n]; + if (next == '\\' || next == ',') { + // Nothing to do, already properly escaped + sb.append(c); + sb.append(next); + i = n; + } else { + // Now we need to escape the \ + sb.append('\\').append('\\'); + } + } + } else if (c == ',') { + sb.append('\\').append(c); + } else { + sb.append(c); + } + } + } + } +} diff --git a/logging/src/main/java/org/xbib/logging/util/StackTraceFormatter.java b/logging/src/main/java/org/xbib/logging/util/StackTraceFormatter.java new file mode 100644 index 0000000..ef77612 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/util/StackTraceFormatter.java @@ -0,0 +1,138 @@ +package org.xbib.logging.util; + +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Set; + +/** + * Formatter used to format the stack trace of an exception. + */ +public class StackTraceFormatter { + + private static final String CAUSED_BY_CAPTION = "Caused by: "; + + private static final String SUPPRESSED_CAPTION = "Suppressed: "; + + private final Set seen; + + private final StringBuilder builder; + + private final int suppressedDepth; + + private int suppressedCount; + + private StackTraceFormatter(final StringBuilder builder, + final int suppressedDepth) { + this.builder = builder; + this.suppressedDepth = suppressedDepth; + this.seen = Collections.newSetFromMap(new IdentityHashMap<>()); + } + + /** + * Writes the stack trace into the builder. + * + * @param builder the string builder ot append the stack trace to + * @param t the throwable to render + * @param suppressedDepth the number of suppressed messages to include + */ + public static void renderStackTrace(final StringBuilder builder, final Throwable t, final int suppressedDepth) { + renderStackTrace(builder, t, false, suppressedDepth); + } + + /** + * Writes the stack trace into the builder. + * + * @param builder the string builder ot append the stack trace to + * @param t the throwable to render + * @param extended ignored + * @param suppressedDepth the number of suppressed messages to include + */ + public static void renderStackTrace(final StringBuilder builder, + final Throwable t, + @SuppressWarnings("unused") final boolean extended, + final int suppressedDepth) { + new StackTraceFormatter(builder, suppressedDepth).renderStackTrace(t); + } + + private void renderStackTrace(final Throwable t) { + suppressedCount = 0; + builder.append(": ").append(t); + newLine(); + final StackTraceElement[] stackTrace = t.getStackTrace(); + for (StackTraceElement element : stackTrace) { + renderTrivial("", element); + } + if (suppressedDepth != 0) { + for (Throwable se : t.getSuppressed()) { + if (suppressedDepth < 0 || suppressedDepth > suppressedCount++) { + renderStackTrace(stackTrace, se, SUPPRESSED_CAPTION, "\t"); + } + } + } + final Throwable ourCause = t.getCause(); + if (ourCause != null) { + renderStackTrace(stackTrace, ourCause, CAUSED_BY_CAPTION, ""); + } + } + + private void renderStackTrace(final StackTraceElement[] parentStack, + final Throwable child, + final String caption, + final String prefix) { + if (seen.contains(child)) { + builder.append(prefix) + .append(caption) + .append("[CIRCULAR REFERENCE: ") + .append(child) + .append(']'); + newLine(); + } else { + seen.add(child); + final StackTraceElement[] causeStack = child.getStackTrace(); + int m = causeStack.length - 1; + int n = parentStack.length - 1; + while (m >= 0 && n >= 0 && causeStack[m].equals(parentStack[n])) { + m--; + n--; + } + final int framesInCommon = causeStack.length - 1 - m; + builder.append(prefix) + .append(caption) + .append(child); + newLine(); + for (int i = 0; i <= m; i++) { + renderTrivial(prefix, causeStack[i]); + } + if (framesInCommon != 0) { + builder.append(prefix) + .append("\t... ") + .append(framesInCommon) + .append(" more"); + newLine(); + } + if (suppressedDepth != 0) { + for (Throwable se : child.getSuppressed()) { + if (suppressedDepth < 0 || suppressedDepth > suppressedCount++) { + renderStackTrace(causeStack, se, SUPPRESSED_CAPTION, prefix + "\t"); + } + } + } + Throwable ourCause = child.getCause(); + if (ourCause != null) { + renderStackTrace(causeStack, ourCause, CAUSED_BY_CAPTION, prefix); + } + } + } + + private void renderTrivial(final String prefix, + final StackTraceElement element) { + builder.append(prefix) + .append("\tat ") + .append(element); + newLine(); + } + + private void newLine() { + builder.append(System.lineSeparator()); + } +} diff --git a/logging/src/main/java/org/xbib/logging/util/StackWalkerUtil.java b/logging/src/main/java/org/xbib/logging/util/StackWalkerUtil.java new file mode 100644 index 0000000..91b6256 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/util/StackWalkerUtil.java @@ -0,0 +1,134 @@ +package org.xbib.logging.util; + +import java.lang.module.ModuleDescriptor; +import java.util.EnumSet; +import java.util.Iterator; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Stream; +import org.xbib.logging.ExtLogRecord; +import org.xbib.logging.LogContext; + +public final class StackWalkerUtil { + + static final StackWalker WALKER = new GetStackWalkerAction().run(); + + private StackWalkerUtil() { + } + + public static Class findCallingClass(Set rejectClassLoaders) { + return WALKER.walk(new FindFirstWalkFunction(rejectClassLoaders)); + } + + public static LogContext logContextFinder(Set rejectClassLoaders, + final Function finder) { + return WALKER.walk(new FindAllWalkFunction(rejectClassLoaders, finder)); + } + + public static void calculateCaller(ExtLogRecord logRecord) { + WALKER.walk(new CallerCalcFunction(logRecord)); + } + + private static void calculateJdkModule(final ExtLogRecord logRecord, final Class clazz) { + final Module module = clazz.getModule(); + if (module != null) { + logRecord.setSourceModuleName(module.getName()); + final ModuleDescriptor descriptor = module.getDescriptor(); + if (descriptor != null) { + final Optional optional = descriptor.version(); + if (optional.isPresent()) { + logRecord.setSourceModuleVersion(optional.get().toString()); + } else { + logRecord.setSourceModuleVersion(null); + } + } + } + } + + private static final class CallerCalcFunction implements Function, Void> { + private final ExtLogRecord logRecord; + + CallerCalcFunction(final ExtLogRecord logRecord) { + this.logRecord = logRecord; + } + + public Void apply(final Stream stream) { + final String loggerClassName = logRecord.getLoggerClassName(); + final Iterator iterator = stream.iterator(); + boolean found = false; + while (iterator.hasNext()) { + final StackWalker.StackFrame frame = iterator.next(); + final Class clazz = frame.getDeclaringClass(); + if (clazz.getName().equals(loggerClassName)) { + // next entry could be the one we want! + found = true; + } else if (found) { + logRecord.setSourceClassName(frame.getClassName()); + logRecord.setSourceMethodName(frame.getMethodName()); + logRecord.setSourceFileName(frame.getFileName()); + logRecord.setSourceLineNumber(frame.getLineNumber()); + calculateJdkModule(logRecord, clazz); + return null; + } + } + logRecord.setUnknownCaller(); + return null; + } + } + + private static final class GetStackWalkerAction { + GetStackWalkerAction() { + } + + public StackWalker run() { + return StackWalker.getInstance(EnumSet.of(StackWalker.Option.RETAIN_CLASS_REFERENCE)); + } + } + + private static final class FindFirstWalkFunction implements Function, Class> { + private final Set rejectClassLoaders; + + FindFirstWalkFunction(final Set rejectClassLoaders) { + this.rejectClassLoaders = rejectClassLoaders; + } + + public Class apply(final Stream stream) { + final Iterator iterator = stream.iterator(); + while (iterator.hasNext()) { + final Class clazz = iterator.next().getDeclaringClass(); + final ClassLoader classLoader = clazz.getClassLoader(); + if (!rejectClassLoaders.contains(classLoader)) { + return clazz; + } + } + return null; + } + } + + private static final class FindAllWalkFunction implements Function, LogContext> { + private final Set rejectClassLoaders; + private final Function finder; + + FindAllWalkFunction(final Set rejectClassLoaders, final Function finder) { + this.rejectClassLoaders = rejectClassLoaders; + this.finder = finder; + } + + @Override + public LogContext apply(final Stream stream) { + final Iterator iterator = stream.iterator(); + while (iterator.hasNext()) { + final Class clazz = iterator.next().getDeclaringClass(); + final ClassLoader classLoader = clazz.getClassLoader(); + if (classLoader != null && !rejectClassLoaders.contains(classLoader)) { + final LogContext result = finder.apply(classLoader); + if (result != null) { + return result; + } + } + } + return null; + } + } +} diff --git a/logging/src/main/java/org/xbib/logging/util/StandardOutputStreams.java b/logging/src/main/java/org/xbib/logging/util/StandardOutputStreams.java new file mode 100644 index 0000000..3230f78 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/util/StandardOutputStreams.java @@ -0,0 +1,65 @@ +package org.xbib.logging.util; + +import java.io.PrintStream; + +/** + * Caches {@link System#out stdout} and {@link System#err stderr} early. + */ +@SuppressWarnings({"WeakerAccess", "unused"}) +public class StandardOutputStreams { + + public static final PrintStream stdout = System.out; + + public static final PrintStream stderr = System.err; + + public StandardOutputStreams() { + } + + /** + * Prints an error messages to {@link #stderr stderr}. + * + * @param msg the message to print + */ + public static void printError(final String msg) { + stderr.println(msg); + } + + /** + * Prints an error messages to {@link #stderr stderr}. + * + * @param format the {@link java.util.Formatter format} + * @param args the arguments for the format + */ + public static void printError(final String format, final Object... args) { + stderr.printf(format, args); + } + + /** + * Prints an error messages to {@link #stderr stderr}. + * + * @param cause the cause of the error, if not {@code null} the {@link Throwable#printStackTrace(PrintStream)} + * writes to {@link #stderr stderr} + * @param msg the message to print + */ + public static void printError(final Throwable cause, final String msg) { + stderr.println(msg); + if (cause != null) { + cause.printStackTrace(stderr); + } + } + + /** + * Prints an error messages to {@link #stderr stderr}. + * + * @param cause the cause of the error, if not {@code null} the {@link Throwable#printStackTrace(PrintStream)} + * writes to {@link #stderr stderr} + * @param format the {@link java.util.Formatter format} + * @param args the arguments for the format + */ + public static void printError(final Throwable cause, final String format, final Object... args) { + stderr.printf(format, args); + if (cause != null) { + cause.printStackTrace(stderr); + } + } +} diff --git a/logging/src/main/java/org/xbib/logging/util/SuffixRotator.java b/logging/src/main/java/org/xbib/logging/util/SuffixRotator.java new file mode 100644 index 0000000..cf34d19 --- /dev/null +++ b/logging/src/main/java/org/xbib/logging/util/SuffixRotator.java @@ -0,0 +1,279 @@ +package org.xbib.logging.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.logging.ErrorManager; +import java.util.zip.GZIPOutputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * A utility for rotating files based on a files suffix. + */ +public class SuffixRotator { + + /** + * The compression type for the rotation + */ + public enum CompressionType { + NONE, + GZIP, + ZIP + } + + /** + * An empty rotation suffix. + */ + public static final SuffixRotator EMPTY = + new SuffixRotator("", "", "", CompressionType.NONE); + + private final String originalSuffix; + private final String datePattern; + private final SimpleDateFormat formatter; + private final String compressionSuffix; + private final CompressionType compressionType; + + private SuffixRotator(final String originalSuffix, final String datePattern, + final String compressionSuffix, final CompressionType compressionType) { + this.originalSuffix = originalSuffix; + this.datePattern = datePattern; + this.compressionSuffix = compressionSuffix; + this.compressionType = compressionType; + if (datePattern.isEmpty()) { + formatter = null; + } else { + formatter = new SimpleDateFormat(datePattern); + } + } + + /** + * Parses a suffix into possible parts for rotation. + * + * @param suffix the suffix to parse + * @return the file rotator used to determine the suffix parts and rotate the file. + */ + public static SuffixRotator parse(final String suffix) { + if (suffix == null || suffix.isEmpty()) { + return EMPTY; + } + // Check the if the suffix contains a compression suffix + String compressionSuffix = ""; + String datePattern = ""; + CompressionType compressionType = CompressionType.NONE; + final String lSuffix = suffix.toLowerCase(Locale.ROOT); + int compressionIndex = lSuffix.indexOf(".gz"); + if (compressionIndex != -1) { + compressionSuffix = suffix.substring(compressionIndex); + datePattern = suffix.substring(0, compressionIndex); + compressionType = CompressionType.GZIP; + } else { + compressionIndex = lSuffix.indexOf(".zip"); + if (compressionIndex != -1) { + compressionSuffix = suffix.substring(compressionIndex); + datePattern = suffix.substring(0, compressionIndex); + compressionType = CompressionType.ZIP; + } + } + if (compressionSuffix.isEmpty() && datePattern.isEmpty()) { + return new SuffixRotator(suffix, suffix, "", CompressionType.NONE); + } + return new SuffixRotator(suffix, datePattern, compressionSuffix, compressionType); + } + + /** + * The {@linkplain SimpleDateFormat date format pattern} for the suffix or an empty + * {@linkplain String string}. + * + * @return the date pattern or an empty string + */ + public String getDatePattern() { + return datePattern; + } + + /** + * The compression suffix or an empty {@linkplain String string} + * + * @return the compression suffix or an empty string + */ + @SuppressWarnings("unused") + public String getCompressionSuffix() { + return compressionSuffix; + } + + /** + * The compression type. + * + * @return the compression type + */ + @SuppressWarnings("unused") + public CompressionType getCompressionType() { + return compressionType; + } + + /** + * Rotates the file to a new file appending the suffix to the target. + *

+ * The compression suffix will automatically be appended to target file if compression is being used. If compression + * is not being used the file is just moved replacing the target file if it already exists. + *

+ * + * @param errorManager the error manager used to report errors to, if {@code null} an {@link IOException} will + * be thrown + * @param source the file to be rotated + * @param suffix the suffix to append to the rotated file. + */ + public void rotate(final ErrorManager errorManager, final Path source, final String suffix) { + final Path target = Paths.get(source + suffix + compressionSuffix); + if (compressionType == CompressionType.GZIP) { + try { + archiveGzip(source, target); + // Delete the file after it's archived to behave like a file move or rename + deleteFile(source); + } catch (Exception e) { + errorManager.error(String.format("Failed to compress %s to %s. Compressed file may be left on the " + + "filesystem corrupted.", source, target), e, ErrorManager.WRITE_FAILURE); + } + } else if (compressionType == CompressionType.ZIP) { + try { + archiveZip(source, target); + // Delete the file after it's archived to behave like a file move or rename + deleteFile(source); + } catch (Exception e) { + errorManager.error(String.format("Failed to compress %s to %s. Compressed file may be left on the " + + "filesystem corrupted.", source, target), e, ErrorManager.WRITE_FAILURE); + } + } else { + move(errorManager, source, target); + } + } + + /** + * Rotates the file to a new file appending the suffix to the target. If a date suffix was specified the suffix + * will be added before the index or compression suffix. The current date will be used for the suffix. + *

+ * If the {@code maxBackupIndex} is greater than 0 previously rotated files will be moved to an numerically + * incremented target. The compression suffix, if required, will be appended to this indexed file name. + *

+ * + * @param errorManager the error manager used to report errors to, if {@code null} an {@link IOException} will + * be thrown + * @param source the file to be rotated + * @param maxBackupIndex the number of backups to keep + */ + public void rotate(final ErrorManager errorManager, final Path source, final int maxBackupIndex) { + if (formatter == null) { + rotate(errorManager, source, "", maxBackupIndex); + } else { + final String suffix; + synchronized (formatter) { + suffix = formatter.format(new Date()); + } + rotate(errorManager, source, suffix, maxBackupIndex); + } + } + + /** + * Rotates the file to a new file appending the suffix to the target. + *

+ * If the {@code maxBackupIndex} is greater than 0 previously rotated files will be moved to an numerically + * incremented target. The compression suffix, if required, will be appended to this indexed file name. + *

+ * + * @param errorManager the error manager used to report errors to, if {@code null} an {@link IOException} will + * be thrown + * @param source the file to be rotated + * @param suffix the optional suffix to append to the file before the index and optional compression suffix + * @param maxBackupIndex the number of backups to keep + */ + public void rotate(final ErrorManager errorManager, final Path source, final String suffix, final int maxBackupIndex) { + if (maxBackupIndex > 0) { + final String rotationSuffix = (suffix == null ? "" : suffix); + final String fileWithSuffix = source.toAbsolutePath() + rotationSuffix; + final Path lastFile = Paths.get(fileWithSuffix + "." + maxBackupIndex + compressionSuffix); + try { + deleteFile(lastFile); + } catch (Exception e) { + errorManager.error(String.format("Failed to delete file %s", lastFile), e, ErrorManager.GENERIC_FAILURE); + } + for (int i = maxBackupIndex - 1; i >= 1; i--) { + final Path src = Paths.get(fileWithSuffix + "." + i + compressionSuffix); + if (fileExists(src)) { + final Path target = Paths.get(fileWithSuffix + "." + (i + 1) + compressionSuffix); + move(errorManager, src, target); + } + } + rotate(errorManager, source, rotationSuffix + ".1"); + } else if (suffix != null && !suffix.isEmpty()) { + rotate(errorManager, source, suffix); + } + } + + @Override + public String toString() { + return originalSuffix; + } + + private void move(final ErrorManager errorManager, final Path src, final Path target) { + try { + Files.move(src, target, StandardCopyOption.REPLACE_EXISTING); + } catch (Exception e) { + // Report the error, but allow the rotation to continue + errorManager.error(String.format("Failed to move file %s to %s.", src, target), e, + ErrorManager.GENERIC_FAILURE); + } + } + + private void archiveGzip(final Path source, final Path target) throws IOException { + final byte[] buff = new byte[512]; + try (final GZIPOutputStream out = new GZIPOutputStream(newOutputStream(target), true)) { + try (final InputStream in = newInputStream(source)) { + int len; + while ((len = in.read(buff)) != -1) { + out.write(buff, 0, len); + } + } + out.finish(); + } + } + + private void archiveZip(final Path source, final Path target) throws IOException { + final byte[] buff = new byte[512]; + try (final ZipOutputStream out = new ZipOutputStream(newOutputStream(target), StandardCharsets.UTF_8)) { + final ZipEntry entry = new ZipEntry(source.getFileName().toString()); + out.putNextEntry(entry); + try (final InputStream in = newInputStream(source)) { + int len; + while ((len = in.read(buff)) != -1) { + out.write(buff, 0, len); + } + } + out.closeEntry(); + } + } + + @SuppressWarnings("UnusedReturnValue") + private boolean deleteFile(final Path path) throws IOException { + return Files.deleteIfExists(path); + } + + private boolean fileExists(final Path file) { + return Files.exists(file); + } + + private InputStream newInputStream(final Path file) throws IOException { + return Files.newInputStream(file); + } + + private OutputStream newOutputStream(final Path file) throws IOException { + return Files.newOutputStream(file); + } +} diff --git a/logging/src/main/resources/META-INF/services/java.lang.System$LoggerFinder b/logging/src/main/resources/META-INF/services/java.lang.System$LoggerFinder new file mode 100644 index 0000000..c702b13 --- /dev/null +++ b/logging/src/main/resources/META-INF/services/java.lang.System$LoggerFinder @@ -0,0 +1 @@ +org.xbib.logging.LoggerFinder \ No newline at end of file diff --git a/logging/src/main/resources/META-INF/services/java.util.logging.LogManager b/logging/src/main/resources/META-INF/services/java.util.logging.LogManager new file mode 100644 index 0000000..3522dbb --- /dev/null +++ b/logging/src/main/resources/META-INF/services/java.util.logging.LogManager @@ -0,0 +1 @@ +org.xbib.logging.LogManager diff --git a/logging/src/main/resources/META-INF/services/org.xbib.logging.LogContextConfiguratorFactory b/logging/src/main/resources/META-INF/services/org.xbib.logging.LogContextConfiguratorFactory new file mode 100644 index 0000000..cff484f --- /dev/null +++ b/logging/src/main/resources/META-INF/services/org.xbib.logging.LogContextConfiguratorFactory @@ -0,0 +1 @@ +org.xbib.logging.configuration.DefaultLogContextConfiguratorFactory diff --git a/logging/src/test/java/module-info.java b/logging/src/test/java/module-info.java new file mode 100644 index 0000000..3ce675e --- /dev/null +++ b/logging/src/test/java/module-info.java @@ -0,0 +1,14 @@ +module org.xbib.logging.test { + requires java.logging; + requires java.xml; + requires transitive org.junit.jupiter.api; + requires transitive org.xbib.logging; + exports org.xbib.logging.test; + exports org.xbib.logging.test.configuration; + exports org.xbib.logging.test.formatters; + exports org.xbib.logging.test.handlers; + opens org.xbib.logging.test to org.junit.platform.commons; + opens org.xbib.logging.test.configuration to org.junit.platform.commons; + opens org.xbib.logging.test.formatters to org.junit.platform.commons; + opens org.xbib.logging.test.handlers to org.junit.platform.commons; +} diff --git a/logging/src/test/java/org/xbib/logging/test/AbstractTest.java b/logging/src/test/java/org/xbib/logging/test/AbstractTest.java new file mode 100644 index 0000000..0d9e14c --- /dev/null +++ b/logging/src/test/java/org/xbib/logging/test/AbstractTest.java @@ -0,0 +1,22 @@ +package org.xbib.logging.test; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.xbib.logging.LogContext; + +public abstract class AbstractTest { + + public AbstractTest() { + } + + @BeforeEach + public void addLogContext() { + final LogContext context = LogContext.create(); + LogContext.setLogContextSelector(() -> context); + } + + @AfterEach + public void removeLogContext() { + LogContext.setLogContextSelector(LogContext.DEFAULT_LOG_CONTEXT_SELECTOR); + } +} diff --git a/logging/src/test/java/org/xbib/logging/test/AcceptFilter.java b/logging/src/test/java/org/xbib/logging/test/AcceptFilter.java new file mode 100644 index 0000000..a83fc16 --- /dev/null +++ b/logging/src/test/java/org/xbib/logging/test/AcceptFilter.java @@ -0,0 +1,15 @@ +package org.xbib.logging.test; + +import java.util.logging.Filter; +import java.util.logging.LogRecord; + +public class AcceptFilter implements Filter { + + public AcceptFilter() { + } + + @Override + public boolean isLoggable(final LogRecord record) { + return true; + } +} diff --git a/logging/src/test/java/org/xbib/logging/test/AssertingErrorManager.java b/logging/src/test/java/org/xbib/logging/test/AssertingErrorManager.java new file mode 100644 index 0000000..08a7246 --- /dev/null +++ b/logging/src/test/java/org/xbib/logging/test/AssertingErrorManager.java @@ -0,0 +1,83 @@ +package org.xbib.logging.test; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.logging.ErrorManager; + +import org.junit.jupiter.api.Assertions; + +public class AssertingErrorManager extends ErrorManager { + + private final int[] allowedCodes; + + private AssertingErrorManager() { + this.allowedCodes = null; + } + + private AssertingErrorManager(final int... allowedCodes) { + this.allowedCodes = allowedCodes; + } + + public static AssertingErrorManager of() { + return new AssertingErrorManager(); + } + + public static AssertingErrorManager of(final int... allowedCodes) { + return new AssertingErrorManager(allowedCodes); + } + + @Override + public void error(final String msg, final Exception ex, final int code) { + if (notAllowed(code)) { + final String codeStr; + switch (code) { + case CLOSE_FAILURE: + codeStr = "CLOSE_FAILURE"; + break; + case FLUSH_FAILURE: + codeStr = "FLUSH_FAILURE"; + break; + case FORMAT_FAILURE: + codeStr = "FORMAT_FAILURE"; + break; + case GENERIC_FAILURE: + codeStr = "GENERIC_FAILURE"; + break; + case OPEN_FAILURE: + codeStr = "OPEN_FAILURE"; + break; + case WRITE_FAILURE: + codeStr = "WRITE_FAILURE"; + break; + default: + codeStr = "INVALID (" + code + ")"; + break; + } + try ( + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw)) { + pw.printf("LogManager error of type %s: %s%n", codeStr, msg); + if (ex != null) { + ex.printStackTrace(pw); + } + Assertions.fail(sw.toString()); + } catch (IOException e) { + // This shouldn't happen, but just fail if it does + e.printStackTrace(); + Assertions.fail(String.format("Failed to print error message: %s", e.getMessage())); + } + } + } + + private boolean notAllowed(final int code) { + if (allowedCodes != null) { + for (int allowedCode : allowedCodes) { + if (code == allowedCode) { + return false; + } + } + } + return true; + } +} diff --git a/logging/src/test/java/org/xbib/logging/test/ExtLogRecordTests.java b/logging/src/test/java/org/xbib/logging/test/ExtLogRecordTests.java new file mode 100644 index 0000000..83e8f02 --- /dev/null +++ b/logging/src/test/java/org/xbib/logging/test/ExtLogRecordTests.java @@ -0,0 +1,18 @@ +package org.xbib.logging.test; + +import org.junit.jupiter.api.Test; +import org.xbib.logging.ExtLogRecord; +import org.xbib.logging.Level; + +public final class ExtLogRecordTests { + + public ExtLogRecordTests() { + } + + @Test + public void checkForLongThreadIdRegression() { + ExtLogRecord rec = new ExtLogRecord(Level.INFO, "Hello world!", ExtLogRecordTests.class.getName()); + // expect this to not blow up + rec.setLongThreadID(1234); + } +} diff --git a/logging/src/test/java/org/xbib/logging/test/FileHandlerPerformanceTests.java b/logging/src/test/java/org/xbib/logging/test/FileHandlerPerformanceTests.java new file mode 100644 index 0000000..0616fca --- /dev/null +++ b/logging/src/test/java/org/xbib/logging/test/FileHandlerPerformanceTests.java @@ -0,0 +1,48 @@ +package org.xbib.logging.test; + +import java.io.File; +import java.io.UnsupportedEncodingException; +import java.util.logging.Formatter; + +import org.xbib.logging.ExtHandler; +import org.xbib.logging.ExtLogRecord; +import org.xbib.logging.Level; +import org.xbib.logging.formatters.PatternFormatter; +import org.xbib.logging.handlers.FileHandler; +import org.junit.jupiter.api.Test; + +public class FileHandlerPerformanceTests { + + private static final Formatter testFormatter = new PatternFormatter("%m\n"); + + public FileHandlerPerformanceTests() { + } + + private static void initHandler(ExtHandler handler) throws UnsupportedEncodingException { + handler.setFormatter(testFormatter); + handler.setLevel(Level.ALL); + handler.setAutoFlush(true); + handler.setEncoding("utf-8"); + handler.setErrorManager(AssertingErrorManager.of()); + } + + private static void publish(final ExtHandler handler, final String msg) { + handler.publish(new ExtLogRecord(Level.INFO, msg, null)); + } + + @Test + public void testPerformance() throws Exception { + final FileHandler handler = new FileHandler(); + initHandler(handler); + final File tempFile = File.createTempFile("jblm-", ".log"); + tempFile.deleteOnExit(); + handler.setFile(tempFile); + final long start = System.currentTimeMillis(); + for (int i = 0; i < 100000; i++) { + publish(handler, "Test message " + i); + } + // the result is system dependant and can therefore only be checked manually + // a 'sluggish' build indicates a problem + System.out.println((System.currentTimeMillis() - start)); + } +} diff --git a/logging/src/test/java/org/xbib/logging/test/FilterTests.java b/logging/src/test/java/org/xbib/logging/test/FilterTests.java new file mode 100644 index 0000000..0c82ee5 --- /dev/null +++ b/logging/src/test/java/org/xbib/logging/test/FilterTests.java @@ -0,0 +1,555 @@ +package org.xbib.logging.test; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Filter; +import java.util.logging.Handler; +import java.util.logging.LogRecord; +import java.util.regex.Pattern; + +import org.xbib.logging.ExtLogRecord; +import org.xbib.logging.ExtLogRecord.FormatStyle; +import org.xbib.logging.Level; +import org.xbib.logging.Logger; +import org.xbib.logging.filters.AcceptAllFilter; +import org.xbib.logging.filters.AllFilter; +import org.xbib.logging.filters.AnyFilter; +import org.xbib.logging.filters.DenyAllFilter; +import org.xbib.logging.filters.InvertFilter; +import org.xbib.logging.filters.LevelChangingFilter; +import org.xbib.logging.filters.LevelFilter; +import org.xbib.logging.filters.LevelRangeFilter; +import org.xbib.logging.filters.RegexFilter; +import org.xbib.logging.filters.SubstituteFilter; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +public final class FilterTests { + + private static final Filter[] NO_FILTERS = new Filter[0]; + + public FilterTests() { + } + + @Test + public void testAcceptAllFilter() { + final Filter filter = AcceptAllFilter.getInstance(); + final AtomicBoolean ran = new AtomicBoolean(); + final Handler handler = new CheckingHandler(ran); + final Logger logger = Logger.getLogger("filterTest"); + logger.setUseParentHandlers(false); + logger.addHandler(handler); + logger.setLevel(Level.INFO); + logger.setFilter(filter); + handler.setLevel(Level.INFO); + logger.info("This is a test."); + assertTrue(ran.get(), "Handler wasn't run"); + } + + @Test + public void testDenyAllFilter() { + final Filter filter = DenyAllFilter.getInstance(); + final AtomicBoolean ran = new AtomicBoolean(); + final Handler handler = new CheckingHandler(ran); + final Logger logger = Logger.getLogger("filterTest"); + logger.setUseParentHandlers(false); + logger.addHandler(handler); + logger.setLevel(Level.INFO); + logger.setFilter(filter); + handler.setLevel(Level.INFO); + logger.info("This is a test."); + assertFalse(ran.get(), "Handler was run"); + } + + @Test + public void testAllFilter0() { + final Filter filter = new AllFilter(NO_FILTERS); + final AtomicBoolean ran = new AtomicBoolean(); + final Handler handler = new CheckingHandler(ran); + final Logger logger = Logger.getLogger("filterTest"); + logger.setUseParentHandlers(false); + logger.addHandler(handler); + logger.setLevel(Level.INFO); + logger.setFilter(filter); + handler.setLevel(Level.INFO); + logger.info("This is a test."); + assertTrue(ran.get(), "Handler wasn't run"); + } + + @Test + public void testAllFilter1() { + final Filter filter = new AllFilter(new Filter[] { + AcceptAllFilter.getInstance(), + AcceptAllFilter.getInstance(), + AcceptAllFilter.getInstance(), + }); + final AtomicBoolean ran = new AtomicBoolean(); + final Handler handler = new CheckingHandler(ran); + final Logger logger = Logger.getLogger("filterTest"); + logger.setUseParentHandlers(false); + logger.addHandler(handler); + logger.setLevel(Level.INFO); + logger.setFilter(filter); + handler.setLevel(Level.INFO); + logger.info("This is a test."); + assertTrue(ran.get(), "Handler wasn't run"); + } + + @Test + public void testAllFilter2() { + final Filter filter = new AllFilter(new Filter[] { + AcceptAllFilter.getInstance(), + DenyAllFilter.getInstance(), + AcceptAllFilter.getInstance(), + }); + final AtomicBoolean ran = new AtomicBoolean(); + final Handler handler = new CheckingHandler(ran); + final Logger logger = Logger.getLogger("filterTest"); + logger.setUseParentHandlers(false); + logger.addHandler(handler); + logger.setLevel(Level.INFO); + logger.setFilter(filter); + handler.setLevel(Level.INFO); + logger.info("This is a test."); + assertFalse(ran.get(), "Handler was run"); + } + + @Test + public void testAllFilter3() { + final Filter filter = new AllFilter(new Filter[] { + DenyAllFilter.getInstance(), + DenyAllFilter.getInstance(), + DenyAllFilter.getInstance(), + }); + final AtomicBoolean ran = new AtomicBoolean(); + final Handler handler = new CheckingHandler(ran); + final Logger logger = Logger.getLogger("filterTest"); + logger.setUseParentHandlers(false); + logger.addHandler(handler); + logger.setLevel(Level.INFO); + logger.setFilter(filter); + handler.setLevel(Level.INFO); + logger.info("This is a test."); + assertFalse(ran.get(), "Handler was run"); + } + + @Test + public void testAnyFilter0() { + final Filter filter = new AnyFilter(NO_FILTERS); + final AtomicBoolean ran = new AtomicBoolean(); + final Handler handler = new CheckingHandler(ran); + final Logger logger = Logger.getLogger("filterTest"); + logger.setUseParentHandlers(false); + logger.addHandler(handler); + logger.setLevel(Level.INFO); + logger.setFilter(filter); + handler.setLevel(Level.INFO); + logger.info("This is a test."); + assertFalse(ran.get(), "Handler was run"); + } + + @Test + public void testAnyFilter1() { + final Filter filter = new AnyFilter(new Filter[] { + AcceptAllFilter.getInstance(), + AcceptAllFilter.getInstance(), + AcceptAllFilter.getInstance(), + }); + final AtomicBoolean ran = new AtomicBoolean(); + final Handler handler = new CheckingHandler(ran); + final Logger logger = Logger.getLogger("filterTest"); + logger.setUseParentHandlers(false); + logger.addHandler(handler); + logger.setLevel(Level.INFO); + logger.setFilter(filter); + handler.setLevel(Level.INFO); + logger.info("This is a test."); + assertTrue(ran.get(), "Handler wasn't run"); + } + + @Test + public void testAnyFilter2() { + final Filter filter = new AnyFilter(new Filter[] { + AcceptAllFilter.getInstance(), + DenyAllFilter.getInstance(), + AcceptAllFilter.getInstance(), + }); + final AtomicBoolean ran = new AtomicBoolean(); + final Handler handler = new CheckingHandler(ran); + final Logger logger = Logger.getLogger("filterTest"); + logger.setUseParentHandlers(false); + logger.addHandler(handler); + logger.setLevel(Level.INFO); + logger.setFilter(filter); + handler.setLevel(Level.INFO); + logger.info("This is a test."); + assertTrue(ran.get(), "Handler wasn't run"); + } + + @Test + public void testAnyFilter3() { + final Filter filter = new AnyFilter(new Filter[] { + DenyAllFilter.getInstance(), + DenyAllFilter.getInstance(), + DenyAllFilter.getInstance(), + }); + final AtomicBoolean ran = new AtomicBoolean(); + final Handler handler = new CheckingHandler(ran); + final Logger logger = Logger.getLogger("filterTest"); + logger.setUseParentHandlers(false); + logger.addHandler(handler); + logger.setLevel(Level.INFO); + logger.setFilter(filter); + handler.setLevel(Level.INFO); + logger.info("This is a test."); + assertFalse(ran.get(), "Handler was run"); + } + + @Test + public void testInvertFilter0() { + final Filter filter = new InvertFilter(AcceptAllFilter.getInstance()); + final AtomicBoolean ran = new AtomicBoolean(); + final Handler handler = new CheckingHandler(ran); + final Logger logger = Logger.getLogger("filterTest"); + logger.setUseParentHandlers(false); + logger.addHandler(handler); + logger.setLevel(Level.INFO); + logger.setFilter(filter); + handler.setLevel(Level.INFO); + logger.info("This is a test."); + assertFalse(ran.get(), "Handler was run"); + } + + @Test + public void testInvertFilter1() { + final Filter filter = new InvertFilter(DenyAllFilter.getInstance()); + final AtomicBoolean ran = new AtomicBoolean(); + final Handler handler = new CheckingHandler(ran); + final Logger logger = Logger.getLogger("filterTest"); + logger.setUseParentHandlers(false); + logger.addHandler(handler); + logger.setLevel(Level.INFO); + logger.setFilter(filter); + handler.setLevel(Level.INFO); + logger.info("This is a test."); + assertTrue(ran.get(), "Handler wasn't run"); + } + + @Test + public void testLevelChangingFilter0() { + final Filter filter = new LevelChangingFilter(Level.INFO); + final AtomicBoolean ran = new AtomicBoolean(); + final Handler handler = new CheckingHandler(ran); + final Logger logger = Logger.getLogger("filterTest"); + logger.setUseParentHandlers(false); + logger.addHandler(handler); + logger.setLevel(Level.FINEST); + logger.setFilter(filter); + handler.setLevel(Level.INFO); + logger.finest("This is a test."); + assertTrue(ran.get(), "Handler wasn't run"); + } + + @Test + public void testLevelFilter0() { + final Filter filter = new LevelFilter(Level.INFO); + final AtomicBoolean ran = new AtomicBoolean(); + final Handler handler = new CheckingHandler(ran); + final Logger logger = Logger.getLogger("filterTest"); + logger.setUseParentHandlers(false); + logger.addHandler(handler); + logger.setLevel(Level.INFO); + logger.setFilter(filter); + handler.setLevel(Level.INFO); + logger.info("This is a test."); + assertTrue(ran.get(), "Handler wasn't run"); + } + + @Test + public void testLevelFilter1() { + final Filter filter = new LevelFilter(Level.WARNING); + final AtomicBoolean ran = new AtomicBoolean(); + final Handler handler = new CheckingHandler(ran); + final Logger logger = Logger.getLogger("filterTest"); + logger.setUseParentHandlers(false); + logger.addHandler(handler); + logger.setLevel(Level.INFO); + logger.setFilter(filter); + handler.setLevel(Level.INFO); + logger.info("This is a test."); + assertFalse(ran.get(), "Handler was run"); + } + + @Test + public void testLevelRangeFilter0() { + final Filter filter = new LevelRangeFilter(Level.DEBUG, true, Level.WARN, true); + final AtomicBoolean ran = new AtomicBoolean(); + final Handler handler = new CheckingHandler(ran); + final Logger logger = Logger.getLogger("filterTest"); + logger.setUseParentHandlers(false); + logger.addHandler(handler); + logger.setLevel(Level.INFO); + logger.setFilter(filter); + handler.setLevel(Level.INFO); + logger.info("This is a test."); + assertTrue(ran.get(), "Handler wasn't run"); + } + + @Test + public void testLevelRangeFilter1() { + final Filter filter = new LevelRangeFilter(Level.DEBUG, true, Level.WARN, true); + final AtomicBoolean ran = new AtomicBoolean(); + final Handler handler = new CheckingHandler(ran); + final Logger logger = Logger.getLogger("filterTest"); + logger.setUseParentHandlers(false); + logger.addHandler(handler); + logger.setLevel(Level.INFO); + logger.setFilter(filter); + handler.setLevel(Level.INFO); + logger.severe("This is a test."); + assertFalse(ran.get(), "Handler was run"); + } + + @Test + public void testLevelRangeFilter2() { + final Filter filter = new LevelRangeFilter(Level.DEBUG, true, Level.WARN, true); + final AtomicBoolean ran = new AtomicBoolean(); + final Handler handler = new CheckingHandler(ran); + final Logger logger = Logger.getLogger("filterTest"); + logger.setUseParentHandlers(false); + logger.addHandler(handler); + logger.setLevel(Level.DEBUG); + logger.setFilter(filter); + handler.setLevel(Level.DEBUG); + logger.log(Level.DEBUG, "This is a test."); + assertTrue(ran.get(), "Handler wasn't run"); + } + + @Test + public void testLevelRangeFilter3() { + final Filter filter = new LevelRangeFilter(Level.DEBUG, false, Level.WARN, true); + final AtomicBoolean ran = new AtomicBoolean(); + final Handler handler = new CheckingHandler(ran); + final Logger logger = Logger.getLogger("filterTest"); + logger.setUseParentHandlers(false); + logger.addHandler(handler); + logger.setLevel(Level.DEBUG); + logger.setFilter(filter); + handler.setLevel(Level.DEBUG); + logger.log(Level.DEBUG, "This is a test."); + assertFalse(ran.get(), "Handler was run"); + } + + @Test + public void testLevelRangeFilter4() { + final Filter filter = new LevelRangeFilter(Level.DEBUG, true, Level.WARN, false); + final AtomicBoolean ran = new AtomicBoolean(); + final Handler handler = new CheckingHandler(ran); + final Logger logger = Logger.getLogger("filterTest"); + logger.setUseParentHandlers(false); + logger.addHandler(handler); + logger.setLevel(Level.INFO); + logger.setFilter(filter); + handler.setLevel(Level.INFO); + logger.warning("This is a test."); + assertFalse(ran.get(), "Handler was run"); + } + + @Test + public void testRegexFilter0() { + final Filter filter = new RegexFilter("test"); + final AtomicBoolean ran = new AtomicBoolean(); + final Handler handler = new CheckingHandler(ran); + final Logger logger = Logger.getLogger("filterTest"); + logger.setUseParentHandlers(false); + logger.addHandler(handler); + logger.setLevel(Level.INFO); + logger.setFilter(filter); + handler.setLevel(Level.INFO); + logger.info("This is a test."); + assertTrue(ran.get(), "Handler wasn't run"); + } + + @Test + public void testRegexFilter1() { + final Filter filter = new RegexFilter("pest"); + final AtomicBoolean ran = new AtomicBoolean(); + final Handler handler = new CheckingHandler(ran); + final Logger logger = Logger.getLogger("filterTest"); + logger.setUseParentHandlers(false); + logger.addHandler(handler); + logger.setLevel(Level.INFO); + logger.setFilter(filter); + handler.setLevel(Level.INFO); + logger.info("This is a test."); + assertFalse(ran.get(), "Handler was run"); + } + + @Test + @Disabled("This test is testing essentially invalid/coincidental behavior") + public void testRegexFilter2() { + final Filter filter = new RegexFilter("pest"); + final AtomicBoolean ran = new AtomicBoolean(); + final Handler handler = new CheckingHandler(ran); + final Logger logger = Logger.getLogger("filterTest"); + final ExtLogRecord record = new ExtLogRecord(Level.INFO, "This is a test %s", FormatStyle.PRINTF, "filterTest"); + record.setParameters(new String[] { "pest" }); + logger.setUseParentHandlers(false); + logger.addHandler(handler); + logger.setLevel(Level.INFO); + logger.setFilter(filter); + handler.setLevel(Level.INFO); + logger.log(record); + assertTrue(ran.get(), "Handler wasn't run"); + } + + @Test + public void testRegexFilter3() { + final Filter filter = new RegexFilter("pest"); + final AtomicBoolean ran = new AtomicBoolean(); + final Handler handler = new CheckingHandler(ran); + final Logger logger = Logger.getLogger("filterTest"); + final ExtLogRecord record = new ExtLogRecord(Level.INFO, "This is a test %s", FormatStyle.PRINTF, "filterTest"); + record.setParameters(new String[] { "test" }); + logger.setUseParentHandlers(false); + logger.addHandler(handler); + logger.setLevel(Level.INFO); + logger.setFilter(filter); + handler.setLevel(Level.INFO); + logger.log(record); + assertFalse(ran.get(), "Handler was run"); + } + + @Test + public void regexFilterExceptionNullMessageTest() { + final ExtLogRecord logRecord = new ExtLogRecord(Level.ALL, null, null); + final Filter filter = new RegexFilter("test"); + boolean isLoggable = filter.isLoggable(logRecord); + assertFalse(isLoggable); + assertNull(logRecord.getFormattedMessage()); + } + + @Test + public void testSubstitueFilter0() { + final Filter filter = new SubstituteFilter(Pattern.compile("test"), "lunch", true); + final AtomicReference result = new AtomicReference(); + final Handler handler = new MessageCheckingHandler(result); + final Logger logger = Logger.getLogger("filterTest"); + logger.setUseParentHandlers(false); + logger.addHandler(handler); + logger.setLevel(Level.INFO); + logger.setFilter(filter); + handler.setLevel(Level.INFO); + logger.info("This is a test test."); + assertEquals("This is a lunch lunch.", result.get(), "Substitution was not correctly applied"); + } + + @Test + public void testSubstituteFilter1() { + final Filter filter = new SubstituteFilter(Pattern.compile("test"), "lunch", false); + final AtomicReference result = new AtomicReference(); + final Handler handler = new MessageCheckingHandler(result); + final Logger logger = Logger.getLogger("filterTest"); + logger.setUseParentHandlers(false); + logger.addHandler(handler); + logger.setLevel(Level.INFO); + logger.setFilter(filter); + handler.setLevel(Level.INFO); + logger.info("This is a test test."); + assertEquals("This is a lunch test.", result.get(), "Substitution was not correctly applied"); + } + + @Test + public void testSubstituteFilter2() { + final Filter filter = new SubstituteFilter(Pattern.compile("t(es)t"), "lunch$1", true); + final AtomicReference result = new AtomicReference(); + final Handler handler = new MessageCheckingHandler(result); + final Logger logger = Logger.getLogger("filterTest"); + logger.setUseParentHandlers(false); + logger.addHandler(handler); + logger.setLevel(Level.INFO); + logger.setFilter(filter); + handler.setLevel(Level.INFO); + logger.info("This is a test test."); + assertEquals("This is a lunches lunches.", result.get(), "Substitution was not correctly applied"); + } + + @Test + public void testSubstituteFilter3() { + final Filter filter = new SubstituteFilter(Pattern.compile("t(es)t"), "lunch$1", true); + final ExtLogRecord record = new ExtLogRecord(Level.INFO, "This is a test %s", FormatStyle.PRINTF, + FilterTests.class.getName()); + record.setParameters(new String[] { "test" }); + filter.isLoggable(record); + assertEquals("This is a lunches lunches", record.getFormattedMessage(), "Substitution was not correctly applied"); + } + + @Test + public void substituteFilterExceptionNullMessageTest() { + final ExtLogRecord logRecord = new ExtLogRecord(Level.ALL, null, null); + final Filter filter = new SubstituteFilter(Pattern.compile("test"), "lunch", true); + filter.isLoggable(logRecord); + assertEquals("null", logRecord.getFormattedMessage()); + } + + @Test + public void substitutionFilterWithLogRecord() { + final AtomicReference result = new AtomicReference(); + final Handler handler = new MessageCheckingHandler(result); + final Logger logger = Logger.getLogger("filterTest"); + final Filter filter = new SubstituteFilter(Pattern.compile("test"), "lunch", true); + + logger.setUseParentHandlers(false); + logger.addHandler(handler); + logger.setLevel(Level.INFO); + logger.setFilter(filter); + handler.setLevel(Level.INFO); + + final LogRecord record = new LogRecord(Level.INFO, "{0}"); + record.setLoggerName("filterTest"); + record.setParameters(new Object[] { "test" }); + + logger.log(record); + assertEquals("lunch", result.get(), "The substitution was not correctly applied"); + } + + private static final class MessageCheckingHandler extends Handler { + private final AtomicReference msg; + + private MessageCheckingHandler(final AtomicReference msg) { + this.msg = msg; + } + + public void publish(final LogRecord record) { + msg.set(record.getMessage()); + } + + public void flush() { + } + + public void close() throws SecurityException { + } + } + + private static final class CheckingHandler extends Handler { + private final AtomicBoolean ran; + + public CheckingHandler(final AtomicBoolean ran) { + this.ran = ran; + } + + public void publish(final LogRecord record) { + if (isLoggable(record)) { + ran.set(true); + } + } + + public void flush() { + } + + public void close() throws SecurityException { + } + } +} diff --git a/logging/src/test/java/org/xbib/logging/test/HandlerTests.java b/logging/src/test/java/org/xbib/logging/test/HandlerTests.java new file mode 100644 index 0000000..29f3743 --- /dev/null +++ b/logging/src/test/java/org/xbib/logging/test/HandlerTests.java @@ -0,0 +1,195 @@ +package org.xbib.logging.test; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.util.logging.Formatter; +import java.util.logging.Handler; +import java.util.logging.LogRecord; + +import org.xbib.logging.ExtHandler; +import org.xbib.logging.ExtLogRecord; +import org.xbib.logging.Level; +import org.xbib.logging.formatters.PatternFormatter; +import org.xbib.logging.handlers.FileHandler; +import org.xbib.logging.handlers.OutputStreamHandler; +import org.xbib.logging.handlers.WriterHandler; +import org.junit.jupiter.api.Test; + +public final class HandlerTests { + + private final Formatter testFormatter = new PatternFormatter("%m"); + + public HandlerTests() { + } + + @Test + public void testNullHandler() { + final ExtHandler handler = new ExtHandler() { + }; + handler.setLevel(Level.ALL); + handler.publish(new ExtLogRecord(Level.INFO, "Test message", null)); + } + + private void initHandler(ExtHandler handler) throws UnsupportedEncodingException { + handler.setFormatter(testFormatter); + handler.setLevel(Level.ALL); + handler.setAutoFlush(true); + handler.setEncoding("utf-8"); + } + + private void testPublish(ExtHandler handler) { + testPublish(handler, Level.INFO); + } + + private void testPublish(ExtHandler handler, Level level) { + handler.publish(new ExtLogRecord(level, "Test message", null)); + } + + @Test + public void testWriterHandler() throws Throwable { + final WriterHandler handler = new WriterHandler(); + initHandler(handler); + final StringWriter writer = new StringWriter(); + handler.setWriter(writer); + testPublish(handler); + assertEquals("Test message", writer.toString()); + } + + @Test + public void testOutputStreamHandler() throws Throwable { + final OutputStreamHandler handler = new OutputStreamHandler(); + initHandler(handler); + final ByteArrayOutputStream stream = new ByteArrayOutputStream(); + handler.setOutputStream(stream); + testPublish(handler); + assertEquals("Test message", new String(stream.toByteArray(), "utf-8")); + } + + @Test + public void testFileHandler() throws Throwable { + final FileHandler handler = new FileHandler(); + initHandler(handler); + final File tempFile = File.createTempFile("jblm-", ".log"); + try { + handler.setAppend(true); + handler.setFile(tempFile); + testPublish(handler); + handler.setFile(tempFile); + testPublish(handler); + handler.setAppend(false); + handler.setFile(tempFile); + testPublish(handler); + handler.close(); + final ByteArrayOutputStream os = new ByteArrayOutputStream(); + try { + final FileInputStream is = new FileInputStream(tempFile); + try { + int r; + while ((r = is.read()) != -1) + os.write(r); + assertEquals("Test message", new String(os.toByteArray(), "utf-8")); + tempFile.deleteOnExit(); + } finally { + is.close(); + } + } finally { + os.close(); + } + } finally { + tempFile.delete(); + } + } + + @Test + public void testEnableDisableHandler() throws Throwable { + final StringListHandler handler = new StringListHandler(); + testPublish(handler); + assertEquals(1, handler.size()); + handler.setEnabled(false); + testPublish(handler); + assertEquals(1, handler.size()); + handler.setEnabled(true); + testPublish(handler); + assertEquals(2, handler.size()); + } + + @Test + public void testHandlerDelegation() throws Throwable { + final StringListHandler debugHandler = new StringListHandler(); + debugHandler.setLevel(Level.DEBUG); + + final StringListHandler infoHandler = new StringListHandler(); + infoHandler.setLevel(Level.INFO); + + final StringListHandler errorHandler = new StringListHandler(); + errorHandler.setLevel(Level.ERROR); + + final MultiHandler handler = MultiHandler.of(debugHandler, infoHandler, errorHandler); + // Turn off the level for the handler + handler.setLevel(Level.OFF); + + // Log a debug message + testPublish(handler, Level.DEBUG); + assertEquals(1, debugHandler.size()); + assertEquals(0, infoHandler.size()); + assertEquals(0, errorHandler.size()); + + // Log an info message + testPublish(handler, Level.INFO); + assertEquals(2, debugHandler.size()); + assertEquals(1, infoHandler.size()); + assertEquals(0, errorHandler.size()); + + // Log a warn message + testPublish(handler, Level.WARN); + assertEquals(3, debugHandler.size()); + assertEquals(2, infoHandler.size()); + assertEquals(0, errorHandler.size()); + + // Log an error message + testPublish(handler, Level.ERROR); + assertEquals(4, debugHandler.size()); + assertEquals(3, infoHandler.size()); + assertEquals(1, errorHandler.size()); + } + + static class MultiHandler extends ExtHandler { + protected MultiHandler() { + setErrorManager(AssertingErrorManager.of()); + } + + public void publish(final LogRecord record) { + if (isEnabled() && record != null) { + doPublish(ExtLogRecord.wrap(record)); + } + } + + public void publish(final ExtLogRecord record) { + if (isEnabled() && record != null) { + doPublish(record); + } + } + + static MultiHandler of(final ExtHandler... handlers) { + final MultiHandler result = new MultiHandler(); + result.setHandlers(handlers); + return result; + } + + @Override + protected void doPublish(final ExtLogRecord record) { + // Process child handlers + for (Handler handler : getHandlers()) { + if (handler.isLoggable(record)) { + handler.publish(record); + } + } + super.doPublish(record); + } + } +} diff --git a/logging/src/test/java/org/xbib/logging/test/LogContextCloseTests.java b/logging/src/test/java/org/xbib/logging/test/LogContextCloseTests.java new file mode 100644 index 0000000..0dcfda8 --- /dev/null +++ b/logging/src/test/java/org/xbib/logging/test/LogContextCloseTests.java @@ -0,0 +1,321 @@ +package org.xbib.logging.test; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.UUID; +import java.util.logging.ErrorManager; +import java.util.logging.Filter; +import java.util.logging.Formatter; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.logging.ExtHandler; +import org.xbib.logging.ExtLogRecord; +import org.xbib.logging.LogContext; +import org.xbib.logging.Logger; +import org.xbib.logging.LoggerNode; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +public class LogContextCloseTests { + + public LogContextCloseTests() { + } + + @BeforeEach + public void resetTestObjects() { + TestErrorManager.POJO_OBJECT = null; + TestFilter.POJO_OBJECT = null; + TestFormatter.POJO_OBJECT = null; + TestHandler.ERROR_MANAGER = null; + TestHandler.FILTER = null; + TestHandler.FORMATTER = null; + TestHandler.HANDLERS = null; + TestHandler.IS_CLOSED = false; + TestHandler.POJO_OBJECT = null; + } + + @Test + public void testCloseLogContext() throws Exception { + LogContext logContext = LogContext.create(); + + // Create a test handler to use + final TestHandler handler = new TestHandler(); + handler.setErrorManager(new TestErrorManager()); + handler.setFilter(new TestFilter()); + handler.setFormatter(new TestFormatter()); + handler.setLevel(org.xbib.logging.Level.TRACE); + + final Logger rootLogger = logContext.getLogger(""); + rootLogger.setLevel(org.xbib.logging.Level.WARN); + final Logger testLogger = logContext.getLogger(LogContextCloseTests.class.getName()); + testLogger.setLevel(Level.FINE); + final Logger randomLogger = logContext.getLogger(UUID.randomUUID().toString()); + randomLogger.setUseParentFilters(true); + + rootLogger.addHandler(handler); + + logContext.close(); + + // Loggers should have no handlers and have been reset + assertEquals(Level.INFO, rootLogger.getLevel()); + final Handler[] handlers = randomLogger.getHandlers(); + assertTrue(handlers == null || handlers.length == 0); + + assertEmptyContext(logContext, rootLogger, testLogger, randomLogger); + } + + @Test + public void testCloseWithAttachment() throws Exception { + LogContext logContext = LogContext.create(); + final Logger.AttachmentKey key = new Logger.AttachmentKey<>(); + final String value = "test value"; + Logger rootLogger = logContext.getLogger(""); + assertNull(rootLogger.attach(key, value)); + + // Close and ensure the context is clean + logContext.close(); + assertNull(rootLogger.getAttachment(key)); + assertEmptyContext(logContext, rootLogger); + + // Test attachIfAbsent() + logContext = LogContext.create(); + rootLogger = logContext.getLogger(""); + assertNull(rootLogger.attachIfAbsent(key, value)); + + // Close and ensure the context is clean + logContext.close(); + assertNull(rootLogger.getAttachment(key)); + assertEmptyContext(logContext, rootLogger); + + // Test detach() + logContext = LogContext.create(); + rootLogger = logContext.getLogger(""); + assertNull(rootLogger.attach(key, value)); + assertEquals(value, rootLogger.detach(key)); + logContext.close(); + assertNull(rootLogger.getAttachment(key)); + assertEmptyContext(logContext, rootLogger); + } + + private void assertEmptyContext(final LogContext logContext, final Logger... loggers) { + // Inspect the log context and ensure it's "empty" + final LoggerNode rootLogger = logContext.getRootLoggerNode(); + final Handler[] handlers = rootLogger.getHandlers(); + assertTrue(handlers == null || handlers.length == 0, "Expected the handlers to be removed."); + assertNull(rootLogger.getFilter(), "Expected the filter to be null"); + assertEquals(Level.INFO, rootLogger.getLevel(), "Expected the level to be INFO for logger the root logger"); + assertFalse(rootLogger.getUseParentFilters(), + "Expected the useParentFilters to be false for the root logger"); + assertTrue(rootLogger.getUseParentHandlers(), + "Expected the useParentHandlers to be true for the root logger"); + final Collection children = rootLogger.getChildren(); + if (!children.isEmpty()) { + final StringBuilder msg = new StringBuilder( + "Expected no children to be remaining on the root logger. Remaining loggers: "); + final Iterator iter = children.iterator(); + while (iter.hasNext()) { + msg.append('\'').append(iter.next().getFullName()).append('\''); + if (iter.hasNext()) { + msg.append(", "); + } + } + fail(msg.toString()); + } + + for (Logger logger : loggers) { + assertLoggerReset(logger); + } + } + + private void assertLoggerReset(final Logger logger) { + String loggerName = logger.getName(); + final Level expectedLevel; + if ("".equals(loggerName)) { + loggerName = "root"; + expectedLevel = Level.INFO; + } else { + expectedLevel = null; + } + final Handler[] handlers = logger.getHandlers(); + assertNull(logger.getFilter(), "Expected the filter to be null for logger " + loggerName); + assertTrue(handlers == null || handlers.length == 0, "Empty handlers expected for logger " + loggerName); + assertEquals(expectedLevel, logger.getLevel(), + "Expected the level to be " + expectedLevel + " for logger " + loggerName); + assertFalse(logger.getUseParentFilters(), + "Expected the useParentFilters to be false for logger " + loggerName); + assertTrue(logger.getUseParentHandlers(), + "Expected the useParentHandlers to be true for logger " + loggerName); + } + + @SuppressWarnings("unused") + public static class TestFilter implements Filter { + + private static PojoObject POJO_OBJECT; + + public TestFilter() { + } + + @Override + public boolean isLoggable(final LogRecord record) { + return true; + } + + public void setPojoObject(final PojoObject pojoObject) { + POJO_OBJECT = pojoObject; + } + } + + @SuppressWarnings("unused") + public static class TestFormatter extends Formatter { + + private static PojoObject POJO_OBJECT; + + public TestFormatter() { + } + + public void setPojoObject(final PojoObject pojoObject) { + POJO_OBJECT = pojoObject; + } + + @Override + public String format(final LogRecord record) { + return ExtLogRecord.wrap(record).getFormattedMessage(); + } + } + + @SuppressWarnings("unused") + public static class TestErrorManager extends ErrorManager { + + private static PojoObject POJO_OBJECT; + + public TestErrorManager() { + } + + public void setPojoObject(final PojoObject pojoObject) { + POJO_OBJECT = pojoObject; + } + } + + @SuppressWarnings({ "unused", "WeakerAccess" }) + public static class TestHandler extends ExtHandler { + private static PojoObject POJO_OBJECT; + private static Handler[] HANDLERS; + private static Formatter FORMATTER; + private static Filter FILTER; + private static ErrorManager ERROR_MANAGER; + private static boolean IS_CLOSED; + + public TestHandler() { + IS_CLOSED = false; + } + + @Override + public void close() throws SecurityException { + // Null out static values + POJO_OBJECT = null; + FORMATTER = null; + HANDLERS = null; + FORMATTER = null; + FILTER = null; + ERROR_MANAGER = null; + IS_CLOSED = true; + super.close(); + } + + @Override + public Handler[] setHandlers(final Handler[] newHandlers) throws SecurityException { + HANDLERS = Arrays.copyOf(newHandlers, newHandlers.length); + return super.setHandlers(newHandlers); + } + + @Override + public void addHandler(final Handler handler) throws SecurityException { + if (handler == null) { + throw new RuntimeException("Cannot add a null handler"); + } + if (HANDLERS == null) { + HANDLERS = new Handler[] { handler }; + } else { + final int len = HANDLERS.length + 1; + HANDLERS = Arrays.copyOf(HANDLERS, len); + HANDLERS[len - 1] = handler; + } + super.addHandler(handler); + } + + @Override + public void removeHandler(final Handler handler) throws SecurityException { + if (handler == null) { + throw new RuntimeException("Cannot remove a null handler"); + } + if (HANDLERS == null) { + throw new RuntimeException("Attempting to remove a handler that does not exist: " + handler); + } else { + if (HANDLERS.length == 1) { + HANDLERS = null; + } else { + boolean success = false; + final Handler[] newHandlers = new Handler[HANDLERS.length - 1]; + int newIndex = 0; + for (int i = 0; i < HANDLERS.length; i++) { + final Handler current = HANDLERS[i]; + if (!success && i > newHandlers.length) { + break; + } + if (handler != current) { + newHandlers[newIndex++] = current; + } else { + success = true; + } + } + if (!success) { + throw new RuntimeException("Failed to remove handler " + handler + " as it did no appear to exist."); + } + } + } + super.removeHandler(handler); + } + + @Override + public void setFormatter(final Formatter newFormatter) throws SecurityException { + FORMATTER = newFormatter; + super.setFormatter(newFormatter); + } + + @Override + public void setFilter(final Filter newFilter) throws SecurityException { + FILTER = newFilter; + super.setFilter(newFilter); + } + + @Override + public void setErrorManager(final ErrorManager em) { + ERROR_MANAGER = em; + super.setErrorManager(em); + } + + @Override + public void setLevel(final Level newLevel) throws SecurityException { + super.setLevel(newLevel); + } + + public void setPojoObject(final PojoObject pojoObject) { + POJO_OBJECT = pojoObject; + } + } + + @SuppressWarnings("WeakerAccess") + public static class PojoObject { + + public PojoObject() { + } + } +} diff --git a/logging/src/test/java/org/xbib/logging/test/LogManagerTests.java b/logging/src/test/java/org/xbib/logging/test/LogManagerTests.java new file mode 100644 index 0000000..08430a3 --- /dev/null +++ b/logging/src/test/java/org/xbib/logging/test/LogManagerTests.java @@ -0,0 +1,73 @@ +package org.xbib.logging.test; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.xbib.logging.Level; +import org.xbib.logging.LogManager; +import org.xbib.logging.Logger; + +public class LogManagerTests extends AbstractTest { + + static { + // Access a logger in initialize the logmanager + java.util.logging.Logger.getAnonymousLogger(); + } + + private final java.util.logging.Level[] levels = { + Level.TRACE, + Level.DEBUG, + Level.INFO, + Level.WARN, + Level.ERROR, + Level.FATAL, + java.util.logging.Level.ALL, + java.util.logging.Level.FINEST, + java.util.logging.Level.FINER, + java.util.logging.Level.FINE, + java.util.logging.Level.INFO, + java.util.logging.Level.CONFIG, + java.util.logging.Level.WARNING, + java.util.logging.Level.SEVERE, + java.util.logging.Level.OFF, + }; + + public LogManagerTests() { + } + + @Test + public void testLevelReplacement() throws Exception { + // Validate each level + for (java.util.logging.Level l : levels) { + java.util.logging.Level level = java.util.logging.Level.parse(l.getName()); + Assertions.assertEquals(l, level); + level = java.util.logging.Level.parse(Integer.toString(l.intValue())); + Assertions.assertEquals(l, level); + } + } + + @Test + public void checkLoggerNames() { + // Get the log manager + final java.util.logging.LogManager logManager = java.util.logging.LogManager.getLogManager(); + // Should be a org.xbib.logging.LogManager + Assertions.assertEquals(LogManager.class, logManager.getClass()); + + final List expectedNames = Arrays.asList("", "test1", "org.xbib", "org.xbib.logging", "other", "stdout"); + + // Create the loggers + for (String name : expectedNames) { + Logger.getLogger(name); + } + compare(expectedNames, Collections.list(logManager.getLoggerNames())); + } + + private void compare(final Collection expected, final Collection actual) { + Assertions.assertTrue(expected.containsAll(actual), () -> "Expected: " + expected + " Actual: " + actual); + Assertions.assertTrue(actual.containsAll(expected), () -> "Expected: " + expected + " Actual: " + actual); + } +} diff --git a/logging/src/test/java/org/xbib/logging/test/LoggerTests.java b/logging/src/test/java/org/xbib/logging/test/LoggerTests.java new file mode 100644 index 0000000..8c8bb7d --- /dev/null +++ b/logging/src/test/java/org/xbib/logging/test/LoggerTests.java @@ -0,0 +1,292 @@ +package org.xbib.logging.test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Handler; +import java.util.logging.LogRecord; + +import org.xbib.logging.ExtHandler; +import org.xbib.logging.ExtLogRecord; +import org.xbib.logging.Level; +import org.xbib.logging.LogContextInitializer; +import org.xbib.logging.Logger; +import org.xbib.logging.LoggerNode; +import org.xbib.logging.filters.RegexFilter; +import org.xbib.logging.formatters.PatternFormatter; +import org.junit.jupiter.api.Test; + +public final class LoggerTests { + + public LoggerTests() { + } + + @Test + public void testInstall() { + assertTrue((java.util.logging.Logger.getLogger("test") instanceof Logger), "Wrong logger class"); + } + + @Test + public void testSafeClone() { + assertSame(LogContextInitializer.NO_HANDLERS, LoggerNode.safeCloneHandlers()); + assertSame(LogContextInitializer.NO_HANDLERS, LoggerNode.safeCloneHandlers((Handler) null)); + assertSame(LogContextInitializer.NO_HANDLERS, LoggerNode.safeCloneHandlers(null, null)); + + Handler h1 = new NullHandler(); + Handler h2 = new NullHandler(); + assertArrayEquals(new Handler[] { h1, h2 }, LoggerNode.safeCloneHandlers(h1, h2)); + assertArrayEquals(new Handler[] { h1, h2 }, LoggerNode.safeCloneHandlers(h1, h2, null)); + assertArrayEquals(new Handler[] { h1, h2 }, LoggerNode.safeCloneHandlers(h1, null, h2)); + assertArrayEquals(new Handler[] { h1, h2 }, LoggerNode.safeCloneHandlers(null, h1, h2)); + assertArrayEquals(new Handler[] { h1, h2 }, LoggerNode.safeCloneHandlers(null, h1, null, h2, null)); + + assertArrayEquals(new Handler[] { h1 }, LoggerNode.safeCloneHandlers(null, h1)); + assertArrayEquals(new Handler[] { h1 }, LoggerNode.safeCloneHandlers(h1, null)); + } + + @Test + public void testCategories() { + assertNotNull(Logger.getLogger(LoggerTests.class.getName()), + "Logger not created with category: " + LoggerTests.class.getName()); + assertNotNull(Logger.getLogger("Spaced Logger Name"), "Logger not created with category: Spaced Logger Name"); + assertNotNull(Logger.getLogger("/../Weird/Path"), "Logger not created with category: /../Weird/Path"); + assertNotNull(Logger.getLogger("random.chars.`~!@#$%^&*()-=_+[]{}\\|;':\",.<>/?"), + "Logger not created with category: random.chars.`~!@#$%^&*()-=_+[]{}\\|;':\",.<>/?"); + } + + @Test + public void testHandlerAdd() { + final NullHandler h1 = new NullHandler(); + final NullHandler h2 = new NullHandler(); + final NullHandler h3 = new NullHandler(); + final Logger logger = Logger.getLogger("testHandlerAdd"); + logger.addHandler(h1); + logger.addHandler(h2); + logger.addHandler(h3); + boolean f1 = false; + boolean f2 = false; + boolean f3 = false; + for (Handler handler : logger.getHandlers()) { + if (handler == h1) + f1 = true; + if (handler == h2) + f2 = true; + if (handler == h3) + f3 = true; + } + assertTrue(f1, "Handler 1 missing"); + assertTrue(f2, "Handler 2 missing"); + assertTrue(f3, "Handler 3 missing"); + } + + @Test + public void testHandlerAdd2() { + final NullHandler h1 = new NullHandler(); + final Logger logger = Logger.getLogger("testHandlerAdd2"); + logger.addHandler(h1); + logger.addHandler(h1); + logger.addHandler(h1); + boolean f1 = false; + final Handler[] handlers = logger.getHandlers(); + for (Handler handler : handlers) { + if (handler == h1) + f1 = true; + } + assertTrue(f1, "Handler 1 missing"); + assertEquals(3, handlers.length, "Extra handlers missing"); + } + + @Test + public void testHandlerRemove() { + final NullHandler h1 = new NullHandler(); + final NullHandler h2 = new NullHandler(); + final NullHandler h3 = new NullHandler(); + final Logger logger = Logger.getLogger("testHandlerRemove"); + logger.addHandler(h1); + logger.addHandler(h2); + logger.addHandler(h3); + logger.removeHandler(h1); + boolean f1 = false; + boolean f2 = false; + boolean f3 = false; + for (Handler handler : logger.getHandlers()) { + if (handler == h1) + f1 = true; + if (handler == h2) + f2 = true; + if (handler == h3) + f3 = true; + } + assertFalse(f1, "Handler 1 wasn't removed"); + assertTrue(f2, "Handler 2 missing"); + assertTrue(f3, "Handler 3 missing"); + } + + @Test + public void testHandlerRemove2() { + final NullHandler h1 = new NullHandler(); + final Logger logger = Logger.getLogger("testHandlerRemove2"); + logger.removeHandler(h1); + final Handler[] handlers = logger.getHandlers(); + assertEquals(0, handlers.length); + } + + @Test + public void testHandlerClear() { + final NullHandler h1 = new NullHandler(); + final NullHandler h2 = new NullHandler(); + final NullHandler h3 = new NullHandler(); + final Logger logger = Logger.getLogger("testHandlerClear"); + logger.addHandler(h1); + logger.addHandler(h2); + logger.addHandler(h3); + logger.clearHandlers(); + boolean f1 = false; + boolean f2 = false; + boolean f3 = false; + for (Handler handler : logger.getHandlers()) { + if (handler == h1) + f1 = true; + if (handler == h2) + f2 = true; + if (handler == h3) + f3 = true; + } + assertFalse(f1, "Handler 1 wasn't removed"); + assertFalse(f2, "Handler 2 wasn't removed"); + assertFalse(f3, "Handler 3 wasn't removed"); + } + + @Test + public void testHandlerRun() { + final AtomicBoolean ran = new AtomicBoolean(); + final Handler handler = new CheckingHandler(ran); + final Logger logger = Logger.getLogger("testHandlerRun"); + logger.setUseParentHandlers(false); + logger.addHandler(handler); + logger.setLevel(Level.INFO); + handler.setLevel(Level.INFO); + logger.info("This is a test."); + assertTrue(ran.get(), "Handler wasn't run"); + } + + @Test + public void testResourceBundle() { + final ListHandler handler = new ListHandler(); + final Logger logger = Logger.getLogger("rbLogger", getClass().getName()); + logger.setLevel(Level.INFO); + handler.setLevel(Level.INFO); + logger.addHandler(handler); + logger.log(Level.INFO, null, new IllegalArgumentException()); + logger.log(Level.INFO, "test", new IllegalArgumentException()); + assertNull(handler.messages.get(0)); + assertEquals("Test message", handler.messages.get(1)); + } + + @Test + public void testJulResourceBundle() { + final ListHandler handler = new ListHandler(); + final java.util.logging.Logger logger = java.util.logging.Logger.getLogger("rbLogger", getClass().getName()); + assertNotNull(logger.getResourceBundleName(), "Resource bundle name was expected"); + assertNotNull(logger.getResourceBundle(), "Resource bundle was expected"); + logger.setLevel(Level.INFO); + handler.setLevel(Level.INFO); + logger.addHandler(handler); + logger.log(Level.INFO, null, new IllegalArgumentException()); + logger.log(Level.INFO, "test", new IllegalArgumentException()); + assertNull(handler.messages.get(0)); + assertEquals("Test message", handler.messages.get(1)); + } + + @Test + public void testInheritedFilter() { + final ListHandler handler = new ListHandler(); + final Logger parent = Logger.getLogger("parent", getClass().getName()); + parent.setLevel(Level.INFO); + handler.setLevel(Level.INFO); + parent.addHandler(handler); + parent.setFilter(new RegexFilter(".*(?i)test.*")); + + final Logger child = Logger.getLogger("parent.child", getClass().getName()); + child.setUseParentFilters(true); + child.setLevel(Level.INFO); + + child.info("This is a test message"); + child.info("This is another test message"); + child.info("One more message"); + + assertEquals(2, handler.messages.size(), "Handler should have only contained two messages"); + + // Clear the handler, reset the inherit filters + handler.messages.clear(); + child.setUseParentFilters(false); + + child.info("This is a test message"); + child.info("This is another test message"); + child.info("One more message"); + + assertEquals(3, handler.messages.size(), "Handler should have only contained three messages"); + + parent.info("This is a test message"); + parent.info("This is another test message"); + parent.info("One more message"); + + assertEquals(5, handler.messages.size(), "Handler should have only contained five messages"); + } + + private static final class ListHandler extends ExtHandler { + final List messages = Collections.synchronizedList(new ArrayList()); + + ListHandler() { + super(); + setFormatter(new PatternFormatter("%s")); + } + + @Override + protected void doPublish(final ExtLogRecord record) { + super.doPublish(record); + messages.add(record.getFormattedMessage()); + } + } + + private static final class CheckingHandler extends Handler { + private final AtomicBoolean ran; + + public CheckingHandler(final AtomicBoolean ran) { + this.ran = ran; + } + + public void publish(final LogRecord record) { + if (isLoggable(record)) { + ran.set(true); + } + } + + public void flush() { + } + + public void close() throws SecurityException { + } + } + + private static final class NullHandler extends Handler { + + public void publish(final LogRecord record) { + } + + public void flush() { + } + + public void close() throws SecurityException { + } + } +} diff --git a/logging/src/test/java/org/xbib/logging/test/MapTestUtils.java b/logging/src/test/java/org/xbib/logging/test/MapTestUtils.java new file mode 100644 index 0000000..453013e --- /dev/null +++ b/logging/src/test/java/org/xbib/logging/test/MapTestUtils.java @@ -0,0 +1,62 @@ +package org.xbib.logging.test; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Supplier; + +import org.junit.jupiter.api.Assertions; + +public class MapTestUtils { + + public MapTestUtils() { + } + + /** + * Compares the two maps have the same keys and same values in any order. + * + * @param m1 the first map used to compare the keys and values + * @param m2 the second map used to compare the keys and values + * @param the key type + * @param the value type + */ + @SuppressWarnings("WeakerAccess") + public static void compareMaps(final Map m1, final Map m2) { + Supplier failureMessage = () -> String.format("Keys did not match%n%s%n%s%n", m1.keySet(), m2.keySet()); + Assertions.assertTrue(m1.keySet().containsAll(m2.keySet()), failureMessage); + Assertions.assertTrue(m2.keySet().containsAll(m1.keySet()), failureMessage); + + // At this point we know that all the keys match + for (Map.Entry entry1 : m1.entrySet()) { + final V value2 = m2.get(entry1.getKey()); + Assertions.assertEquals(entry1.getValue(), value2, + () -> String.format("Value %s from the first map does not match value %s from the second map with key %s.", + entry1.getValue(), value2, entry1.getKey())); + } + } + + /** + * A helper to easily build maps. The resulting map is immutable and the order is predictable with the + * {@link #add(Object, Object)} order. + */ + public static class MapBuilder { + private final Map result; + + private MapBuilder(final Map result) { + this.result = result; + } + + public static MapBuilder create() { + return new MapBuilder<>(new LinkedHashMap()); + } + + public MapBuilder add(final K key, final V value) { + result.put(key, value); + return this; + } + + public Map build() { + return Collections.unmodifiableMap(result); + } + } +} diff --git a/logging/src/test/java/org/xbib/logging/test/PropertyValuesTests.java b/logging/src/test/java/org/xbib/logging/test/PropertyValuesTests.java new file mode 100644 index 0000000..9a337f3 --- /dev/null +++ b/logging/src/test/java/org/xbib/logging/test/PropertyValuesTests.java @@ -0,0 +1,134 @@ +package org.xbib.logging.test; + +import java.util.EnumMap; +import java.util.Map; + +import org.xbib.logging.formatters.StructuredFormatter.Key; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.xbib.logging.util.PropertyValues; + +public class PropertyValuesTests extends MapTestUtils { + + public PropertyValuesTests() { + } + + @Test + public void testStringToMap() { + Map map = MapBuilder. create() + .add("key1", "value1") + .add("key2", "value2") + .add("key3", "value3") + .build(); + Map parsedMap = PropertyValues.stringToMap("key1=value1,key2=value2,key3=value3"); + compareMaps(map, parsedMap); + + map = MapBuilder. create() + .add("key=1", "value1") + .add("key=2", "value,2") + .add("key3", "value,3") + .build(); + parsedMap = PropertyValues.stringToMap("key\\=1=value1,key\\=2=value\\,2,key3=value\\,3"); + compareMaps(map, parsedMap); + + map = MapBuilder. create() + .add("key=", "value,") + .add("key2", "value2") + .add("key\\", "value\\") + .add("this", "some=thing\\thing=some") + .build(); + parsedMap = PropertyValues.stringToMap("key\\==value\\,,key2=value2,key\\\\=value\\\\,this=some=thing\\\\thing=some"); + compareMaps(map, parsedMap); + + map = MapBuilder. create() + .add("key1", "value1") + .add("key2", null) + .add("key3", "value3") + .add("key4", null) + .build(); + parsedMap = PropertyValues.stringToMap("key1=value1,key2=,key3=value3,key4"); + compareMaps(map, parsedMap); + + map = MapBuilder. create() + .add("company", "SnakeOil Ltd.") + .add("product", "FooBar") + .add("name", "First \"nick\" Last") + .build(); + parsedMap = PropertyValues.stringToMap("company=SnakeOil Ltd.,product=FooBar,name=First \"nick\" Last"); + compareMaps(map, parsedMap); + + Assertions.assertTrue(PropertyValues.stringToMap(null).isEmpty(), "Map is not empty"); + Assertions.assertTrue(PropertyValues.stringToMap("").isEmpty(), "Map is not empty"); + } + + @Test + public void testStringToMapValueExpressions() { + Map map = MapBuilder. create() + .add("key1", "${org.xbib.logging.test.sysprop1}") + .add("key2=", "${org.xbib.logging.test.sysprop2}") + .build(); + Map parsedMap = PropertyValues + .stringToMap("key1=${org.xbib.logging.test.sysprop1},key2\\==${org.xbib.logging.test.sysprop2}"); + compareMaps(map, parsedMap); + } + + @Test + public void testStringToEnumMap() throws Exception { + Map map = MapBuilder. create() + .add(Key.EXCEPTION_CAUSED_BY, "cause") + .add(Key.MESSAGE, "msg") + .add(Key.HOST_NAME, "hostname") + .build(); + EnumMap parsedMap = PropertyValues.stringToEnumMap(Key.class, + "EXCEPTION_CAUSED_BY=cause,MESSAGE=msg,HOST_NAME=hostname"); + compareMaps(map, parsedMap); + + parsedMap = PropertyValues.stringToEnumMap(Key.class, "exception-caused-by=cause,message=msg,host-name=hostname"); + compareMaps(map, parsedMap); + } + + @Test + public void testMapToString() throws Exception { + Map map = MapBuilder. create() + .add("key1", "value1") + .add("key2", "value2") + .add("key3", "value3") + .build(); + Assertions.assertEquals("key1=value1,key2=value2,key3=value3", PropertyValues.mapToString(map)); + + map = MapBuilder. create() + .add("key=1", "value1") + .add("key=2", "value,2") + .add("key3", "value,3") + .build(); + Assertions.assertEquals("key\\=1=value1,key\\=2=value\\,2,key3=value\\,3", PropertyValues.mapToString(map)); + + map = MapBuilder. create() + .add("key=", "value,") + .add("key2", "value2") + .add("key\\", "value\\") + .add("this", "some=thing\\thing=some") + .build(); + Assertions.assertEquals("key\\==value\\,,key2=value2,key\\\\=value\\\\,this=some=thing\\\\thing=some", + PropertyValues.mapToString(map)); + + map = MapBuilder. create() + .add("key1", "value1") + .add("key2", null) + .add("key3", "value3") + .add("key4", null) + .build(); + Assertions.assertEquals("key1=value1,key2=,key3=value3,key4=", PropertyValues.mapToString(map)); + + map = MapBuilder. create() + .add("company", "SnakeOil Ltd.") + .add("product", "FooBar") + .add("name", "First \"nick\" Last") + .build(); + Assertions.assertEquals("company=SnakeOil Ltd.,product=FooBar,name=First \"nick\" Last", + PropertyValues.mapToString(map)); + + Assertions.assertTrue(PropertyValues.stringToMap(null).isEmpty(), "Expected an empty map"); + Assertions.assertTrue(PropertyValues.stringToMap("").isEmpty(), "Expected an empty map"); + } +} diff --git a/logging/src/test/java/org/xbib/logging/test/StringListHandler.java b/logging/src/test/java/org/xbib/logging/test/StringListHandler.java new file mode 100644 index 0000000..b45e1f1 --- /dev/null +++ b/logging/src/test/java/org/xbib/logging/test/StringListHandler.java @@ -0,0 +1,30 @@ +package org.xbib.logging.test; + +import java.util.ArrayList; +import java.util.List; +import org.xbib.logging.ExtHandler; +import org.xbib.logging.ExtLogRecord; + +class StringListHandler extends ExtHandler { + private final List messages = new ArrayList(); + + @Override + protected void doPublish(final ExtLogRecord record) { + super.doPublish(record); + messages.add(record.getFormattedMessage()); + } + + public String getMessage(final int index) { + return messages.get(index); + } + + public int size() { + return messages.size(); + } + + @Override + public void close() throws SecurityException { + super.close(); + messages.clear(); + } +} diff --git a/logging/src/test/java/org/xbib/logging/test/TestConfiguratorFactory.java b/logging/src/test/java/org/xbib/logging/test/TestConfiguratorFactory.java new file mode 100644 index 0000000..253d5c8 --- /dev/null +++ b/logging/src/test/java/org/xbib/logging/test/TestConfiguratorFactory.java @@ -0,0 +1,20 @@ +package org.xbib.logging.test; + +import org.xbib.logging.LogContextConfigurator; +import org.xbib.logging.LogContextConfiguratorFactory; + +public class TestConfiguratorFactory implements LogContextConfiguratorFactory { + + public TestConfiguratorFactory() { + } + + @Override + public LogContextConfigurator create() { + return new TestLogContextConfigurator(false); + } + + @Override + public int priority() { + return 50; + } +} diff --git a/logging/src/test/java/org/xbib/logging/test/TestLogContextConfigurator.java b/logging/src/test/java/org/xbib/logging/test/TestLogContextConfigurator.java new file mode 100644 index 0000000..fb6bbf7 --- /dev/null +++ b/logging/src/test/java/org/xbib/logging/test/TestLogContextConfigurator.java @@ -0,0 +1,79 @@ +package org.xbib.logging.test; + +import java.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; +import java.io.UncheckedIOException; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.xbib.logging.ExtFormatter; +import org.xbib.logging.ExtLogRecord; +import org.xbib.logging.LogContext; +import org.xbib.logging.LogContextConfigurator; +import org.xbib.logging.handlers.FileHandler; + +public class TestLogContextConfigurator implements LogContextConfigurator { + + public TestLogContextConfigurator() { + this(true); + } + + TestLogContextConfigurator(final boolean assumedJul) { + if (assumedJul) { + configure(null); + } + } + + @Override + public void configure(final LogContext logContext, final InputStream inputStream) { + configure(logContext); + } + + private static void configure(final LogContext logContext) { + if (Boolean.getBoolean("org.xbib.logging.test.configure")) { + final Logger rootLogger; + if (logContext == null) { + rootLogger = Logger.getLogger(""); + } else { + rootLogger = logContext.getLogger(""); + } + try { + final String fileName = System.getProperty("test.log.file.name"); + final FileHandler handler = new FileHandler(fileName, false); + handler.setAutoFlush(true); + handler.setFormatter(new TestFormatter()); + rootLogger.addHandler(handler); + rootLogger.setLevel(Level.INFO); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } + + private static class TestFormatter extends ExtFormatter { + + @Override + public String format(final ExtLogRecord record) { + StringWriter stringWriter = new StringWriter(); + /*try (JsonGenerator generator = Json.createGenerator(stringWriter)) { + generator.writeStartObject(); + + generator.write("loggerClassName", record.getLoggerClassName()); + generator.write("level", record.getLevel().toString()); + generator.write("message", record.getMessage()); + generator.writeStartObject("mdc"); + final Map mdc = record.getMdcCopy(); + mdc.forEach(generator::write); + generator.writeEnd(); // end MDC + + generator.writeEnd(); // end object + generator.flush(); + stringWriter.write(System.lineSeparator()); + return stringWriter.toString(); + }*/ + return stringWriter.toString(); + } + } +} diff --git a/logging/src/test/java/org/xbib/logging/test/configuration/PropertyConfigurationTests.java b/logging/src/test/java/org/xbib/logging/test/configuration/PropertyConfigurationTests.java new file mode 100644 index 0000000..cc81859 --- /dev/null +++ b/logging/src/test/java/org/xbib/logging/test/configuration/PropertyConfigurationTests.java @@ -0,0 +1,378 @@ +package org.xbib.logging.test.configuration; + +import java.io.PrintWriter; +import java.io.Reader; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Properties; +import java.util.logging.ErrorManager; +import java.util.logging.Filter; +import java.util.logging.Formatter; +import java.util.logging.Handler; +import java.util.logging.LogRecord; +import java.util.logging.Logger; + +import java.util.stream.Stream; +import org.xbib.logging.ExtHandler; +import org.xbib.logging.ExtLogRecord; +import org.xbib.logging.Level; +import org.xbib.logging.LogContext; +import org.xbib.logging.configuration.PropertyContextConfiguration; +import org.xbib.logging.formatters.PatternFormatter; +import org.xbib.logging.handlers.ConsoleHandler; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class PropertyConfigurationTests { + + private static final String DEFAULT_PATTERN = "%d{HH:mm:ss,SSS} %-5p [%c] (%t) %s%e%n"; + + private LogContext logContext; + + public PropertyConfigurationTests() { + } + + @BeforeEach + public void setup() { + logContext = LogContext.create(); + TestHandler.INITIALIZED = false; + TestFormatter.INITIALIZED = false; + TestErrorManager.INITIALIZED = false; + TestFilter.INITIALIZED = false; + } + + @AfterEach + public void tearDown() throws Exception { + logContext.close(); + } + + @Test + public void readConfigs() throws Exception { + final Path configDir = Paths.get(System.getProperty("user.dir"), "src", "test", "resources", "configs"); + Assertions.assertTrue(Files.exists(configDir), "Missing config dir: " + configDir); + try (Stream stream = Files.walk(configDir).filter(Files::isRegularFile)) { + stream.forEach((configFile) -> { + try ( + LogContext logContext = LogContext.create(); + Reader reader = Files.newBufferedReader(configFile, StandardCharsets.UTF_8)) { + final Properties properties = new Properties(); + properties.load(reader); + PropertyContextConfiguration.configure(logContext, properties); + } catch (Exception e) { + final StringWriter writer = new StringWriter(); + writer.append("Failed to configure ") + .append(configFile.getFileName().toString()) + .append(System.lineSeparator()); + e.printStackTrace(new PrintWriter(writer)); + Assertions.fail(writer.toString()); + } + }); + } + } + + @Test + public void testSimple() { + final Properties config = defaultProperties(); + PropertyContextConfiguration.configure(logContext, config); + testDefault(2, 1); + } + + @Test + public void testLogger() { + final String loggerName = PropertyConfigurationTests.class.getName(); + final Properties config = defaultProperties(loggerName); + + config.setProperty(String.join(",", "logger", loggerName, "level"), "INFO"); + config.setProperty(String.join(".", "logger", loggerName, "handlers"), "TEST"); + + config.setProperty("handler.TEST", TestHandler.class.getName()); + + PropertyContextConfiguration.configure(logContext, config); + testDefault(3, 1); + + final Logger logger = logContext.getLoggerIfExists(loggerName); + Assertions.assertNotNull(logger); + final TestHandler testHandler = findType(TestHandler.class, logger.getHandlers()); + Assertions.assertNotNull(testHandler, "Failed to find TestHandler"); + Assertions.assertTrue(TestHandler.INITIALIZED); + } + + @Test + public void testErrorManager() { + final Properties config = defaultProperties(); + String handlers = config.getProperty("logger.handlers"); + if (handlers == null) { + handlers = "TEST"; + } else { + handlers = handlers + ",TEST"; + } + config.setProperty("logger.handlers", handlers); + + config.setProperty("handler.TEST", TestHandler.class.getName()); + config.setProperty("handler.TEST.errorManager", "TEST"); + + config.setProperty("errorManager.TEST", TestErrorManager.class.getName()); + + PropertyContextConfiguration.configure(logContext, config); + testDefault(2, 2); + + final Logger rootLogger = logContext.getLogger(""); + final TestHandler handler = findType(TestHandler.class, rootLogger.getHandlers()); + Assertions.assertNotNull(handler); + Assertions.assertTrue(TestHandler.INITIALIZED); + + final ErrorManager errorManager = handler.getErrorManager(); + Assertions.assertTrue(errorManager instanceof TestErrorManager); + Assertions.assertTrue(TestErrorManager.INITIALIZED); + } + + @Test + public void testUnusedHandler() { + final Properties config = defaultProperties(); + config.setProperty("handler.TEST", TestHandler.class.getName()); + + PropertyContextConfiguration.configure(logContext, config); + testDefault(2, 1); + + // The test handler should not have been activated + Assertions.assertFalse(TestHandler.INITIALIZED, "The handler should not have been initialized"); + } + + @Test + public void testUnusedFormatter() { + final Properties config = defaultProperties(); + config.setProperty("formatter.TEST", TestFormatter.class.getName()); + + PropertyContextConfiguration.configure(logContext, config); + testDefault(2, 1); + + // The test handler should not have been activated + Assertions.assertFalse(TestFormatter.INITIALIZED, "The formatter should not have been initialized"); + } + + @Test + public void testUnusedErrorManager() { + final Properties config = defaultProperties(); + config.setProperty("errorManager.TEST", TestErrorManager.class.getName()); + + PropertyContextConfiguration.configure(logContext, config); + testDefault(2, 1); + + // The test handler should not have been activated + Assertions.assertFalse(TestErrorManager.INITIALIZED, "The error manager should not have been initialized"); + } + + @Test + public void testUnusedFilter() { + final Properties config = defaultProperties(); + config.setProperty("filter.TEST", TestFilter.class.getName()); + + PropertyContextConfiguration.configure(logContext, config); + testDefault(2, 1); + + // The test handler should not have been activated + Assertions.assertFalse(TestFilter.INITIALIZED, "The filter should not have been initialized"); + } + + private void testDefault(final int expectedLoggers, final int expectedRootHandlers) { + final Collection loggerNames = Collections.list(logContext.getLoggerNames()); + // We should have two defined loggers + Assertions.assertEquals(expectedLoggers, loggerNames.size(), + () -> "Expected two loggers to be defined found: " + loggerNames); + + // Test the configured root logger + final Logger rootLogger = logContext.getLoggerIfExists(""); + Assertions.assertNotNull(rootLogger, "Root logger was not configured"); + Assertions.assertEquals(Level.DEBUG.intValue(), rootLogger.getLevel().intValue()); + + // There should only be a console handler, check that it's configured + final Handler[] handlers = rootLogger.getHandlers(); + Assertions.assertNotNull(handlers, "Expected handles to be defined"); + Assertions.assertEquals(expectedRootHandlers, handlers.length, + () -> String.format("Expected %d handlers found %d: %s", expectedRootHandlers, handlers.length, + Arrays.toString(handlers))); + final Handler handler = findType(ConsoleHandler.class, handlers); + Assertions.assertNotNull(handler, "Failed to find the console handler"); + Assertions.assertEquals(ConsoleHandler.class, handler.getClass()); + Assertions.assertEquals(Level.TRACE.intValue(), handler.getLevel().intValue()); + } + + private static Properties defaultProperties(final String... additionalLoggers) { + final Properties config = new Properties(); + // Configure some default loggers + final StringBuilder sb = new StringBuilder("org.xbib.logging.ext"); + for (String additionalLogger : additionalLoggers) { + sb.append(',').append(additionalLogger); + } + config.setProperty("loggers", sb.toString()); + config.setProperty("logger.level", "DEBUG"); + config.setProperty("logger.handlers", "CONSOLE"); + + // Configure a handler + config.setProperty("handler.CONSOLE", ConsoleHandler.class.getName()); + config.setProperty("handler.CONSOLE.level", "TRACE"); + config.setProperty("handler.CONSOLE.formatter", "PATTERN"); + + // Configure a formatter + config.setProperty("formatter.PATTERN", PatternFormatter.class.getName()); + config.setProperty("formatter.PATTERN.properties", "pattern"); + config.setProperty("formatter.PATTERN.pattern", DEFAULT_PATTERN); + + return config; + } + + private static void addPojoConfiguration(final Properties config) { + String handlers = config.getProperty("logger.handlers"); + if (handlers == null) { + handlers = "POJO"; + } else { + handlers = handlers + ",POJO"; + } + config.setProperty("logger.handlers", handlers); + + // Configure the POJO handler + config.setProperty("handler.POJO", PojoHandler.class.getName()); + config.setProperty("handler.POJO.properties", "pojoObject"); + config.setProperty("handler.POJO.pojoObject", "POJO_OBJECT"); + + // Configure the POJO object + config.setProperty("pojos", "POJO_OBJECT"); + config.setProperty("pojo.POJO_OBJECT", PojoObject.class.getName()); + config.setProperty("pojo.POJO_OBJECT.properties", "value"); + config.setProperty("pojo.POJO_OBJECT.value", "testValue"); + config.setProperty("pojo.POJO_OBJECT.postConfiguration", "init,checkInitialized"); + + } + + private static T findType(final Class type, final Object[] array) { + if (array != null) { + for (Object obj : array) { + if (obj.getClass().isAssignableFrom(type)) { + return type.cast(obj); + } + } + } + return null; + } + + @SuppressWarnings("unused") + public static class PojoObject { + String name; + String value; + boolean initialized; + + public PojoObject() { + initialized = false; + } + + public void init() { + initialized = true; + } + + public void checkInitialized() { + if (!initialized) { + throw new IllegalStateException("Not initialized"); + } + } + + public String getName() { + return name; + } + + public void setName(final String name) { + this.name = name; + } + + public String getValue() { + return value; + } + + public void setValue(final String value) { + this.value = value; + } + } + + @SuppressWarnings("unused") + public static class PojoHandler extends ExtHandler { + PojoObject pojoObject; + + public PojoHandler() { + pojoObject = null; + } + + @Override + protected void doPublish(final ExtLogRecord record) { + super.doPublish(record); + } + + public PojoObject getPojoObject() { + return pojoObject; + } + + public void setPojoObject(final PojoObject pojoObject) { + this.pojoObject = pojoObject; + } + } + + public static class TestHandler extends Handler { + static boolean INITIALIZED; + + public TestHandler() { + INITIALIZED = true; + } + + @Override + public void publish(final LogRecord record) { + } + + @Override + public void flush() { + } + + @Override + public void close() throws SecurityException { + } + } + + public static class TestFormatter extends Formatter { + static boolean INITIALIZED; + + public TestFormatter() { + INITIALIZED = true; + } + + @Override + public String format(final LogRecord record) { + return record.getMessage(); + } + } + + public static class TestErrorManager extends ErrorManager { + static boolean INITIALIZED; + + public TestErrorManager() { + INITIALIZED = true; + } + } + + public static class TestFilter implements Filter { + static boolean INITIALIZED; + + public TestFilter() { + INITIALIZED = true; + } + + @Override + public boolean isLoggable(final LogRecord record) { + return true; + } + } +} diff --git a/logging/src/test/java/org/xbib/logging/test/formatters/AbstractTest.java b/logging/src/test/java/org/xbib/logging/test/formatters/AbstractTest.java new file mode 100644 index 0000000..8019b2b --- /dev/null +++ b/logging/src/test/java/org/xbib/logging/test/formatters/AbstractTest.java @@ -0,0 +1,27 @@ +package org.xbib.logging.test.formatters; + +import org.xbib.logging.ExtLogRecord; +import org.xbib.logging.ExtLogRecord.FormatStyle; +import org.xbib.logging.test.MapTestUtils; + +abstract class AbstractTest extends MapTestUtils { + + ExtLogRecord createLogRecord(final String msg) { + return createLogRecord(org.xbib.logging.Level.INFO, msg); + } + + ExtLogRecord createLogRecord(final String format, final Object... args) { + return createLogRecord(org.xbib.logging.Level.INFO, format, args); + } + + private ExtLogRecord createLogRecord(final org.xbib.logging.Level level, final String msg) { + return new ExtLogRecord(level, msg, getClass().getName()); + } + + ExtLogRecord createLogRecord(final org.xbib.logging.Level level, final String format, final Object... args) { + final ExtLogRecord record = new ExtLogRecord(level, format, FormatStyle.PRINTF, getClass().getName()); + record.setParameters(args); + return record; + } + +} diff --git a/logging/src/test/java/org/xbib/logging/test/formatters/BannerFormatterTests.java b/logging/src/test/java/org/xbib/logging/test/formatters/BannerFormatterTests.java new file mode 100644 index 0000000..ed957e1 --- /dev/null +++ b/logging/src/test/java/org/xbib/logging/test/formatters/BannerFormatterTests.java @@ -0,0 +1,91 @@ +package org.xbib.logging.test.formatters; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Objects; +import java.util.function.Supplier; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.xbib.logging.formatters.PatternFormatter; +import org.xbib.logging.formatters.TextBannerFormatter; + +/** + * Tests of the banner formatting capability. + */ +public class BannerFormatterTests { + + public static final String FALLBACK_OK = "fallback OK!"; + public static final String TEST_BANNER_FILE = "Test banner file!\n"; + + public BannerFormatterTests() { + } + + @Test + public void testBanner() throws Exception { + final PatternFormatter emptyFormatter = new PatternFormatter(""); + final Supplier fallbackSupplier = TextBannerFormatter.createStringSupplier(FALLBACK_OK); + Assertions.assertEquals("", emptyFormatter.getHead(null)); + Assertions.assertEquals(FALLBACK_OK, fallbackSupplier.get()); + TextBannerFormatter tbf = new TextBannerFormatter(fallbackSupplier, emptyFormatter); + Assertions.assertEquals(FALLBACK_OK, tbf.getHead(null)); + tbf = new TextBannerFormatter(createResourceSupplier("non-existent-banner.txt", fallbackSupplier), emptyFormatter); + Assertions.assertEquals(FALLBACK_OK, tbf.getHead(null)); + tbf = new TextBannerFormatter(createResourceSupplier("test-banner.txt", fallbackSupplier), emptyFormatter); + final InputStream is = getClass().getResourceAsStream("/org/xbib/logging/test/formatters/test-banner.txt"); + Assertions.assertNotNull(is); + try (is) { + final String s = new String(is.readAllBytes(), StandardCharsets.UTF_8); + Assertions.assertEquals(s, tbf.getHead(null)); + } + final Path tempFile = Files.createTempFile(Path.of(System.getProperty("user.dir"),"build"), "banner-format", null); + try { + Files.writeString(tempFile, TEST_BANNER_FILE); + tbf = new TextBannerFormatter(TextBannerFormatter.createFileSupplier(tempFile, fallbackSupplier), emptyFormatter); + Assertions.assertEquals(TEST_BANNER_FILE, tbf.getHead(null)); + // and, the URL version... + tbf = new TextBannerFormatter(TextBannerFormatter.createUrlSupplier(tempFile.toUri().toURL(), fallbackSupplier), + emptyFormatter); + Assertions.assertEquals(TEST_BANNER_FILE, tbf.getHead(null)); + } finally { + try { + Files.delete(tempFile); + } catch (Throwable ignored) { + } + } + // non-existent file + tbf = new TextBannerFormatter(TextBannerFormatter.createFileSupplier(Path.of("does not exist"), fallbackSupplier), + emptyFormatter); + Assertions.assertEquals(FALLBACK_OK, tbf.getHead(null)); + } + + /** + * Create a supplier which loads the banner from a resource in the given class loader, + * falling back to the given fallback supplier on error. + * + * @param resource the resource name (must not be {@code null}) + * @param fallback the fallback supplier (must not be {@code null}) + * @return the supplier (not {@code null}) + */ + private Supplier createResourceSupplier(String resource, Supplier fallback) { + Objects.requireNonNull(resource, "resource"); + Objects.requireNonNull(fallback, "fallback"); + return () -> { + try { + final InputStream is = getClass().getResourceAsStream(resource); + return is == null ? fallback.get() : loadStringFromStream(is); + } catch (IOException ignored) { + return fallback.get(); + } + }; + } + + private static String loadStringFromStream(final InputStream is) throws IOException { + try (is) { + return new String(is.readAllBytes(), StandardCharsets.UTF_8); + } + } +} diff --git a/logging/src/test/java/org/xbib/logging/test/formatters/PatternFormatterTests.java b/logging/src/test/java/org/xbib/logging/test/formatters/PatternFormatterTests.java new file mode 100644 index 0000000..b897d22 --- /dev/null +++ b/logging/src/test/java/org/xbib/logging/test/formatters/PatternFormatterTests.java @@ -0,0 +1,305 @@ +package org.xbib.logging.test.formatters; + +import org.xbib.logging.ExtLogRecord; +import org.xbib.logging.MDC; +import org.xbib.logging.NDC; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.xbib.logging.formatters.PatternFormatter; + +public class PatternFormatterTests { + + static final String CATEGORY = "org.xbib.logging.formatters.PatternFormatterTests"; + + static { + // Set a system property + System.setProperty("org.xbib.logging.testProp", "testValue"); + } + + public PatternFormatterTests() { + } + + @Test + public void categories() throws Exception { + final ExtLogRecord record = createLogRecord("test"); + PatternFormatter formatter = new PatternFormatter("%c"); + Assertions.assertEquals(CATEGORY, formatter.format(record)); + + formatter = new PatternFormatter("%c{1}"); + Assertions.assertEquals("PatternFormatterTests", formatter.format(record)); + + formatter = new PatternFormatter("%c{2}"); + Assertions.assertEquals("formatters.PatternFormatterTests", formatter.format(record)); + + formatter = new PatternFormatter("%c{1.}"); + Assertions.assertEquals("o.x.l.f.PatternFormatterTests", formatter.format(record)); + + formatter = new PatternFormatter("%c{1.~}"); + Assertions.assertEquals("o.~.~.~.PatternFormatterTests", formatter.format(record)); + + formatter = new PatternFormatter("%c{.}"); + Assertions.assertEquals("....PatternFormatterTests", formatter.format(record)); + + formatter = new PatternFormatter("%c{1~.}"); + Assertions.assertEquals("o~.x~.l~.f~.PatternFormatterTests", formatter.format(record)); + + // Test a simple logger name + record.setLoggerName("test"); + formatter = new PatternFormatter("%c{1}"); + Assertions.assertEquals("test", formatter.format(record)); + + formatter = new PatternFormatter("%c{.}"); + Assertions.assertEquals("test", formatter.format(record)); + + formatter = new PatternFormatter("%c{1~.}"); + Assertions.assertEquals("test", formatter.format(record)); + } + + @Test + public void classNames() throws Exception { + final ExtLogRecord record = createLogRecord("test"); + PatternFormatter formatter = new PatternFormatter("%C"); + Assertions.assertEquals(PatternFormatterTests.class.getName(), formatter.format(record)); + + formatter = new PatternFormatter("%C{1}"); + Assertions.assertEquals("PatternFormatterTests", formatter.format(record)); + + formatter = new PatternFormatter("%C{2}"); + Assertions.assertEquals("formatters.PatternFormatterTests", formatter.format(record)); + + formatter = new PatternFormatter("%C{1.}"); + Assertions.assertEquals("o.x.l.t.f.PatternFormatterTests", formatter.format(record)); + + formatter = new PatternFormatter("%C{1.~}"); + Assertions.assertEquals("o.~.~.~.~.PatternFormatterTests", formatter.format(record)); + + formatter = new PatternFormatter("%C{.}"); + Assertions.assertEquals(".....PatternFormatterTests", formatter.format(record)); + + formatter = new PatternFormatter("%C{1~.}"); + Assertions.assertEquals("o~.x~.l~.t~.f~.PatternFormatterTests", formatter.format(record)); + } + + @Test + public void ndc() throws Exception { + NDC.push("value1"); + NDC.push("value2"); + NDC.push("value3"); + final ExtLogRecord record = createLogRecord("test"); + + PatternFormatter formatter = new PatternFormatter("%x"); + Assertions.assertEquals("value1.value2.value3", formatter.format(record)); + + formatter = new PatternFormatter("%x{1}"); + Assertions.assertEquals("value3", formatter.format(record)); + + formatter = new PatternFormatter("%x{2}"); + Assertions.assertEquals("value2.value3", formatter.format(record)); + } + + @Test + public void mdc() throws Exception { + try { + MDC.put("primaryKey", "primaryValue"); + MDC.put("key1", "value1"); + MDC.put("key2", "value2"); + final ExtLogRecord record = createLogRecord("test"); + + PatternFormatter formatter = new PatternFormatter("%X{key1}"); + Assertions.assertEquals("value1", formatter.format(record)); + + formatter = new PatternFormatter("%X{not.found}"); + Assertions.assertEquals("", formatter.format(record)); + + formatter = new PatternFormatter("%X"); + String formatted = formatter.format(record); + Assertions.assertEquals("{key1=value1, key2=value2, primaryKey=primaryValue}", formatted); + } finally { + MDC.clear(); + } + } + + @Test + public void threads() throws Exception { + final ExtLogRecord record = createLogRecord("test"); + record.setThreadName("testThreadName"); + record.setThreadID(33); + PatternFormatter formatter = new PatternFormatter("%t"); + Assertions.assertEquals("testThreadName", formatter.format(record)); + + formatter = new PatternFormatter("%t{id}"); + Assertions.assertEquals("33", formatter.format(record)); + + formatter = new PatternFormatter("%t{ID}"); + Assertions.assertEquals("33", formatter.format(record)); + } + + @Test + public void systemProperties() throws Exception { + systemProperties("$"); + systemProperties("#"); + } + + @Test + public void truncation() throws Exception { + final ExtLogRecord record = createLogRecord("test"); + PatternFormatter formatter = new PatternFormatter("'%-10.-21c' %m"); + Assertions.assertEquals("'PatternFormatterTests' test", formatter.format(record)); + + formatter = new PatternFormatter("'%10.-21c' %m"); + Assertions.assertEquals("'PatternFormatterTests' test", formatter.format(record)); + + formatter = new PatternFormatter("%.-21C %m"); + Assertions.assertEquals("PatternFormatterTests test", formatter.format(record)); + + formatter = new PatternFormatter("%.2m"); + Assertions.assertEquals("te", formatter.format(record)); + + formatter = new PatternFormatter("%.-2m"); + Assertions.assertEquals("st", formatter.format(record)); + + formatter = new PatternFormatter("%5m"); + Assertions.assertEquals(" test", formatter.format(record)); + + formatter = new PatternFormatter("%-5.-10m"); + Assertions.assertEquals("test ", formatter.format(record)); + + formatter = new PatternFormatter("%-5.10m"); + Assertions.assertEquals("test ", formatter.format(record)); + + // Exact length truncation + final String msg = "test message"; + formatter = new PatternFormatter("%c %-5.-7m"); + Assertions.assertEquals(CATEGORY + " message", formatter.format(createLogRecord(msg))); + } + + @Test + public void extendedThrowable() throws Exception { + ExtLogRecord record = createLogRecord("test"); + + Throwable cause = new IllegalArgumentException("cause"); + Throwable level1 = new RuntimeException("level1", cause); + Throwable suppressedLevel1 = new IllegalStateException("suppressedLevel1"); + Throwable suppressedLevel1a = new RuntimeException("suppressedLevel1a"); + Throwable suppressedLevel2 = new IllegalThreadStateException("suppressedLevel2"); + suppressedLevel1.addSuppressed(suppressedLevel2); + + level1.addSuppressed(suppressedLevel1); + level1.addSuppressed(suppressedLevel1a); + + record.setThrown(level1); + + // All exceptions + PatternFormatter formatter = new PatternFormatter("%e"); + + String formatted = formatter.format(record); + + // Should contain, level1, cause, level1a, suppressedLevel1 and suppressedLevel2 + Assertions.assertTrue(formatted.contains("cause")); + Assertions.assertTrue(formatted.contains("level1")); + Assertions.assertTrue(formatted.contains("suppressedLevel1")); + Assertions.assertTrue(formatted.contains("suppressedLevel1a")); + Assertions.assertTrue(formatted.contains("suppressedLevel2")); + + // No suppressed exceptions + formatter = new PatternFormatter("%e{0}"); + formatted = formatter.format(record); + + // Should only contain cause and level1 + Assertions.assertTrue(formatted.contains("cause")); + Assertions.assertTrue(formatted.contains("level1")); + Assertions.assertFalse(formatted.contains("suppressedLevel1")); + Assertions.assertFalse(formatted.contains("suppressedLevel1a")); + Assertions.assertFalse(formatted.contains("suppressedLevel2")); + + // One level suppressed exceptions + formatter = new PatternFormatter("%E{1}"); + formatted = formatter.format(record); + + // Should only contain cause and level1 + Assertions.assertTrue(formatted.contains("cause")); + Assertions.assertTrue(formatted.contains("level1")); + Assertions.assertTrue(formatted.contains("suppressedLevel1")); + Assertions.assertFalse(formatted.contains("suppressedLevel1a")); + Assertions.assertFalse(formatted.contains("suppressedLevel2")); + + // Add a circular reference to the cause. This should test both that the caused suppressed exceptions are being + // printed and that circular exceptions aren't being processed again. + formatter = new PatternFormatter("%e"); + cause.addSuppressed(suppressedLevel1); + formatted = formatter.format(record); + Assertions.assertTrue(formatted.contains("CIRCULAR REFERENCE: java.lang.IllegalStateException: suppressedLevel1")); + } + + @Test + public void unqualifiedHost() { + final String hostName = "xbib.org"; + final ExtLogRecord record = createLogRecord("test"); + record.setHostName(hostName); + PatternFormatter formatter = new PatternFormatter("%h"); + Assertions.assertEquals("xbib", formatter.format(record)); + + // This should still return just the first portion + formatter = new PatternFormatter("%h{2}"); + Assertions.assertEquals("xbib", formatter.format(record)); + + // Should truncate from the end + formatter = new PatternFormatter("%.-8h"); + Assertions.assertEquals("xbib", formatter.format(record)); + } + + @Test + public void qualifiedHost() { + final String hostName = "xbib.org"; + final ExtLogRecord record = createLogRecord("test"); + record.setHostName(hostName); + PatternFormatter formatter = new PatternFormatter("%H"); + Assertions.assertEquals(hostName, formatter.format(record)); + formatter = new PatternFormatter("%H{1}"); + Assertions.assertEquals("xbib", formatter.format(record)); + formatter = new PatternFormatter("%H{2}"); + Assertions.assertEquals("xbib.org", formatter.format(record)); + formatter = new PatternFormatter("%.-3H"); + Assertions.assertEquals("org", formatter.format(record)); + formatter = new PatternFormatter("%.-5H{2}"); + Assertions.assertEquals("b.org", formatter.format(record)); + } + + private void systemProperties(final String propertyPrefix) throws Exception { + final ExtLogRecord record = createLogRecord("test"); + PatternFormatter formatter = new PatternFormatter("%" + propertyPrefix + "{org.xbib.logging.testProp}"); + Assertions.assertEquals("testValue", formatter.format(record)); + + formatter = new PatternFormatter("%" + propertyPrefix + "{invalid:defaultValue}"); + Assertions.assertEquals("defaultValue", formatter.format(record)); + + formatter = new PatternFormatter("%" + propertyPrefix + "{invalid}"); + Assertions.assertEquals("null", formatter.format(record)); + + // Test null arguments + try { + formatter = new PatternFormatter("%" + propertyPrefix); + formatter.format(record); + Assertions.fail("Should not allow null arguments"); + } catch (IllegalArgumentException ignore) { + + } + + try { + formatter = new PatternFormatter("%" + propertyPrefix + "{}"); + formatter.format(record); + Assertions.fail("Should not allow null arguments"); + } catch (IllegalArgumentException ignore) { + + } + } + + private static ExtLogRecord createLogRecord(final String msg) { + final ExtLogRecord result = new ExtLogRecord(org.xbib.logging.Level.INFO, msg, + PatternFormatterTests.class.getName()); + result.setSourceClassName(PatternFormatterTests.class.getName()); + result.setLoggerName(CATEGORY); + return result; + } + +} diff --git a/logging/src/test/java/org/xbib/logging/test/formatters/StackTraceFormatterTests.java b/logging/src/test/java/org/xbib/logging/test/formatters/StackTraceFormatterTests.java new file mode 100644 index 0000000..6e573f4 --- /dev/null +++ b/logging/src/test/java/org/xbib/logging/test/formatters/StackTraceFormatterTests.java @@ -0,0 +1,187 @@ +package org.xbib.logging.test.formatters; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; +import org.xbib.logging.util.StackTraceFormatter; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class StackTraceFormatterTests { + + private static final boolean IS_IBM_JDK = System.getProperty("java.vendor").startsWith("IBM"); + + public StackTraceFormatterTests() { + } + + @Test + public void compareSimpleStackTrace() { + final RuntimeException e = new RuntimeException(); + final StringWriter writer = new StringWriter(); + e.printStackTrace(new PrintWriter(writer)); + + final StringBuilder sb = new StringBuilder(); + StackTraceFormatter.renderStackTrace(sb, e, false, -1); + + assertEquals(writer.toString(), sanitize(sb.toString())); + } + + @Test + public void compareCauseStackTrace() { + final RuntimeException e = new RuntimeException("Test Exception", new IllegalStateException("Cause")); + + final StringWriter writer = new StringWriter(); + e.printStackTrace(new PrintWriter(writer)); + + final StringBuilder sb = new StringBuilder(); + StackTraceFormatter.renderStackTrace(sb, e, false, -1); + + assertEquals(writer.toString(), sanitize(sb.toString())); + } + + @Test + public void compareSuppressedAndCauseStackTrace() { + Assumptions.assumeFalse(IS_IBM_JDK, "The IBM JDK does not show print circular references."); + final RuntimeException r1 = new RuntimeException("Exception 1"); + final RuntimeException r2 = new RuntimeException("Exception 2", r1); + final RuntimeException r3 = new RuntimeException("Exception 3", r2); + + final RuntimeException cause = new RuntimeException("This is the cause", r1); + cause.addSuppressed(r2); + cause.addSuppressed(r3); + + final StringWriter writer = new StringWriter(); + cause.printStackTrace(new PrintWriter(writer)); + + final StringBuilder sb = new StringBuilder(); + StackTraceFormatter.renderStackTrace(sb, cause, false, -1); + + assertEquals(writer.toString(), sanitize(sb.toString())); + } + + @Test + public void compareNestedSuppressedStackTrace() { + Assumptions.assumeFalse(IS_IBM_JDK, "The IBM JDK does not show print circular references."); + final RuntimeException r1 = new RuntimeException("Exception 1"); + final RuntimeException r2 = new RuntimeException("Exception 2", r1); + final RuntimeException r3 = new RuntimeException("Exception 3", r2); + final IllegalStateException nested = new IllegalStateException("Nested 1"); + nested.addSuppressed(new RuntimeException("Nested 1a")); + r3.addSuppressed(nested); + r3.addSuppressed(new IllegalStateException("Nested 2")); + + final RuntimeException cause = new RuntimeException("This is the cause", r1); + cause.addSuppressed(r2); + cause.addSuppressed(r3); + + final StringWriter writer = new StringWriter(); + cause.printStackTrace(new PrintWriter(writer)); + + final StringBuilder sb = new StringBuilder(); + StackTraceFormatter.renderStackTrace(sb, cause, false, -1); + + assertEquals(writer.toString(), sanitize(sb.toString())); + } + + @Test + public void compareMultiNestedSuppressedStackTrace() { + Assumptions.assumeFalse(IS_IBM_JDK, "The IBM JDK does not show print circular references."); + final Throwable cause = createMultiNestedCause(); + + final StringWriter writer = new StringWriter(); + cause.printStackTrace(new PrintWriter(writer)); + + final StringBuilder sb = new StringBuilder(); + StackTraceFormatter.renderStackTrace(sb, cause, false, -1); + + assertEquals(writer.toString(), sanitize(sb.toString())); + } + + @Test + public void compareMultiNestedSuppressedAndNestedCauseStackTrace() { + Assumptions.assumeFalse(IS_IBM_JDK, "The IBM JDK does not show print circular references."); + final Throwable rootCause = createMultiNestedCause(); + final RuntimeException cause = new RuntimeException("This is the parent", rootCause); + + final StringWriter writer = new StringWriter(); + cause.printStackTrace(new PrintWriter(writer)); + + final StringBuilder sb = new StringBuilder(); + StackTraceFormatter.renderStackTrace(sb, cause, false, -1); + + assertEquals(writer.toString(), sanitize(sb.toString())); + } + + @Test + public void testNestedSuppressStackTraceDepth() { + // Test that all messages exist + testDepth(-1); + // Now test up to 11 messages + for (int i = 0; i < 12; i++) { + testDepth(i); + } + } + + private void testDepth(final int depth) { + final Throwable cause = createMultiNestedCause(); + + final StringBuilder sb = new StringBuilder(); + StackTraceFormatter.renderStackTrace(sb, cause, false, depth); + + String msg = sb.toString(); + + // Check the buffer for suppressed messages, should only have Suppressed 1 + checkMessage(msg, "Suppressed 1", depth > 0, depth); + checkMessage(msg, "Nested 1", depth > 1, depth); + checkMessage(msg, "Nested 1a", depth > 2, depth); + checkMessage(msg, "Nested 1-2", depth > 3, depth); + checkMessage(msg, "Suppressed 2", depth > 4, depth); + checkMessage(msg, "Nested 2", depth > 5, depth); + checkMessage(msg, "Nested 2a", depth > 6, depth); + checkMessage(msg, "Nested 2-2", depth > 7, depth); + checkMessage(msg, "Suppressed 3", depth > 8, depth); + checkMessage(msg, "Nested 3", depth > 9, depth); + checkMessage(msg, "Nested 3a", depth > 10, depth); + checkMessage(msg, "Nested 3-2", depth > 11, depth); + } + + private void checkMessage(final String msg, final String text, final boolean shouldExist, final int depth) { + final boolean test = (shouldExist || depth < 0); + assertEquals(msg.contains(text), test, + () -> String.format("Depth %d should %s contained \"%s\": %s", depth, (test ? "have" : "not have"), text, msg)); + } + + private Throwable createMultiNestedCause() { + final RuntimeException suppressed1 = new RuntimeException("Suppressed 1"); + final IllegalStateException nested1 = new IllegalStateException("Nested 1"); + nested1.addSuppressed(new RuntimeException("Nested 1a")); + suppressed1.addSuppressed(nested1); + suppressed1.addSuppressed(new IllegalStateException("Nested 1-2")); + + final RuntimeException suppressed2 = new RuntimeException("Suppressed 2"); + final IllegalStateException nested2 = new IllegalStateException("Nested 2"); + nested2.addSuppressed(new RuntimeException("Nested 2a")); + suppressed2.addSuppressed(nested2); + suppressed2.addSuppressed(new IllegalStateException("Nested 2-2")); + + final RuntimeException suppressed3 = new RuntimeException("Suppressed 3"); + final IllegalStateException nested3 = new IllegalStateException("Nested 3"); + nested3.addSuppressed(new RuntimeException("Nested 3a")); + suppressed3.addSuppressed(nested3); + suppressed3.addSuppressed(new IllegalStateException("Nested 3-2")); + + final RuntimeException cause = new RuntimeException("This is the cause"); + cause.addSuppressed(suppressed1); + cause.addSuppressed(suppressed2); + cause.addSuppressed(suppressed3); + return cause; + } + + private static String sanitize(final String s) { + if (s.startsWith(": ")) { + return s.substring(2); + } + return s; + } +} diff --git a/logging/src/test/java/org/xbib/logging/test/formatters/XmlFormatterTests.java b/logging/src/test/java/org/xbib/logging/test/formatters/XmlFormatterTests.java new file mode 100644 index 0000000..2360643 --- /dev/null +++ b/logging/src/test/java/org/xbib/logging/test/formatters/XmlFormatterTests.java @@ -0,0 +1,298 @@ +package org.xbib.logging.test.formatters; + +import java.io.StringReader; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; +import javax.xml.transform.stream.StreamSource; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import javax.xml.validation.Validator; + +import org.xbib.logging.ExtFormatter; +import org.xbib.logging.ExtLogRecord; +import org.xbib.logging.Level; +import org.xbib.logging.formatters.StructuredFormatter; +import org.xbib.logging.formatters.StructuredFormatter.Key; +import org.xbib.logging.formatters.XmlFormatter; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; +import org.xml.sax.ErrorHandler; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +public class XmlFormatterTests extends AbstractTest { + + private static final DateTimeFormatter DATE_TIME_FORMATTER = + DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(ZoneId.systemDefault()); + + public XmlFormatterTests() { + } + + @Test + public void validate() throws Exception { + // Configure the formatter + final XmlFormatter formatter = new XmlFormatter(); + formatter.setPrintNamespace(true); + formatter.setPrintDetails(true); + formatter.setExceptionOutputType(StructuredFormatter.ExceptionOutputType.DETAILED_AND_FORMATTED); + formatter.setMetaData("key1=value1,key2=value2"); + // Create the record get format a message + final ExtLogRecord record = createLogRecord(Level.ERROR, "Test formatted %s", "message"); + record.setLoggerName("org.xbib.logging.test"); + record.setMillis(System.currentTimeMillis()); + record.setThrown(createMultiNestedCause()); + record.putMdc("testMdcKey", "testMdcValue"); + record.setNdc("testNdc"); + final String message = formatter.format(record); + + final ErrorHandler handler = new ErrorHandler() { + @Override + public void warning(final SAXParseException exception) throws SAXException { + fail(exception); + } + + @Override + public void error(final SAXParseException exception) throws SAXException { + fail(exception); + } + + @Override + public void fatalError(final SAXParseException exception) throws SAXException { + fail(exception); + } + + private void fail(final SAXParseException exception) { + final StringBuilder failureMessage = new StringBuilder(); + failureMessage.append(exception.getLocalizedMessage()) + .append(": line ") + .append(exception.getLineNumber()) + .append(" column ") + .append(exception.getColumnNumber()) + .append(System.lineSeparator()) + .append(message); + Assertions.fail(failureMessage.toString()); + } + }; + + final SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + factory.setErrorHandler(handler); + + final Schema schema = factory.newSchema(getClass().getResource("xml-formatter.xsd")); + final Validator validator = schema.newValidator(); + validator.setErrorHandler(handler); + validator.setFeature("http://apache.org/xml/features/validation/schema", true); + validator.validate(new StreamSource(new StringReader(message))); + } + + @Test + public void testFormat() throws Exception { + final XmlFormatter formatter = new XmlFormatter(); + formatter.setPrintDetails(true); + ExtLogRecord record = createLogRecord("Test formatted %s", "message"); + compare(record, formatter); + + record = createLogRecord("Test Message"); + compare(record, formatter); + + record = createLogRecord(Level.ERROR, "Test formatted %s", "message"); + record.setLoggerName("org.xbib.logging.test"); + record.setMillis(System.currentTimeMillis()); + final Throwable t = new RuntimeException("Test cause exception"); + final Throwable dup = new IllegalStateException("Duplicate"); + t.addSuppressed(dup); + final Throwable cause = new RuntimeException("Test Exception", t); + dup.addSuppressed(cause); + cause.addSuppressed(new IllegalArgumentException("Suppressed")); + cause.addSuppressed(dup); + record.setThrown(cause); + record.putMdc("testMdcKey", "testMdcValue"); + record.setNdc("testNdc"); + //formatter.setExceptionOutputType(JsonFormatter.ExceptionOutputType.DETAILED_AND_FORMATTED); + //compare(record, formatter); + } + + @Test + public void metaData() throws Exception { + // Configure the formatter + final var formatter = new XmlFormatter(); + formatter.setMetaData("key1=value1,key2=value2,noValue="); + + final var record = createLogRecord("Test Message"); + final var xml = formatter.format(record); + + final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + final DocumentBuilder builder = factory.newDocumentBuilder(); + final Document doc = builder.parse(new InputSource(new StringReader(xml))); + final var metaData = doc.getElementsByTagName("metaData"); + Assertions.assertEquals(3, metaData.getLength()); + var item = metaData.item(0); + Assertions.assertEquals("key1", + item.getAttributes().getNamedItem("key").getNodeValue(), "Expected key attribute of key1"); + Assertions.assertEquals("value1", item.getTextContent(), "Expected a value of value1"); + + item = metaData.item(1); + Assertions.assertEquals("key2", + item.getAttributes().getNamedItem("key").getNodeValue(), "Expected key attribute of key2"); + Assertions.assertEquals("value2", item.getTextContent(), "Expected a value of value2"); + + item = metaData.item(2); + Assertions.assertEquals("noValue", + item.getAttributes().getNamedItem("key").getNodeValue(), "Expected key attribute of noValue"); + Assertions.assertEquals("", item.getTextContent(), "Expected no value"); + } + + private static int getInt(final XMLStreamReader reader) throws XMLStreamException { + final String value = getString(reader); + if (value != null) { + return Integer.parseInt(value); + } + return 0; + } + + private static long getLong(final XMLStreamReader reader) throws XMLStreamException { + final String value = getString(reader); + if (value != null) { + return Long.parseLong(value); + } + return 0L; + } + + private static String getString(final XMLStreamReader reader) throws XMLStreamException { + final int state = reader.next(); + if (state == XMLStreamConstants.END_ELEMENT) { + return null; + } + if (state == XMLStreamConstants.CHARACTERS) { + final String text = reader.getText(); + return sanitize(text); + } + throw new IllegalStateException("No text"); + } + + private static Map getMap(final XMLStreamReader reader) throws XMLStreamException { + if (reader.hasNext()) { + int state; + final Map result = new LinkedHashMap<>(); + while (reader.hasNext() && (state = reader.next()) != XMLStreamConstants.END_ELEMENT) { + if (state == XMLStreamConstants.CHARACTERS) { + String text = sanitize(reader.getText()); + if (text == null || text.isEmpty()) + continue; + Assertions.fail(String.format("Invalid text found: %s", text)); + } + final String key = reader.getLocalName(); + Assertions.assertTrue(reader.hasNext()); + final String value = getString(reader); + Assertions.assertNotNull(value); + result.put(key, value); + } + return result; + } + return Collections.emptyMap(); + } + + private static String sanitize(final String value) { + return value == null ? null : value.replaceAll("\n", "").trim(); + } + + private static void compare(final ExtLogRecord record, final ExtFormatter formatter) throws XMLStreamException { + compare(record, formatter.format(record)); + } + + private static void compare(final ExtLogRecord record, final String xmlString) throws XMLStreamException { + + final XMLInputFactory inputFactory = XMLInputFactory.newInstance(); + final XMLStreamReader reader = inputFactory.createXMLStreamReader(new StringReader(xmlString)); + + boolean inException = false; + while (reader.hasNext()) { + final int state = reader.next(); + if (state == XMLStreamConstants.END_ELEMENT && reader.getLocalName().equals(Key.EXCEPTION.getKey())) { + inException = false; + } + if (state == XMLStreamConstants.START_ELEMENT) { + final String localName = reader.getLocalName(); + if (localName.equals(Key.EXCEPTION.getKey())) { + inException = true;// TODO (jrp) stack trace may need to be validated + } else if (localName.equals(Key.LEVEL.getKey())) { + Assertions.assertEquals(record.getLevel(), Level.parse(getString(reader))); + } else if (localName.equals(Key.LOGGER_CLASS_NAME.getKey())) { + Assertions.assertEquals(record.getLoggerClassName(), getString(reader)); + } else if (localName.equals(Key.LOGGER_NAME.getKey())) { + Assertions.assertEquals(record.getLoggerName(), getString(reader)); + } else if (localName.equals(Key.MDC.getKey())) { + compareMap(record.getMdcCopy(), getMap(reader)); + } else if (!inException && localName.equals(Key.MESSAGE.getKey())) { + Assertions.assertEquals(record.getFormattedMessage(), getString(reader)); + } else if (localName.equals(Key.NDC.getKey())) { + final String value = getString(reader); + Assertions.assertEquals(record.getNdc(), (value == null ? "" : value)); + } else if (localName.equals(Key.SEQUENCE.getKey())) { + Assertions.assertEquals(record.getSequenceNumber(), getLong(reader)); + } else if (localName.equals(Key.SOURCE_CLASS_NAME.getKey())) { + Assertions.assertEquals(record.getSourceClassName(), getString(reader)); + } else if (localName.equals(Key.SOURCE_FILE_NAME.getKey())) { + Assertions.assertEquals(record.getSourceFileName(), getString(reader)); + } else if (localName.equals(Key.SOURCE_LINE_NUMBER.getKey())) { + Assertions.assertEquals(record.getSourceLineNumber(), getInt(reader)); + } else if (localName.equals(Key.SOURCE_METHOD_NAME.getKey())) { + Assertions.assertEquals(record.getSourceMethodName(), getString(reader)); + } else if (localName.equals(Key.THREAD_ID.getKey())) { + Assertions.assertEquals(record.getThreadID(), getInt(reader)); + } else if (localName.equals(Key.THREAD_NAME.getKey())) { + Assertions.assertEquals(record.getThreadName(), getString(reader)); + } else if (localName.equals(Key.TIMESTAMP.getKey())) { + final String dateTime = DATE_TIME_FORMATTER.format(record.getInstant()); + Assertions.assertEquals(dateTime, getString(reader)); + } + } + } + } + + private static void compareMap(final Map m1, final Map m2) { + Assertions.assertEquals(m1.size(), m2.size(), "Map sizes do not match"); + for (String key : m1.keySet()) { + Assertions.assertTrue(m2.containsKey(key), () -> "Second map does not contain key " + key); + Assertions.assertEquals(m1.get(key), m2.get(key)); + } + } + + private static Throwable createMultiNestedCause() { + final RuntimeException suppressed1 = new RuntimeException("Suppressed 1"); + final IllegalStateException nested1 = new IllegalStateException("Nested 1"); + nested1.addSuppressed(new RuntimeException("Nested 1a")); + suppressed1.addSuppressed(nested1); + suppressed1.addSuppressed(new IllegalStateException("Nested 1-2")); + + final RuntimeException suppressed2 = new RuntimeException("Suppressed 2", suppressed1); + final IllegalStateException nested2 = new IllegalStateException("Nested 2"); + nested2.addSuppressed(new RuntimeException("Nested 2a")); + suppressed2.addSuppressed(nested2); + suppressed2.addSuppressed(new IllegalStateException("Nested 2-2")); + + final RuntimeException suppressed3 = new RuntimeException("Suppressed 3"); + final IllegalStateException nested3 = new IllegalStateException("Nested 3"); + nested3.addSuppressed(new RuntimeException("Nested 3a")); + suppressed3.addSuppressed(nested3); + suppressed3.addSuppressed(new IllegalStateException("Nested 3-2")); + + final RuntimeException cause = new RuntimeException("This is the cause"); + cause.addSuppressed(suppressed1); + cause.addSuppressed(suppressed2); + cause.addSuppressed(suppressed3); + return cause; + } +} diff --git a/logging/src/test/java/org/xbib/logging/test/handlers/AbstractHandlerTest.java b/logging/src/test/java/org/xbib/logging/test/handlers/AbstractHandlerTest.java new file mode 100644 index 0000000..f2048d6 --- /dev/null +++ b/logging/src/test/java/org/xbib/logging/test/handlers/AbstractHandlerTest.java @@ -0,0 +1,214 @@ +package org.xbib.logging.test.handlers; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Stream; +import java.util.zip.GZIPInputStream; + +import org.xbib.logging.ExtHandler; +import org.xbib.logging.ExtLogRecord; +import org.xbib.logging.formatters.PatternFormatter; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; + +public class AbstractHandlerTest { + + private static final Path BASE_LOG_DIR = Path.of(System.getProperty("user.dir") + "/build"); + + final static PatternFormatter FORMATTER = new PatternFormatter("%d{HH:mm:ss,SSS} %-5p [%c] (%t) %s%E%n"); + + private TestInfo testInfo; + + public AbstractHandlerTest() { + } + + @BeforeEach + public void setup(final TestInfo testInfo) throws Exception { + this.testInfo = testInfo; + deletePath(logDirectory(testInfo)); + } + + @Test + public void simple() { + Assertions.assertTrue(testInfo.getTestMethod().isPresent()); + } + + protected Path resolvePath(final String filename) throws IOException { + return logDirectory().resolve(filename); + } + + protected Path logDirectory() throws IOException { + return logDirectory(testInfo); + } + + protected Path logDirectory(final TestInfo testInfo) throws IOException { + Assertions.assertTrue(testInfo.getTestClass().isPresent()); + Assertions.assertTrue(testInfo.getTestMethod().isPresent()); + final Path dir = + BASE_LOG_DIR.resolve(testInfo.getTestClass().get().getSimpleName() + "-" + testInfo.getTestMethod().get().getName()); + if (Files.notExists(dir)) { + Files.createDirectories(dir); + } + return dir; + } + + private static void deletePath(final Path path) throws IOException { + if (Files.isDirectory(path)) { + try (Stream paths = Files.walk(path)) { + paths.sorted(Comparator.reverseOrder()) + .forEach(p -> { + try { + Files.delete(p); + } catch (IOException e) { + System.out.printf("Failed to delete path %s%n", p); + e.printStackTrace(System.out); + } + }); + } + } else { + Files.delete(path); + } + } + + protected static void configureHandlerDefaults(final ExtHandler handler) { + handler.setAutoFlush(true); + handler.setFormatter(FORMATTER); + handler.setErrorManager(AssertingErrorManager.of()); + } + + protected ExtLogRecord createLogRecord(final String msg) { + return createLogRecord(org.xbib.logging.Level.INFO, msg); + } + + protected ExtLogRecord createLogRecord(final String format, final Object... args) { + return createLogRecord(org.xbib.logging.Level.INFO, format, args); + } + + protected ExtLogRecord createLogRecord(final org.xbib.logging.Level level, final String msg) { + return new ExtLogRecord(level, msg, getClass().getName()); + } + + protected ExtLogRecord createLogRecord(final org.xbib.logging.Level level, final String format, final Object... args) { + return new ExtLogRecord(level, String.format(format, args), getClass().getName()); + } + + /** + * Validates that at least one line of the GZIP'd file contains the expected text. + * + * @param path the path to the GZIP file + * @param expectedContains the expected text + * + * @throws IOException if an error occurs while reading the GZIP file + */ + static void validateGzipContents(final Path path, final String expectedContains) throws IOException { + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(new GZIPInputStream(Files.newInputStream(path))))) { + String line; + while ((line = reader.readLine()) != null) { + if (line.contains(expectedContains)) { + return; + } + } + } + Assertions.fail(String.format("GZIP file %s missing contents: %s", path, expectedContains)); + } + + /** + * Validates that the ZIP file contains the expected file, the expected file is not empty and that the first line + * contains the expected text. + * + * @param path the path to the zip file + * @param expectedFileName the name of the file inside the zip file + * @param expectedContains the expected text + * + * @throws IOException if an error occurs reading the zip file + */ + static void validateZipContents(final Path path, final String expectedFileName, final String expectedContains) + throws IOException { + try (final FileSystem zipFs = FileSystems.newFileSystem(URI.create("jar:" + path.toUri().toASCIIString()), + Collections.singletonMap("create", "true"))) { + final Path file = zipFs.getPath(zipFs.getSeparator(), expectedFileName); + Assertions.assertTrue(Files.exists(file), () -> String.format("Expected file %s not found.", expectedFileName)); + final List lines = Files.readAllLines(file, StandardCharsets.UTF_8); + Assertions.assertFalse(lines.isEmpty(), + () -> String.format("File %s appears to be empty in zip file %s.", expectedFileName, path)); + Assertions.assertTrue(lines.get(0).contains(expectedContains), + () -> String.format("ZIP file %s missing contents: %s", path, expectedContains)); + } + } + + static void compareArchiveContents(final Path archive1, final Path archive2, final String expectedFileName) + throws IOException { + Collection lines1 = Collections.emptyList(); + Collection lines2 = Collections.emptyList(); + + if (archive1.getFileName().toString().endsWith(".zip")) { + lines1 = readAllLinesFromZip(archive1, expectedFileName); + lines2 = readAllLinesFromZip(archive2, expectedFileName); + } else if (archive1.getFileName().toString().endsWith(".gz")) { + lines1 = readAllLinesFromGzip(archive1); + lines2 = readAllLinesFromGzip(archive2); + } else { + Assertions.fail(String.format("Files %s and %s are not archives.", archive1, archive2)); + } + + // Assert the contents aren't empty + Assertions.assertFalse(lines1.isEmpty(), () -> String.format("Archive %s contained no data", archive1)); + Assertions.assertFalse(lines2.isEmpty(), () -> String.format("Archive %s contained no data", archive2)); + + final Collection copy1 = new ArrayList<>(lines1); + final Collection copy2 = new ArrayList<>(lines2); + boolean altered = copy1.removeAll(copy2); + if (copy1.isEmpty()) { + Assertions.fail(String.format("The contents of %s and %s are identical and should not be", archive1, archive2)); + } else if (altered) { + final StringBuilder msg = new StringBuilder(1024) + .append("The following contents are in both ") + .append(archive1) + .append(" and ") + .append(archive2); + // Find the identical lines and report + for (String line : lines1) { + if (lines2.contains(line)) { + msg.append(System.lineSeparator()).append(line); + } + } + Assertions.fail(msg.toString()); + } + } + + private static Collection readAllLinesFromZip(final Path path, final String expectedFileName) throws IOException { + try (final FileSystem zipFs = FileSystems.newFileSystem(URI.create("jar:" + path.toUri().toASCIIString()), + Collections.singletonMap("create", "true"))) { + final Path file = zipFs.getPath(zipFs.getSeparator(), expectedFileName); + Assertions.assertTrue(Files.exists(file), () -> String.format("Expected file %s not found.", expectedFileName)); + return Files.readAllLines(file, StandardCharsets.UTF_8); + } + } + + private static Collection readAllLinesFromGzip(final Path path) throws IOException { + final Collection lines = new ArrayList<>(); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(new GZIPInputStream(Files.newInputStream(path))))) { + String line; + while ((line = reader.readLine()) != null) { + lines.add(line); + } + } + return lines; + } +} diff --git a/logging/src/test/java/org/xbib/logging/test/handlers/AssertingErrorManager.java b/logging/src/test/java/org/xbib/logging/test/handlers/AssertingErrorManager.java new file mode 100644 index 0000000..a41b698 --- /dev/null +++ b/logging/src/test/java/org/xbib/logging/test/handlers/AssertingErrorManager.java @@ -0,0 +1,65 @@ +package org.xbib.logging.test.handlers; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.logging.ErrorManager; + +import org.junit.jupiter.api.Assertions; + +public class AssertingErrorManager extends ErrorManager { + + private final int[] allowedCodes; + + private AssertingErrorManager() { + this.allowedCodes = null; + } + + private AssertingErrorManager(final int... allowedCodes) { + this.allowedCodes = allowedCodes; + } + + public static AssertingErrorManager of() { + return new AssertingErrorManager(); + } + + public static AssertingErrorManager of(final int... allowedCodes) { + return new AssertingErrorManager(allowedCodes); + } + + @Override + public void error(final String msg, final Exception ex, final int code) { + if (notAllowed(code)) { + final String codeStr = switch (code) { + case CLOSE_FAILURE -> "CLOSE_FAILURE"; + case FLUSH_FAILURE -> "FLUSH_FAILURE"; + case FORMAT_FAILURE -> "FORMAT_FAILURE"; + case GENERIC_FAILURE -> "GENERIC_FAILURE"; + case OPEN_FAILURE -> "OPEN_FAILURE"; + case WRITE_FAILURE -> "WRITE_FAILURE"; + default -> "INVALID (" + code + ")"; + }; + try ( + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw)) { + pw.printf("LogManager error of type %s: %s%n", codeStr, msg); + ex.printStackTrace(pw); + Assertions.fail(sw.toString()); + } catch (IOException e) { + // This shouldn't happen, but just fail if it does + Assertions.fail(String.format("Failed to print error message: %s", e.getMessage())); + } + } + } + + private boolean notAllowed(final int code) { + if (allowedCodes != null) { + for (int allowedCode : allowedCodes) { + if (code == allowedCode) { + return false; + } + } + } + return true; + } +} diff --git a/logging/src/test/java/org/xbib/logging/test/handlers/AsyncHandlerTests.java b/logging/src/test/java/org/xbib/logging/test/handlers/AsyncHandlerTests.java new file mode 100644 index 0000000..8b7053e --- /dev/null +++ b/logging/src/test/java/org/xbib/logging/test/handlers/AsyncHandlerTests.java @@ -0,0 +1,144 @@ +package org.xbib.logging.test.handlers; + +import java.util.concurrent.BlockingDeque; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; + +import org.xbib.logging.ExtHandler; +import org.xbib.logging.ExtLogRecord; +import org.xbib.logging.Level; +import org.xbib.logging.MDC; +import org.xbib.logging.NDC; +import org.xbib.logging.formatters.PatternFormatter; +import org.xbib.logging.handlers.AsyncHandler; +import org.xbib.logging.handlers.AsyncHandler.OverflowAction; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class AsyncHandlerTests { + + private BlockingQueueHandler handler; + + private AsyncHandler asyncHandler; + + public AsyncHandlerTests() { + } + + @BeforeEach + public void setup() { + handler = new BlockingQueueHandler(); + + asyncHandler = new AsyncHandler(); + asyncHandler.setOverflowAction(OverflowAction.BLOCK); + asyncHandler.addHandler(handler); + } + + @AfterEach + public void tearDown() { + asyncHandler.close(); + handler.close(); + NDC.clear(); + MDC.clear(); + } + + @Test + public void testNdc() throws Exception { + handler.setFormatter(new PatternFormatter("%x")); + String ndcValue = "Test NDC value"; + NDC.push(ndcValue); + asyncHandler.doPublish(createRecord()); + Assertions.assertEquals(ndcValue, NDC.pop()); + Assertions.assertEquals(ndcValue, handler.getFirst()); + + // Next value should be blank + asyncHandler.doPublish(createRecord()); + Assertions.assertEquals("", handler.getFirst()); + + ndcValue = "New test NDC value"; + NDC.push(ndcValue); + asyncHandler.doPublish(createRecord()); + NDC.push("invalid"); + Assertions.assertEquals(ndcValue, handler.getFirst()); + } + + @Test + public void testMdc() throws Exception { + handler.setFormatter(new PatternFormatter("%X{key}")); + String mdcValue = "Test MDC value"; + MDC.put("key", mdcValue); + asyncHandler.doPublish(createRecord()); + MDC.remove("key"); + Assertions.assertEquals(mdcValue, handler.getFirst()); + + asyncHandler.doPublish(createRecord()); + Assertions.assertEquals("", handler.getFirst()); + + mdcValue = "New test MDC value"; + MDC.put("key", mdcValue); + asyncHandler.doPublish(createRecord()); + MDC.put("key", "invalid"); + Assertions.assertEquals(mdcValue, handler.getFirst()); + } + + @Test + public void reentry() throws Exception { + final ExtHandler reLogHandler = new ExtHandler() { + private final ThreadLocal entered = ThreadLocal.withInitial(() -> false); + + @Override + protected void doPublish(final ExtLogRecord record) { + if (entered.get()) { + return; + } + try { + entered.set(true); + super.doPublish(record); + // Create a new record and act is if this was through a logger + asyncHandler.publish(createRecord()); + } finally { + entered.set(false); + } + } + }; + handler.addHandler(reLogHandler); + handler.setFormatter(new PatternFormatter("%s")); + asyncHandler.publish(createRecord()); + // We should end up with two messages and a third should not happen + Assertions.assertEquals("Test message", handler.getFirst()); + Assertions.assertEquals("Test message", handler.getFirst()); + // This should time out. Then we end up with a null value. We could instead sleep for a shorter time and check + // if the queue is empty. However, a 5 second delay does not seem too long. + Assertions.assertNull(handler.getFirst(), () -> "Expected no more entries, but found " + handler.queue); + } + + static ExtLogRecord createRecord() { + return new ExtLogRecord(Level.INFO, "Test message", AsyncHandlerTests.class.getName()); + } + + static class BlockingQueueHandler extends ExtHandler { + private final BlockingDeque queue; + + BlockingQueueHandler() { + queue = new LinkedBlockingDeque<>(); + setErrorManager(AssertingErrorManager.of()); + } + + @Override + protected void doPublish(final ExtLogRecord record) { + queue.addLast(getFormatter().format(record)); + publishToNestedHandlers(record); + super.doPublish(record); + } + + String getFirst() throws InterruptedException { + return queue.pollFirst(5, TimeUnit.SECONDS); + } + + @Override + public void close() throws SecurityException { + queue.clear(); + } + } +} diff --git a/logging/src/test/java/org/xbib/logging/test/handlers/DelayedHandlerTests.java b/logging/src/test/java/org/xbib/logging/test/handlers/DelayedHandlerTests.java new file mode 100644 index 0000000..da1db83 --- /dev/null +++ b/logging/src/test/java/org/xbib/logging/test/handlers/DelayedHandlerTests.java @@ -0,0 +1,236 @@ +package org.xbib.logging.test.handlers; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import org.xbib.logging.handlers.DelayedHandler; +import org.xbib.logging.test.AssertingErrorManager; +import org.xbib.logging.ExtHandler; +import org.xbib.logging.ExtLogRecord; +import org.xbib.logging.LogContext; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class DelayedHandlerTests { + + private static final int ITERATIONS = 190; + + public DelayedHandlerTests() { + } + + @AfterEach + public void cleanup() { + TestHandler.MESSAGES.clear(); + } + + @Test + public void testQueuedMessages() { + final LogContext logContext = LogContext.create(); + + final Logger rootLogger = logContext.getLogger(""); + final DelayedHandler delayedHandler = new DelayedHandler(); + delayedHandler.setErrorManager(AssertingErrorManager.of()); + rootLogger.addHandler(delayedHandler); + rootLogger.info("Test message 1"); + rootLogger.fine("Test message 2"); + + final Logger testLogger = logContext.getLogger(DelayedHandlerTests.class.getName()); + testLogger.warning("Test message 3"); + + final Logger randomLogger = logContext.getLogger("org.xbib.logging." + UUID.randomUUID()); + randomLogger.severe("Test message 4"); + + delayedHandler.addHandler(new TestHandler()); + + rootLogger.info("Test message 5"); + testLogger.severe("Test message 6"); + randomLogger.finest("Test message 7"); + + // The default root logger message is INFO so FINE and FINEST should not be logged + Assertions.assertEquals(5, TestHandler.MESSAGES.size()); + + // Test the messages actually logged + Assertions.assertEquals("Test message 1", TestHandler.MESSAGES.get(0).getFormattedMessage()); + Assertions.assertEquals("Test message 3", TestHandler.MESSAGES.get(1).getFormattedMessage()); + Assertions.assertEquals("Test message 4", TestHandler.MESSAGES.get(2).getFormattedMessage()); + Assertions.assertEquals("Test message 5", TestHandler.MESSAGES.get(3).getFormattedMessage()); + Assertions.assertEquals("Test message 6", TestHandler.MESSAGES.get(4).getFormattedMessage()); + } + + @Test + public void testAllLoggedAfterActivation() throws Exception { + final ExecutorService service = createExecutor(); + final LogContext logContext = LogContext.create(); + + final DelayedHandler handler = new DelayedHandler(); + handler.setErrorManager(AssertingErrorManager.of()); + final Logger rootLogger = logContext.getLogger(""); + rootLogger.addHandler(handler); + + try { + for (int i = 0; i < ITERATIONS; i++) { + final int current = i; + service.submit(() -> rootLogger.info(Integer.toString(current))); + } + // Wait until all the messages have been logged before + service.shutdown(); + service.awaitTermination(5, TimeUnit.SECONDS); + handler.addHandler(new TestHandler()); + + // Test that all messages have been flushed to the handler + Assertions.assertEquals(ITERATIONS, TestHandler.MESSAGES.size()); + + // Test that all messages have been flushed to the handler + final List missing = new ArrayList<>(ITERATIONS); + for (int i = 0; i < ITERATIONS; i++) { + missing.add(i); + } + + final List ints = TestHandler.MESSAGES.stream() + .map(extLogRecord -> Integer.parseInt(extLogRecord.getFormattedMessage())) + .collect(Collectors.toList()); + missing.removeAll(ints); + Collections.sort(missing); + Assertions.assertEquals(ITERATIONS, TestHandler.MESSAGES.size(), + () -> String.format("Missing the following entries: %s", missing)); + + } finally { + Assertions.assertTrue(service.shutdownNow().isEmpty()); + } + } + + @Test + public void testAllLoggedMidActivation() throws Exception { + final ExecutorService service = createExecutor(); + final LogContext logContext = LogContext.create(); + + final DelayedHandler handler = new DelayedHandler(); + handler.setErrorManager(AssertingErrorManager.of()); + final Logger rootLogger = logContext.getLogger(""); + rootLogger.addHandler(handler); + final Random r = new Random(); + + try { + for (int i = 0; i < ITERATIONS; i++) { + final int current = i; + service.submit(() -> { + TimeUnit.MILLISECONDS.sleep(r.nextInt(15)); + rootLogger.info(Integer.toString(current)); + return null; + }); + } + // Wait for a short time to make sure some messages are logged before we activate + TimeUnit.MILLISECONDS.sleep(150L); + handler.addHandler(new TestHandler()); + // Wait until all the messages have been logged before + service.shutdown(); + service.awaitTermination(5, TimeUnit.SECONDS); + + // Test that all messages have been flushed to the handler + final List missing = new ArrayList<>(ITERATIONS); + for (int i = 0; i < ITERATIONS; i++) { + missing.add(i); + } + + final List ints = TestHandler.MESSAGES.stream() + .map(extLogRecord -> Integer.parseInt(extLogRecord.getFormattedMessage())) + .collect(Collectors.toList()); + missing.removeAll(ints); + Collections.sort(missing); + Assertions.assertEquals(ITERATIONS, TestHandler.MESSAGES.size(), + () -> String.format("Missing the following entries: %s", missing)); + + } finally { + Assertions.assertTrue(service.shutdownNow().isEmpty()); + } + } + + @Test + public void testOrder() throws Exception { + final List expected = new ArrayList<>(ITERATIONS); + final ExecutorService service = createExecutor(); + final LogContext logContext = LogContext.create(); + + final DelayedHandler handler = new DelayedHandler(); + handler.setErrorManager(AssertingErrorManager.of()); + final Logger rootLogger = logContext.getLogger(""); + rootLogger.addHandler(handler); + final Random r = new Random(); + + try { + for (int i = 0; i < ITERATIONS; i++) { + final int current = i; + service.submit(() -> { + TimeUnit.MILLISECONDS.sleep(r.nextInt(15)); + final String msg = Integer.toString(current); + // Need to synchronize to ensure order of the logged messages and the expected messages + synchronized (expected) { + expected.add(msg); + rootLogger.info(msg); + } + return null; + }); + } + // Wait for a short time to make sure some messages are logged before we commit + TimeUnit.MILLISECONDS.sleep(150L); + handler.addHandler(new TestHandler()); + // Wait until all the messages have been logged before + service.shutdown(); + service.awaitTermination(5, TimeUnit.SECONDS); + + // Get the current messages from the handler + final List found = TestHandler.MESSAGES.stream() + .map(ExtLogRecord::getFormattedMessage) + .collect(Collectors.toList()); + final List missing = new ArrayList<>(expected); + missing.removeAll(found); + Assertions.assertTrue(missing.isEmpty(), () -> "Missing the following entries: " + missing); + + // This shouldn't happen as the above should find it, but it's better to fail here than below. + Assertions.assertEquals(expected.size(), TestHandler.MESSAGES.size()); + + // Now we need to test the order of what we have vs what is expected. These should be in the same order + for (int i = 0; i < expected.size(); i++) { + final String expectedMessage = expected.get(i); + final ExtLogRecord record = TestHandler.MESSAGES.get(i); + Assertions.assertEquals(expectedMessage, record.getFormattedMessage()); + } + + } finally { + Assertions.assertTrue(service.shutdownNow().isEmpty()); + } + } + + private static ExecutorService createExecutor() { + return Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2); + } + + public static class TestHandler extends ExtHandler { + static final List MESSAGES = new ArrayList<>(); + + @SuppressWarnings("WeakerAccess") + public TestHandler() { + setErrorManager(AssertingErrorManager.of()); + } + + @Override + protected synchronized void doPublish(final ExtLogRecord record) { + MESSAGES.add(record); + } + + @Override + public void close() throws SecurityException { + MESSAGES.clear(); + super.close(); + } + } +} diff --git a/logging/src/test/java/org/xbib/logging/test/handlers/ExtHandlerTests.java b/logging/src/test/java/org/xbib/logging/test/handlers/ExtHandlerTests.java new file mode 100644 index 0000000..b4c3861 --- /dev/null +++ b/logging/src/test/java/org/xbib/logging/test/handlers/ExtHandlerTests.java @@ -0,0 +1,141 @@ +package org.xbib.logging.test.handlers; + +import java.util.logging.SimpleFormatter; + +import org.xbib.logging.test.AssertingErrorManager; +import org.xbib.logging.ExtHandler; +import org.xbib.logging.formatters.PatternFormatter; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ExtHandlerTests { + + public ExtHandlerTests() { + } + + @Test + public void testHandlerClose() { + final CloseHandler parent = new CloseHandler(); + final CloseHandler child1 = new CloseHandler(); + final CloseHandler child2 = new CloseHandler(); + parent.setHandlers(new CloseHandler[] { child1, child2, new CloseHandler() }); + + // Ensure all handlers are not closed + assertFalse(parent.closed); + assertFalse(child1.closed); + assertFalse(child2.closed); + + // Close the parent handler, the children should be closed + parent.close(); + assertTrue(parent.closed); + assertTrue(child1.closed); + assertTrue(child2.closed); + + // Reset and wrap + parent.reset(); + child1.reset(); + child2.reset(); + + parent.setCloseChildren(false); + + // Ensure all handlers are not closed + assertFalse(parent.closed); + assertFalse(child1.closed); + assertFalse(child2.closed); + + parent.close(); + + // The parent should be closed, the others should be open + assertTrue(parent.closed); + assertFalse(child1.closed); + assertFalse(child2.closed); + + } + + @Test + public void testCallerCalculationCheckFormatterChange() { + try (CloseHandler parent = new CloseHandler()) { + + // Create a formatter for the parent that will require caller calculation + final PatternFormatter formatter = new PatternFormatter("%d %M %s%e%n"); + parent.setFormatter(formatter); + + assertTrue(parent.isCallerCalculationRequired()); + + // Change the formatter to not require calculation, this should trigger false to be returned + formatter.setPattern("%d %s%e%n"); + assertFalse(parent.isCallerCalculationRequired()); + } + } + + @Test + public void testCallerCalculationCheckNewFormatter() { + try (CloseHandler parent = new CloseHandler()) { + + // Create a formatter for the parent that will require caller calculation + final PatternFormatter formatter = new PatternFormatter("%d %M %s%e%n"); + parent.setFormatter(formatter); + + assertTrue(parent.isCallerCalculationRequired()); + + // Add a new formatter which should result in the caller calculation not to be required + parent.setFormatter(new PatternFormatter("%d %s%e%n")); + assertFalse(parent.isCallerCalculationRequired()); + } + } + + @Test + public void testCallerCalculationCheckChildFormatter() { + try (CloseHandler parent = new CloseHandler(); CloseHandler child = new CloseHandler()) { + parent.addHandler(child); + + // Create a formatter for the parent that will require caller calculation + final PatternFormatter formatter = new PatternFormatter("%d %M %s%e%n"); + child.setFormatter(formatter); + assertTrue(parent.isCallerCalculationRequired()); + + // Remove the child handler which should result in the caller calculation not being required since the formatter + // is null + parent.removeHandler(child); + assertFalse(parent.isCallerCalculationRequired()); + + // Add a formatter to the parent and add he child back, the parent handler will not require calculation, but + // the child should + parent.setFormatter(new PatternFormatter("%d %s%e%n")); + parent.addHandler(child); + assertTrue(parent.isCallerCalculationRequired()); + } + } + + @Test + public void testCallerCalculationNonExtFormatter() { + try (CloseHandler parent = new CloseHandler(); CloseHandler child = new CloseHandler()) { + parent.addHandler(child); + // Create a non ExtFormatter for the parent that will require caller calculation + child.setFormatter(new SimpleFormatter()); + assertTrue(child.isCallerCalculationRequired()); + assertTrue(parent.isCallerCalculationRequired()); + parent.setFormatter(new SimpleFormatter()); + assertTrue(parent.isCallerCalculationRequired()); + } + } + + static class CloseHandler extends ExtHandler { + private boolean closed = false; + + CloseHandler() { + setErrorManager(AssertingErrorManager.of()); + } + + @Override + public void close() { + closed = true; + super.close(); + } + + void reset() { + closed = false; + } + } +} diff --git a/logging/src/test/java/org/xbib/logging/test/handlers/OutputStreamHandlerTest.java b/logging/src/test/java/org/xbib/logging/test/handlers/OutputStreamHandlerTest.java new file mode 100644 index 0000000..830746f --- /dev/null +++ b/logging/src/test/java/org/xbib/logging/test/handlers/OutputStreamHandlerTest.java @@ -0,0 +1,74 @@ +package org.xbib.logging.test.handlers; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.logging.Formatter; +import java.util.logging.LogRecord; + +import org.xbib.logging.handlers.ConsoleHandler; +import org.xbib.logging.handlers.OutputStreamHandler; +import org.xbib.logging.test.AssertingErrorManager; +import org.xbib.logging.ExtLogRecord; +import org.xbib.logging.Level; +import org.xbib.logging.handlers.ConsoleHandler.Target; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class OutputStreamHandlerTest { + + private StringWriter out; + private OutputStreamHandler handler; + + private static final Formatter NO_FORMATTER = new Formatter() { + public String format(final LogRecord record) { + return record.getMessage(); + } + }; + + public OutputStreamHandlerTest() { + } + + @BeforeEach + public void prepareBuffer() { + out = new StringWriter(); + } + + @AfterEach + public void cleanAll() throws IOException { + handler.flush(); + handler.close(); + out.close(); + } + + @Test + public void testSetEncoding() throws Exception { + handler = new OutputStreamHandler(); + handler.setErrorManager(AssertingErrorManager.of()); + handler.setEncoding("UTF-8"); + Assertions.assertEquals("UTF-8", handler.getEncoding()); + } + + @Test + public void testSetEncodingOnOutputStream() throws Exception { + handler = new ConsoleHandler(Target.CONSOLE, NO_FORMATTER); + handler.setErrorManager(AssertingErrorManager.of()); + handler.setWriter(out); + handler.setEncoding("UTF-8"); + Assertions.assertEquals("UTF-8", handler.getEncoding()); + handler.publish(new ExtLogRecord(Level.INFO, "Hello World", getClass().getName())); + Assertions.assertEquals("Hello World", out.toString()); + } + + @Test + public void testSetNullEncodingOnOutputStream() throws Exception { + handler = new OutputStreamHandler(NO_FORMATTER); + handler.setErrorManager(AssertingErrorManager.of()); + handler.setWriter(out); + handler.setEncoding(null); + handler.publish(new ExtLogRecord(Level.INFO, "Hello World", getClass().getName())); + Assertions.assertEquals("Hello World", out.toString()); + } + +} diff --git a/logging/src/test/java/org/xbib/logging/test/handlers/QueueHandlerTests.java b/logging/src/test/java/org/xbib/logging/test/handlers/QueueHandlerTests.java new file mode 100644 index 0000000..ac47bbb --- /dev/null +++ b/logging/src/test/java/org/xbib/logging/test/handlers/QueueHandlerTests.java @@ -0,0 +1,84 @@ +package org.xbib.logging.test.handlers; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.xbib.logging.ExtHandler; +import org.xbib.logging.ExtLogRecord; +import org.xbib.logging.Level; +import org.xbib.logging.handlers.QueueHandler; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class QueueHandlerTests extends AbstractHandlerTest { + + public QueueHandlerTests() { + } + + @Test + public void testQueueSize() { + try (QueueHandler handler = new QueueHandler(5)) { + handler.setErrorManager(AssertingErrorManager.of()); + // Log 6 records and ensure only 5 are available + for (int i = 0; i < 6; i++) { + handler.publish(createLogRecord("Test message %d", i)); + } + final ExtLogRecord[] records = handler.getQueue(); + assertEquals(5, records.length, "QueueHandler held onto more records than allowed"); + // Check each message, the last should be the sixth + assertEquals("Test message 1", records[0].getMessage()); + assertEquals("Test message 2", records[1].getMessage()); + assertEquals("Test message 3", records[2].getMessage()); + assertEquals("Test message 4", records[3].getMessage()); + assertEquals("Test message 5", records[4].getMessage()); + } + } + + @Test + public void testNestedHandlers() throws Exception { + // Create a nested a handler + final NestedHandler nestedHandler = new NestedHandler(); + nestedHandler.setLevel(Level.INFO); + + try (QueueHandler handler = new QueueHandler(10)) { + handler.setErrorManager(AssertingErrorManager.of()); + handler.setLevel(Level.ERROR); + handler.addHandler(nestedHandler); + + // Log an info messages + handler.publish(createLogRecord(Level.INFO, "Test message")); + assertEquals(1, nestedHandler.getRecords().size()); + assertEquals(0, handler.getQueue().length); + + // Log a warn messages + handler.publish(createLogRecord(Level.WARN, "Test message")); + assertEquals(2, nestedHandler.getRecords().size()); + assertEquals(0, handler.getQueue().length); + + // Log an error messages + handler.publish(createLogRecord(Level.ERROR, "Test message")); + assertEquals(3, nestedHandler.getRecords().size()); + assertEquals(1, handler.getQueue().length); + } + } + + static class NestedHandler extends ExtHandler { + private final List records = new ArrayList<>(); + + NestedHandler() { + setErrorManager(AssertingErrorManager.of()); + } + + @Override + protected void doPublish(final ExtLogRecord record) { + records.add(record); + super.doPublish(record); + } + + Collection getRecords() { + return Collections.unmodifiableCollection(records); + } + } +} diff --git a/logging/src/test/java/org/xbib/logging/test/handlers/SimpleServer.java b/logging/src/test/java/org/xbib/logging/test/handlers/SimpleServer.java new file mode 100644 index 0000000..58793a5 --- /dev/null +++ b/logging/src/test/java/org/xbib/logging/test/handlers/SimpleServer.java @@ -0,0 +1,218 @@ +package org.xbib.logging.test.handlers; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.net.ServerSocketFactory; +import javax.net.ssl.SSLServerSocketFactory; + +abstract class SimpleServer implements Runnable, AutoCloseable { + + private final BlockingQueue data; + private final ExecutorService service; + + private SimpleServer(final BlockingQueue data) { + this.data = data; + service = Executors.newSingleThreadExecutor(r -> { + final Thread thread = new Thread(r); + thread.setDaemon(true); + return thread; + }); + } + + static SimpleServer createTcpServer(final int port) throws IOException { + final SimpleServer server = new TcpServer(ServerSocketFactory.getDefault(), new LinkedBlockingDeque<>(), port); + server.start(); + return server; + } + + static SimpleServer createTcpServer() throws IOException { + final SimpleServer server = new TcpServer(ServerSocketFactory.getDefault(), new LinkedBlockingDeque<>()); + server.start(); + return server; + } + + static SimpleServer createTlsServer(final int port) throws IOException { + final SimpleServer server = new TcpServer(SSLServerSocketFactory.getDefault(), new LinkedBlockingDeque<>(), port); + server.start(); + return server; + } + + static SimpleServer createTlsServer() throws IOException { + final SimpleServer server = new TcpServer(SSLServerSocketFactory.getDefault(), new LinkedBlockingDeque<>()); + server.start(); + return server; + } + + static SimpleServer createUdpServer() throws IOException { + final SimpleServer server = new UdpServer(new LinkedBlockingDeque<>()); + server.start(); + return server; + } + + String timeoutPoll() throws InterruptedException { + return data.poll(10, TimeUnit.SECONDS); + } + + String poll() throws InterruptedException { + return data.poll(); + } + + String peek() { + return data.peek(); + } + + private void start() { + service.submit(this); + } + + @Override + public void close() throws IOException { + service.shutdown(); + try { + service.awaitTermination(30, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + } + + abstract int getPort(); + + private static class TcpServer extends SimpleServer { + private final BlockingQueue data; + private final AtomicBoolean closed = new AtomicBoolean(true); + private final ServerSocket serverSocket; + private volatile Socket socket; + + private TcpServer(final ServerSocketFactory serverSocketFactory, final BlockingQueue data, final int port) + throws IOException { + super(data); + this.serverSocket = serverSocketFactory.createServerSocket(port); + this.data = data; + } + + private TcpServer(final ServerSocketFactory serverSocketFactory, final BlockingQueue data) throws IOException { + super(data); + this.serverSocket = serverSocketFactory.createServerSocket(0); + this.data = data; + } + + @Override + public void run() { + closed.set(false); + try { + socket = serverSocket.accept(); + InputStream in = socket.getInputStream(); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + while (!closed.get()) { + final byte[] buffer = new byte[512]; + int len; + while ((len = in.read(buffer)) != -1) { + final byte lastByte = buffer[len - 1]; + if (lastByte == '\n') { + out.write(buffer, 0, (len - 1)); + data.put(out.toString()); + out.reset(); + } else { + out.write(buffer, 0, len); + } + } + } + } catch (IOException e) { + if (!closed.get()) { + throw new UncheckedIOException(e); + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + @Override + public void close() throws IOException { + try { + closed.set(true); + try { + socket.close(); + } finally { + serverSocket.close(); + } + } finally { + super.close(); + } + } + + @Override + int getPort() { + return serverSocket.getLocalPort(); + } + } + + private static class UdpServer extends SimpleServer { + private final BlockingQueue data; + private final AtomicBoolean closed = new AtomicBoolean(true); + private final DatagramSocket socket; + + private UdpServer(final BlockingQueue data) throws SocketException { + super(data); + this.data = data; + socket = new DatagramSocket(); + } + + @Override + public void run() { + closed.set(false); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + while (!closed.get()) { + try { + final DatagramPacket packet = new DatagramPacket(new byte[2048], 2048); + socket.receive(packet); + final int len = packet.getLength(); + byte[] bytes = new byte[len]; + System.arraycopy(packet.getData(), 0, bytes, 0, len); + final byte lastByte = bytes[len - 1]; + if (lastByte == '\n') { + out.write(bytes, 0, (len - 1)); + data.put(out.toString()); + out.reset(); + } else { + out.write(bytes, 0, len); + } + } catch (IOException e) { + if (!closed.get()) { + throw new UncheckedIOException(e); + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + @Override + public void close() throws IOException { + try { + closed.set(true); + socket.close(); + } finally { + super.close(); + } + } + + @Override + int getPort() { + return socket.getLocalPort(); + } + } +} diff --git a/logging/src/test/java/org/xbib/logging/test/handlers/SocketHandlerTests.java b/logging/src/test/java/org/xbib/logging/test/handlers/SocketHandlerTests.java new file mode 100644 index 0000000..1690582 --- /dev/null +++ b/logging/src/test/java/org/xbib/logging/test/handlers/SocketHandlerTests.java @@ -0,0 +1,223 @@ +package org.xbib.logging.test.handlers; + +import java.io.UnsupportedEncodingException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import java.util.logging.ErrorManager; + +import javax.net.ssl.SSLContext; + +import org.xbib.logging.ExtLogRecord; +import org.xbib.logging.LogContext; +import org.xbib.logging.Logger; +import org.xbib.logging.formatters.PatternFormatter; +import org.xbib.logging.handlers.SocketHandler; +import org.xbib.logging.handlers.SocketHandler.Protocol; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class SocketHandlerTests extends AbstractHandlerTest { + + private final InetAddress address; + + public SocketHandlerTests() throws UnknownHostException { + address = InetAddress.getByName(System.getProperty("org.xbib.test.address", "127.0.0.1")); + } + + @Test + public void testTcpConnection() throws Exception { + try (SimpleServer server = SimpleServer.createTcpServer(); + SocketHandler handler = createHandler(Protocol.TCP, server.getPort())) { + final ExtLogRecord record = createLogRecord("Test TCP handler"); + handler.publish(record); + final String msg = server.timeoutPoll(); + Assertions.assertNotNull(msg); + Assertions.assertEquals("Test TCP handler", msg); + } + } + + @Test + public void testTlsConnection() throws Exception { + try (SimpleServer server = SimpleServer.createTlsServer(); + SocketHandler handler = createHandler(Protocol.SSL_TCP, server.getPort())) { + final ExtLogRecord record = createLogRecord("Test TLS handler"); + handler.publish(record); + final String msg = server.timeoutPoll(); + Assertions.assertNotNull(msg); + Assertions.assertEquals("Test TLS handler", msg); + } + } + + @Test + public void testUdpConnection() throws Exception { + try (SimpleServer server = SimpleServer.createUdpServer(); + SocketHandler handler = createHandler(Protocol.UDP, server.getPort())) { + final ExtLogRecord record = createLogRecord("Test UDP handler"); + handler.publish(record); + final String msg = server.timeoutPoll(); + Assertions.assertNotNull(msg); + Assertions.assertEquals("Test UDP handler", msg); + } + } + + @Test + public void testTcpPortChange() throws Exception { + try ( + SimpleServer server1 = SimpleServer.createTcpServer(); + SimpleServer server2 = SimpleServer.createTcpServer(); + SocketHandler handler = createHandler(Protocol.TCP, server1.getPort())) { + final int port = server1.getPort(); + final int altPort = server2.getPort(); + ExtLogRecord record = createLogRecord("Test TCP handler " + port); + handler.publish(record); + String msg = server1.timeoutPoll(); + Assertions.assertNotNull(msg); + Assertions.assertEquals("Test TCP handler " + port, msg); + + // Change the port on the handler which should close the first connection and open a new one + handler.setPort(altPort); + record = createLogRecord("Test TCP handler " + altPort); + handler.publish(record); + msg = server2.timeoutPoll(); + Assertions.assertNotNull(msg); + Assertions.assertEquals("Test TCP handler " + altPort, msg); + + // There should be nothing on server1, we won't know if the real connection is closed but we shouldn't + // have any data remaining on the first server + Assertions.assertNull(server1.peek(), "Expected no data on server1"); + } + } + + @Test + public void testProtocolChange() throws Exception { + SocketHandler handler = null; + try { + try (SimpleServer server = SimpleServer.createTcpServer()) { + handler = createHandler(Protocol.TCP, server.getPort()); + final ExtLogRecord record = createLogRecord("Test TCP handler"); + handler.publish(record); + final String msg = server.timeoutPoll(); + Assertions.assertNotNull(msg); + Assertions.assertEquals("Test TCP handler", msg); + } + // wait until the OS really release used port + Thread.sleep(50); + + // Change the protocol on the handler which should close the first connection and open a new one + handler.setProtocol(Protocol.SSL_TCP); + + try (SimpleServer server = SimpleServer.createTlsServer(handler.getPort())) { + final ExtLogRecord record = createLogRecord("Test TLS handler"); + handler.publish(record); + final String msg = server.timeoutPoll(); + Assertions.assertNotNull(msg); + Assertions.assertEquals("Test TLS handler", msg); + } + } finally { + if (handler != null) { + handler.close(); + } + } + } + + @Test + public void testTcpReconnect() throws Exception { + SocketHandler handler = null; + try { + + // Publish a record to a running server + try ( + SimpleServer server = SimpleServer.createTcpServer()) { + handler = createHandler(Protocol.TCP, server.getPort()); + handler.setErrorManager(AssertingErrorManager.of(ErrorManager.FLUSH_FAILURE)); + final ExtLogRecord record = createLogRecord("Test TCP handler"); + handler.publish(record); + final String msg = server.timeoutPoll(); + Assertions.assertNotNull(msg); + Assertions.assertEquals("Test TCP handler", msg); + } + // wait until the OS really release used port + Thread.sleep(50); + + // Publish a record to a down server, this likely won't put the handler in an error state yet. However once + // we restart the server and loop the first socket should fail before a reconnect is attempted. + final ExtLogRecord record = createLogRecord("Test TCP handler"); + handler.publish(record); + try ( + SimpleServer server = SimpleServer.createTcpServer(handler.getPort())) { + final SocketHandler socketHandler = handler; + // Keep writing a record until a successful record is published or a timeout occurs + final String msg = timeout(() -> { + final ExtLogRecord r = createLogRecord("Test TCP handler"); + socketHandler.publish(r); + try { + return server.poll(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }, 10); + Assertions.assertNotNull(msg); + Assertions.assertEquals("Test TCP handler", msg); + } + } finally { + if (handler != null) { + handler.close(); + } + } + } + + @Test + public void testTlsConfig() throws Exception { + try (SimpleServer server = SimpleServer.createTlsServer()) { + final int port = server.getPort(); + final LogContext logContext = LogContext.create(); + final PatternFormatter patternFormatter = new PatternFormatter("%s\n"); + final SocketHandler socketHandler = new SocketHandler(); + socketHandler.setSocketFactory(SSLContext.getDefault().getSocketFactory()); + socketHandler.setProtocol(Protocol.SSL_TCP); + socketHandler.setAddress(address); + socketHandler.setPort(port); + socketHandler.setAutoFlush(true); + socketHandler.setEncoding("utf-8"); + socketHandler.setFormatter(patternFormatter); + socketHandler.setErrorManager(AssertingErrorManager.of()); + // Create the root logger + final Logger rootLogger = logContext.getLogger(""); + rootLogger.addHandler(socketHandler); + rootLogger.info("Test TCP handler " + port + " 1"); + String msg = server.timeoutPoll(); + Assertions.assertNotNull(msg); + Assertions.assertEquals("Test TCP handler " + port + " 1", msg); + } + } + + private SocketHandler createHandler(final Protocol protocol, final int port) throws UnsupportedEncodingException { + final SocketHandler handler = new SocketHandler(protocol, address, port); + handler.setAutoFlush(true); + handler.setEncoding("utf-8"); + handler.setFormatter(new PatternFormatter("%s\n")); + handler.setErrorManager(AssertingErrorManager.of()); + + return handler; + } + + private static R timeout(final Supplier supplier, final int timeout) throws InterruptedException { + R value = null; + long t = timeout * 1000L; + final long sleep = 100L; + while (t > 0) { + final long before = System.currentTimeMillis(); + value = supplier.get(); + if (value != null) { + break; + } + t -= (System.currentTimeMillis() - before); + TimeUnit.MILLISECONDS.sleep(sleep); + t -= sleep; + } + Assertions.assertFalse((t <= 0), () -> String.format("Failed to get value in %d seconds.", timeout)); + return value; + } +} diff --git a/logging/src/test/java/org/xbib/logging/test/handlers/SyslogHandlerTests.java b/logging/src/test/java/org/xbib/logging/test/handlers/SyslogHandlerTests.java new file mode 100644 index 0000000..20af40c --- /dev/null +++ b/logging/src/test/java/org/xbib/logging/test/handlers/SyslogHandlerTests.java @@ -0,0 +1,277 @@ +package org.xbib.logging.test.handlers; + +import java.io.ByteArrayOutputStream; +import java.io.UnsupportedEncodingException; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; + +import org.xbib.logging.ExtLogRecord; +import org.xbib.logging.formatters.PatternFormatter; +import org.xbib.logging.handlers.SyslogHandler; +import org.xbib.logging.handlers.SyslogHandler.SyslogType; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class SyslogHandlerTests { + + private static final String ENCODING = "UTF-8"; + + private static final String BOM = Character.toString((char) 0xFEFF); + + private static final String MSG = "This is a test message"; + + private static final String HOSTNAME = "localhost"; + + private static final int PORT = 10999; + + private SyslogHandler handler; + + public SyslogHandlerTests() { + } + + @BeforeEach + public void setupHandler() throws Exception { + handler = new SyslogHandler(HOSTNAME, PORT); + handler.setFormatter(new PatternFormatter("%s")); + handler.setErrorManager(AssertingErrorManager.of()); + } + + @AfterEach + public void closeHandler() throws Exception { + // Close the handler + handler.flush(); + handler.close(); + } + + @Test + public void testRFC5424Tcp() throws Exception { + // Setup the handler + handler.setSyslogType(SyslogType.RFC5424); + handler.setMessageDelimiter("\n"); + handler.setUseMessageDelimiter(true); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + handler.setOutputStream(out); + + Instant instant = getInstant(); + // Create the record + handler.setHostname("test"); + ExtLogRecord record = createRecord(instant, MSG); + String expectedMessage = "<14>1 2012-01-09T04:39:22.000" + getTimeZone(instant) + " test java " + + ProcessHandle.current().pid() + + " - - " + BOM + MSG + '\n'; + handler.publish(record); + Assertions.assertEquals(expectedMessage, createString(out)); + + // Create the record + out.reset(); + record = createRecord(instant, MSG); + expectedMessage = "<14>1 2012-01-09T04:39:22.000" + getTimeZone(instant) + " test java " + + ProcessHandle.current().pid() + + " - - " + BOM + MSG + '\n'; + handler.publish(record); + Assertions.assertEquals(expectedMessage, createString(out)); + + out.reset(); + instant = instant.plus(22L, ChronoUnit.DAYS); + record = createRecord(instant, MSG); + handler.setHostname("test"); + handler.setAppName("java"); + expectedMessage = "<14>1 2012-01-31T04:39:22.000" + getTimeZone(instant) + " test java " + + ProcessHandle.current().pid() + + " - - " + + BOM + MSG + '\n'; + handler.publish(record); + Assertions.assertEquals(expectedMessage, createString(out)); + } + + @Test + public void testRFC31644Format() throws Exception { + // Setup the handler + handler.setSyslogType(SyslogType.RFC3164); + handler.setHostname("test"); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + handler.setOutputStream(out); + Instant instant = getInstant(); + // Create the record + ExtLogRecord record = createRecord(instant, MSG); + handler.publish(record); + String expectedMessage = "<14>Jan 9 04:39:22 test java[" + + ProcessHandle.current().pid() + + "]: " + MSG; + Assertions.assertEquals(expectedMessage, createString(out)); + out.reset(); + instant = instant.plus(22L, ChronoUnit.DAYS); + record = createRecord(instant, MSG); + handler.publish(record); + expectedMessage = "<14>Jan 31 04:39:22 test java[" + + ProcessHandle.current().pid() + + "]: " + MSG; + Assertions.assertEquals(expectedMessage, createString(out)); + } + + @Test + public void testOctetCounting() throws Exception { + // Setup the handler + handler.setSyslogType(SyslogType.RFC5424); + handler.setUseMessageDelimiter(false); + handler.setUseCountingFraming(true); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + handler.setOutputStream(out); + Instant instant = getInstant(); + // Create the record + handler.setHostname("test"); + ExtLogRecord record = createRecord(instant, MSG); + String expectedMessage = "<14>1 2012-01-09T04:39:22.000" + getTimeZone(instant) + " test java " + + ProcessHandle.current().pid() + + " - - " + BOM + MSG; + expectedMessage = byteLen(expectedMessage) + " " + expectedMessage; + handler.publish(record); + Assertions.assertEquals(expectedMessage, createString(out)); + } + + @Test + public void testTruncation() throws Exception { + // Setup the handler + handler.setSyslogType(SyslogType.RFC5424); + handler.setUseMessageDelimiter(false); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + handler.setOutputStream(out); + + Instant instant = getInstant(); + // Create the record + handler.setHostname("test"); + final String part1 = "This is a longer message and should be truncated after this."; + final String part2 = "Truncated portion of the message that will not be shown in."; + final String message = part1 + " " + part2; + + final String header = "<14>1 2012-01-09T04:39:22.000" + getTimeZone(instant) + " test java " + + ProcessHandle.current().pid() + + " - - " + BOM; + + handler.setMaxLength(byteLen(header, part1)); + handler.setTruncate(true); + + ExtLogRecord record = createRecord(instant, message); + String expectedMessage = header + part1; + handler.publish(record); + Assertions.assertEquals(expectedMessage, createString(out)); + + out.reset(); + // Wrap a message + handler.setTruncate(false); + handler.publish(record); + // Extra space from message + expectedMessage = header + part1 + header + " " + part2; + Assertions.assertEquals(expectedMessage, createString(out)); + } + + @Test + public void testMultibyteTruncation() throws Exception { + String part1 = "This is a longer message and should be truncated after this À あ"; + String part2 = "あ À Truncated portion of the message that will not be shown in."; + testMultibyteTruncation(part1, part2, 1); + + // Test some characters with surrogates + part1 = "Truncate after double byte 𥹖"; + part2 = "second message 𥹖"; + testMultibyteTruncation(part1, part2, 2); + + } + + @Test + public void testNullMessage() throws Exception { + // Setup the handler + handler.setSyslogType(SyslogType.RFC5424); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + handler.setOutputStream(out); + Instant instant = getInstant(); + // Create the record + handler.setHostname("test"); + ExtLogRecord record = createRecord(instant, null); + final String expectedMessage = "<14>1 2012-01-09T04:39:22.000" + getTimeZone(instant) + " test java " + + ProcessHandle.current().pid() + + " - - " + BOM + "null"; + handler.publish(record); + Assertions.assertEquals(expectedMessage, createString(out)); + } + + private void testMultibyteTruncation(final String part1, final String part2, final int charsToTruncate) throws Exception { + // Setup the handler + handler.setSyslogType(SyslogType.RFC5424); + handler.setUseMessageDelimiter(false); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + handler.setOutputStream(out); + Instant instant = getInstant(); + // Create the record + handler.setHostname("test"); + String message = part1 + " " + part2; + + final String header = "<14>1 2012-01-09T04:39:22.000" + getTimeZone(instant) + " test java " + + ProcessHandle.current().pid() + + " - - " + BOM; + + handler.setMaxLength(byteLen(header, part1)); + handler.setTruncate(true); + + ExtLogRecord record = createRecord(instant, message); + String expectedMessage = header + part1; + handler.publish(record); + Assertions.assertArrayEquals(expectedMessage.getBytes(ENCODING), out.toByteArray(), + String.format("Expected: %s:%n Received: %s", expectedMessage, createString(out))); + + out.reset(); + // Wrap a message + handler.setTruncate(false); + handler.publish(record); + // Extra space from message + expectedMessage = header + part1 + header + " " + part2; + Assertions.assertArrayEquals(expectedMessage.getBytes(ENCODING), out.toByteArray(), + String.format("Expected: %s:%n Received: %s", expectedMessage, createString(out))); + + // Reset out, write the message with a maximum length of the current length minus 1 to ensure the multi-byte character was not written at the end + out.reset(); + handler.setTruncate(true); + handler.setMaxLength(byteLen(header, part1) - 1); + expectedMessage = header + part1.substring(0, part1.length() - charsToTruncate); + handler.publish(record); + Assertions.assertArrayEquals(expectedMessage.getBytes(ENCODING), out.toByteArray(), + String.format("Expected: %s:%n Received: %s", expectedMessage, createString(out))); + } + + private static ExtLogRecord createRecord(final Instant instant, final String message) { + final String loggerName = SyslogHandlerTests.class.getName(); + final ExtLogRecord record = new ExtLogRecord(Level.INFO, message, loggerName); + record.setMillis(instant.toEpochMilli()); + return record; + } + + private static Instant getInstant() { + return LocalDateTime.parse("2012-01-09T04:39:22").toInstant(ZoneOffset.UTC); + } + + private static String getTimeZone(Instant instant) { + ZoneOffset zoneOffset = ZoneOffset.UTC.getRules().getOffset(instant); + long seconds = zoneOffset.getTotalSeconds(); + long hours = TimeUnit.HOURS.convert(seconds, TimeUnit.SECONDS); + long minutes = TimeUnit.MINUTES.convert(seconds, TimeUnit.SECONDS) % 60; + return (seconds >= 0L ? "+" : "-") + String.format("%02d:%02d", hours, minutes); + } + + private static String createString(final ByteArrayOutputStream out) throws UnsupportedEncodingException { + return out.toString(ENCODING); + } + + private static int byteLen(final String s) throws UnsupportedEncodingException { + return s.getBytes(ENCODING).length; + } + + private static int byteLen(final String s1, final String s2) throws UnsupportedEncodingException { + return s1.getBytes(ENCODING).length + s2.getBytes(ENCODING).length; + } +} diff --git a/logging/src/test/resources/client-keystore.jks b/logging/src/test/resources/client-keystore.jks new file mode 100644 index 0000000..35c5a96 Binary files /dev/null and b/logging/src/test/resources/client-keystore.jks differ diff --git a/logging/src/test/resources/configs/default-logging.properties b/logging/src/test/resources/configs/default-logging.properties new file mode 100644 index 0000000..af89bd2 --- /dev/null +++ b/logging/src/test/resources/configs/default-logging.properties @@ -0,0 +1,59 @@ +loggers=org.xbib.logging,Spaced\\Logger,Special:Char\\Logger,org.xbib.filter1,org.xbib.filter2,/../Weird/Path + +# Root logger +logger.level=INFO +logger.handlers=CONSOLE + +logger.org.xbib.logging.useParentHandlers=true +logger.org.xbib.logging.level=INFO + +logger.Spaced\\Logger.useParentHandlers=true +logger.Spaced\\Logger.level=INFO + +logger.Special\:Char\\Logger.useParentHandlers=true +logger.Special\:Char\\Logger.level=INFO + +logger.org.xbib.filter1.filter=match(".*") +logger.org.xbib.filter2.filter=FILTER + +logger./../Weird/Path.level=INFO + +handler.CONSOLE=org.xbib.logging.handlers.ConsoleHandler +handler.CONSOLE.formatter=PATTERN +handler.CONSOLE.properties=autoFlush,target +handler.CONSOLE.autoFlush=true +handler.CONSOLE.target=SYSTEM_OUT +handler.CONSOLE.filter=FILTER + +handler.FILE=org.xbib.logging.handlers.FileHandler +handler.FILE.formatter=PATTERN +handler.FILE.level=TRACE +handler.FILE.properties=autoFlush,append,fileName +handler.FILE.constructorProperties=fileName,append +handler.FILE.autoFlush=true +handler.FILE.append=false +handler.FILE.fileName=build/test.log +handler.FILE.encoding=UTF-8 +handler.FILE.filter=match(".*") +handler.FILE.errorManager=DFT + +handlers=FILE + +filter.FILTER=org.xbib.logging.filters.AcceptAllFilter + +filters=FILTER2 +filter.FILTER2=org.xbib.logging.filters.AcceptAllFilter + +errorManager.DFT=org.xbib.logging.errormanager.OnlyOnceErrorManager + +errorManagers=OTHER +errorManager.OTHER=java.util.logging.ErrorManager + +formatter.PATTERN=org.xbib.logging.formatters.PatternFormatter +formatter.PATTERN.properties=pattern +formatter.PATTERN.pattern=%d{HH:mm:ss,SSS} %-5p [%c] (%t) %s%E%n + +formatters=OTHER +formatter.OTHER=org.xbib.logging.formatters.PatternFormatter +formatter.OTHER.properties=pattern +formatter.OTHER.pattern=%d{HH:mm:ss,SSS} %-5p [%c] (%t) %s%E%n diff --git a/logging/src/test/resources/configs/expected-spaced-value-logging.properties b/logging/src/test/resources/configs/expected-spaced-value-logging.properties new file mode 100644 index 0000000..96fd1ea --- /dev/null +++ b/logging/src/test/resources/configs/expected-spaced-value-logging.properties @@ -0,0 +1,26 @@ +loggers=org.xbib.logging + +# Root logger +logger.level=INFO +logger.handlers=CONSOLE + +logger.org.xbib.logging.filter=any(accept) +logger.org.xbib.logging.useParentHandlers=true +logger.org.xbib.logging.level=INFO + +handler.CONSOLE=org.xbib.logging.handlers.ConsoleHandler +handler.CONSOLE.formatter=PATTERN +handler.CONSOLE.properties=autoFlush,target +handler.CONSOLE.autoFlush=\u0020true\u0020 +handler.CONSOLE.target=\u0020SYSTEM_OUT +handler.CONSOLE.filter=FILTER +handler.CONSOLE.encoding=UTF-8 +handler.CONSOLE.errorManager=DFT + +filter.FILTER=org.xbib.logging.filters.AcceptAllFilter + +errorManager.DFT=org.xbib.logging.errormanager.OnlyOnceErrorManager + +formatter.PATTERN=org.xbib.logging.formatters.PatternFormatter +formatter.PATTERN.properties=pattern +formatter.PATTERN.pattern='\u0020'%d{HH:mm:ss,SSS} %-5p [%c] (%t) %s%E%n diff --git a/logging/src/test/resources/configs/expression-logging.properties b/logging/src/test/resources/configs/expression-logging.properties new file mode 100644 index 0000000..ed95a5c --- /dev/null +++ b/logging/src/test/resources/configs/expression-logging.properties @@ -0,0 +1,36 @@ +loggers=org.xbib.logging + +# Root logger +logger.level=${default.log.level:INFO} +logger.handlers=CONSOLE,FILE + +logger.org.xbib.logging.useParentHandlers=${logging.use.parent.handlers:true} +logger.org.xbib.logging.level=${default.log.level:INFO} + +handler.CONSOLE=org.xbib.logging.handlers.ConsoleHandler +handler.CONSOLE.formatter=PATTERN +handler.CONSOLE.properties=autoFlush,target,encoding,filter +handler.CONSOLE.autoFlush=${handler.autoFlush:true} +handler.CONSOLE.target=${handler.console.target:SYSTEM_OUT} +handler.CONSOLE.filter=${handler.console.filter:FILTER} +handler.CONSOLE.encoding=${handler.encoding:UTF-8} + +handler.FILE=org.xbib.logging.handlers.FileHandler +handler.FILE.formatter=PATTERN +handler.FILE.level=${handler.level:TRACE} +handler.FILE.properties=autoFlush,append,fileName,errorManager,encoding,filter +handler.FILE.constructorProperties=fileName,append +handler.FILE.autoFlush=${handler.autoFlush:true} +handler.FILE.append=${handler.file.append:false} +handler.FILE.fileName=build/${handler.file.fileName:test.log} +handler.FILE.encoding=${handler.encoding:UTF-8} +handler.FILE.filter=${handler.file.filter:match(".*")} +handler.FILE.errorManager=${handler.errorManager:DFT} + +errorManager.DFT=org.xbib.logging.errormanager.OnlyOnceErrorManager + +formatter.PATTERN=org.xbib.logging.formatters.PatternFormatter +formatter.PATTERN.properties=pattern +formatter.PATTERN.pattern=${format.pattern:%d{HH:mm:ss,SSS} %-5p [%c] (%t) %s%E%n} + +filter.FILTER=org.xbib.logging.filters.AcceptAllFilter \ No newline at end of file diff --git a/logging/src/test/resources/configs/invalid-logging.properties b/logging/src/test/resources/configs/invalid-logging.properties new file mode 100644 index 0000000..b04bc0d --- /dev/null +++ b/logging/src/test/resources/configs/invalid-logging.properties @@ -0,0 +1,42 @@ +loggers=org.xbib.logging,org.xbib.filter1,org.xbib.filter2 + +# Root logger +logger.level=INFO +logger.handlers=CONSOLE,INVALID + +logger.org.xbib.logging.useParentHandlers=true +logger.org.xbib.logging.level=INFO +# logger.org.xbib.logging.filter=INVALID TODO (jrp) filters need to be revisited + +logger.org.xbib.filter1.filter=match(".*") +logger.org.xbib.filter2.filter=FILTER + +handler.CONSOLE=org.xbib.logging.handlers.ConsoleHandler +handler.CONSOLE.formatter=PATTERN +handler.CONSOLE.properties=autoFlush,target +handler.CONSOLE.autoFlush=true +handler.CONSOLE.target=SYSTEM_OUT +# handler.CONSOLE.filter=INVALID TODO (jrp) filters need to be revisited +handler.CONSOLE.errorManager=INVALID + +handler.FILE=org.xbib.logging.handlers.FileHandler +handler.FILE.formatter=INVALID +handler.FILE.level=TRACE +handler.FILE.properties=autoFlush,append,fileName +handler.FILE.constructorProperties=fileName,append +handler.FILE.autoFlush=true +handler.FILE.append=false +handler.FILE.fileName=build/test.log +handler.FILE.encoding=UTF-8 +handler.FILE.filter=match(".*") +handler.FILE.errorManager=DFT + +handlers=FILE + +filter.FILTER=org.xbib.logging.filters.AcceptAllFilter + +errorManager.DFT=org.xbib.logging.errormanager.OnlyOnceErrorManager + +formatter.PATTERN=org.xbib.logging.formatters.PatternFormatter +formatter.PATTERN.properties=pattern +formatter.PATTERN.pattern=%d{HH:mm:ss,SSS} %-5p [%c] (%t) %s%E%n diff --git a/logging/src/test/resources/configs/simple-logging.properties b/logging/src/test/resources/configs/simple-logging.properties new file mode 100644 index 0000000..636f03f --- /dev/null +++ b/logging/src/test/resources/configs/simple-logging.properties @@ -0,0 +1,18 @@ +loggers=org.xbib.logging + +# Root logger +logger.level=INFO +logger.handlers=CONSOLE + +logger.org.xbib.logging.useParentHandlers=true +logger.org.xbib.logging.level=INFO + +handler.CONSOLE=org.xbib.logging.handlers.ConsoleHandler +handler.CONSOLE.formatter=PATTERN +handler.CONSOLE.properties=autoFlush,target +handler.CONSOLE.autoFlush=true +handler.CONSOLE.target=SYSTEM_OUT + +formatter.PATTERN=org.xbib.logging.formatters.PatternFormatter +formatter.PATTERN.properties=pattern +formatter.PATTERN.pattern=%d{HH:mm:ss,SSS} %-5p [%c] (%t) %s%E%n \ No newline at end of file diff --git a/logging/src/test/resources/configs/spaced-value-logging.properties b/logging/src/test/resources/configs/spaced-value-logging.properties new file mode 100644 index 0000000..2b13043 --- /dev/null +++ b/logging/src/test/resources/configs/spaced-value-logging.properties @@ -0,0 +1,26 @@ +loggers=org.xbib.logging + +# Root logger +logger.level=\u0020INFO +logger.handlers=CONSOLE + +logger.org.xbib.logging.filter=\u0020any(accept)\u0020 +logger.org.xbib.logging.useParentHandlers=true\u0020 +logger.org.xbib.logging.level=\u0020INFO\u0020 + +handler.CONSOLE=\u0020org.xbib.logging.handlers.ConsoleHandler +handler.CONSOLE.formatter=PATTERN\u0020 +handler.CONSOLE.properties=autoFlush,target +handler.CONSOLE.autoFlush=\u0020true\u0020 +handler.CONSOLE.target=\u0020SYSTEM_OUT +handler.CONSOLE.filter=FILTER\u0020 +handler.CONSOLE.encoding=\u0020UTF-8\u0020 +handler.CONSOLE.errorManager=\u0020DFT\u0020 + +filter.FILTER=org.xbib.logging.filters.AcceptAllFilter + +errorManager.DFT=org.xbib.logging.errormanager.OnlyOnceErrorManager + +formatter.PATTERN=org.xbib.logging.formatters.PatternFormatter +formatter.PATTERN.properties=pattern +formatter.PATTERN.pattern='\u0020'%d{HH:mm:ss,SSS} %-5p [%c] (%t) %s%E%n diff --git a/logging/src/test/resources/org/xbib/logging/test/LoggerTests.properties b/logging/src/test/resources/org/xbib/logging/test/LoggerTests.properties new file mode 100644 index 0000000..b9bc4da --- /dev/null +++ b/logging/src/test/resources/org/xbib/logging/test/LoggerTests.properties @@ -0,0 +1 @@ +test=Test message diff --git a/logging/src/test/resources/org/xbib/logging/test/LoggerTests_en.properties b/logging/src/test/resources/org/xbib/logging/test/LoggerTests_en.properties new file mode 100644 index 0000000..b9bc4da --- /dev/null +++ b/logging/src/test/resources/org/xbib/logging/test/LoggerTests_en.properties @@ -0,0 +1 @@ +test=Test message diff --git a/logging/src/test/resources/org/xbib/logging/test/formatters/test-banner.txt b/logging/src/test/resources/org/xbib/logging/test/formatters/test-banner.txt new file mode 100644 index 0000000..f0aa002 --- /dev/null +++ b/logging/src/test/resources/org/xbib/logging/test/formatters/test-banner.txt @@ -0,0 +1 @@ +This is a test banner! Please preserve newlines and whitespace! diff --git a/logging/src/test/resources/org/xbib/logging/test/formatters/xml-formatter.xsd b/logging/src/test/resources/org/xbib/logging/test/formatters/xml-formatter.xsd new file mode 100644 index 0000000..b4ae6ac --- /dev/null +++ b/logging/src/test/resources/org/xbib/logging/test/formatters/xml-formatter.xsd @@ -0,0 +1,327 @@ + + + + + + + + + Defines a log record. + + + + + + + The date and time the log record was recorded. The format is configured via the formatter. + + + + + + + The sequence number of the record. + + + + + + + The name of the logger class that created the message. + + + + + + + The name of the logger. + + + + + + + The level the message was logged at. + + + + + + + The message that was logged. + + + + + + + The name of the thread where the message was logged from. + + + + + + + The threads id where the message was logged from. + + + + + + + Defines the key of the MDC entry as an element with the value being the value of the element. + + + + + + + + + + + + The nested diagnostics for the logged message. + + + + + + + The host name from the record if known. + + + + + + + The process name from the record if known. + + + + + + + The process id from the record if known. + + + + + + + The exception from the logged message. + + + + + + + The stack trace of the exception logged. + + + + + + + The name of the class that (allegedly) issued the logging request. + + + + + + + The source file name. + + + + + + + The name of the method that (allegedly) issued the logging request. + + + + + + + The source line number. + + + + + + + The name of the module that (allegedly) issued the logging request. + + + + + + + The version of the module that (allegedly) issued the logging request. + + + + + + + The source line number. + + + + + + + + + + Defines the key of the MDC entry as an element with the value being the value of the element. + + + + + + + + + + + Defines the frame of a stack trace element. + + + + + + + + The type of the exception. + + + + + + + The message from the exception. + + + + + + + + The optional suppressed exceptions. + + + + + + + The optional cause + + + + + + + + + + A reference id. + + + + + + + + + Defines the suppressed exceptions. + + + + + + + + + + + A circular reference to a previous exception in the stack. + + + + + + + + + + + The cause of the exception. + + + + + + + + + + + Defines the frame of a stack trace element. + + + + + + + The fully qualified class name. + + + + + + + The name of the method. + + + + + + + The line the error occurred on. + + + + + + + + + + The exception stack trace frames. + + + + + + + + + + + Represents key/value meta data. + + + + + + + + + \ No newline at end of file diff --git a/logging/src/test/resources/server-keystore.jks b/logging/src/test/resources/server-keystore.jks new file mode 100644 index 0000000..b96a08c Binary files /dev/null and b/logging/src/test/resources/server-keystore.jks differ diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..9760f03 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,46 @@ +pluginManagement { + repositories { + mavenLocal() + mavenCentral { + metadataSources { + mavenPom() + artifact() + ignoreGradleMetadataRedirection() + } + } + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + versionCatalogs { + libs { + version('gradle', '8.7') + version('log4j', '2.23.1') + version('slf4j', '2.0.13') + library('log4j-core', 'org.apache.logging.log4j', 'log4j-core').versionRef('log4j') + library('log4j-jul', 'org.apache.logging.log4j', 'log4j-jul').versionRef('log4j') + library('log4j-jcl', 'org.apache.logging.log4j', 'log4j-jcl').versionRef('log4j') + library('log4j-slf4j-impl', 'org.apache.logging.log4j', 'log4j-slf4j-impl').versionRef('log4j') + library('log4j-slf4j', 'org.apache.logging.log4j', 'log4j-to-slf4j').versionRef('log4j') + library('slf4j-api', 'org.slf4j', 'slf4j-api').versionRef('slf4j') + library('slf4j-jul', 'org.slf4j', 'jul-to-slf4j').versionRef('slf4j') + library('slf4j-jcl', 'org.slf4j', 'jcl-over-slf4j').versionRef('slf4j') + library('slf4j-simple', 'org.slf4j', 'slf4j-simple').versionRef('slf4j') + } + testLibs { + version('junit', '5.10.2') + library('junit-jupiter-api', 'org.junit.jupiter', 'junit-jupiter-api').versionRef('junit') + library('junit-jupiter-params', 'org.junit.jupiter', 'junit-jupiter-params').versionRef('junit') + library('junit-jupiter-engine', 'org.junit.jupiter', 'junit-jupiter-engine').versionRef('junit') + library('junit-vintage-engine', 'org.junit.vintage', 'junit-vintage-engine').versionRef('junit') + library('junit-jupiter-platform-launcher', 'org.junit.platform', 'junit-platform-launcher').version('1.10.1') + library('hamcrest', 'org.hamcrest', 'hamcrest-library').version('2.2') + library('junit4', 'junit', 'junit').version('4.13.2') + } + } +} + +include 'logging' +include 'logging-adapter-log4j' +include 'logging-adapter-slf4j'