org.opensingular.form.wicket.util.WicketFormProcessing.java Source code

Java tutorial

Introduction

Here is the source code for org.opensingular.form.wicket.util.WicketFormProcessing.java

Source

/*
 * Copyright (C) 2016 Singular Studios (a.k.a Atom Tecnologia) - www.opensingular.com
 *
 * 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 org.opensingular.form.wicket.util;

import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.wicket.Component;
import org.apache.wicket.MarkupContainer;
import org.apache.wicket.MetaDataKey;
import org.apache.wicket.Page;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.markup.html.form.FormComponent;
import org.apache.wicket.markup.html.panel.FeedbackPanel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.util.visit.IVisit;
import org.apache.wicket.util.visit.Visits;
import org.opensingular.form.SIComposite;
import org.opensingular.form.SInstance;
import org.opensingular.form.SInstances;
import org.opensingular.form.SType;
import org.opensingular.form.STypeList;
import org.opensingular.form.document.SDocument;
import org.opensingular.form.event.ISInstanceListener.EventCollector;
import org.opensingular.form.validation.InstanceValidationContext;
import org.opensingular.form.validation.ValidationErrorLevel;
import org.opensingular.form.wicket.SValidationFeedbackHandler;
import org.opensingular.form.wicket.WicketBuildContext;
import org.opensingular.lib.commons.lambda.IConsumer;
import org.opensingular.lib.commons.util.Loggable;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.util.stream.Collectors.toSet;

/*
 * TODO: depois, acho que esta classe tem que deixar de ter mtodos estticos, e se tornar algo plugvel e estendvel,
 *  anlogo ao RequestCycle do Wicket.
 * @author ronaldtm
 */
public class WicketFormProcessing implements Loggable {

    public final static MetaDataKey<Boolean> MDK_SKIP_VALIDATION_ON_REQUEST = new MetaDataKey<Boolean>() {
    };
    private final static MetaDataKey<Boolean> MDK_PROCESSED = new MetaDataKey<Boolean>() {
    };
    public final static MetaDataKey<Boolean> MDK_FIELD_UPDATED = new MetaDataKey<Boolean>() {
    };

    private WicketFormProcessing() {
    }

    public static void onFormError(MarkupContainer container, AjaxRequestTarget target) {
        container.visitChildren((c, v) -> {
            if (c instanceof FeedbackPanel && ((FeedbackPanel) c).anyMessage()) {
                Optional.ofNullable(target).ifPresent(t -> t.add(c));
            } else if (c.hasFeedbackMessage()) {
                refreshComponentOrCellContainer(target, c);
            }
        });
    }

    public static boolean onFormSubmit(MarkupContainer container, AjaxRequestTarget target,
            IModel<? extends SInstance> baseInstance, boolean validate) {
        return processAndPrepareForm(container, target, baseInstance, validate);
    }

    public static boolean onFormPrepare(MarkupContainer container, IModel<? extends SInstance> baseInstance,
            boolean validate) {
        return processAndPrepareForm(container, null, baseInstance, validate);
    }

    private static boolean processAndPrepareForm(MarkupContainer container, AjaxRequestTarget target,
            IModel<? extends SInstance> baseInstanceModel, boolean validate) {

        final Function<Boolean, Boolean> setAndReturn = (value) -> {
            RequestCycle.get().setMetaData(MDK_PROCESSED, value);
            return value;
        };

        if (RequestCycle.get().getMetaData(MDK_PROCESSED) == null) {
            if (baseInstanceModel == null) {
                return setAndReturn.apply(Boolean.FALSE);
            }

            final SInstance baseInstance = baseInstanceModel.getObject();
            final SDocument document = baseInstance.getDocument();

            // Validao do valor do componente
            boolean hasErrors = false;
            if (validate) {
                InstanceValidationContext validationContext = new InstanceValidationContext();
                validationContext.validateAll(baseInstance);
                if (validationContext.hasErrorsAboveLevel(ValidationErrorLevel.ERROR)) {
                    hasErrors = true;
                    refreshComponentOrCellContainer(target, container);
                }
            }

            updateValidationFeedbackOnDescendants(target, container);

            if (hasErrors)
                return setAndReturn.apply(Boolean.FALSE);

            // atualizar documento e recuperar instancias com atributos alterados
            document.updateAttributes(baseInstance, null);

            // re-renderizar form
            refreshComponentOrCellContainer(target, container);
        }
        return setAndReturn.apply(Boolean.TRUE);
    }

    /**
     * Forma uma chava apartir dos indexes de lista
     *
     * @param path da instancia
     * @return chaves concatenadas
     */
    private static String getIndexesKey(String path) {

        final Pattern indexFinder = Pattern.compile("(\\[\\d\\])");
        final Pattern bracketsFinder = Pattern.compile("[\\[\\]]");

        final Matcher matcher = indexFinder.matcher(path);
        final StringBuilder key = new StringBuilder();

        while (matcher.find()) {
            key.append(bracketsFinder.matcher(matcher.group()).replaceAll(StringUtils.EMPTY));
        }

        return key.toString();
    }

    public static void onFieldValidate(FormComponent<?> formComponent, AjaxRequestTarget target,
            IModel<? extends SInstance> fieldInstance) {

        if (fieldInstance == null || fieldInstance.getObject() == null)
            return;

        if (isSkipValidationOnRequest())
            return;

        // Validao do valor do componente
        final InstanceValidationContext validationContext = new InstanceValidationContext();
        validationContext.validateSingle(fieldInstance.getObject());
        SValidationFeedbackHandler.findNearest(formComponent).ifPresent(it -> it.updateValidationMessages(target));
    }

    /**
     * Executa o update listener dos tipos depentens da instancia informada, sendo chamada recursivamente para os tipos
     * que foram atualizados.
     * <p>
     * Motivao: Tendo um tipo composto com tres tipos filhos (a,b e c),
     * onde "b"  dependente de "a" e "c"  dependente de "b", "b" possui update listener que modifica o seu valor,
     * e "c" ser visivel se o valor de "b" no for nulo.  Ao atualizar "a"  necessario executar o listener dos seus
     * tipos dependentes("b") e tambm dos tipos dependentes do seu dependente("c") para que a avaliao de visibilidade
     * seja avaliada corretamente.
     *
     * @param i instancia a ser avaliada
     * @see <a href="https://www.pivotaltracker.com/story/show/131103577">[#131103577]</a>
     */
    private static Set<SInstance> evaluateUpdateListenersAndCollect(SInstance i) {
        return SInstances.streamDescendants(i.getRoot(), true).filter(isDependantOf(i))
                .filter(WicketFormProcessing::isNotOrphan)
                .filter(dependant -> isNotInListOrIsBothInSameList(i, dependant)).map(dependant -> {
                    List<SInstance> instances = new ArrayList<>();
                    IConsumer<SInstance> updateListener = dependant.asAtr().getUpdateListener();
                    if (updateListener != null) {
                        updateListener.accept(dependant);
                    }
                    instances.add(dependant);
                    instances.addAll(WicketFormProcessing.evaluateUpdateListenersAndCollect(dependant));
                    return instances;
                }).flatMap(Collection::stream).collect(Collectors.toSet());
    }

    private static Predicate<SInstance> isDependantOf(SInstance i) {
        return (x) -> i.getType().getDependentTypes().contains(x.getType());
    }

    private static boolean isOrphan(SInstance i) {
        return !(i instanceof SIComposite) && i.getParent() == null;
    }

    private static boolean isNotOrphan(SInstance i) {
        return !isOrphan(i);
    }

    /**
     * Busca todas as instancias dependentes da instancia informada e executa o update listener.
     * Apos a execuo ira procurar os componente de tela vinculados as instancias atualizadas
     * e atulizar via ajax
     *
     * @param page     a pagina
     * @param target   o ajaxtarget
     * @param instance a instancia
     */
    public static void processDependentTypes(Page page, AjaxRequestTarget target, SInstance instance) {
        updateBoundComponents(page, target, new HashSet<>(evaluateUpdateListenersAndCollect(instance)));
    }

    /**
     * Atualiza todos os componentes vinculados as instancias informadas
     */
    private static void updateBoundComponents(Page page, AjaxRequestTarget target, Set<SInstance> instances) {
        page.visitChildren(Component.class, new SInstanceBoudComponentUpdateVisitor(target, instances));
    }

    public static void onFieldProcess(Component component, AjaxRequestTarget target,
            IModel<? extends SInstance> model) {

        SInstance instance;

        if (model == null || (instance = model.getObject()) == null || target == null) {
            return;
        }

        validate(component, target, instance);

        Set<SInstance> updatedInstances = evaluateUpdateListenersAndCollect(instance);

        EventCollector eventCollector = new EventCollector();

        updateAttributes(instance, eventCollector);

        Set<SInstance> instancesToUpdateComponents = new HashSet<>();

        instancesToUpdateComponents.addAll(eventCollector.getEventSourceInstances());
        instancesToUpdateComponents.addAll(updatedInstances);

        updateBoundComponents(component.getPage(), target, instancesToUpdateComponents);
    }

    private static boolean isNotInListOrIsBothInSameList(SInstance a, SInstance b) {
        final String pathA = pathFromList(a);
        final String pathB = pathFromList(b);
        return !(pathA != null && pathB != null && Objects.equals(pathA, pathB))
                || Objects.equals(getIndexesKey(b.getPathFull()), getIndexesKey(a.getPathFull()));
    }

    private static String pathFromList(SInstance i) {
        return SInstances.findAncestor(i, STypeList.class).map(SInstance::getPathFull).orElse(null);
    }

    private static void validate(Component component, AjaxRequestTarget target, SInstance fieldInstance) {
        if (!isSkipValidationOnRequest()) {

            final InstanceValidationContext validationContext;

            // Validao do valor do componente
            validationContext = new InstanceValidationContext();
            validationContext.validateSingle(fieldInstance);

            // limpa erros de instancias dependentes, e limpa o valor caso de este no seja vlido para o provider
            for (SType<?> dependentType : fieldInstance.getType().getDependentTypes()) {
                fieldInstance.findNearest(dependentType).ifPresent(it -> {
                    it.getDocument().clearValidationErrors(it.getId());
                    //Executa validaes que dependem do valor preenchido
                    if (!it.isEmptyOfData()) {
                        validationContext.validateSingle(it);
                    }
                });
            }

            WicketBuildContext.findNearest(component).flatMap(ctx -> Optional.of(ctx.getRootContext()))
                    .flatMap(ctx -> Optional.of(
                            Stream.builder().add(ctx.getRootContainer()).add(ctx.getExternalContainer()).build()))
                    .ifPresent(containers -> containers.forEach(container -> {
                        updateValidationFeedbackOnDescendants(target, (MarkupContainer) container);
                    }));
        }
    }

    private static boolean isSkipValidationOnRequest() {
        return RequestCycle.get().getMetaData(MDK_SKIP_VALIDATION_ON_REQUEST) != null
                && RequestCycle.get().getMetaData(MDK_SKIP_VALIDATION_ON_REQUEST);
    }

    private static void updateAttributes(final SInstance fieldInstance, EventCollector eventCollector) {
        final SDocument document = fieldInstance.getDocument();
        document.updateAttributes(eventCollector);
    }

    public static void refreshComponentOrCellContainer(AjaxRequestTarget target, Component component) {
        if (target != null && component != null) {
            component.getRequestCycle().setMetaData(MDK_FIELD_UPDATED, Boolean.TRUE);
            target.add(WicketFormUtils.resolveRefreshingComponent(
                    ObjectUtils.defaultIfNull(WicketFormUtils.getCellContainer(component), component)));
        }
    }

    public static void updateValidationFeedbackOnDescendants(AjaxRequestTarget target, MarkupContainer container) {

        if (container != null) {
            Visits.visitPostOrder(container, (Component comp, IVisit<Void> visit) -> {
                if (SValidationFeedbackHandler.isBound(comp)) {
                    SValidationFeedbackHandler.get(comp).updateValidationMessages(target);
                }
            });
        }
    }
}