/* * 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.flow.base; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Array; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.jsoup.nodes.Element; import org.jsoup.parser.Tag; import com.astamuse.asta4d.data.ContextDataHolder; import com.astamuse.asta4d.data.InjectTrace; import com.astamuse.asta4d.render.ElementSetter; import com.astamuse.asta4d.render.Renderer; import com.astamuse.asta4d.util.ElementUtil; import com.astamuse.asta4d.util.SelectorUtil; import com.astamuse.asta4d.util.annotation.AnnotatedPropertyInfo; import com.astamuse.asta4d.web.form.CascadeArrayFunctions; import com.astamuse.asta4d.web.form.annotation.CascadeFormField; import com.astamuse.asta4d.web.form.field.FormFieldPrepareRenderer; public interface BasicFormFlowSnippetTrait extends CascadeArrayFunctions { /** * Sub class should tell us the current rendering mode. Since we have no any information about the concrete cases, we always return true * by default. * * @param step * @param form * @param fieldName * @return */ default boolean renderForEdit(String step, Object form, String fieldName) { return true; } /** * The entry of form rendering. Sub classes could override it in case of necessarily. * * @return * @throws Exception */ default Renderer render(FormRenderingData renderingData) throws Exception { Renderer renderer = preRender(renderingData); renderer.add(renderTraceId(renderingData.getTraceId())); Object form = retrieveRenderTargetForm(renderingData); renderer.add(renderForm(renderingData.getRenderTargetStep(), form, EMPTY_INDEXES)); Element clientJs = retrieveClientCascadeUtilJsContent(); if (clientJs != null) { renderer.add(":root", (Element elem) -> { elem.appendChild(clientJs); }); } renderer.add(postRender(renderingData)); return renderer; } default Renderer preRender(FormRenderingData renderingData) { return Renderer.create(); } default Renderer postRender(FormRenderingData renderingData) { return Renderer.create(); } /** * We only render the form trace map when it exists * * @return */ default Renderer renderTraceId(String traceId) { if (StringUtils.isEmpty(traceId)) { return Renderer.create(); } else { return Renderer.create(":root", new ElementSetter() { @Override public void set(Element elem) { Element hide = new Element(Tag.valueOf("input"), ""); hide.attr("name", FormFlowConstants.FORM_FLOW_TRACE_ID_QUERY_PARAM); hide.attr("type", "hidden"); hide.attr("value", traceId); elem.appendChild(hide); } }); } } default Object retrieveRenderTargetForm(FormRenderingData renderingData) { return renderingData.getTraceData().getStepFormMap().get(renderingData.getRenderTargetStep()); } /** * * PriorRenderMethod the whole given form instance. All the {@link FormFieldPrepareRenderer}s would be invoked here too. * * @param renderTargetStep * @param form * @param indexes * @return * @throws Exception */ default Renderer renderForm(String renderTargetStep, Object form, int[] indexes) throws Exception { Renderer render = Renderer.create(); if (form == null) { return render; } if (form instanceof StepRepresentableForm) { String[] formRepresentingSteps = ((StepRepresentableForm) form).retrieveRepresentingSteps(); if (ArrayUtils.contains(formRepresentingSteps, renderTargetStep)) { // it is OK } else { return render; } } render.disableMissingSelectorWarning(); render.add(preRenderForm(renderTargetStep, form, indexes)); List<FormFieldPrepareRenderer> fieldDataPrepareRendererList = retrieveFieldPrepareRenderers(renderTargetStep, form); for (FormFieldPrepareRenderer formFieldDataPrepareRenderer : fieldDataPrepareRendererList) { BasicFormFlowTraitHelper.FieldRenderingInfo renderingInfo = BasicFormFlowTraitHelper.getRenderingInfo(this, formFieldDataPrepareRenderer.targetField(), indexes); render.add(formFieldDataPrepareRenderer.preRender(renderingInfo.editSelector, renderingInfo.displaySelector)); } render.add(renderValueOfFields(renderTargetStep, form, indexes)); for (FormFieldPrepareRenderer formFieldDataPrepareRenderer : fieldDataPrepareRendererList) { BasicFormFlowTraitHelper.FieldRenderingInfo renderingInfo = BasicFormFlowTraitHelper.getRenderingInfo(this, formFieldDataPrepareRenderer.targetField(), indexes); render.add(formFieldDataPrepareRenderer.postRender(renderingInfo.editSelector, renderingInfo.displaySelector)); } render.add(postRenderForm(renderTargetStep, form, indexes)); return render.enableMissingSelectorWarning(); } default Renderer preRenderForm(String renderTargetStep, Object form, int[] indexes) throws Exception { return Renderer.create(); } default Renderer postRenderForm(String renderTargetStep, Object form, int[] indexes) throws Exception { return Renderer.create(); } /** * * PriorRenderMethod the value of all the given form's fields.The rendering of cascade forms will be done here as well(recursively call * the {@link #renderForm(String, Object, int)}). * * @param renderTargetStep * @param form * @param indexes * @return * @throws Exception */ default Renderer renderValueOfFields(String renderTargetStep, Object form, int[] indexes) throws Exception { Renderer render = Renderer.create(); List<AnnotatedPropertyInfo> fieldList = BasicFormFlowTraitHelper.retrieveRenderTargetFieldList(form); for (AnnotatedPropertyInfo field : fieldList) { Object v = field.retrieveValue(form); CascadeFormField cff = field.getAnnotation(CascadeFormField.class); if (cff != null) { String containerSelector = cff.containerSelector(); if (field.getType().isArray()) {// a cascade form for array int len = Array.getLength(v); List<Renderer> subRendererList = new ArrayList<>(len); int loopStart = 0; if (renderForEdit(renderTargetStep, form, cff.name())) { // for rendering a template DOM loopStart = -1; } Class<?> subFormType = field.getType().getComponentType(); Object subForm; for (int i = loopStart; i < len; i++) { int[] newIndex = indexes.clone(); // retrieve the form instance if (i >= 0) { newIndex = ArrayUtils.add(newIndex, i); subForm = Array.get(v, i); } else { // create a template instance subForm = createFormInstanceForCascadeFormArrayTemplate(subFormType); } Renderer subRenderer = Renderer.create(); // only rewrite the refs for normal instances if (i >= 0) { subRenderer.add(rewriteCascadeFormFieldArrayRef(renderTargetStep, subForm, newIndex)); } subRenderer.add(renderForm(renderTargetStep, subForm, newIndex)); // hide the template DOM if (i < 0) { subRenderer.add(":root", hideCascadeFormTemplateDOM(subFormType)); } subRendererList.add(subRenderer); } containerSelector = rewriteArrayIndexPlaceHolder(containerSelector, indexes); render.add(containerSelector, subRendererList); } else {// a simple cascade form if (StringUtils.isNotEmpty(containerSelector)) { render.add(containerSelector, renderForm(renderTargetStep, v, indexes)); } else { render.add(renderForm(renderTargetStep, v, indexes)); } } continue; } if (v == null) { @SuppressWarnings("rawtypes") ContextDataHolder valueHolder; if (field.getField() != null) { valueHolder = InjectTrace.getInstanceInjectionTraceInfo(form, field.getField()); } else { valueHolder = InjectTrace.getInstanceInjectionTraceInfo(form, field.getSetter()); } if (valueHolder != null) { v = convertRawInjectionTraceDataToRenderingData(field.getName(), field.getType(), valueHolder.getFoundOriginalData()); } } BasicFormFlowTraitHelper.FieldRenderingInfo renderingInfo = BasicFormFlowTraitHelper.getRenderingInfo(this, field, indexes); // render.addDebugger("whole form before: " + field.getName()); if (renderForEdit(renderTargetStep, form, field.getName())) { render.add(renderingInfo.valueRenderer.renderForEdit(renderingInfo.editSelector, v)); } else { render.add(renderingInfo.valueRenderer.renderForDisplay(renderingInfo.editSelector, renderingInfo.displaySelector, v)); } } return render; } default String defaultDisplayElementSelectorForField(String fieldName) { return SelectorUtil.id(fieldName + "-display"); } default String defaultEditElementSelectorForField(String fieldName) { return SelectorUtil.attr("name", fieldName); } default Renderer hideCascadeFormTemplateDOM(Class<?> subFormType) { return Renderer.create(":root", new ElementSetter() { @Override public void set(Element elem) { String style = elem.attr("style"); if (StringUtils.isEmpty(style)) { style = "display:none"; } else { if (!style.endsWith(";")) { style += ";"; } style += "display:none"; } elem.attr("style", style); } }); } @SuppressWarnings("rawtypes") default Object createFormInstanceForCascadeFormArrayTemplate(Class subFormType) throws InstantiationException, IllegalAccessException { return subFormType.newInstance(); } /** * Sub classes could override this method to customize how to rewrite the array index for cascade array forms. * * @param renderTargetStep * @param form * @param indexes * @return */ default Renderer rewriteCascadeFormFieldArrayRef(final String renderTargetStep, final Object form, final int[] indexes) { final String[] targetAttrs = rewriteCascadeFormFieldArrayRefTargetAttrs(); String[] attrSelectors = new String[targetAttrs.length]; for (int i = 0; i < attrSelectors.length; i++) { attrSelectors[i] = SelectorUtil.attr(targetAttrs[i]); } return Renderer.create(StringUtils.join(attrSelectors, ","), new ElementSetter() { @Override public void set(Element elem) { String v; for (String attr : targetAttrs) { v = elem.attr(attr); if (StringUtils.isNotEmpty(v)) { elem.attr(attr, rewriteArrayIndexPlaceHolder(v, indexes)); } } } }); } /** * The attributes returned by this method will be rewritten for array index. * <p> * The default is {"id", "name", "cascade-ref", "cascade-ref-target", "cascade-ref-info-1", ..., "cascade-ref-info-9"}. * * @return */ default String[] rewriteCascadeFormFieldArrayRefTargetAttrs() { return BasicFormFlowTraitHelper.DefaultCascadeFormFieldArrayRefTargetAttrs; } /** * Sub classes should override this method to supply field prepare renderers. * * @return * @throws Exception */ default List<FormFieldPrepareRenderer> retrieveFieldPrepareRenderers(String renderTargetStep, Object form) { return new LinkedList<>(); } /** * Sub classes could override this method to customize how to handle the injection trace data for type unmatch errors. * * @param fieldName * @param fieldDataType * @param rawTraceData * @return */ default Object convertRawInjectionTraceDataToRenderingData(String fieldName, Class<?> fieldDataType, Object rawTraceData) { if (fieldDataType.isArray() && rawTraceData.getClass().isArray()) { return rawTraceData; } else if (rawTraceData.getClass().isArray()) {// but field data type is // not array if (Array.getLength(rawTraceData) > 0) { return Array.get(rawTraceData, 0); } else { return null; } } else { return rawTraceData; } } default Element retrieveClientCascadeUtilJsContent() { String exportName = clientCascadeUtilJsExportName(); if (exportName == null) { return null; } if (BasicFormFlowTraitHelper.ClientCascadeJsContentCache != null) { return BasicFormFlowTraitHelper.ClientCascadeJsContentCache.clone(); } StringBuilder jsContent = new StringBuilder(300); jsContent.append("<script>\n"); jsContent.append("var ").append(exportName).append("=(\n"); try (InputStream jsInput = clientCascadeUtilJsInputStream()) { jsContent.append(IOUtils.toString(jsInput, StandardCharsets.UTF_8)); } catch (IOException ex) { throw new RuntimeException(ex); } jsContent.append(")();\n"); jsContent.append("</script>"); BasicFormFlowTraitHelper.ClientCascadeJsContentCache = ElementUtil.parseAsSingle(jsContent.toString()); return BasicFormFlowTraitHelper.ClientCascadeJsContentCache.clone(); } default String clientCascadeUtilJsExportName() { return null; } default InputStream clientCascadeUtilJsInputStream() { String jsPath = "/com/astamuse/asta4d/web/form/js/ClientCascadeUtil.js"; return this.getClass().getClassLoader().getResourceAsStream(jsPath); } }