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 }