22
33import edu .umd .cs .findbugs .annotations .CheckForNull ;
44import java .lang .reflect .Method ;
5+ import java .util .ArrayList ;
56import java .util .List ;
67import java .util .logging .Logger ;
78import org .apache .commons .jelly .JellyContext ;
1011import org .apache .commons .jelly .Script ;
1112import org .apache .commons .jelly .XMLOutput ;
1213import org .dom4j .Element ;
14+ import org .dom4j .Node ;
15+ import org .dom4j .io .SAXContentHandler ;
16+ import org .dom4j .io .SAXWriter ;
17+ import org .kohsuke .stapler .WebApp ;
18+ import org .kohsuke .stapler .html .HtmlInclude ;
19+ import org .kohsuke .stapler .jelly .JellyClassTearOff ;
1320
1421final class HtmlJellyScript implements Script {
1522
@@ -34,13 +41,14 @@ public void run(JellyContext context, XMLOutput output) throws JellyTagException
3441 if (it == null ) {
3542 throw new JellyTagException ("No `it` bound" );
3643 }
44+ // TODO set thread name like JellyViewScript does
3745 try {
3846 var record = (Record ) method .invoke (it );
3947 LOGGER .info (() -> "TODO " + method + " on " + it + " ⇒ " + record );
4048 // TODO find a more efficient way to render without allocation:
4149 var rendered = (Element ) root .clone ();
42- render (rendered , record );
43- rendered . write (output . asWriter () );
50+ render (context , rendered , record );
51+ new SAXWriter (output , output ). write ( rendered );
4452 } catch (Exception x ) {
4553 throw new JellyTagException (x );
4654 }
@@ -51,7 +59,7 @@ var record = (Record) method.invoke(it);
5159 * {@link Visitor} cannot be used easily here because it lacks any way to prune a tree
5260 * or record entry and exit from an element.
5361 */
54- private void render (Element rendered , Record record ) throws Exception {
62+ private void render (JellyContext context , Element rendered , Record record ) throws Exception {
5563 for (var field : record .getClass ().getRecordComponents ()) {
5664 var id = "st." + field .getName ();
5765 var elt = find (rendered , id );
@@ -60,22 +68,47 @@ private void render(Element rendered, Record record) throws Exception {
6068 }
6169 elt .remove (elt .attribute ("id" ));
6270 var value = field .getAccessor ().invoke (record );
63- if (value instanceof String text ) {
64- elt .setText (text );
71+ var include = field .getAnnotation (HtmlInclude .class );
72+ if (include != null ) {
73+ var metaClass = WebApp .getCurrent ().getMetaClass (value .getClass ());
74+ var script = metaClass .loadTearOff (JellyClassTearOff .class ).findScript (include .value ());
75+ var subcontext = new JellyContext (context );
76+ subcontext .setExportLibraries (false ); // defaults to true, weirdly
77+ subcontext .setVariable ("from" , value );
78+ subcontext .setVariable ("it" , value );
79+ var handler = new SAXContentHandler ();
80+ var oldCCL = Thread .currentThread ().getContextClassLoader ();
81+ Thread .currentThread ().setContextClassLoader (metaClass .classLoader .loader );
82+ try {
83+ script .run (subcontext , new XMLOutput (handler ));
84+ } finally {
85+ Thread .currentThread ().setContextClassLoader (oldCCL );
86+ }
87+ // TODO honor JellyFacet.TRACE somehow
88+ replace (elt , List .of (handler .getDocument ().getRootElement ()));
89+ } else if (value instanceof String text ) {
90+ // TODO why do we need to replace entities like this?
91+ // It is not necessary if run calls rendered.write(output.asWriter())
92+ // but that does not seem right because it would be just sending text content, not XML.
93+ // (And then includes do not work at all: the SAXContentHandler is left empty.)
94+ // Also using SAXWriter causes DefaultScriptInvoker.createXMLOutput to use HTMLWriterOutput
95+ // and thus dropping </p>, which does not match what actual Jenkins text/html output is like.
96+ // Retest in the context of Jenkins which might wrap things differently.
97+ elt .setText (text .replace ("&" , "&" ).replace ("<" , "<" ).replace (">" , ">" ));
6598 } else if (value instanceof Boolean enabled ) {
6699 if (!enabled ) {
67100 elt .detach ();
68101 }
69102 } else if (value instanceof Record subrecord ) {
70- render (elt , subrecord );
103+ render (context , elt , subrecord );
71104 } else if (value instanceof List <?> list ) {
72- var parent = elt .getParent ();
73- elt .detach ();
105+ var replacements = new ArrayList <Element >();
74106 for (var item : list ) {
75107 var elt2 = (Element ) elt .clone ();
76- render (elt2 , (Record ) item );
77- parent .add (elt2 );
108+ render (context , elt2 , (Record ) item );
109+ replacements .add (elt2 );
78110 }
111+ replace (elt , replacements );
79112 } else if (value == null ) {
80113 elt .detach ();
81114 } else {
@@ -88,7 +121,7 @@ private void render(Element rendered, Record record) throws Exception {
88121 * Like {@link Element#elementByID} except using attribute {@code id} not {@code ID}.
89122 */
90123 @ CheckForNull
91- private Element find (Element elt , String id ) {
124+ private static Element find (Element elt , String id ) {
92125 if (id .equals (elt .attributeValue ("id" ))) {
93126 return elt ;
94127 }
@@ -100,4 +133,17 @@ private Element find(Element elt, String id) {
100133 }
101134 return null ;
102135 }
136+
137+ // TODO dom4j does not seem to define an insertAt or replace
138+ // also Element.node(int) does not work as documented
139+ private static void replace (Element elt , List <Element > replacements ) {
140+ var parent = elt .getParent ();
141+ var kids = new ArrayList <Node >();
142+ parent .nodeIterator ().forEachRemaining (kids ::add );
143+ kids .stream ().forEach (Node ::detach );
144+ int index = kids .indexOf (elt );
145+ kids .remove (index );
146+ kids .addAll (index , replacements );
147+ kids .stream ().forEach (parent ::add );
148+ }
103149}
0 commit comments