/* * 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(); } }