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