001package ball.xml;
002/*-
003 * ##########################################################################
004 * Utilities
005 * %%
006 * Copyright (C) 2008 - 2022 Allen D. Ball
007 * %%
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *      http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 * ##########################################################################
020 */
021import ball.lang.reflect.FacadeProxyInvocationHandler;
022import java.util.ArrayList;
023import java.util.Arrays;
024import java.util.HashMap;
025import java.util.LinkedHashSet;
026import java.util.List;
027import java.util.Map;
028import java.util.Objects;
029import java.util.stream.Collectors;
030import java.util.stream.Stream;
031import lombok.NoArgsConstructor;
032import lombok.ToString;
033import org.w3c.dom.Attr;
034import org.w3c.dom.CDATASection;
035import org.w3c.dom.Comment;
036import org.w3c.dom.Document;
037import org.w3c.dom.DocumentFragment;
038import org.w3c.dom.DocumentType;
039import org.w3c.dom.Element;
040import org.w3c.dom.Entity;
041import org.w3c.dom.EntityReference;
042import org.w3c.dom.Node;
043import org.w3c.dom.Notation;
044import org.w3c.dom.ProcessingInstruction;
045import org.w3c.dom.Text;
046
047import static lombok.AccessLevel.PROTECTED;
048
049/**
050 * Fluent {@link Node} interface Note: This interface is an implementation
051 * detail of {@link FluentDocument.Builder} and should not be implemented or
052 * extended directly.
053 *
054 * @author {@link.uri mailto:ball@hcf.dev Allen D. Ball}
055 */
056public interface FluentNode extends Node {
057
058    /**
059     * {@link Map} {@link Node#getNodeType()} to type ({@link Class}).
060     *
061     * {@include #NODE_TYPE_MAP}
062     */
063    public static final Map<Short,Class<? extends Node>> NODE_TYPE_MAP =
064        Stream.of(new Object[][] {
065            { ATTRIBUTE_NODE, Attr.class },
066            { CDATA_SECTION_NODE, CDATASection.class },
067            { COMMENT_NODE, Comment.class },
068            { DOCUMENT_FRAGMENT_NODE, DocumentFragment.class },
069            { DOCUMENT_NODE, Document.class },
070            { DOCUMENT_TYPE_NODE, DocumentType.class },
071            { ELEMENT_NODE, Element.class },
072            { ENTITY_NODE, Entity.class },
073            { ENTITY_REFERENCE_NODE, EntityReference.class },
074            { NOTATION_NODE, Notation.class },
075            { PROCESSING_INSTRUCTION_NODE, ProcessingInstruction.class },
076            { TEXT_NODE, Text.class }
077        }).collect(Collectors.toMap(t -> (Short) t[0],
078                                    t -> ((Class<?>) t[1])
079                                             .asSubclass(Node.class)));
080
081    /**
082     * See {@link Node#getOwnerDocument()}.
083     *
084     * @return  The owner {@link FluentDocument}.
085     */
086    default FluentDocument owner() {
087        return (FluentDocument) getOwnerDocument();
088    }
089
090    /**
091     * See {@link #getNodeName()}.
092     *
093     * @return  {@link #getNodeName()}
094     */
095    default String name() { return getNodeName(); }
096
097    /**
098     * See {@link #getNodeValue()}.
099     *
100     * @return  {@link #getNodeValue()}
101     */
102    default String value() { return getNodeValue(); }
103
104    /**
105     * See {@link #setNodeValue(String)}.
106     *
107     * @param   value           The {@link Node} value.
108     *
109     * @return  {@link.this}
110     */
111    default FluentNode value(String value) {
112        setNodeValue(value);
113
114        return this;
115    }
116
117    /**
118     * See {@link #getTextContent()}.
119     *
120     * @return  {@link #getTextContent()}
121     */
122    default String content() { return getTextContent(); }
123
124    /**
125     * See {@link #setTextContent(String)}.
126     *
127     * @param   content         The {@link Node} content.
128     *
129     * @return  {@link.this}
130     */
131    default FluentNode content(String content) {
132        setTextContent(content);
133
134        return this;
135    }
136
137    /**
138     * Method to add {@link Node}s to {@link.this} {@link FluentNode}.
139     *
140     * @param   stream          The {@link Stream} of {@link Node}s to
141     *                          add.
142     *
143     * @return  {@link.this}
144     */
145    default FluentNode add(Stream<Node> stream) {
146        return add(stream.toArray(Node[]::new));
147    }
148
149    /**
150     * Method to add {@link Node}s to {@link.this} {@link FluentNode}.
151     *
152     * @param   nodes           The {@link Node}s to add.
153     *
154     * @return  {@link.this}
155     */
156    default FluentNode add(Node... nodes) {
157        for (Node node : nodes) {
158            switch (node.getNodeType()) {
159            case ATTRIBUTE_NODE:
160                getAttributes().setNamedItem(node);
161                break;
162
163            default:
164                appendChild(node);
165                break;
166            }
167        }
168
169        return this;
170    }
171
172    /**
173     * Create an {@link DocumentFragment} {@link Node}.
174     *
175     * @param   stream          The {@link Stream} of {@link Node}s to
176     *                          append to the newly created
177     *                          {@link DocumentFragment}.
178     *
179     * @return  The newly created {@link DocumentFragment}.
180     */
181    default FluentNode fragment(Stream<Node> stream) {
182        return fragment(stream.toArray(Node[]::new));
183    }
184
185    /**
186     * Create an {@link DocumentFragment} {@link Node}.
187     *
188     * @param   nodes           The {@link Node}s to append to the newly
189     *                          created {@link DocumentFragment}.
190     *
191     * @return  The newly created {@link DocumentFragment}.
192     */
193    default FluentNode fragment(Node... nodes) {
194        return ((FluentNode) owner().createDocumentFragment()).add(nodes);
195    }
196
197    /**
198     * Create an {@link Element} {@link Node}.
199     *
200     * @param   name            The {@link Element} name.
201     * @param   stream          The {@link Stream} of {@link Node}s to
202     *                          append to the newly created {@link Element}.
203     *
204     * @return  The newly created {@link Element}.
205     */
206    default FluentNode element(String name, Stream<Node> stream) {
207        return element(name, stream.toArray(Node[]::new));
208    }
209
210    /**
211     * Create an {@link Element} {@link Node}.
212     *
213     * @param   name            The {@link Element} name.
214     * @param   nodes           The {@link Node}s to append to the newly
215     *                          created {@link Element}.
216     *
217     * @return  The newly created {@link Element}.
218     */
219    default FluentNode element(String name, Node... nodes) {
220        return ((FluentNode) owner().createElement(name)).add(nodes);
221    }
222
223    /**
224     * Create an {@link Element} {@link Node}.
225     *
226     * @param   ns              The {@link Element} namespace.
227     * @param   qn              The {@link Element} qualified name.
228     * @param   stream          The {@link Stream} of {@link Node}s to
229     *                          append to the newly created {@link Element}.
230     *
231     * @return  The newly created {@link Element}.
232     */
233    default FluentNode elementNS(String ns, String qn, Stream<Node> stream) {
234        return elementNS(ns, qn, stream.toArray(Node[]::new));
235    }
236
237    /**
238     * Create an {@link Element} {@link Node}.
239     *
240     * @param   ns              The {@link Element} namespace.
241     * @param   qn              The {@link Element} qualified name.
242     * @param   nodes           The {@link Node}s to append to the newly
243     *                          created {@link Element}.
244     *
245     * @return  The newly created {@link Element}.
246     */
247    default FluentNode elementNS(String ns, String qn, Node... nodes) {
248        return ((FluentNode) owner().createElementNS(ns, qn)).add(nodes);
249    }
250
251    /**
252     * Create an {@link Attr} {@link Node}.
253     *
254     * @param   name            The {@link Attr} name.
255     *
256     * @return  The newly created {@link Attr}.
257     */
258    default FluentNode attr(String name) {
259        return (FluentNode) owner().createAttribute(name);
260    }
261
262    /**
263     * Create an {@link Attr} {@link Node}.
264     *
265     * @param   name            The {@link Attr} name.
266     * @param   value           The {@link Attr} value.
267     *
268     * @return  The newly created {@link Attr}.
269     */
270    default FluentNode attr(String name, String value) {
271        FluentNode node = attr(name);
272
273        ((Attr) node).setValue(value);
274
275        return node;
276    }
277
278    /**
279     * Create an {@link Attr} {@link Node}.
280     *
281     * @param   ns              The {@link Attr} namespace.
282     * @param   qn              The {@link Attr} qualified name.
283     *
284     * @return  The newly created {@link Attr}.
285     */
286    default FluentNode attrNS(String ns, String qn) {
287        return (FluentNode) owner().createAttributeNS(ns, qn);
288    }
289
290    /**
291     * Create an {@link Attr} {@link Node}.
292     *
293     * @param   ns              The {@link Attr} namespace.
294     * @param   qn              The {@link Attr} qualified name.
295     * @param   value           The {@link Attr} value.
296     *
297     * @return  The newly created {@link Attr}.
298     */
299    default FluentNode attrNS(String ns, String qn, String value) {
300        FluentNode node = attrNS(ns, qn);
301
302        ((Attr) node).setValue(value);
303
304        return node;
305    }
306
307    /**
308     * Create a {@link Text} {@link Node}.
309     *
310     * @param   content         The {@link Text} content.
311     *
312     * @return  The newly created {@link Text}.
313     */
314    default FluentNode text(String content) {
315        return (FluentNode) owner().createTextNode(content);
316    }
317
318    /**
319     * Create a {@link CDATASection} {@link Node}.
320     *
321     * @param   data            The {@link CDATASection} data.
322     *
323     * @return  The newly created {@link CDATASection}.
324     */
325    default FluentNode cdata(String data) {
326        return (FluentNode) owner().createCDATASection(data);
327    }
328
329    /**
330     * Create a {@link Comment} {@link Node}.
331     *
332     * @param   data            The {@link Comment} data.
333     *
334     * @return  The newly created {@link Comment}.
335     */
336    default FluentNode comment(String data) {
337        return (FluentNode) owner().createComment(data);
338    }
339
340    /**
341     * {@link FluentNode} {@link java.lang.reflect.InvocationHandler}.
342     */
343    @NoArgsConstructor(access = PROTECTED) @ToString
344    public class InvocationHandler extends FacadeProxyInvocationHandler {
345        private final HashMap<List<Class<?>>,Class<?>> map =
346            new HashMap<>();
347
348        /**
349         * Implementation provides {@link java.lang.reflect.Proxy} class if
350         * {@link Object} implements {@link Node} with the corresponding
351         * {@link FluentNode} sub-interface(s).
352         *
353         * {@inheritDoc}
354         */
355        @Override
356        protected Class<?> getProxyClassFor(Object object) {
357            Class<?> type = null;
358
359            if (object instanceof Node && (! (object instanceof FluentNode))) {
360                Node node = (Node) object;
361                List<Class<?>> key =
362                    Arrays.asList(NODE_TYPE_MAP.getOrDefault(node.getNodeType(), Node.class), node.getClass());
363
364                type = map.computeIfAbsent(key, k -> compute(k));
365            }
366
367            return type;
368        }
369
370        private Class<?> compute(List<Class<?>> key) {
371            LinkedHashSet<Class<?>> implemented =
372                key.stream()
373                .flatMap(t -> getImplementedInterfacesOf(t).stream())
374                .filter(t -> Node.class.isAssignableFrom(t))
375                .filter(t -> Node.class.getPackage().equals(t.getPackage()))
376                .collect(Collectors.toCollection(LinkedHashSet::new));
377            LinkedHashSet<Class<?>> interfaces =
378                implemented.stream()
379                .map(t -> fluent(t))
380                .filter(Objects::nonNull)
381                .collect(Collectors.toCollection(LinkedHashSet::new));
382
383            interfaces.addAll(implemented);
384
385            new ArrayList<>(interfaces).stream()
386                .forEach(t -> interfaces.removeAll(Arrays.asList(t.getInterfaces())));
387
388            return getProxyClass(interfaces.toArray(new Class<?>[] { }));
389        }
390
391        private Class<?> fluent(Class<?> type) {
392            Class<?> fluent = null;
393
394            if (Node.class.isAssignableFrom(type)
395                && Node.class.getPackage().equals(type.getPackage())) {
396                try {
397                    String name =
398                        String.format("%s.Fluent%s", FluentNode.class.getPackage().getName(), type.getSimpleName());
399
400                    fluent = Class.forName(name).asSubclass(FluentNode.class);
401                } catch (Exception exception) {
402                }
403            }
404
405            return fluent;
406        }
407    }
408}