001/** 002 * 003 * Copyright 2018 Florian Schmaus 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * 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.jxmpp.xml.splitter; 018 019import java.io.IOException; 020 021import org.jxmpp.xml.splitter.XmlSplitter.State; 022 023public class XmlPrettyPrinter extends XmlPrinter { 024 025 private final int indent; 026 private final int attributeIndent; 027 private final int tabWidth; 028 029 private final PrettyPrintedXmlChunkWithCurrentPartCallback newChunkCallback; 030 private final PrettyPrintedXmlPartCallback newPartCallback; 031 private final PrettyPrintedXmlChunkSink prettyWriter; 032 033 private StringBuilder currentPart; 034 private StringBuilder currentChunk; 035 private StringBuilder currentChunkWithCurrentPart; 036 037 /** 038 * Construct a new XML pretty printer. 039 * 040 * @param partCallback a part callback. 041 */ 042 public XmlPrettyPrinter(PrettyPrintedXmlPartCallback partCallback) { 043 this(builder().setPartCallback(partCallback)); 044 } 045 046 /** 047 * Construct a new XML pretty printer. 048 * 049 * @param prettyWriter a writer for the pretty printed XML stream. 050 */ 051 public XmlPrettyPrinter(PrettyPrintedXmlChunkSink prettyWriter) { 052 this(builder().setPrettyWriter(prettyWriter)); 053 } 054 055 private XmlPrettyPrinter(Builder builder) { 056 this.indent = builder.indent; 057 this.attributeIndent = builder.attributeIndent; 058 this.tabWidth = builder.tabWidth; 059 this.newChunkCallback = builder.newChunkCallback; 060 this.newPartCallback = builder.newPartCallback; 061 this.prettyWriter = builder.prettyWriter; 062 } 063 064 @Override 065 void onChunkStart() { 066 if (newChunkCallback != null) { 067 currentChunkWithCurrentPart = new StringBuilder(currentPart.length() + 1024); 068 currentChunkWithCurrentPart.append(currentPart); 069 currentChunkWithCurrentPart.append('['); 070 } 071 072 if (prettyWriter != null) { 073 currentChunk = new StringBuilder(1024); 074 } 075 } 076 077 @Override 078 void onChunkEnd() { 079 if (newChunkCallback != null) { 080 currentChunkWithCurrentPart.append(']'); 081 newChunkCallback.onPrettyPrintedXmlChunk(currentChunkWithCurrentPart); 082 currentChunkWithCurrentPart = null; 083 } 084 085 if (prettyWriter != null) { 086 prettyWriter.sink(currentChunk); 087 currentChunk = null; 088 } 089 } 090 091 @SuppressWarnings("incomplete-switch") 092 @Override 093 void onNextChar(char c, int depth, State initialState, State currentState) throws IOException { 094 final boolean stateChange = initialState != currentState; 095 final StringBuilder sb = new StringBuilder(stateChange ? 16 : 1); 096 097 if (stateChange) { 098 boolean deferredLeftAngle = false; 099 int indent = 0; 100 switch (currentState) { 101 case TAG_LEFT_ANGLE_BRACKET: 102 // Note that we return here because we need to see if this is a start tag or end tag. 103 return; 104 case END_TAG_SOLIDUS: 105 indent = getElementIndent(depth - 1); 106 deferredLeftAngle = true; 107 break; 108 case IN_TAG_NAME: 109 indent = getElementIndent(depth); 110 deferredLeftAngle = true; 111 break; 112 case IN_ATTRIBUTE_NAME: 113 if (attributeIndent > 0) { 114 indent = getAttributeIndent(depth); 115 } 116 break; 117 case START: 118 indent = getElementIndent(depth); 119 break; 120 } 121 122 if (indent > 0 || deferredLeftAngle) { 123 sb.append('\n'); 124 } 125 126 appendIndent(sb, indent); 127 128 if (deferredLeftAngle) { 129 sb.append('<'); 130 } 131 } 132 133 sb.append(c); 134 135 if (currentChunkWithCurrentPart != null) { 136 currentChunkWithCurrentPart.append(sb); 137 } 138 if (newPartCallback != null) { 139 if (currentPart == null) { 140 currentPart = new StringBuilder(1024); 141 } 142 currentPart.append(sb); 143 } 144 if (prettyWriter != null) { 145 currentChunk.append(sb); 146 } 147 } 148 149 @Override 150 void onCompleteElement() { 151 if (newPartCallback == null) { 152 return; 153 } 154 if (currentPart.charAt(0) == '\n') { 155 currentPart.deleteCharAt(0); 156 } 157 newPartCallback.onPrettyPrintedXmlPart(currentPart); 158 currentPart = null; 159 } 160 161 private int getElementIndent(int depth) { 162 return indent * depth; 163 } 164 165 private int getAttributeIndent(int depth) { 166 return getElementIndent(depth) + attributeIndent; 167 } 168 169 private void appendIndent(StringBuilder sb, int indent) { 170 int spaces = indent; 171 if (tabWidth > 0) { 172 spaces = indent % tabWidth; 173 int tabs = indent / tabWidth; 174 for (int i = 0; i < tabs; i++) { 175 sb.append('\t'); 176 } 177 } 178 for (int i = 0; i < spaces; i++) { 179 sb.append(' '); 180 } 181 } 182 183 public interface PrettyPrintedXmlChunkWithCurrentPartCallback { 184 185 /** 186 * Invoked after the XML pretty printer handled a chunk. The pretty printed chunk will contain the current part 187 * and the newly handled chunk enclosing in '[' and ']'. 188 * 189 * @param chunk the state of the current part with the newly handled chunk marked. 190 */ 191 void onPrettyPrintedXmlChunk(StringBuilder chunk); 192 } 193 194 public interface PrettyPrintedXmlPartCallback { 195 196 /** 197 * Invoked after a part was completed. 198 * 199 * @param part the pretty printed part. 200 */ 201 void onPrettyPrintedXmlPart(StringBuilder part); 202 } 203 204 /** 205 * A functional interface which acts as sink for character sequences. 206 */ 207 public interface PrettyPrintedXmlChunkSink { 208 209 /** 210 * Sink of the pretty printed XML chunk. 211 * 212 * @param stringBuilder a StringBuilder containing the pretty printed XML of the current chunk. 213 */ 214 void sink(StringBuilder stringBuilder); 215 } 216 217 /** 218 * Create a new builder. 219 * 220 * @return a new builder. 221 */ 222 public static Builder builder() { 223 return new Builder(); 224 } 225 226 public static final class Builder { 227 private int indent = 2; 228 private int attributeIndent; 229 private int tabWidth; 230 231 private PrettyPrintedXmlChunkWithCurrentPartCallback newChunkCallback; 232 private PrettyPrintedXmlPartCallback newPartCallback; 233 private PrettyPrintedXmlChunkSink prettyWriter; 234 235 private Builder() { 236 } 237 238 /** 239 * Set the indent for elements in whitespace characters. 240 * 241 * @param indent the indent for elements in whitespace characters. 242 * @return a reference to this builders. 243 */ 244 public Builder setIndent(int indent) { 245 ensureNotNegative(indent); 246 247 this.indent = indent; 248 return this; 249 } 250 251 /** 252 * Set the attribute indent in whitespace characters. Use a value smaller one to disable attribute indentation. 253 * 254 * @param attributeIndent the attribute indent in whitespace characters. 255 * @return a reference to this builder. 256 */ 257 public Builder setAttributeIndent(int attributeIndent) { 258 ensureNotNegative(attributeIndent); 259 260 this.attributeIndent = attributeIndent; 261 return this; 262 } 263 264 /** 265 * Set the tab width in whitespace characters. Use a value smaller one to disable pretty printing with tabs. 266 * 267 * @param tabWidth the tab width in whitespace characters. 268 * @return a reference to this builder. 269 */ 270 public Builder setTabWidth(int tabWidth) { 271 ensureNotNegative(tabWidth); 272 273 this.tabWidth = tabWidth; 274 return this; 275 } 276 277 /** 278 * Set a chunk callback. 279 * 280 * @param chunkCallback the chunk callback. 281 * @return a reference to this builder. 282 */ 283 public Builder setChunkCallback(PrettyPrintedXmlChunkWithCurrentPartCallback chunkCallback) { 284 this.newChunkCallback = chunkCallback; 285 return this; 286 } 287 288 /** 289 * Set a part callback. 290 * 291 * @param partCallback the part callback. 292 * @return a reference to this builder. 293 */ 294 public Builder setPartCallback(PrettyPrintedXmlPartCallback partCallback) { 295 this.newPartCallback = partCallback; 296 return this; 297 } 298 299 /** 300 * Set a {@link PrettyPrintedXmlChunkSink} for the pretty printed XML stream. 301 * 302 * @param prettyWriter the writer to pretty print to. 303 * @return a reference to this builder. 304 */ 305 public Builder setPrettyWriter(PrettyPrintedXmlChunkSink prettyWriter) { 306 this.prettyWriter = prettyWriter; 307 return this; 308 } 309 310 /** 311 * Build an XML pretty printer. 312 * 313 * @return the newly build XML pretty printer. 314 */ 315 public XmlPrettyPrinter build() { 316 return new XmlPrettyPrinter(this); 317 } 318 319 private static void ensureNotNegative(int i) { 320 if (i < 0) { 321 throw new IllegalArgumentException(); 322 } 323 } 324 } 325}