package xapi.ui.api; import xapi.collect.X_Collect; import xapi.collect.api.IntTo; import xapi.collect.api.StringTo; import xapi.util.X_Debug; import xapi.util.X_String; import xapi.util.api.ReceivesValue; import xapi.util.impl.DeferredCharSequence; import static xapi.collect.X_Collect.newStringMap; import javax.inject.Provider; import java.io.IOException; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; public abstract class NodeBuilder<E> implements Widget<E> { protected static final String EMPTY = ""; private CharSequence buffer; protected NodeBuilder<E> children; protected NodeBuilder<E> siblings; protected E el; protected Provider<E> elementProvider; @SuppressWarnings("rawtypes") protected NodeBuilder childTarget = this; protected final IntTo<ReceivesValue<E>> createdCallbacks; protected final boolean searchableChildren; protected Function<String, String> bodySanitizer; protected BiFunction<String, Boolean, NodeBuilder<E>> newNode; protected NodeBuilder() { this(false); } public NodeBuilder(boolean searchableChildren) { this.searchableChildren = searchableChildren; createdCallbacks = X_Collect.newList(ReceivesValue.class); } public boolean hasSiblings() { return siblings != null; } public class ClassnameBuilder extends AttributeBuilder { public ClassnameBuilder() { super("class", true); } @Override public <C extends NodeBuilder<String>> C addChild(C child) { return addToSet(child, " "); // classname is a space-separated list of classname tokens // TODO: also send a validator for the classnames } } protected AttributeBuilder newAttributeBuilder(CharSequence attr) { return new AttributeBuilder(attr); } protected static class AttributeBuilder extends NodeBuilder<String> { private final StringTo<String> existing; protected AttributeBuilder(CharSequence value) { this(value, false); } protected AttributeBuilder(CharSequence value, boolean searchableChildren) { super(searchableChildren); existing = newStringMap(String.class); append(value); } @Override public void append(Widget<String> child) { append(child.getElement()); } @Override protected String create(CharSequence node) { return node.toString(); } @Override protected NodeBuilder<String> wrapChars(CharSequence body) { return newAttributeBuilder(body); } @Override protected BiFunction<String, Boolean, NodeBuilder<String>> getCreator() { Function<CharSequence, AttributeBuilder> ctor = AttributeBuilder::new; // ignore the searchable attribute for now. return (str, searchable)->ctor.apply(str); } @Override public <C extends NodeBuilder<String>> C addChild(C child) { String value = child.getElement(); final NodeBuilder<String> wrapped = wrapChars(value); super.addChild(wrapped); return child; } public <C extends NodeBuilder<String>> C addToSet(C child, String separator ) { return addToSet(child, (c, txt)->(separator+txt+separator).split(separator), // take the list apart ()->separator // put it back together. ); } public <C extends NodeBuilder<String>> C addToSet(C child, BiFunction<C, CharSequence, String[]> splitter, Supplier<CharSequence> joinChars ) { // The api of the bifunction demands that we figure out the string value from the element type we are working with. String value = child.getElement(); // Since we are a string type, this seems pretty convoluted, // but when working on live elements and nodes, this lets it see the object and the to string'd version at once. for (String part : splitter.apply(child, value)) { if (part.length() > 0) { if (existing.put(part, part) == null) { if (existing.size() > 1) { part = part + joinChars.get(); } super.addChild(wrapChars(part)); } } } return child; } } public <C extends NodeBuilder<E>> C addChild(C child) { return addChild(child, false); } @SuppressWarnings("unchecked") public <C extends NodeBuilder<E>> C addChild(C child, boolean makeTarget) { if (buffer != null) { CharSequence was = buffer; buffer = null; // Tear off the current set of char sequences as a child node addChild0(wrapChars(was)); } childTarget.addChild0(child); if (makeTarget) { childTarget = child; } return child; } private <C extends NodeBuilder<E>> void addChild0(C child) { if (children == null) { children = child; } else { assert children != child; children.addSibling(child); } } public void clearChildren() { buffer = null; children = null; } public void clearAll() { clearChildren(); siblings = null; el = null; } public <C extends NodeBuilder<E>> C addSibling(C sibling) { if (siblings == null) { siblings = sibling; } else { siblings.addSibling(sibling); } return sibling; } public NodeBuilder<E> append(CharSequence chars) { if (children == null) { if (buffer == null) { buffer = chars; } else { buffer = join(buffer, chars); } } else { if (chars != null && chars != EMPTY) { addChild0(wrapChars(chars)); } } return this; } protected abstract E create(CharSequence node); protected CharSequence getCharsAfter(CharSequence self) { return EMPTY; } protected CharSequence getCharsBefore() { return EMPTY; } public E getElement() { if (el == null) { // MAYBE: double checked lock for jvms? if (elementProvider != null) { el = elementProvider.get(); } if (el == null) { initialize(); } else { elementProvider = null; onInitialize(el); } } return el; } public NodeBuilder<E> onCreated(ReceivesValue<E> callback) { if (el == null) { assert searchableChildren : "Cannot handle created callbacks without searchableChildren in "+this; createdCallbacks.add(callback); if (elementProvider != null) { getElement(); // will trigger our callback } } else { callback.set(el); } return this; } private final E initialize() { StringBuilder b = new StringBuilder(); toHtml(b); final String html = b.toString(); if (X_String.isEmpty(html)) { return null; } el = create(html); startInitialize(el); try { if (!searchableChildren) { return el; } boolean initChildren = false; if (children != null) { initChildren = children.resolveChildren(el); } onInitialize(el); if (initChildren) { children.onInitialize(children.getElement()); } } finally { finishInitialize(el); } return el; } protected void finishInitialize(E el) {} protected void startInitialize(E el) {} protected void onInitialize(E el) { if (!createdCallbacks.isEmpty()) { final ReceivesValue<E>[] callbacks = createdCallbacks.toArray(); createdCallbacks.clear(); for (ReceivesValue<E> callback : callbacks) { callback.set(el); } assert createdCallbacks.isEmpty() : "You are adding more created callbacks during onInitialize."; // TODO consider draining the callbacks for a few iterations } } private boolean resolveChildren(E root) { boolean needsCallback = false; if (children != null) { if (children.resolveChildren(root)) { onCreated( new ReceivesValue<E>() { @Override public void set(E value) { children.onInitialize(children.getElement()); } } ); needsCallback = true; } } if (siblings != null) { if (siblings.resolveChildren(root)) { onCreated( new ReceivesValue<E>() { @Override public void set(E value) { siblings.onInitialize(siblings.getElement()); } } ); needsCallback = true; } } resolve(root); return needsCallback || !createdCallbacks.isEmpty(); } protected void resolve(final E root) { if (this.el == null) { // We need to resolve our element! elementProvider = new Provider<E>() { @Override public E get() { return findSelf(root); } }; } } protected E findSelf(E root) { throw new UnsupportedOperationException("NodeBuilder " + getClass() + " does not support nested .getElement()"); } protected CharSequence join(final CharSequence body, final CharSequence chars) { return new DeferredCharSequence<E>(body, chars); } protected void toHtml(Appendable out) { CharSequence chars = getCharsBefore(); boolean printChars = chars != null && chars != EMPTY; if (printChars) { print(out, chars); } if (children == null) { if (buffer != null && buffer != EMPTY) { print(out, buffer); } } else { if (buffer != null && buffer != EMPTY) { addChild0(wrapChars(buffer)); } children.toHtml(out); } if (printChars) { CharSequence after = getCharsAfter(chars); if (after != null && after != EMPTY) { print(out, after); } } if (siblings != null) { siblings.toHtml(out); } } protected void print(Appendable out, CharSequence was) { try { out.append(was); } catch (IOException e) { throw X_Debug.rethrow(e); } } protected abstract NodeBuilder<E> wrapChars(CharSequence body); protected boolean isChildrenEmpty() { return children == null && buffer == null; } protected boolean isEmpty() { return isChildrenEmpty(); } public void cleanup() { if (children != null) { children.cleanup(); children = null; } if (siblings != null) { siblings.cleanup(); siblings = null; } this.buffer = ""; this.el = null; this.elementProvider = null; this.childTarget = null; } public NodeBuilder<E> createChild(String value) { return wrapChars(value); } public final void setNodeFactory(BiFunction<String, Boolean, NodeBuilder<E>> factory) { setNodeFactory(factory, false); } public void setBodySanitizer(Function<String, String> bodySanitizer) { this.bodySanitizer = bodySanitizer; } public void setNodeFactory(BiFunction<String, Boolean, NodeBuilder<E>> factory, boolean shareFactories) { if (shareFactories) { this.newNode = factory.andThen(this::shareFactories); } else { this.newNode = factory; } } protected NodeBuilder<E> shareFactories(NodeBuilder<E> with) { // Make sure we pass along our factories to our children! // We expose this method so that it is reusable and optional, // so custom behaviors can choose whether to opt in to factory sharing or not. // Scenarios where nodes set different factories preclude sharing, thus, it is an optional operation. with.setBodySanitizer(bodySanitizer); with.setNodeFactory(newNode); return with; } protected void setDefaultFactories() { setBodySanitizer(html -> html.replaceAll("\n", "<br/>")); setNodeFactory(getCreator(), true); } protected abstract BiFunction<String, Boolean, NodeBuilder<E>> getCreator(); }