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 '&gt;' 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    }