diff options
author | 2016-09-02 12:57:42 +0000 | |
---|---|---|
committer | 2016-09-06 15:37:24 +0000 | |
commit | d9d524ab77ebff673009e7405678c237bdb510fc (patch) | |
tree | 8fd60f11e9aa8a37ed55f00447b0b4fb818373a5 /src/java_tools/junitrunner | |
parent | 7ef4273d14398575303f651e48ec828c21218b3f (diff) |
Removing GUAVA XML escapers from junitrunner.
Bazel users that are using a different Guava version than the one in the
junitrunner jar are getting an IncompatibleClassChangeError. Rewriting
parts of junitrunner code so it won't depend on Guava anymore.
Continuing progress on issue #1150.
--
MOS_MIGRATED_REVID=132054901
Diffstat (limited to 'src/java_tools/junitrunner')
3 files changed, 322 insertions, 1 deletions
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/XmlWriter.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/XmlWriter.java index 0910486d12..d60fea3e33 100644 --- a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/XmlWriter.java +++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/XmlWriter.java @@ -16,7 +16,7 @@ package com.google.testing.junit.runner.model; import static java.nio.charset.StandardCharsets.UTF_8; -import com.google.common.xml.XmlEscapers; +import com.google.testing.junit.runner.util.XmlEscapers; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/CharEscaper.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/CharEscaper.java new file mode 100644 index 0000000000..f7b1d86f5a --- /dev/null +++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/CharEscaper.java @@ -0,0 +1,184 @@ +// Copyright 2016 The Bazel Authors. All Rights Reserved. +// +// 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 com.google.testing.junit.runner.util; + +import java.util.Collections; +import java.util.Map; + +/** + * An escaper that uses an array to quickly look up replacement characters for a given {@code char} + * value. An additional safe range is provided that determines whether {@code char} values without + * specific replacements are to be considered safe and left unescaped or should be escaped in a + * general way. + */ +public abstract class CharEscaper { + // The replacement array. + private final char[][] replacements; + // The number of elements in the replacement array. + private final int replacementsLength; + // The first character in the safe range. + private final char safeMin; + // The last character in the safe range. + private final char safeMax; + + // The multiplier for padding to use when growing the escape buffer. + private static final int DEST_PAD_MULTIPLIER = 2; + + public CharEscaper(Map<Character, String> replacementMap, char safeMin, char safeMax) { + this.replacements = createReplacementArray(replacementMap); + this.replacementsLength = replacements.length; + this.safeMin = safeMin; + this.safeMax = safeMax; + } + + public final String escape(String s) { + if (s == null) { + throw new NullPointerException(); + } + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if ((c < replacementsLength && replacements[c] != null) || c > safeMax || c < safeMin) { + return escapeSlow(s, i); + } + } + return s; + } + + /** + * A thread-local destination buffer to keep us from creating new buffers. The starting size is + * 1024 characters. + */ + private static final ThreadLocal<char[]> DEST_TL = + new ThreadLocal<char[]>() { + @Override + protected char[] initialValue() { + return new char[1024]; + } + }; + + /** + * Returns the escaped form of a given literal string, starting at the given index. This method is + * called by the {@link #escape(String)} method when it discovers that escaping is required. + * + * @param s the literal string to be escaped + * @param index the index to start escaping from + * @return the escaped form of {@code string} + * @throws NullPointerException if {@code string} is null + */ + final String escapeSlow(String s, int index) { + int slen = s.length(); + + // Get a destination buffer and setup some loop variables. + char[] dest = DEST_TL.get(); + int destSize = dest.length; + int destIndex = 0; + int lastEscape = 0; + + // Loop through the rest of the string, replacing when needed into the + // destination buffer, which gets grown as needed as well. + for (; index < slen; index++) { + + // Get a replacement for the current character. + char[] r = escape(s.charAt(index)); + + // If no replacement is needed, just continue. + if (r == null) { + continue; + } + + int rlen = r.length; + int charsSkipped = index - lastEscape; + + // This is the size needed to add the replacement, not the full size + // needed by the string. We only regrow when we absolutely must, and + // when we do grow, grow enough to avoid excessive growing. Grow. + int sizeNeeded = destIndex + charsSkipped + rlen; + if (destSize < sizeNeeded) { + destSize = sizeNeeded + DEST_PAD_MULTIPLIER * (slen - index); + dest = growBuffer(dest, destIndex, destSize); + } + + // If we have skipped any characters, we need to copy them now. + if (charsSkipped > 0) { + s.getChars(lastEscape, index, dest, destIndex); + destIndex += charsSkipped; + } + + // Copy the replacement string into the dest buffer as needed. + if (rlen > 0) { + System.arraycopy(r, 0, dest, destIndex, rlen); + destIndex += rlen; + } + lastEscape = index + 1; + } + + // Copy leftover characters if there are any. + int charsLeft = slen - lastEscape; + if (charsLeft > 0) { + int sizeNeeded = destIndex + charsLeft; + if (destSize < sizeNeeded) { + + // Regrow and copy, expensive! No padding as this is the final copy. + dest = growBuffer(dest, destIndex, sizeNeeded); + } + s.getChars(lastEscape, slen, dest, destIndex); + destIndex = sizeNeeded; + } + return new String(dest, 0, destIndex); + } + + final char[] escape(char c) { + if (c < replacementsLength) { + char[] chars = replacements[c]; + if (chars != null) { + return chars; + } + } + if (c >= safeMin && c <= safeMax) { + return null; + } + return escapeUnsafe(c); + } + + abstract char[] escapeUnsafe(char c); + + /** + * Helper method to grow the character buffer as needed, this only happens once in a while so it's + * ok if it's in a method call. If the index passed in is 0 then no copying will be done. + */ + private static char[] growBuffer(char[] dest, int index, int size) { + char[] copy = new char[size]; + if (index > 0) { + System.arraycopy(dest, 0, copy, 0, index); + } + return copy; + } + + private static char[][] createReplacementArray(Map<Character, String> map) { + if (map == null) { + throw new NullPointerException(); + } + if (map.isEmpty()) { + return new char[0][0]; + } + char max = Collections.max(map.keySet()); + char[][] replacements = new char[max + 1][]; + for (char c : map.keySet()) { + replacements[c] = map.get(c).toCharArray(); + } + return replacements; + } +} + diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/XmlEscapers.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/XmlEscapers.java new file mode 100644 index 0000000000..7621a076bc --- /dev/null +++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/XmlEscapers.java @@ -0,0 +1,137 @@ +// Copyright 2016 The Bazel Authors. All Rights Reserved. +// +// 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 com.google.testing.junit.runner.util; + +import java.util.HashMap; +import java.util.Map; +import javax.annotation.Nullable; + +/** + * Utility class for dealing with escaping XML content and attributes. + */ +public class XmlEscapers { + private XmlEscapers() {} + + private static final char MIN_ASCII_CONTROL_CHAR = 0x00; + private static final char MAX_ASCII_CONTROL_CHAR = 0x1F; + + public static CharEscaper xmlContentEscaper() { + return XML_CONTENT_ESCAPER; + } + + public static CharEscaper xmlAttributeEscaper() { + return XML_ATTRIBUTE_ESCAPER; + } + + private static final CharEscaper XML_CONTENT_ESCAPER; + private static final CharEscaper XML_ATTRIBUTE_ESCAPER; + + static { + Builder builder = Builder.builder(); + builder.setSafeRange(Character.MIN_VALUE, '\uFFFD'); + builder.setUnsafeReplacement("\uFFFD"); + + for (char c = MIN_ASCII_CONTROL_CHAR; c <= MAX_ASCII_CONTROL_CHAR; c++) { + if (c != '\t' && c != '\n' && c != '\r') { + builder.addEscape(c, "\uFFFD"); + } + } + + builder.addEscape('&', "&"); + builder.addEscape('<', "<"); + builder.addEscape('>', ">"); + XML_CONTENT_ESCAPER = builder.build(); + builder.addEscape('\'', "'"); + builder.addEscape('"', """); + builder.addEscape('\t', "	"); + builder.addEscape('\n', "
"); + builder.addEscape('\r', "
"); + XML_ATTRIBUTE_ESCAPER = builder.build(); + } + + /** + * A builder for CharEscaper. + */ + static final class Builder { + private final Map<Character, String> replacementMap = new HashMap<>(); + private char safeMin = Character.MIN_VALUE; + private char safeMax = Character.MAX_VALUE; + private String unsafeReplacement = null; + + static Builder builder() { + return new Builder(); + } + // The constructor is exposed via the builder() method above. + private Builder() {} + + /** + * Sets the safe range of characters for the escaper. Characters in this range that have no + * explicit replacement are considered 'safe' and remain unescaped in the output. If + * {@code safeMax < safeMin} then the safe range is empty. + * + * @return the builder instance + */ + Builder setSafeRange(char safeMin, char safeMax) { + this.safeMin = safeMin; + this.safeMax = safeMax; + return this; + } + + /** + * Sets the replacement string for any characters outside the 'safe' range that have no explicit + * replacement. If {@code unsafeReplacement} is {@code null} then no replacement will occur, if + * it is {@code ""} then the unsafe characters are removed from the output. + * + * @return the builder instance + */ + Builder setUnsafeReplacement(@Nullable String unsafeReplacement) { + this.unsafeReplacement = unsafeReplacement; + return this; + } + + /** + * Adds a replacement string for the given input character. The specified character will be + * replaced by the given string whenever it occurs in the input, irrespective of whether it lies + * inside or outside the 'safe' range. + * + * @return the builder instance + * @throws NullPointerException if {@code replacement} is null + */ + Builder addEscape(char c, String replacement) { + if (replacement == null) { + throw new NullPointerException(); + } + // This can replace an existing character (the builder is re-usable). + replacementMap.put(c, replacement); + return this; + } + + /** + * Returns a new CharEscaper based on the current state of the builder. + */ + CharEscaper build() { + return new CharEscaper(replacementMap, safeMin, safeMax) { + private final char[] replacementChars = + unsafeReplacement != null ? unsafeReplacement.toCharArray() : null; + + @Override + char[] escapeUnsafe(char c) { + return replacementChars; + } + }; + } + } +} + |