001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau; 018 019import static org.apache.juneau.commons.utils.IoUtils.*; 020 021import java.util.regex.*; 022 023/** 024 * Shared test-support utilities for validating XML output. 025 */ 026@SuppressWarnings({ 027 "java:S112" // checkXmlWhitespace() and its helper deliberately throw plain Exception to report whitespace errors. 028}) 029public final class XmlTestUtils { 030 031 // Whitespace-validation patterns. The lookbehind on the trailing '>' distinguishes start tags ('(?<!/)') 032 // from self-closing tags ('(?<=/)') in a single linear pass, avoiding the super-linear backtracking of the 033 // previous '.*'/nested-quantifier forms (java:S8786). Hoisted to static final fields since they're 034 // stateless and otherwise get recompiled on every checkXmlWhitespace() call/recursion. 035 private static final Pattern START_TAG = Pattern.compile("^(\\s*)<[^/>][^>]*(?<!/)>$"); 036 private static final Pattern END_TAG = Pattern.compile("^(\\s*)</[^>]+>$"); 037 private static final Pattern COMBINED_TAG = Pattern.compile("^(\\s*)<[^/>][^>]*(?<=/)>$"); 038 private static final Pattern CONTENT_ONLY = Pattern.compile("^(\\s*)[^\\s\\<]+$"); 039 040 private XmlTestUtils() {} 041 042 /** 043 * Validates that the whitespace is correct in the specified XML. 044 * 045 * @param out The XML to validate. 046 * @throws Exception If the whitespace/indentation in the XML is incorrect. 047 */ 048 public static void checkXmlWhitespace(String out) throws Exception { 049 if (out.indexOf('\u0000') != -1) { 050 for (var s : out.split("\u0000")) 051 checkXmlWhitespace(s); 052 return; 053 } 054 055 var lines = out.split("\n"); 056 var indent = -1; 057 try { 058 for (var i = 0; i < lines.length; i++) 059 indent = checkLineIndent(lines[i], i + 1, indent); 060 if (indent != -1) 061 throw new Exception("Possible unmatched tag. indent=''" + indent + "''"); 062 } catch (Exception e) { 063 printLines(lines); 064 throw e; 065 } 066 } 067 068 /** 069 * Validates the indentation of a single line of XML and returns the indent depth after processing it. 070 * 071 * @param line The line to validate. 072 * @param lineNum The 1-based line number (for error messages). 073 * @param indent The indent depth before processing this line. 074 * @return The indent depth after processing this line. 075 * @throws Exception If the line's indentation is wrong, or the line doesn't match any recognized form. 076 */ 077 private static int checkLineIndent(String line, int lineNum, int indent) throws Exception { 078 var m = START_TAG.matcher(line); 079 if (m.matches()) { 080 checkIndent(indent + 1, m.group(1).length(), lineNum, "start tag"); 081 return indent + 1; 082 } 083 084 m = END_TAG.matcher(line); 085 if (m.matches()) { 086 checkIndent(indent, m.group(1).length(), lineNum, "end tag"); 087 return indent - 1; 088 } 089 090 m = COMBINED_TAG.matcher(line); 091 if (m.matches()) { 092 checkIndent(indent + 1, m.group(1).length(), lineNum, "combined tag"); 093 return indent; 094 } 095 096 m = CONTENT_ONLY.matcher(line); 097 if (m.matches()) { 098 checkIndent(indent + 1, m.group(1).length(), lineNum, "content-only"); 099 return indent; 100 } 101 102 var twc = tagWithContentIndent(line); 103 if (twc != -1) { 104 checkIndent(indent + 1, twc, lineNum, "tag-with-content"); 105 return indent; 106 } 107 108 throw new Exception("Unmatched whitespace line at line number ''" + lineNum + "''"); 109 } 110 111 /** 112 * Verifies that an observed indent width matches the expected one, throwing a descriptive exception otherwise. 113 * 114 * @param expected The expected indent width. 115 * @param actual The observed indent width. 116 * @param lineNum The 1-based line number (for error messages). 117 * @param lineType A human-readable description of the line kind being checked (for error messages). 118 * @throws Exception If <c>actual != expected</c>. 119 */ 120 private static void checkIndent(int expected, int actual, int lineNum, String lineType) throws Exception { 121 if (actual != expected) 122 throw new Exception("Wrong indentation detected on " + lineType + " line ''" + lineNum + "''"); 123 } 124 125 /** 126 * Returns the leading-whitespace indent width of a line that is a single tag-with-content line. 127 * 128 * <p> 129 * A tag-with-content line is one that — after any optional leading whitespace — starts with an opening tag and 130 * ends with a closing tag, such as <c><foo>bar</foo></c>. This is a linear-time replacement for the 131 * <c>^(\s*)<[^>]+>.*</[^>]+>$</c> regular expression that avoids its super-linear backtracking. 132 * 133 * @param line The line to test. 134 * @return 135 * The number of leading whitespace characters on the line, or <c>-1</c> if the line is not a single 136 * tag-with-content line. 137 */ 138 private static int tagWithContentIndent(String line) { 139 var n = line.length(); 140 var w = 0; 141 while (w < n) { 142 var c = line.charAt(w); 143 if (c != ' ' && c != '\t' && c != '\n' && c != '\u000B' && c != '\f' && c != '\r') 144 break; 145 w++; 146 } 147 if (n - w < 2 || line.charAt(w) != '<' || line.charAt(n - 1) != '>') 148 return -1; 149 var open = line.indexOf('>', w); // End of the opening tag. 150 if (open - w < 2) 151 return -1; 152 var close = line.lastIndexOf("</"); // Start of the final closing tag. 153 if (close <= open || close + 3 > n - 1) 154 return -1; 155 for (var j = close + 2; j < n - 1; j++) // Closing tag name must not contain '>'. 156 if (line.charAt(j) == '>') 157 return -1; 158 return w; 159 } 160}