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