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
015package org.apache.tapestry5.internal.services;
016
017import org.apache.tapestry5.MarkupWriter;
018import org.apache.tapestry5.MarkupWriterListener;
019import org.apache.tapestry5.dom.*;
020
021import java.io.PrintWriter;
022import java.util.Collection;
023import java.util.List;
024import java.util.concurrent.CopyOnWriteArrayList;
025
026public 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