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
015package org.apache.tapestry.markup;
016
017import java.io.PrintWriter;
018import java.util.ArrayList;
019import java.util.List;
020
021import org.apache.hivemind.ApplicationRuntimeException;
022import org.apache.hivemind.util.Defense;
023import org.apache.tapestry.IMarkupWriter;
024import org.apache.tapestry.NestedMarkupWriter;
025
026/**
027 * Completely revised (for 4.0) implementation of {@link org.apache.tapestry.IMarkupWriter}. No
028 * longer does internal buffering (since the servlet/portlet APIs support that natively) and wraps
029 * around a {@link java.io.PrintWriter} (rather than an {@link java.io.OutputStream}).
030 * 
031 * @author Howard M. Lewis Ship
032 * @since 4.0
033 */
034public class MarkupWriterImpl implements IMarkupWriter
035{
036    /**
037     * The underlying {@link PrintWriter}that output is sent to.
038     */
039
040    private PrintWriter _writer;
041
042    /**
043     * Filter used to "escape" characters that need any kind of special encoding for the output
044     * content type.
045     */
046
047    private MarkupFilter _filter;
048
049    /**
050     * Indicates whether a tag is open or not. A tag is opened by {@link #begin(String)}or
051     * {@link #beginEmpty(String)}. It stays open while calls to the <code>attribute()</code>
052     * methods are made. It is closed (the '&gt;' is written) when any other method is invoked.
053     */
054
055    private boolean _openTag = false;
056
057    /**
058     * Indicates that the tag was opened with {@link #beginEmpty(String)}, which affects how the
059     * tag is closed (a slash is added to indicate the lack of a body). This is compatible with
060     * HTML, but reflects an XML/XHTML leaning.
061     */
062
063    private boolean _emptyTag = false;
064
065    private String _contentType;
066
067    /**
068     * A Stack of Strings used to track the active tag elements. Elements are active until the
069     * corresponding close tag is written. The {@link #push(String)}method adds elements to the
070     * stack, {@link #pop()}removes them.
071     */
072
073    private List _activeElementStack;
074
075    public MarkupWriterImpl(String contentType, PrintWriter writer, MarkupFilter filter)
076    {
077        Defense.notNull(contentType, "contentType");
078        Defense.notNull(writer, "writer");
079        Defense.notNull(filter, "filter");
080
081        _contentType = contentType;
082        _writer = writer;
083        _filter = filter;
084    }
085
086    public void attribute(String name, int value)
087    {
088        checkTagOpen();
089
090        _writer.print(' ');
091        _writer.print(name);
092        _writer.print("=\"");
093        _writer.print(value);
094        _writer.print('"');
095    }
096
097    public void attribute(String name, boolean value)
098    {
099        checkTagOpen();
100
101        _writer.print(' ');
102        _writer.print(name);
103        _writer.print("=\"");
104        _writer.print(value);
105        _writer.print('"');
106    }
107
108    public void attribute(String name, String value)
109    {
110        attribute(name, value, false);
111    }
112
113    public void attribute(String name, String value, boolean raw)
114    {
115        checkTagOpen();
116
117        _writer.print(' ');
118
119        // Could use a check here that name contains only valid characters
120
121        _writer.print(name);
122        _writer.print("=\"");
123
124        if (value != null)
125        {
126            char[] data = value.toCharArray();
127            maybePrintFiltered(data, 0, data.length, raw, true);
128        }
129
130        _writer.print('"');
131    }
132
133    /**
134     * Prints the value, if non-null. May pass it through the filter, unless raw is true.
135     */
136
137    private void maybePrintFiltered(char[] data, int offset, int length, boolean raw,
138            boolean isAttribute)
139    {
140        if (data == null || length <= 0)
141            return;
142
143        if (raw)
144        {
145            _writer.write(data, offset, length);
146            return;
147        }
148
149        _filter.print(_writer, data, offset, length, isAttribute);
150    }
151
152    public void attributeRaw(String name, String value)
153    {
154        attribute(name, value, true);
155    }
156
157    public void begin(String name)
158    {
159        if (_openTag)
160            closeTag();
161
162        push(name);
163
164        _writer.print('<');
165        _writer.print(name);
166
167        _openTag = true;
168        _emptyTag = false;
169    }
170
171    public void beginEmpty(String name)
172    {
173        if (_openTag)
174            closeTag();
175
176        _writer.print('<');
177        _writer.print(name);
178
179        _openTag = true;
180        _emptyTag = true;
181    }
182
183    public boolean checkError()
184    {
185        return _writer.checkError();
186    }
187
188    public void close()
189    {
190        if (_openTag)
191            closeTag();
192
193        // Close any active elements.
194
195        while (!stackEmpty())
196        {
197            _writer.print("</");
198            _writer.print(pop());
199            _writer.print('>');
200        }
201
202        _writer.close();
203
204        _writer = null;
205        _filter = null;
206        _activeElementStack = null;
207    }
208
209    public void closeTag()
210    {
211        if (_emptyTag)
212            _writer.print('/');
213
214        _writer.print('>');
215
216        _openTag = false;
217        _emptyTag = false;
218    }
219
220    public void comment(String value)
221    {
222        if (_openTag)
223            closeTag();
224
225        _writer.print("<!-- ");
226        _writer.print(value);
227        _writer.println(" -->");
228    }
229
230    public void end()
231    {
232        if (_openTag)
233            closeTag();
234
235        if (stackEmpty())
236            throw new ApplicationRuntimeException(MarkupMessages.endWithEmptyStack());
237
238        _writer.print("</");
239        _writer.print(pop());
240        _writer.print('>');
241    }
242
243    public void end(String name)
244    {
245        if (_openTag)
246            closeTag();
247
248        if (_activeElementStack == null || !_activeElementStack.contains(name))
249            throw new ApplicationRuntimeException(MarkupMessages.elementNotOnStack(
250                    name,
251                    _activeElementStack));
252
253        while (true)
254        {
255            String tagName = pop();
256
257            _writer.print("</");
258            _writer.print(tagName);
259            _writer.print('>');
260
261            if (tagName.equals(name))
262                break;
263        }
264    }
265
266    public void flush()
267    {
268        _writer.flush();
269    }
270
271    public NestedMarkupWriter getNestedWriter()
272    {
273        return new NestedMarkupWriterImpl(this, _filter);
274    }
275
276    public void print(char[] data, int offset, int length)
277    {
278        print(data, offset, length, false);
279    }
280
281    public void printRaw(char[] buffer, int offset, int length)
282    {
283        print(buffer, offset, length, true);
284    }
285
286    public void print(char[] buffer, int offset, int length, boolean raw)
287    {
288        if (_openTag)
289            closeTag();
290
291        maybePrintFiltered(buffer, offset, length, raw, false);
292    }
293
294    public void print(String value)
295    {
296        print(value, false);
297    }
298
299    public void printRaw(String value)
300    {
301        print(value, true);
302    }
303
304    public void print(String value, boolean raw)
305    {
306        if (value == null || value.length() == 0)
307        {
308            print(null, 0, 0, raw);
309            return;
310        }
311
312        char[] buffer = value.toCharArray();
313
314        print(buffer, 0, buffer.length, raw);
315    }
316
317    public void print(char value)
318    {
319        char[] data = new char[]
320        { value };
321
322        print(data, 0, 1);
323    }
324
325    public void print(int value)
326    {
327        if (_openTag)
328            closeTag();
329
330        _writer.print(value);
331    }
332
333    public void println()
334    {
335        if (_openTag)
336            closeTag();
337
338        _writer.println();
339    }
340
341    public String getContentType()
342    {
343        return _contentType;
344    }
345
346    private void checkTagOpen()
347    {
348        if (!_openTag)
349            throw new IllegalStateException(MarkupMessages.tagNotOpen());
350    }
351
352    private void push(String name)
353    {
354        if (_activeElementStack == null)
355            _activeElementStack = new ArrayList();
356
357        _activeElementStack.add(name);
358    }
359
360    private String pop()
361    {
362        int lastIndex = _activeElementStack.size() - 1;
363
364        return (String) _activeElementStack.remove(lastIndex);
365    }
366
367    private boolean stackEmpty()
368    {
369        return _activeElementStack == null || _activeElementStack.isEmpty();
370    }
371}