001 // Copyright 2005 The Apache Software Foundation 002 // 003 // Licensed under the Apache License, Version 2.0 (the "License"); 004 // you may not use this file except in compliance with the License. 005 // You may obtain a copy of the License at 006 // 007 // http://www.apache.org/licenses/LICENSE-2.0 008 // 009 // Unless required by applicable law or agreed to in writing, software 010 // distributed under the License is distributed on an "AS IS" BASIS, 011 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 012 // See the License for the specific language governing permissions and 013 // limitations under the License. 014 015 package org.apache.tapestry.markup; 016 017 import org.apache.hivemind.ApplicationRuntimeException; 018 import org.apache.hivemind.util.Defense; 019 import org.apache.tapestry.IMarkupWriter; 020 import org.apache.tapestry.NestedMarkupWriter; 021 022 import java.io.PrintWriter; 023 import java.util.*; 024 025 /** 026 * Completely revised (for 4.0) implementation of {@link org.apache.tapestry.IMarkupWriter}. No 027 * longer does internal buffering (since the servlet/portlet APIs support that natively) and wraps 028 * around a {@link java.io.PrintWriter} (rather than an {@link java.io.OutputStream}). 029 * 030 * @author Howard M. Lewis Ship 031 * @since 4.0 032 */ 033 public class MarkupWriterImpl implements IMarkupWriter 034 { 035 /** 036 * The underlying {@link PrintWriter}that output is sent to. 037 */ 038 039 private PrintWriter _writer; 040 041 /** 042 * Filter used to "escape" characters that need any kind of special encoding for the output 043 * content type. 044 */ 045 046 private MarkupFilter _filter; 047 048 /** 049 * Indicates whether a tag is open or not. A tag is opened by {@link #begin(String)}or 050 * {@link #beginEmpty(String)}. It stays open while calls to the <code>attribute()</code> 051 * methods are made. It is closed (the '>' is written) when any other method is invoked. 052 */ 053 054 private boolean _openTag = false; 055 056 /** 057 * Indicates that the tag was opened with {@link #beginEmpty(String)}, which affects how the 058 * tag is closed (a slash is added to indicate the lack of a body). This is compatible with 059 * HTML, but reflects an XML/XHTML leaning. 060 */ 061 062 private boolean _emptyTag = false; 063 064 private String _contentType; 065 066 /** 067 * A Stack of Strings used to track the active tag elements. Elements are active until the 068 * corresponding close tag is written. The {@link #push(String)}method adds elements to the 069 * stack, {@link #pop()}removes them. 070 */ 071 072 private List _activeElementStack; 073 074 /** 075 * Attributes are stored in a map until an open tag is closed. The linked hashmap ensures that 076 * ordering remains constant. 077 */ 078 079 private final Map _attrMap = new LinkedHashMap(); 080 081 public MarkupWriterImpl(String contentType, PrintWriter writer, MarkupFilter filter) 082 { 083 Defense.notNull(contentType, "contentType"); 084 Defense.notNull(writer, "writer"); 085 Defense.notNull(filter, "filter"); 086 087 _contentType = contentType; 088 _writer = writer; 089 _filter = filter; 090 } 091 092 public void attribute(String name, int value) 093 { 094 checkTagOpen(); 095 096 _attrMap.put(name, new DefaultAttribute(String.valueOf(value), false)); 097 } 098 099 public void attribute(String name, boolean value) 100 { 101 checkTagOpen(); 102 103 _attrMap.put(name, new DefaultAttribute(String.valueOf(value), false)); 104 } 105 106 public void attribute(String name, String value) 107 { 108 attribute(name, value, false); 109 } 110 111 public void attribute(String name, String value, boolean raw) 112 { 113 checkTagOpen(); 114 115 _attrMap.put(name, new DefaultAttribute(value, raw)); 116 } 117 118 public void appendAttribute(String name, boolean value) 119 { 120 checkTagOpen(); 121 122 appendAttribute(name, String.valueOf(value)); 123 } 124 125 public void appendAttribute(String name, int value) 126 { 127 checkTagOpen(); 128 129 appendAttribute(name, String.valueOf(value)); 130 } 131 132 public void appendAttribute(String name, String value) 133 { 134 checkTagOpen(); 135 136 DefaultAttribute attr = (DefaultAttribute)_attrMap.get(name); 137 138 if (attr == null) 139 { 140 attr = new DefaultAttribute(value, false); 141 142 _attrMap.put(name, attr); 143 return; 144 } 145 146 attr.append(value); 147 } 148 149 public void appendAttributeRaw(String name, String value) 150 { 151 checkTagOpen(); 152 153 DefaultAttribute attr = (DefaultAttribute)_attrMap.get(name); 154 155 if (attr == null) { 156 attr = new DefaultAttribute(value, true); 157 158 _attrMap.put(name, attr); 159 return; 160 } 161 162 attr.setRaw(true); 163 attr.append(value); 164 } 165 166 public Attribute getAttribute(String name) 167 { 168 checkTagOpen(); 169 170 return (Attribute)_attrMap.get(name); 171 } 172 173 public boolean hasAttribute(String name) 174 { 175 checkTagOpen(); 176 177 return _attrMap.containsKey(name); 178 } 179 180 public void clearAttributes() 181 { 182 checkTagOpen(); 183 184 _attrMap.clear(); 185 } 186 187 public Attribute removeAttribute(String name) 188 { 189 checkTagOpen(); 190 191 return (Attribute)_attrMap.remove(name); 192 } 193 194 /** 195 * Prints the value, if non-null. May pass it through the filter, unless raw is true. 196 */ 197 198 private void maybePrintFiltered(char[] data, int offset, int length, boolean raw, boolean isAttribute) 199 { 200 if (data == null || length <= 0) 201 return; 202 203 if (raw) 204 { 205 _writer.write(data, offset, length); 206 return; 207 } 208 209 _filter.print(_writer, data, offset, length, isAttribute); 210 } 211 212 public void attributeRaw(String name, String value) 213 { 214 attribute(name, value, true); 215 } 216 217 public void begin(String name) 218 { 219 if (_openTag) 220 closeTag(); 221 222 push(name); 223 224 _writer.print('<'); 225 _writer.print(name); 226 227 _openTag = true; 228 _emptyTag = false; 229 } 230 231 public void beginEmpty(String name) 232 { 233 if (_openTag) 234 closeTag(); 235 236 _writer.print('<'); 237 _writer.print(name); 238 239 _openTag = true; 240 _emptyTag = true; 241 } 242 243 public boolean checkError() 244 { 245 return _writer.checkError(); 246 } 247 248 public void close() 249 { 250 if (_openTag) 251 closeTag(); 252 253 // Close any active elements. 254 255 while (!stackEmpty()) 256 { 257 _writer.print("</"); 258 _writer.print(pop()); 259 _writer.print('>'); 260 } 261 262 _writer.close(); 263 264 _writer = null; 265 _filter = null; 266 _activeElementStack = null; 267 } 268 269 public void closeTag() 270 { 271 flushAttributes(); 272 273 if (_emptyTag) 274 _writer.print(" /"); 275 276 _writer.print('>'); 277 278 _openTag = false; 279 _emptyTag = false; 280 } 281 282 /** 283 * Causes any pending attributes on the current open tag 284 * to be written out to the writer. 285 */ 286 void flushAttributes() 287 { 288 if (_attrMap.size() > 0) { 289 290 Iterator it = _attrMap.keySet().iterator(); 291 while (it.hasNext()) { 292 293 String key = (String)it.next(); 294 DefaultAttribute attr = (DefaultAttribute)_attrMap.get(key); 295 296 attr.print(key, _writer, _filter); 297 } 298 299 _attrMap.clear(); 300 } 301 302 } 303 304 public void comment(String value) 305 { 306 if (_openTag) 307 closeTag(); 308 309 _writer.print("<!-- "); 310 _writer.print(value); 311 _writer.println(" -->"); 312 } 313 314 public void end() 315 { 316 if (_openTag) 317 closeTag(); 318 319 if (stackEmpty()) 320 throw new ApplicationRuntimeException(MarkupMessages.endWithEmptyStack()); 321 322 _writer.print("</"); 323 _writer.print(pop()); 324 _writer.print('>'); 325 } 326 327 public void end(String name) 328 { 329 if (_openTag) 330 closeTag(); 331 332 if (_activeElementStack == null || !_activeElementStack.contains(name)) 333 throw new ApplicationRuntimeException(MarkupMessages.elementNotOnStack( 334 name, 335 _activeElementStack)); 336 337 while (true) 338 { 339 String tagName = pop(); 340 341 _writer.print("</"); 342 _writer.print(tagName); 343 _writer.print('>'); 344 345 if (tagName.equals(name)) 346 break; 347 } 348 } 349 350 public void flush() 351 { 352 _writer.flush(); 353 } 354 355 public NestedMarkupWriter getNestedWriter() 356 { 357 return new NestedMarkupWriterImpl(this, _filter); 358 } 359 360 public void print(char[] data, int offset, int length) 361 { 362 print(data, offset, length, false); 363 } 364 365 public void printRaw(char[] buffer, int offset, int length) 366 { 367 print(buffer, offset, length, true); 368 } 369 370 public void print(char[] buffer, int offset, int length, boolean raw) 371 { 372 if (_openTag) 373 closeTag(); 374 375 maybePrintFiltered(buffer, offset, length, raw, false); 376 } 377 378 public void print(String value) 379 { 380 print(value, false); 381 } 382 383 public void printRaw(String value) 384 { 385 print(value, true); 386 } 387 388 public void print(String value, boolean raw) 389 { 390 if (value == null || value.length() == 0) 391 { 392 print(null, 0, 0, raw); 393 return; 394 } 395 396 char[] buffer = value.toCharArray(); 397 398 print(buffer, 0, buffer.length, raw); 399 } 400 401 public void print(char value) 402 { 403 char[] data = new char[] 404 { value }; 405 406 print(data, 0, 1); 407 } 408 409 public void print(int value) 410 { 411 if (_openTag) 412 closeTag(); 413 414 _writer.print(value); 415 } 416 417 public void println() 418 { 419 if (_openTag) 420 closeTag(); 421 422 _writer.println(); 423 } 424 425 public String getContentType() 426 { 427 return _contentType; 428 } 429 430 private void checkTagOpen() 431 { 432 if (!_openTag) 433 throw new IllegalStateException(MarkupMessages.tagNotOpen()); 434 } 435 436 private void push(String name) 437 { 438 if (_activeElementStack == null) 439 _activeElementStack = new ArrayList(); 440 441 _activeElementStack.add(name); 442 } 443 444 private String pop() 445 { 446 int lastIndex = _activeElementStack.size() - 1; 447 448 return (String) _activeElementStack.remove(lastIndex); 449 } 450 451 private boolean stackEmpty() 452 { 453 return _activeElementStack == null || _activeElementStack.isEmpty(); 454 } 455 }