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