001    // Copyright 2006, 2007, 2008, 2009, 2010, 2011 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.tapestry5.internal.services;
016    
017    import org.apache.tapestry5.MarkupWriter;
018    import org.apache.tapestry5.MarkupWriterListener;
019    import org.apache.tapestry5.dom.*;
020    
021    import java.io.PrintWriter;
022    import java.util.Collection;
023    import java.util.List;
024    import java.util.concurrent.CopyOnWriteArrayList;
025    
026    public class MarkupWriterImpl implements MarkupWriter
027    {
028        private final Document document;
029    
030        private Element current;
031    
032        private Text currentText;
033    
034        private List<MarkupWriterListener> listeners;
035    
036        /**
037         * Creates a new instance of the MarkupWriter with a {@link org.apache.tapestry5.dom.DefaultMarkupModel}.
038         */
039        public MarkupWriterImpl()
040        {
041            this(new DefaultMarkupModel());
042        }
043    
044        public MarkupWriterImpl(MarkupModel model)
045        {
046            this(model, null);
047        }
048    
049        public MarkupWriterImpl(MarkupModel model, String encoding)
050        {
051            document = new Document(model, encoding);
052        }
053    
054        public void toMarkup(PrintWriter writer)
055        {
056            document.toMarkup(writer);
057        }
058    
059        @Override
060        public String toString()
061        {
062            return document.toString();
063        }
064    
065        public Document getDocument()
066        {
067            return document;
068        }
069    
070        public Element getElement()
071        {
072            return current;
073        }
074    
075        public void cdata(String content)
076        {
077            currentText = null;
078    
079            if (current == null)
080            {
081                document.cdata(content);
082            } else
083            {
084                current.cdata(content);
085            }
086        }
087    
088        public void write(String text)
089        {
090            if (text == null) return;
091    
092            if (currentText == null)
093            {
094                currentText =
095                        current == null
096                                ? document.text(text)
097                                : current.text(text);
098    
099                return;
100            }
101    
102            currentText.write(text);
103        }
104    
105        public void writef(String format, Object... args)
106        {
107            // A bit of a cheat:
108    
109            write("");
110            currentText.writef(format, args);
111        }
112    
113        public void attributes(Object... namesAndValues)
114        {
115            ensureCurrentElement();
116    
117            int i = 0;
118    
119            int length = namesAndValues.length;
120    
121            if (length % 2 != 0)
122                throw new IllegalArgumentException(ServicesMessages.markupWriterAttributeNameOrValueOmitted(current.getName(), namesAndValues));
123    
124            while (i < length)
125            {
126                // name should never be null.
127    
128                String name = namesAndValues[i++].toString();
129                Object value = namesAndValues[i++];
130    
131                if (value == null) continue;
132    
133                current.attribute(name, value.toString());
134            }
135        }
136    
137        private void ensureCurrentElement()
138        {
139            if (current == null)
140                throw new IllegalStateException(ServicesMessages.markupWriterNoCurrentElement());
141        }
142    
143        public Element element(String name, Object... namesAndValues)
144        {
145            if (current == null)
146            {
147                Element existingRootElement = document.getRootElement();
148    
149                if (existingRootElement != null)
150                    throw new IllegalStateException(String.format(
151                            "A document must have exactly one root element. Element <%s> is already the root element.",
152                            existingRootElement.getName()));
153    
154                current = document.newRootElement(name);
155            } else
156            {
157                current = current.element(name);
158            }
159    
160            attributes(namesAndValues);
161    
162            currentText = null;
163    
164            fireElementDidStart();
165    
166            return current;
167        }
168    
169        public void writeRaw(String text)
170        {
171            currentText = null;
172    
173            if (current == null)
174            {
175                document.raw(text);
176            } else
177            {
178                current.raw(text);
179            }
180        }
181    
182        public Element end()
183        {
184            ensureCurrentElement();
185    
186            fireElementDidEnd();
187    
188            current = current.getContainer();
189    
190            currentText = null;
191    
192            return current;
193        }
194    
195        public void comment(String text)
196        {
197            currentText = null;
198    
199            if (current == null)
200            {
201                document.comment(text);
202            } else
203            {
204                current.comment(text);
205            }
206        }
207    
208        public Element attributeNS(String namespace, String attributeName, String attributeValue)
209        {
210            ensureCurrentElement();
211    
212            current.attribute(namespace, attributeName, attributeValue);
213    
214            return current;
215        }
216    
217        public Element defineNamespace(String namespace, String namespacePrefix)
218        {
219            ensureCurrentElement();
220    
221            current.defineNamespace(namespace, namespacePrefix);
222    
223            return current;
224        }
225    
226        public Element elementNS(String namespace, String elementName)
227        {
228            if (current == null) current = document.newRootElement(namespace, elementName);
229            else current = current.elementNS(namespace, elementName);
230    
231            currentText = null;
232    
233            fireElementDidStart();
234    
235            return current;
236        }
237    
238        public void addListener(MarkupWriterListener listener)
239        {
240            assert listener != null;
241    
242            if (listeners == null)
243            {
244                // TAP5-XXX: Using a copy-on-write list means we don't have to make defensive copies
245                // while iterating the listeners (to protect against listeners that add or remove listeners).
246                listeners = new CopyOnWriteArrayList<MarkupWriterListener>();
247            }
248    
249            listeners.add(listener);
250        }
251    
252        public void removeListener(MarkupWriterListener listener)
253        {
254            if (listeners != null)
255                listeners.remove(listener);
256        }
257    
258        private void fireElementDidStart()
259        {
260            if (isEmpty(listeners)) return;
261    
262            for (MarkupWriterListener l : listeners)
263            {
264                l.elementDidStart(current);
265            }
266        }
267    
268        private static boolean isEmpty(Collection<?> collection)
269        {
270            return collection == null || collection.isEmpty();
271        }
272    
273        private void fireElementDidEnd()
274        {
275            if (isEmpty(listeners)) return;
276    
277            for (MarkupWriterListener l : listeners)
278            {
279                l.elementDidEnd(current);
280            }
281        }
282    }
283