001    /*
002     $Id: MarkupBuilder.java,v 1.9 2005/02/03 03:25:33 sstirling Exp $
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: 1.9 $
063     */
064    public class MarkupBuilder extends BuilderSupport {
065    
066        private IndentPrinter out;
067        private boolean nospace;
068        private int state;
069        private boolean nodeIsEmpty = true;
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        protected void setParent(Object parent, Object child) {
088        }
089    
090        /*
091        public Object getProperty(String property) {
092            if (property.equals("_")) {
093                nospace = true;
094                return null;
095            } else {
096                Object node = createNode(property);
097                nodeCompleted(getCurrent(), node);
098                return node;
099            }
100        }
101        */
102    
103        protected Object createNode(Object name) {
104            toState(1, name);
105            return name;
106        }
107    
108        protected Object createNode(Object name, Object value) {
109            toState(2, name);
110            out.print(">");
111            out.print(transformValue(value.toString()));
112            return name;
113        }
114    
115        protected Object createNode(Object name, Map attributes, Object value) {
116            toState(1, name);
117            for (Iterator iter = attributes.entrySet().iterator(); iter.hasNext();) {
118                Map.Entry entry = (Map.Entry) iter.next();
119                out.print(" ");
120                print(transformName(entry.getKey().toString()));
121                out.print("='");
122                print(transformValue(entry.getValue().toString()));
123                out.print("'");
124            }
125            if (value != null)
126            {
127                nodeIsEmpty = false;
128                out.print(">" + transformValue(value.toString()) + "</" + name + ">");
129            }
130            return name;
131        }
132    
133        protected Object createNode(Object name, Map attributes) {
134            return createNode(name, attributes, null);
135        }
136        
137        protected void nodeCompleted(Object parent, Object node) {
138            toState(3, node);
139            out.flush();
140        }
141    
142        protected void print(Object node) {
143            out.print(node == null ? "null" : node.toString());
144        }
145    
146        protected Object getName(String methodName) {
147                    return super.getName(transformName(methodName));
148            }
149    
150        protected String transformName(String name) {
151            if (name.startsWith("_")) name = name.substring(1);
152            return name.replace('_', '-');
153        }
154    
155        /**
156         * Returns a String with special XML characters escaped as entities so that
157         * output XML is valid. Escapes the following characters as corresponding 
158         * entities:
159         * <ul>
160         *   <li>\' as &quot;</li>
161         *   <li>& as &amp;</li>
162         *   <li>< as &lt;</li>
163         *   <li>> as &gt;</li>
164         * </ul>
165         * 
166         * @param value to be searched and replaced for XML special characters.
167         * @return value with XML characters escaped
168         */
169        protected String transformValue(String value) {
170            // & has to be checked and replaced before others
171            if (value.matches(".*&.*")) {
172                value = value.replaceAll("&", "&");
173            }
174            if (value.matches(".*\\'.*")) {
175                value = value.replaceAll("\\'", """);
176            }
177            if (value.matches(".*<.*")) {
178                value = value.replaceAll("<", "<");
179            }
180            if (value.matches(".*>.*")) {
181                value = value.replaceAll(">", ">");
182            }
183            return value;
184        }
185    
186        private void toState(int next, Object name) {
187            switch (state) {
188                case 0:
189                    switch (next) {
190                        case 1:
191                        case 2:
192                            out.print("<");
193                            print(name);
194                            break;
195                        case 3:
196                            throw new Error();
197                    }
198                    break;
199                case 1:
200                    switch (next) {
201                        case 1:
202                        case 2:
203                            out.print(">");
204                            if (nospace) {
205                                nospace = false;
206                            } else {
207                                out.println();
208                                out.incrementIndent();
209                                out.printIndent();
210                            }
211                            out.print("<");
212                            print(name);
213                            break;
214                        case 3:
215                            if (nodeIsEmpty) {
216                                out.print(" />");
217                            }
218                            break;
219                    }
220                    break;
221                case 2:
222                    switch (next) {
223                        case 1:
224                        case 2:
225                            throw new Error();
226                        case 3:
227                            out.print("</");
228                            print(name);
229                            out.print(">");
230                            break;
231                    }
232                    break;
233                case 3:
234                    switch (next) {
235                        case 1:
236                        case 2:
237                            if (nospace) {
238                                nospace = false;
239                            } else {
240                                out.println();
241                                out.printIndent();
242                            }
243                            out.print("<");
244                            print(name);
245                            break;
246                        case 3:
247                            if (nospace) {
248                                nospace = false;
249                            } else {
250                                out.println();
251                                out.decrementIndent();
252                                out.printIndent();
253                            }
254                            out.print("</");
255                            print(name);
256                            out.print(">");
257                            break;
258                    }
259                    break;
260            }
261            state = next;
262        }
263    
264    }