com.astamuse.asta4d.web.form.field.impl.AbstractRadioAndCheckboxPrepareRenderer.java Source code

Java tutorial

Introduction

Here is the source code for com.astamuse.asta4d.web.form.field.impl.AbstractRadioAndCheckboxPrepareRenderer.java

Source

/*
 * Copyright 2014 astamuse company,Ltd.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
package com.astamuse.asta4d.web.form.field.impl;

import static com.astamuse.asta4d.render.SpecialRenderer.Clear;

import java.util.LinkedList;
import java.util.List;

import org.apache.commons.lang3.StringUtils;
import org.jsoup.nodes.Element;
import org.jsoup.parser.Tag;

import com.astamuse.asta4d.Configuration;
import com.astamuse.asta4d.extnode.ExtNodeConstants;
import com.astamuse.asta4d.extnode.GroupNode;
import com.astamuse.asta4d.render.ElementSetter;
import com.astamuse.asta4d.render.Renderable;
import com.astamuse.asta4d.render.Renderer;
import com.astamuse.asta4d.render.transformer.ElementTransformer;
import com.astamuse.asta4d.util.IdGenerator;
import com.astamuse.asta4d.util.SelectorUtil;
import com.astamuse.asta4d.util.annotation.AnnotatedPropertyInfo;
import com.astamuse.asta4d.web.form.field.OptionValueMap;
import com.astamuse.asta4d.web.form.field.PrepareRenderingDataUtil;
import com.astamuse.asta4d.web.form.field.SimpleFormFieldPrepareRenderer;

@SuppressWarnings("rawtypes")
public abstract class AbstractRadioAndCheckboxPrepareRenderer<T extends AbstractRadioAndCheckboxPrepareRenderer>
        extends SimpleFormFieldPrepareRenderer {

    public static final String LABEL_REF_ATTR = Configuration.getConfiguration().getTagNameSpace() + ":"
            + "label-ref-for-inputbox-id";

    public static final String DUPLICATOR_REF_ID_ATTR = Configuration.getConfiguration().getTagNameSpace() + ":"
            + "input-radioandcheck-duplicator-ref-id";

    public static final String DUPLICATOR_REF_ATTR = Configuration.getConfiguration().getTagNameSpace() + ":"
            + "input-radioandcheck-duplicator-ref";

    private final static class WrapperIdHolder {
        String inputId = null;
        String wrapperId = null;
        String labelSelector = null;
        List<Element> relocatingLabels = new LinkedList<>();
    }

    private String labelWrapperIndicatorAttr = null;
    private boolean inputIdByValue = false;
    private String duplicateSelector = null;
    private OptionValueMap optionMap = null;

    /**
     * For test purpose
     * 
     * @param fieldName
     */
    @Deprecated
    public AbstractRadioAndCheckboxPrepareRenderer(String fieldName) {
        super(fieldName);
    }

    public AbstractRadioAndCheckboxPrepareRenderer(AnnotatedPropertyInfo field) {
        super(field);
    }

    public AbstractRadioAndCheckboxPrepareRenderer(Class cls, String fieldName) {
        super(cls, fieldName);
    }

    @SuppressWarnings("unchecked")
    public T setOptionData(OptionValueMap optionMap) {
        this.optionMap = optionMap;
        return (T) this;
    }

    /**
     * for log purpose, "radio" or "checkbox" is expected.
     * 
     * @return
     */
    protected abstract String getTypeString();

    /**
     * By default, there must be a label tag which "for" attribute is specified to the against input element, then this prepare renderer
     * will use a select as "label[for=id]" to retrieve the label element of the input element. <br>
     * User can specify a special attribute name to tell this prepare renderer to use selector as "[attrName=id]" to retrieve the against
     * label element which may be a label element with some decorating outer parent elements.
     * 
     * 
     * @param attrName
     * @return
     */
    @SuppressWarnings("unchecked")
    public T setLabelWrapperIndicatorAttr(String attrName) {
        this.labelWrapperIndicatorAttr = attrName;
        return (T) this;
    }

    /**
     * This prepare renderer will generate new uuids for duplicated input elements but it make test verification difficult. specify true for
     * inputIdByValue will make the generated id fixed to the test value.
     * <p>
     * <b>NOTE:</b> This method is for test purpose and we do not recommend to use it in normal rendering logic.
     * 
     * @param inputIdByValue
     * @return
     */
    @SuppressWarnings("unchecked")
    public T setInputIdByValue(boolean inputIdByValue) {
        this.inputIdByValue = inputIdByValue;
        return (T) this;
    }

    /**
     * This prepare renderer will simply duplicate the continuous input/label pair. If the duplicateSelector is specified, the
     * duplicateSelector will be used to duplicate the target element which is assumed to be containing the actual input/label pair.
     * 
     * @param duplicateSelector
     * @return
     */
    @SuppressWarnings("unchecked")
    public T setDuplicateSelector(String duplicateSelector) {
        this.duplicateSelector = duplicateSelector;
        return (T) this;
    }

    @Override
    public Renderer preRender(final String editSelector, final String displaySelector) {

        if (duplicateSelector != null && labelWrapperIndicatorAttr != null) {
            String msg = "duplicateSelector (%s) and labelWrapperIndicatorAttr (%s) cannot be specified at same time.";
            throw new IllegalArgumentException(String.format(msg, duplicateSelector, labelWrapperIndicatorAttr));
        }

        Renderer renderer = super.preRender(editSelector, displaySelector);

        renderer.disableMissingSelectorWarning();

        // create wrapper for input element
        final WrapperIdHolder wrapperIdHolder = new WrapperIdHolder();

        if (duplicateSelector == null && optionMap != null) {

            renderer.add(new Renderer(editSelector, new ElementTransformer(null) {
                @Override
                public Element invoke(Element elem) {

                    if (wrapperIdHolder.wrapperId != null) {
                        throw new RuntimeException("The target of selector[" + editSelector
                                + "] must be unique but over than 1 target was found."
                                + "Perhaps you have specified an option value map on a group of elements "
                                + "which is intented to be treated as predefined static options by html directly.");
                    }

                    String id = elem.id();
                    if (StringUtils.isEmpty(id)) {
                        String msg = "A %s input element must have id value being configured:%s";
                        throw new RuntimeException(String.format(msg, getTypeString(), elem.outerHtml()));
                    }

                    GroupNode wrapper = new GroupNode();

                    // cheating the rendering engine for not skipping the rendering on group node
                    wrapper.attr(ExtNodeConstants.GROUP_NODE_ATTR_TYPE,
                            ExtNodeConstants.GROUP_NODE_ATTR_TYPE_USERDEFINE);

                    // put the input element under the wrapper node
                    wrapper.appendChild(elem.clone());

                    String wrapperId = IdGenerator.createId();
                    wrapper.attr("id", wrapperId);

                    wrapperIdHolder.inputId = id;
                    wrapperIdHolder.wrapperId = wrapperId;

                    // record the selector for against label
                    if (labelWrapperIndicatorAttr == null) {
                        wrapperIdHolder.labelSelector = SelectorUtil.attr("label", "for", wrapperIdHolder.inputId);
                    } else {
                        wrapperIdHolder.labelSelector = SelectorUtil.attr(labelWrapperIndicatorAttr,
                                wrapperIdHolder.inputId);
                    }

                    return wrapper;
                }
            }));

            renderer.add(":root", new Renderable() {
                @Override
                public Renderer render() {
                    if (wrapperIdHolder.wrapperId == null) {
                        // for display mode?
                        return Renderer.create();
                    }

                    // remove the label element and cache it in warpperIdHolder, we will relocate it later(since we have to duplicate the
                    // input
                    // and label pair by given option value map, we have to make sure that the input and label elements are in same parent
                    // node
                    // which can be duplicated)
                    Renderer renderer = Renderer.create().disableMissingSelectorWarning();
                    renderer.add(new Renderer(wrapperIdHolder.labelSelector, new ElementTransformer(null) {
                        @Override
                        public Element invoke(Element elem) {
                            wrapperIdHolder.relocatingLabels.add(elem.clone());
                            return new GroupNode();
                        }

                    }));

                    return renderer.enableMissingSelectorWarning();
                }
            });

            renderer.add(":root", new Renderable() {
                @Override
                public Renderer render() {

                    if (wrapperIdHolder.wrapperId == null) {
                        // for display mode?
                        return Renderer.create();
                    }

                    String selector = SelectorUtil.id(wrapperIdHolder.wrapperId);

                    // relocate the label element to the wrapper node
                    return Renderer.create(selector, new ElementSetter() {
                        @Override
                        public void set(Element elem) {
                            if (wrapperIdHolder.relocatingLabels.isEmpty()) {// no existing label found
                                Element label = new Element(Tag.valueOf("label"), "");
                                label.attr("for", wrapperIdHolder.inputId);
                                elem.appendChild(label);
                            } else {
                                for (Element label : wrapperIdHolder.relocatingLabels) {
                                    elem.appendChild(label);
                                }
                            }
                        }
                    });

                }
            });

        } else {
            if (duplicateSelector != null && optionMap != null) {
                // if duplicateSelector is specified, we just only need to store the input element id
                renderer.add(editSelector, new ElementSetter() {
                    @Override
                    public void set(Element elem) {
                        if (wrapperIdHolder.inputId != null) {
                            String msg = "The target of selector[%s] (inside duplicator:%s) must be unique but over than 1 target was found.";
                            throw new RuntimeException(String.format(msg, editSelector, duplicateSelector));
                        }
                        String id = elem.id();
                        if (StringUtils.isEmpty(id)) {
                            String msg = "A %s input element (inside duplicator:%s) must have id value being configured:%s";
                            throw new RuntimeException(
                                    String.format(msg, getTypeString(), duplicateSelector, elem.outerHtml()));
                        }
                        wrapperIdHolder.inputId = id;

                        // record the selector for against label
                        // labelWrapperIndicatorAttr would not be null since we checked it at the entry of this method.
                        wrapperIdHolder.labelSelector = SelectorUtil.attr("label", "for", wrapperIdHolder.inputId);
                    }
                });
            }
        }

        // here we finished restructure the input element and its related label element and then we begin to manufacture all the input/label
        // pairs for option list

        renderer.add(":root", new Renderable() {
            @Override
            public Renderer render() {

                if (optionMap == null) {
                    // for static options
                    Renderer renderer = Renderer.create();
                    final List<String> inputIdList = new LinkedList<>();
                    renderer.add(editSelector, new ElementSetter() {
                        @Override
                        public void set(Element elem) {
                            inputIdList.add(elem.id());
                        }
                    });
                    renderer.add(":root", new Renderable() {
                        @Override
                        public Renderer render() {
                            Renderer render = Renderer.create().disableMissingSelectorWarning();
                            for (String id : inputIdList) {
                                render.add(SelectorUtil.attr(labelWrapperIndicatorAttr, id), LABEL_REF_ATTR, id);
                                render.add(SelectorUtil.attr("label", "for", id), LABEL_REF_ATTR, id);
                            }
                            return render.enableMissingSelectorWarning();
                        }
                    });

                    if (duplicateSelector != null) {
                        renderer.add(duplicateSelector, new Renderable() {
                            @Override
                            public Renderer render() {
                                String duplicatorRef = IdGenerator.createId();
                                Renderer render = Renderer.create(":root", DUPLICATOR_REF_ID_ATTR, duplicatorRef);
                                render.add("input", DUPLICATOR_REF_ATTR, duplicatorRef);
                                String labelSelector;
                                if (labelWrapperIndicatorAttr == null) {
                                    labelSelector = SelectorUtil.tag("label");
                                } else {
                                    labelSelector = SelectorUtil.attr(labelWrapperIndicatorAttr);
                                }
                                render.add(labelSelector, DUPLICATOR_REF_ATTR, duplicatorRef);
                                return render;
                            }
                        });
                    }
                    return renderer;
                } else {
                    if (wrapperIdHolder.wrapperId == null && duplicateSelector == null) {
                        // for display mode?
                        return Renderer.create();
                    }
                    if (wrapperIdHolder.inputId == null) {
                        // target input element not found
                        return Renderer.create();
                    }
                    String selector = duplicateSelector == null ? SelectorUtil.id(wrapperIdHolder.wrapperId)
                            : duplicateSelector;
                    return Renderer.create(selector, optionMap.getOptionList(), row -> {

                        Renderer renderer = Renderer.create().disableMissingSelectorWarning();

                        String inputSelector = SelectorUtil.id("input", wrapperIdHolder.inputId);
                        renderer.add(inputSelector, "value", row.getValue());

                        // we have to generate a new uuid for the input element to make sure its id is unique even we duplicated it.
                        String newInputId = inputIdByValue ? row.getValue() : IdGenerator.createId();

                        // make the generated id more understandable by prefixing with original id
                        newInputId = wrapperIdHolder.inputId + "-" + newInputId;

                        String duplicatorRef = null;

                        if (duplicateSelector != null) {
                            duplicatorRef = IdGenerator.createId();
                        }

                        renderer.add(":root", DUPLICATOR_REF_ID_ATTR, duplicatorRef);

                        renderer.add(inputSelector, DUPLICATOR_REF_ATTR, duplicatorRef);
                        renderer.add(inputSelector, "id", newInputId);

                        // may be a wrapper container of label
                        renderer.add(wrapperIdHolder.labelSelector, LABEL_REF_ATTR, newInputId);
                        if (labelWrapperIndicatorAttr != null) {
                            renderer.add(wrapperIdHolder.labelSelector, labelWrapperIndicatorAttr, newInputId);
                        }
                        renderer.add(wrapperIdHolder.labelSelector, DUPLICATOR_REF_ATTR, duplicatorRef);

                        renderer.add("label", "for", newInputId);
                        renderer.add("label", row.getDisplayText());

                        return renderer.enableMissingSelectorWarning();
                    });
                }
            }
        });

        // since we cheated the rendering engine, we should set the type of group node created to faked for fast clean up
        renderer.add(":root", new Renderable() {
            @Override
            public Renderer render() {
                if (wrapperIdHolder.wrapperId == null) {
                    // for display mode?
                    return Renderer.create();
                }
                String selector = SelectorUtil.id(wrapperIdHolder.wrapperId);
                return Renderer.create(selector, new ElementSetter() {
                    @Override
                    public void set(Element elem) {
                        elem.attr(ExtNodeConstants.GROUP_NODE_ATTR_TYPE,
                                ExtNodeConstants.GROUP_NODE_ATTR_TYPE_FAKE);
                    }
                });
            }
        });

        PrepareRenderingDataUtil.storeDataToContextBySelector(editSelector, displaySelector, optionMap);

        return renderer.enableMissingSelectorWarning();
    }

    @Override
    public Renderer postRender(String editSelector, String displaySelector) {

        Renderer render = Renderer.create().disableMissingSelectorWarning();

        String[] clearAttrs = { LABEL_REF_ATTR, DUPLICATOR_REF_ATTR, DUPLICATOR_REF_ID_ATTR };
        for (String attr : clearAttrs) {
            render.add(SelectorUtil.attr(attr), attr, Clear);
        }
        render.add(super.postRender(editSelector, displaySelector));

        return render.enableMissingSelectorWarning();
    }

}