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 attr = new DefaultAttribute(value, false); 140 _attrMap.put(name, attr); 141 return; 142 } 143 144 attr.append(value); 145 } 146 147 public void appendAttributeRaw(String name, String value) 148 { 149 checkTagOpen(); 150 151 DefaultAttribute attr = (DefaultAttribute)_attrMap.get(name); 152 153 if (attr == null) { 154 attr = new DefaultAttribute(value, true); 155 156 _attrMap.put(name, attr); 157 return; 158 } 159 160 attr.setRaw(true); 161 attr.append(value); 162 } 163 164 public Attribute getAttribute(String name) 165 { 166 checkTagOpen(); 167 168 return (Attribute)_attrMap.get(name); 169 } 170 171 public boolean hasAttribute(String name) 172 { 173 checkTagOpen(); 174 175 return _attrMap.containsKey(name); 176 } 177 178 public void clearAttributes() 179 { 180 checkTagOpen(); 181 182 _attrMap.clear(); 183 } 184 185 public Attribute removeAttribute(String name) 186 { 187 checkTagOpen(); 188 189 return (Attribute)_attrMap.remove(name); 190 } 191 192 /** 193 * Prints the value, if non-null. May pass it through the filter, unless raw is true. 194 */ 195 196 private void maybePrintFiltered(char[] data, int offset, int length, boolean raw, boolean isAttribute) 197 { 198 if (data == null || length <= 0) 199 return; 200 201 if (raw) 202 { 203 _writer.write(data, offset, length); 204 return; 205 } 206 207 _filter.print(_writer, data, offset, length, isAttribute); 208 } 209 210 public void attributeRaw(String name, String value) 211 { 212 attribute(name, value, true); 213 } 214 215 public void begin(String name) 216 { 217 if (_openTag) 218 closeTag(); 219 220 push(name); 221 222 _writer.print('<'); 223 _writer.print(name); 224 225 _openTag = true; 226 _emptyTag = false; 227 } 228 229 public void beginEmpty(String name) 230 { 231 if (_openTag) 232 closeTag(); 233 234 _writer.print('<'); 235 _writer.print(name); 236 237 _openTag = true; 238 _emptyTag = true; 239 } 240 241 public boolean checkError() 242 { 243 return _writer.checkError(); 244 } 245 246 public void close() 247 { 248 if (_openTag) 249 closeTag(); 250 251 // Close any active elements. 252 253 while (!stackEmpty()) 254 { 255 _writer.print("</"); 256 _writer.print(pop()); 257 _writer.print('>'); 258 } 259 260 _writer.close(); 261 262 _writer = null; 263 _filter = null; 264 _activeElementStack = null; 265 } 266 267 public void closeTag() 268 { 269 flushAttributes(); 270 271 if (_emptyTag) 272 _writer.print(" /"); 273 274 _writer.print('>'); 275 276 _openTag = false; 277 _emptyTag = false; 278 } 279 280 /** 281 * Causes any pending attributes on the current open tag 282 * to be written out to the writer. 283 */ 284 void flushAttributes() 285 { 286 if (_attrMap.size() > 0) { 287 288 Iterator it = _attrMap.keySet().iterator(); 289 while (it.hasNext()) { 290 291 String key = (String)it.next(); 292 DefaultAttribute attr = (DefaultAttribute)_attrMap.get(key); 293 294 attr.print(key, _writer, _filter); 295 } 296 297 _attrMap.clear(); 298 } 299 300 } 301 302 public void comment(String value) 303 { 304 if (_openTag) 305 closeTag(); 306 307 _writer.print("<!-- "); 308 _writer.print(value); 309 _writer.println(" -->"); 310 } 311 312 public void end() 313 { 314 if (_openTag) 315 closeTag(); 316 317 if (stackEmpty()) 318 throw new ApplicationRuntimeException(MarkupMessages.endWithEmptyStack()); 319 320 _writer.print("</"); 321 _writer.print(pop()); 322 _writer.print('>'); 323 } 324 325 public void end(String name) 326 { 327 if (_openTag) 328 closeTag(); 329 330 if (_activeElementStack == null || !_activeElementStack.contains(name)) 331 throw new ApplicationRuntimeException(MarkupMessages.elementNotOnStack( 332 name, 333 _activeElementStack)); 334 335 while (true) 336 { 337 String tagName = pop(); 338 339 _writer.print("</"); 340 _writer.print(tagName); 341 _writer.print('>'); 342 343 if (tagName.equals(name)) 344 break; 345 } 346 } 347 348 public void flush() 349 { 350 _writer.flush(); 351 } 352 353 public NestedMarkupWriter getNestedWriter() 354 { 355 return new NestedMarkupWriterImpl(this, _filter); 356 } 357 358 public void print(char[] data, int offset, int length) 359 { 360 print(data, offset, length, false); 361 } 362 363 public void printRaw(char[] buffer, int offset, int length) 364 { 365 print(buffer, offset, length, true); 366 } 367 368 public void print(char[] buffer, int offset, int length, boolean raw) 369 { 370 if (_openTag) 371 closeTag(); 372 373 maybePrintFiltered(buffer, offset, length, raw, false); 374 } 375 376 public void print(String value) 377 { 378 print(value, false); 379 } 380 381 public void printRaw(String value) 382 { 383 print(value, true); 384 } 385 386 public void print(String value, boolean raw) 387 { 388 if (value == null || value.length() == 0) 389 { 390 print(null, 0, 0, raw); 391 return; 392 } 393 394 char[] buffer = value.toCharArray(); 395 396 print(buffer, 0, buffer.length, raw); 397 } 398 399 public void print(char value) 400 { 401 char[] data = new char[] 402 { value }; 403 404 print(data, 0, 1); 405 } 406 407 public void print(int value) 408 { 409 if (_openTag) 410 closeTag(); 411 412 _writer.print(value); 413 } 414 415 public void println() 416 { 417 if (_openTag) 418 closeTag(); 419 420 _writer.println(); 421 } 422 423 public String getContentType() 424 { 425 return _contentType; 426 } 427 428 private void checkTagOpen() 429 { 430 if (!_openTag) 431 throw new IllegalStateException(MarkupMessages.tagNotOpen()); 432 } 433 434 private void push(String name) 435 { 436 if (_activeElementStack == null) 437 _activeElementStack = new ArrayList(); 438 439 _activeElementStack.add(name); 440 } 441 442 private String pop() 443 { 444 int lastIndex = _activeElementStack.size() - 1; 445 446 return (String) _activeElementStack.remove(lastIndex); 447 } 448 449 private boolean stackEmpty() 450 { 451 return _activeElementStack == null || _activeElementStack.isEmpty(); 452 } 453 }