    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