diff --git a/projectDependencies.gradle b/projectDependencies.gradle index 6ba628d..27aef41 100644 --- a/projectDependencies.gradle +++ b/projectDependencies.gradle @@ -100,6 +100,7 @@ project(':sulky-logging') { description = 'This file is part of the sulky modules. It contains a LoggingPropertyChangeListener and some Swing logging.' dependencies { compile libraries.'slf4j-api' + testCompile libraries.'logback-classic' } } diff --git a/sulky-formatting/src/main/java/de/huxhorn/sulky/formatting/SimpleXml.java b/sulky-formatting/src/main/java/de/huxhorn/sulky/formatting/SimpleXml.java index 2106021..4207d5b 100644 --- a/sulky-formatting/src/main/java/de/huxhorn/sulky/formatting/SimpleXml.java +++ b/sulky-formatting/src/main/java/de/huxhorn/sulky/formatting/SimpleXml.java @@ -1,6 +1,6 @@ /* * sulky-modules - several general-purpose modules. - * Copyright (C) 2007-2011 Joern Huxhorn + * Copyright (C) 2007-2014 Joern Huxhorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by @@ -17,7 +17,7 @@ */ /* - * Copyright 2007-2011 Joern Huxhorn + * Copyright 2007-2014 Joern Huxhorn * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -60,10 +60,15 @@ private SimpleXml() */ public static boolean isValidXMLCharacter(char character) { - return character == '\t' || character == '\r' || character == '\n' || - (character >= XML_CHAR_RANGE_A_START && character <= XML_CHAR_RANGE_A_END) || - (character >= XML_CHAR_RANGE_B_START && character <= XML_CHAR_RANGE_B_END) || - (character >= XML_CHAR_RANGE_C_START && character <= XML_CHAR_RANGE_C_END); + return isValidXMLCharacter(0xFFFF & character); + } + + public static boolean isValidXMLCharacter(int codePoint) + { + return codePoint == '\t' || codePoint == '\r' || codePoint == '\n' || + (codePoint >= XML_CHAR_RANGE_A_START && codePoint <= XML_CHAR_RANGE_A_END) || + (codePoint >= XML_CHAR_RANGE_B_START && codePoint <= XML_CHAR_RANGE_B_END) || + (codePoint >= XML_CHAR_RANGE_C_START && codePoint <= XML_CHAR_RANGE_C_END); } /** @@ -135,12 +140,12 @@ public static String replaceNonValidXMLCharacters(String in, char replacementCha if(!isValidXMLCharacter(replacementChar)) { throw new IllegalArgumentException("Replacement character 0x" - + Integer.toString(replacementChar, 16) + " is invalid itself!"); + + Integer.toString(replacementChar, 16).toUpperCase() + " is invalid itself!"); } for(int i = 0; i < in.length(); i++) { - char current = in.charAt(i); + int current = in.codePointAt(i); if(isValidXMLCharacter(current)) { diff --git a/sulky-formatting/src/test/groovy/de/huxhorn/sulky/formatting/SimpleXmlSpec.groovy b/sulky-formatting/src/test/groovy/de/huxhorn/sulky/formatting/SimpleXmlSpec.groovy new file mode 100644 index 0000000..b7480ab --- /dev/null +++ b/sulky-formatting/src/test/groovy/de/huxhorn/sulky/formatting/SimpleXmlSpec.groovy @@ -0,0 +1,203 @@ +/* + * sulky-modules - several general-purpose modules. + * Copyright (C) 2007-2014 Joern Huxhorn + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +/* + * Copyright 2007-2014 Joern Huxhorn + * + * 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. + */ + +package de.huxhorn.sulky.formatting + +import spock.lang.Specification +import spock.lang.Unroll + +class SimpleXmlSpec extends Specification { + + def escapedXmlCharacters() { + [ + ['&' as char, '&'], + ['<' as char, '<'], + ['>' as char, '>'], + ['\"' as char, '"'], + ] + } + + @Unroll + def "escape(character=#character) should return #escaped"(char character, String escaped) { + when: + String result = SimpleXml.escape(''+character) + + then: + result == escaped + + where: + [character, escaped] << escapedXmlCharacters() + } + + def "special case escape(character=0) should return space"() { + expect: + ' ' == SimpleXml.escape(''+((char)0)) + } + + @Unroll + def "unescape(escaped=#escaped) should return #character"(char character, String escaped) { + when: + String result = SimpleXml.unescape(escaped) + + then: + result == ''+character + + where: + [character, escaped] << escapedXmlCharacters() + } + + + /** + * Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] + * (any Unicode character, excluding the surrogate blocks, FFFE, and FFFF.) + * + * See http://www.w3.org/TR/REC-xml#charsets + */ + def validXmlEdgeCaseChars() { + [ + // @formatter:off + [false, 0x8], + [true , 0x9], + [true , 0xA], + [false, 0xB], + [false, 0xC], + [true , 0xD], + [false, 0xE], + [false, 0x19], + [true, 0x20], + [true, 0xD7FF], + [false, 0xD800], + [false, 0xDFFF], + [true, 0xE000], + [true, 0xFFFD], + [false, 0xFFFE], + [false, 0xFFFF], + // @formatter:on + ] + } + + /** + * Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] + * (any Unicode character, excluding the surrogate blocks, FFFE, and FFFF.) + * + * See http://www.w3.org/TR/REC-xml#charsets + */ + def validXmlEdgeCaseIntegers() { + [ + // @formatter:off + [true, 0x10000], + [true, 0x10FFFF], + [true, 0x10FFFF], + [false, 0x110000], + // @formatter:on + ] + } + + @Unroll + def "char edge case: isValidXMLCharacter((int)codePoint=#codePoint) should return #valid"(boolean valid, int codePoint) { + when: + boolean result = SimpleXml.isValidXMLCharacter(codePoint) + + then: + result == valid + + where: + [valid, codePoint] << validXmlEdgeCaseChars() + } + + @Unroll + def "integer edge case: isValidXMLCharacter((int)codePoint=#codePoint) should return #valid"(boolean valid, int codePoint) { + when: + boolean result = SimpleXml.isValidXMLCharacter(codePoint) + + then: + result == valid + + where: + [valid, codePoint] << validXmlEdgeCaseIntegers() + } + + @Unroll + def "isValidXMLCharacter((char)codePoint=#codePoint) should return #valid"(boolean valid, int codePoint) { + when: + boolean result = SimpleXml.isValidXMLCharacter((char)codePoint) + + then: + result == valid || codePoint != (codePoint & 0xFFFF) + + where: + [valid, codePoint] << validXmlEdgeCaseChars() + } + + @Unroll + def "replaceNonValidXMLCharacters((char)codePoint=#codePoint)"(boolean valid, int codePoint) { + when: + char replacement = '#' + String input = new StringBuilder().appendCodePoint(codePoint).toString(); + String result = SimpleXml.replaceNonValidXMLCharacters(input, replacement) + + then: + (valid && result == input) || (!valid && result.codePointAt(0) == (int)replacement) + + where: + [valid, codePoint] << validXmlEdgeCaseChars() + } + + def "replaceNonValidXMLCharacters with invalid replacement character"() { + when: + SimpleXml.replaceNonValidXMLCharacters('foo', (char)0xFFFF) + + then: + IllegalArgumentException ex = thrown() + ex.message == 'Replacement character 0xFFFF is invalid itself!' + } + + def "replaceNonValidXMLCharacters with valid input"() { + when: + String input = 'foo' + String result = SimpleXml.replaceNonValidXMLCharacters('foo', ' ' as char) + + then: + input == result + input.is(result) + } + + def "replaceNonValidXMLCharacters with some replacement"() { + when: + String result = SimpleXml.replaceNonValidXMLCharacters('foo\u0019\uD800bar', ' ' as char) + + then: + 'foo bar' == result + } +} diff --git a/sulky-formatting/src/test/java/de/huxhorn/sulky/formatting/SafeStringTest.java b/sulky-formatting/src/test/java/de/huxhorn/sulky/formatting/SafeStringTest.java index 9300149..d6a5d90 100644 --- a/sulky-formatting/src/test/java/de/huxhorn/sulky/formatting/SafeStringTest.java +++ b/sulky-formatting/src/test/java/de/huxhorn/sulky/formatting/SafeStringTest.java @@ -61,6 +61,7 @@ public void showMapRecursionProblem() b.put("bar", a); a.put("foo", b); // the following line will throw an java.lang.StackOverflowError! + //noinspection ResultOfMethodCallIgnored a.toString(); } @@ -88,6 +89,7 @@ public void showCollectionRecursionProblem() b.add(a); a.add(b); // the following line will throw an java.lang.StackOverflowError! + //noinspection ResultOfMethodCallIgnored a.toString(); } @@ -150,6 +152,12 @@ public void verifyExceptionWithMessageInToStringWorks() evaluate(expected, o); } + @Test + public void foo() + { + evaluate("UnproblematicToString", new UnproblematicToString()); + } + @Test public void verifyRecursiveObjectArray() { @@ -311,6 +319,14 @@ private void evaluate(String expected, Object o) assertEquals(expected, result); } + private static class UnproblematicToString + { + public String toString() + { + return "UnproblematicToString"; + } + } + private static class ProblematicToString { private String message; diff --git a/sulky-logging/src/main/java/de/huxhorn/sulky/logging/LoggingPropertyChangeListener.java b/sulky-logging/src/main/java/de/huxhorn/sulky/logging/LoggingPropertyChangeListener.java index f7d8b69..0232773 100644 --- a/sulky-logging/src/main/java/de/huxhorn/sulky/logging/LoggingPropertyChangeListener.java +++ b/sulky-logging/src/main/java/de/huxhorn/sulky/logging/LoggingPropertyChangeListener.java @@ -43,7 +43,21 @@ public class LoggingPropertyChangeListener implements PropertyChangeListener { - final Logger logger = LoggerFactory.getLogger(LoggingPropertyChangeListener.class); + private final Logger logger; + + public LoggingPropertyChangeListener() + { + this(LoggerFactory.getLogger(LoggingPropertyChangeListener.class)); + } + + public LoggingPropertyChangeListener(Logger logger) + { + this.logger = logger; + if(logger == null) + { + throw new IllegalArgumentException("logger must not be null!"); + } + } public void propertyChange(PropertyChangeEvent event) { diff --git a/sulky-logging/src/test/groovy/de/huxhorn/sulky/logging/LoggingPropertyChangeListenerSpec.groovy b/sulky-logging/src/test/groovy/de/huxhorn/sulky/logging/LoggingPropertyChangeListenerSpec.groovy new file mode 100644 index 0000000..f7a081c --- /dev/null +++ b/sulky-logging/src/test/groovy/de/huxhorn/sulky/logging/LoggingPropertyChangeListenerSpec.groovy @@ -0,0 +1,94 @@ +/* + * sulky-modules - several general-purpose modules. + * Copyright (C) 2007-2014 Joern Huxhorn + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +/* + * Copyright 2007-2014 Joern Huxhorn + * + * 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. + */ + +package de.huxhorn.sulky.logging + +import ch.qos.logback.core.Appender +import org.slf4j.LoggerFactory +import spock.lang.Specification + +import java.beans.PropertyChangeEvent; + +public class LoggingPropertyChangeListenerSpec extends Specification { + def "log a change with default logger"() { + setup: + ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME); + + Appender appenderMock = Mock(Appender) + root.addAppender(appenderMock); + + when: + LoggingPropertyChangeListener instance = new LoggingPropertyChangeListener() + def event = new PropertyChangeEvent(new Object(), 'valueName', 'oldValue', 'newValue') + instance.propertyChange(event) + + then: + 1 * appenderMock.doAppend({ + assert it.formattedMessage == 'PropertyChangeEvent:\n\tpropertyName=\'valueName\'\n\toldValue=oldValue\n\tnewValue=newValue' + assert it.loggerName == 'de.huxhorn.sulky.logging.LoggingPropertyChangeListener' + true + } ) + + } + + def "log a change with custom logger"() { + setup: + ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME); + + Appender appenderMock = Mock(Appender) + root.addAppender(appenderMock); + + when: + LoggingPropertyChangeListener instance = new LoggingPropertyChangeListener(LoggerFactory.getLogger("foo")) + def event = new PropertyChangeEvent(new Object(), 'valueName', 'oldValue', 'newValue') + instance.propertyChange(event) + + then: + 1 * appenderMock.doAppend({ + assert it.formattedMessage == 'PropertyChangeEvent:\n\tpropertyName=\'valueName\'\n\toldValue=oldValue\n\tnewValue=newValue' + assert it.loggerName == 'foo' + true + } ) + + } + + def "null logger in constructor"() { + when: + new LoggingPropertyChangeListener(null); + + then: + IllegalArgumentException ex = thrown() + ex.message == 'logger must not be null!' + } +} diff --git a/sulky-logging/src/test/resources/logback-test.xml b/sulky-logging/src/test/resources/logback-test.xml new file mode 100644 index 0000000..23212af --- /dev/null +++ b/sulky-logging/src/test/resources/logback-test.xml @@ -0,0 +1,15 @@ + + + + + + %-5level - %d{HH:mm:ss.SSS} [%thread] - %file:%line - %msg%n%ex{full} + + + + + + + + +