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                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    }