1/*
2 * Copyright (c) 2005, 2012, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package com.sun.xml.internal.txw2;
27
28import com.sun.xml.internal.txw2.annotation.XmlAttribute;
29import com.sun.xml.internal.txw2.annotation.XmlElement;
30import com.sun.xml.internal.txw2.annotation.XmlNamespace;
31import com.sun.xml.internal.txw2.annotation.XmlValue;
32import com.sun.xml.internal.txw2.annotation.XmlCDATA;
33
34import javax.xml.namespace.QName;
35import java.lang.reflect.InvocationHandler;
36import java.lang.reflect.InvocationTargetException;
37import java.lang.reflect.Method;
38import java.lang.reflect.Proxy;
39
40/**
41 * Dynamically implements {@link TypedXmlWriter} interfaces.
42 *
43 * @author Kohsuke Kawaguchi
44 */
45final class ContainerElement implements InvocationHandler, TypedXmlWriter {
46
47    final Document document;
48
49    /**
50     * Initially, point to the start tag token, but
51     * once we know we are done with the start tag, we will reset it to null
52     * so that the token sequence can be GC-ed.
53     */
54    StartTag startTag;
55    final EndTag endTag = new EndTag();
56
57    /**
58     * Namespace URI of this element.
59     */
60    private final String nsUri;
61
62    /**
63     * When this element can accept more child content, this value
64     * is non-null and holds the last child {@link Content}.
65     *
66     * If this element is committed, this parameter is null.
67     */
68    private Content tail;
69
70    /**
71     * Uncommitted {@link ContainerElement}s form a doubly-linked list,
72     * so that the parent can close them recursively.
73     */
74    private ContainerElement prevOpen;
75    private ContainerElement nextOpen;
76    private final ContainerElement parent;
77    private ContainerElement lastOpenChild;
78
79    /**
80     * Set to true if the start eleent is blocked.
81     */
82    private boolean blocked;
83
84    public ContainerElement(Document document,ContainerElement parent,String nsUri, String localName) {
85        this.parent = parent;
86        this.document = document;
87        this.nsUri = nsUri;
88        this.startTag = new StartTag(this,nsUri,localName);
89        tail = startTag;
90
91        if(isRoot())
92            document.setFirstContent(startTag);
93    }
94
95    private boolean isRoot() {
96        return parent==null;
97    }
98
99    private boolean isCommitted() {
100        return tail==null;
101    }
102
103    public Document getDocument() {
104        return document;
105    }
106
107    boolean isBlocked() {
108        return blocked && !isCommitted();
109    }
110
111    public void block() {
112        blocked = true;
113    }
114
115    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
116        if(method.getDeclaringClass()==TypedXmlWriter.class || method.getDeclaringClass()==Object.class) {
117            // forward to myself
118            try {
119                return method.invoke(this,args);
120            } catch (InvocationTargetException e) {
121                throw e.getTargetException();
122            }
123        }
124
125        XmlAttribute xa = method.getAnnotation(XmlAttribute.class);
126        XmlValue xv = method.getAnnotation(XmlValue.class);
127        XmlElement xe = method.getAnnotation(XmlElement.class);
128
129
130        if(xa!=null) {
131            if(xv!=null || xe!=null)
132                throw new IllegalAnnotationException(method.toString());
133
134            addAttribute(xa,method,args);
135            return proxy; // allow method chaining
136        }
137        if(xv!=null) {
138            if(xe!=null)
139                throw new IllegalAnnotationException(method.toString());
140
141            _pcdata(args);
142            return proxy; // allow method chaining
143        }
144
145        return addElement(xe,method,args);
146    }
147
148    /**
149     * Writes an attribute.
150     */
151    private void addAttribute(XmlAttribute xa, Method method, Object[] args) {
152        assert xa!=null;
153
154        checkStartTag();
155
156        String localName = xa.value();
157        if(xa.value().length()==0)
158            localName = method.getName();
159
160        _attribute(xa.ns(),localName,args);
161    }
162
163    private void checkStartTag() {
164        if(startTag==null)
165            throw new IllegalStateException("start tag has already been written");
166    }
167
168    /**
169     * Writes a new element.
170     */
171    private Object addElement(XmlElement e, Method method, Object[] args) {
172        Class<?> rt = method.getReturnType();
173
174        // the last precedence: default name
175        String nsUri = "##default";
176        String localName = method.getName();
177
178        if(e!=null) {
179            // then the annotation on this method
180            if(e.value().length()!=0)
181                localName = e.value();
182            nsUri = e.ns();
183        }
184
185        if(nsUri.equals("##default")) {
186            // look for the annotation on the declaring class
187            Class<?> c = method.getDeclaringClass();
188            XmlElement ce = c.getAnnotation(XmlElement.class);
189            if(ce!=null) {
190                nsUri = ce.ns();
191            }
192
193            if(nsUri.equals("##default"))
194                // then default to the XmlNamespace
195                nsUri = getNamespace(c.getPackage());
196        }
197
198
199
200        if(rt==Void.TYPE) {
201            // leaf element with just a value
202
203            boolean isCDATA = method.getAnnotation(XmlCDATA.class)!=null;
204
205            StartTag st = new StartTag(document,nsUri,localName);
206            addChild(st);
207            for( Object arg : args ) {
208                Text text;
209                if(isCDATA)     text = new Cdata(document,st,arg);
210                else            text = new Pcdata(document,st,arg);
211                addChild(text);
212            }
213            addChild(new EndTag());
214            return null;
215        }
216        if(TypedXmlWriter.class.isAssignableFrom(rt)) {
217            // sub writer
218            return _element(nsUri,localName,(Class)rt);
219        }
220
221        throw new IllegalSignatureException("Illegal return type: "+rt);
222    }
223
224    /**
225     * Decides the namespace URI of the given package.
226     */
227    private String getNamespace(Package pkg) {
228        if(pkg==null)       return "";
229
230        String nsUri;
231        XmlNamespace ns = pkg.getAnnotation(XmlNamespace.class);
232        if(ns!=null)
233            nsUri = ns.value();
234        else
235            nsUri = "";
236        return nsUri;
237    }
238
239    /**
240     * Appends this child object to the tail.
241     */
242    private void addChild(Content child) {
243        tail.setNext(document,child);
244        tail = child;
245    }
246
247    public void commit() {
248        commit(true);
249    }
250
251    public void commit(boolean includingAllPredecessors) {
252        _commit(includingAllPredecessors);
253        document.flush();
254    }
255
256    private void _commit(boolean includingAllPredecessors) {
257        if(isCommitted())  return;
258
259        addChild(endTag);
260        if(isRoot())
261            addChild(new EndDocument());
262        tail = null;
263
264        // _commit predecessors if so told
265        if(includingAllPredecessors) {
266            for( ContainerElement e=this; e!=null; e=e.parent ) {
267                while(e.prevOpen!=null) {
268                    e.prevOpen._commit(false);
269                    // e.prevOpen should change as a result of committing it.
270                }
271            }
272        }
273
274        // _commit all children recursively
275        while(lastOpenChild!=null)
276            lastOpenChild._commit(false);
277
278        // remove this node from the link
279        if(parent!=null) {
280            if(parent.lastOpenChild==this) {
281                assert nextOpen==null : "this must be the last one";
282                parent.lastOpenChild = prevOpen;
283            } else {
284                assert nextOpen.prevOpen==this;
285                nextOpen.prevOpen = this.prevOpen;
286            }
287            if(prevOpen!=null) {
288                assert prevOpen.nextOpen==this;
289                prevOpen.nextOpen = this.nextOpen;
290            }
291        }
292
293        this.nextOpen = null;
294        this.prevOpen = null;
295    }
296
297    public void _attribute(String localName, Object value) {
298        _attribute("",localName,value);
299    }
300
301    public void _attribute(String nsUri, String localName, Object value) {
302        checkStartTag();
303        startTag.addAttribute(nsUri,localName,value);
304    }
305
306    public void _attribute(QName attributeName, Object value) {
307        _attribute(attributeName.getNamespaceURI(),attributeName.getLocalPart(),value);
308    }
309
310    public void _namespace(String uri) {
311        _namespace(uri,false);
312    }
313
314    public void _namespace(String uri, String prefix) {
315        if(prefix==null)
316            throw new IllegalArgumentException();
317        checkStartTag();
318        startTag.addNamespaceDecl(uri,prefix,false);
319    }
320
321    public void _namespace(String uri, boolean requirePrefix) {
322        checkStartTag();
323        startTag.addNamespaceDecl(uri,null,requirePrefix);
324    }
325
326    public void _pcdata(Object value) {
327        // we need to allow this method even when startTag has already been completed.
328        // checkStartTag();
329        addChild(new Pcdata(document,startTag,value));
330    }
331
332    public void _cdata(Object value) {
333        addChild(new Cdata(document,startTag,value));
334    }
335
336    public void _comment(Object value) throws UnsupportedOperationException {
337        addChild(new Comment(document,startTag,value));
338    }
339
340    public <T extends TypedXmlWriter> T _element(String localName, Class<T> contentModel) {
341        return _element(nsUri,localName,contentModel);
342    }
343
344    public <T extends TypedXmlWriter> T _element(QName tagName, Class<T> contentModel) {
345        return _element(tagName.getNamespaceURI(),tagName.getLocalPart(),contentModel);
346    }
347
348    public <T extends TypedXmlWriter> T _element(Class<T> contentModel) {
349        return _element(TXW.getTagName(contentModel),contentModel);
350    }
351
352    public <T extends TypedXmlWriter> T _cast(Class<T> facadeType) {
353        return facadeType.cast(Proxy.newProxyInstance(facadeType.getClassLoader(),new Class[]{facadeType},this));
354    }
355
356    public <T extends TypedXmlWriter> T _element(String nsUri, String localName, Class<T> contentModel) {
357        ContainerElement child = new ContainerElement(document,this,nsUri,localName);
358        addChild(child.startTag);
359        tail = child.endTag;
360
361        // update uncommitted link list
362        if(lastOpenChild!=null) {
363            assert lastOpenChild.parent==this;
364
365            assert child.prevOpen==null;
366            assert child.nextOpen==null;
367            child.prevOpen = lastOpenChild;
368            assert lastOpenChild.nextOpen==null;
369            lastOpenChild.nextOpen = child;
370        }
371
372        this.lastOpenChild = child;
373
374        return child._cast(contentModel);
375    }
376}
377