001    /*
002     $Id: MarkupBuilder.java 4350 2006-12-11 19:21:50Z tug $
003    
004     Copyright 2003 (C) James Strachan and Bob Mcwhirter. All Rights Reserved.
005    
006     Redistribution and use of this software and associated documentation
007     ("Software"), with or without modification, are permitted provided
008     that the following conditions are met:
009    
010     1. Redistributions of source code must retain copyright
011        statements and notices.  Redistributions must also contain a
012        copy of this document.
013    
014     2. Redistributions in binary form must reproduce the
015        above copyright notice, this list of conditions and the
016        following disclaimer in the documentation and/or other
017        materials provided with the distribution.
018    
019     3. The name "groovy" must not be used to endorse or promote
020        products derived from this Software without prior written
021        permission of The Codehaus.  For written permission,
022        please contact info@codehaus.org.
023    
024     4. Products derived from this Software may not be called "groovy"
025        nor may "groovy" appear in their names without prior written
026        permission of The Codehaus. "groovy" is a registered
027        trademark of The Codehaus.
028    
029     5. Due credit should be given to The Codehaus -
030        http://groovy.codehaus.org/
031    
032     THIS SOFTWARE IS PROVIDED BY THE CODEHAUS AND CONTRIBUTORS
033     ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
034     NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
035     FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
036     THE CODEHAUS OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
037     INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
038     (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
039     SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
040     HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
041     STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
042     ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
043     OF THE POSSIBILITY OF SUCH DAMAGE.
044    
045     */
046    package groovy.xml;
047    
048    import groovy.util.BuilderSupport;
049    import groovy.util.IndentPrinter;
050    
051    import java.io.PrintWriter;
052    import java.io.Writer;
053    import java.util.Iterator;
054    import java.util.Map;
055    
056    /**
057     * A helper class for creating XML or HTML markup
058     * 
059     * @author <a href="mailto:james@coredevelopers.net">James Strachan</a>
060     * @author Stefan Matthias Aust
061     * @author <a href="mailto:scottstirling@rcn.com">Scott Stirling</a>
062     * @version $Revision: 4350 $
063     */
064    public class MarkupBuilder extends BuilderSupport {
065        private IndentPrinter out;
066        private boolean nospace;
067        private int state;
068        private boolean nodeIsEmpty = true;
069        private boolean useDoubleQuotes = false;
070    
071        public MarkupBuilder() {
072            this(new IndentPrinter());
073        }
074    
075        public MarkupBuilder(PrintWriter writer) {
076            this(new IndentPrinter(writer));
077        }
078    
079        public MarkupBuilder(Writer writer) {
080            this(new IndentPrinter(new PrintWriter(writer)));
081        }
082    
083        public MarkupBuilder(IndentPrinter out) {
084            this.out = out;
085        }
086    
087        /**
088         * Returns <code>true</code> if attribute values are output with
089         * double quotes; <code>false</code> if single quotes are used.
090         * By default, single quotes are used.
091         */
092        public boolean getDoubleQuotes() {
093            return this.useDoubleQuotes;
094        }
095    
096        /**
097         * Sets whether the builder outputs attribute values in double
098         * quotes or single quotes.
099         * @param useDoubleQuotes If this parameter is <code>true</code>,
100         * double quotes are used; otherwise, single quotes are.
101         */
102        public void setDoubleQuotes(boolean useDoubleQuotes) {
103            this.useDoubleQuotes = useDoubleQuotes;
104        }
105    
106        protected IndentPrinter getPrinter() {
107            return this.out;
108        }
109    
110        protected void setParent(Object parent, Object child) { }
111    
112        protected Object createNode(Object name) {
113            this.nodeIsEmpty = true;
114            toState(1, name);
115            return name;
116        }
117    
118        protected Object createNode(Object name, Object value) {
119            toState(2, name);
120            this.nodeIsEmpty = false;
121            out.print(">");
122            out.print(escapeElementContent(value.toString()));
123            return name;
124        }
125    
126        protected Object createNode(Object name, Map attributes, Object value) {
127            toState(1, name);
128            for (Iterator iter = attributes.entrySet().iterator(); iter.hasNext();) {
129                Map.Entry entry = (Map.Entry) iter.next();
130                out.print(" ");
131    
132                // Output the attribute name,
133                print(entry.getKey().toString());
134    
135                // Output the attribute value within quotes. Use whichever
136                // type of quotes are currently configured.
137                out.print(this.useDoubleQuotes ? "=\"" : "='");
138                print(escapeAttributeValue(entry.getValue().toString()));
139                out.print(this.useDoubleQuotes ? "\"" : "'");
140            }
141    
142            if (value != null) {
143                nodeIsEmpty = false;
144                out.print(">" + escapeElementContent(value.toString()) + "</" + name + ">");
145            }
146            else {
147                nodeIsEmpty = true;
148            }
149    
150            return name;
151        }
152    
153        protected Object createNode(Object name, Map attributes) {
154            return createNode(name, attributes, null);
155        }
156        
157        protected void nodeCompleted(Object parent, Object node) {
158            toState(3, node);
159            out.flush();
160        }
161    
162        protected void print(Object node) {
163            out.print(node == null ? "null" : node.toString());
164        }
165    
166        protected Object getName(String methodName) {
167            return super.getName(methodName);
168        }
169    
170        /**
171         * Returns a String with special XML characters escaped as entities so that
172         * output XML is valid. Escapes the following characters as corresponding 
173         * entities:
174         * <ul>
175         *   <li>\' as &amp;apos;</li>
176         *   <li>&amp; as &amp;amp;</li>
177         *   <li>&lt; as &amp;lt;</li>
178         *   <li>&gt; as &amp;gt;</li>
179         * </ul>
180         * 
181         * @param value to be searched and replaced for XML special characters.
182         * @return value with XML characters escaped
183         * @deprecated
184         * @see #escapeXmlValue(String, boolean)
185         */
186        protected String transformValue(String value) {
187            // & has to be checked and replaced before others
188            if (value.matches(".*&.*")) {
189                value = value.replaceAll("&", "&amp;");
190            }
191            if (value.matches(".*\\'.*")) {
192                value = value.replaceAll("\\'", "&apos;");
193            }
194            if (value.matches(".*<.*")) {
195                value = value.replaceAll("<", "&lt;");
196            }
197            if (value.matches(".*>.*")) {
198                value = value.replaceAll(">", "&gt;");
199            }
200            return value;
201        }
202    
203        /**
204         * Escapes a string so that it can be used directly as an XML
205         * attribute value.
206         * @param value The string to escape.
207         * @return A new string in which all characters that require escaping
208         * have been replaced with the corresponding XML entities.
209         * @see #escapeXmlValue(String, boolean)
210         */
211        private String escapeAttributeValue(String value) {
212            return escapeXmlValue(value, true);
213        }
214    
215        /**
216         * Escapes a string so that it can be used directly in XML element
217         * content.
218         * @param value The string to escape.
219         * @return A new string in which all characters that require escaping
220         * have been replaced with the corresponding XML entities.
221         * @see #escapeXmlValue(String, boolean)
222         */
223        private String escapeElementContent(String value) {
224            return escapeXmlValue(value, false);
225        }
226    
227        /**
228         * Escapes a string so that it can be used in XML text successfully.
229         * It replaces the following characters with the corresponding XML
230         * entities:
231         * <ul>
232         *   <li>&amp; as &amp;amp;</li>
233         *   <li>&lt; as &amp;lt;</li>
234         *   <li>&gt; as &amp;gt;</li>
235         * </ul>
236         * If the string is to be added as an attribute value, these
237         * characters are also escaped:
238         * <ul>
239         *   <li>' as &amp;apos;</li>
240         * </ul>
241         * @param value The string to escape.
242         * @param isAttrValue <code>true</code> if the string is to be used
243         * as an attribute value, otherwise <code>false</code>.
244         * @return A new string in which all characters that require escaping
245         * have been replaced with the corresponding XML entities.
246         */
247        private String escapeXmlValue(String value, boolean isAttrValue) {
248            StringBuffer buffer = new StringBuffer(value);
249            for (int i = 0, n = buffer.length(); i < n; i++) {
250                switch (buffer.charAt(i)) {
251                case '&':
252                    buffer.replace(i, i + 1, "&amp;");
253    
254                    // We're replacing a single character by a string of
255                    // length 5, so we need to update the index variable
256                    // and the total length.
257                    i += 4;
258                    n += 4;
259                    break;
260    
261                case '<':
262                    buffer.replace(i, i + 1, "&lt;");
263    
264                    // We're replacing a single character by a string of
265                    // length 4, so we need to update the index variable
266                    // and the total length.
267                    i += 3;
268                    n += 3;
269                    break;
270    
271                case '>':
272                    buffer.replace(i, i + 1, "&gt;");
273    
274                    // We're replacing a single character by a string of
275                    // length 4, so we need to update the index variable
276                    // and the total length.
277                    i += 3;
278                    n += 3;
279                    break;
280    
281                case '"':
282                    // The double quote is only escaped if the value is for
283                    // an attribute and the builder is configured to output
284                    // attribute values inside double quotes.
285                    if (isAttrValue && this.useDoubleQuotes) {
286                        buffer.replace(i, i + 1, "&quot;");
287    
288                        // We're replacing a single character by a string of
289                        // length 6, so we need to update the index variable
290                        // and the total length.
291                        i += 5;
292                        n += 5;
293                    }
294                    break;
295    
296                case '\'':
297                    // The apostrophe is only escaped if the value is for an
298                    // attribute, as opposed to element content, and if the
299                    // builder is configured to surround attribute values with
300                    // single quotes.
301                    if (isAttrValue && !this.useDoubleQuotes){
302                        buffer.replace(i, i + 1, "&apos;");
303    
304                        // We're replacing a single character by a string of
305                        // length 6, so we need to update the index variable
306                        // and the total length.
307                        i += 5;
308                        n += 5;
309                    }
310                    break;
311    
312                default:
313                    break;
314                }
315            }
316    
317            return buffer.toString();
318        }
319    
320        private void toState(int next, Object name) {
321            switch (state) {
322                case 0:
323                    switch (next) {
324                        case 1:
325                        case 2:
326                            out.print("<");
327                            print(name);
328                            break;
329                        case 3:
330                            throw new Error();
331                    }
332                    break;
333                case 1:
334                    switch (next) {
335                        case 1:
336                        case 2:
337                            out.print(">");
338                            if (nospace) {
339                                nospace = false;
340                            } else {
341                                out.println();
342                                out.incrementIndent();
343                                out.printIndent();
344                            }
345                            out.print("<");
346                            print(name);
347                            break;
348                        case 3:
349                            if (nodeIsEmpty) {
350                                out.print(" />");
351                            }
352                            break;
353                    }
354                    break;
355                case 2:
356                    switch (next) {
357                        case 1:
358                        case 2:
359                            throw new Error();
360                        case 3:
361                            out.print("</");
362                            print(name);
363                            out.print(">");
364                            break;
365                    }
366                    break;
367                case 3:
368                    switch (next) {
369                        case 1:
370                        case 2:
371                            if (nospace) {
372                                nospace = false;
373                            } else {
374                                out.println();
375                                out.printIndent();
376                            }
377                            out.print("<");
378                            print(name);
379                            break;
380                        case 3:
381                            if (nospace) {
382                                nospace = false;
383                            } else {
384                                out.println();
385                                out.decrementIndent();
386                                out.printIndent();
387                            }
388                            out.print("</");
389                            print(name);
390                            out.print(">");
391                            break;
392                    }
393                    break;
394            }
395            state = next;
396        }
397    }