de.metas.ui.web.view.SqlViewFactory.java Source code

Java tutorial

Introduction

Here is the source code for de.metas.ui.web.view.SqlViewFactory.java

Source

package de.metas.ui.web.view;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import javax.annotation.Nullable;

import org.adempiere.ad.expression.api.NullStringExpression;
import org.adempiere.exceptions.AdempiereException;
import org.slf4j.Logger;
import org.springframework.stereotype.Service;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;

import de.metas.cache.CCache;
import de.metas.logging.LogManager;
import de.metas.ui.web.document.filter.DocumentFilter;
import de.metas.ui.web.document.filter.DocumentFilter.Builder;
import de.metas.ui.web.document.filter.DocumentFilterDescriptor;
import de.metas.ui.web.document.filter.DocumentFilterDescriptorsProvider;
import de.metas.ui.web.document.filter.DocumentFilterParam;
import de.metas.ui.web.document.filter.DocumentFilterParam.Operator;
import de.metas.ui.web.document.filter.DocumentFilterParamDescriptor;
import de.metas.ui.web.document.filter.DocumentFiltersList;
import de.metas.ui.web.document.filter.sql.SqlDocumentFilterConverterDecorator;
import de.metas.ui.web.view.descriptor.SqlViewBinding;
import de.metas.ui.web.view.descriptor.SqlViewRowFieldBinding;
import de.metas.ui.web.view.descriptor.SqlViewRowFieldBinding.SqlViewRowFieldLoader;
import de.metas.ui.web.view.descriptor.ViewLayout;
import de.metas.ui.web.view.json.JSONViewDataType;
import de.metas.ui.web.window.datatypes.DocumentPath;
import de.metas.ui.web.window.datatypes.Values;
import de.metas.ui.web.window.datatypes.WindowId;
import de.metas.ui.web.window.descriptor.DocumentEntityDescriptor;
import de.metas.ui.web.window.descriptor.DocumentFieldDescriptor.Characteristic;
import de.metas.ui.web.window.descriptor.DocumentFieldWidgetType;
import de.metas.ui.web.window.descriptor.LookupDescriptor;
import de.metas.ui.web.window.descriptor.factory.DocumentDescriptorFactory;
import de.metas.ui.web.window.descriptor.sql.DocumentFieldValueLoader;
import de.metas.ui.web.window.descriptor.sql.SqlDocumentEntityDataBindingDescriptor;
import de.metas.ui.web.window.descriptor.sql.SqlDocumentFieldDataBindingDescriptor;
import de.metas.ui.web.window.model.DocumentReference;
import de.metas.ui.web.window.model.DocumentReferencesService;
import de.metas.util.Check;
import de.metas.util.time.SystemTime;
import lombok.NonNull;
import lombok.Value;

/*
 * #%L
 * metasfresh-webui-api
 * %%
 * Copyright (C) 2017 metas GmbH
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this program. If not, see
 * <http://www.gnu.org/licenses/gpl-2.0.html>.
 * #L%
 */

/**
 * View factory which is based on {@link DocumentEntityDescriptor} having SQL repository.<br>
 * Creates {@link DefaultView}s with are backed by a {@link SqlViewBinding}.
 *
 * @author metas-dev <dev@metasfresh.com>
 *
 */
@Service
public class SqlViewFactory implements IViewFactory {
    private static final Logger logger = LogManager.getLogger(SqlViewFactory.class);

    private final DocumentDescriptorFactory documentDescriptorFactory;
    private final DocumentReferencesService documentReferencesService;

    private final ImmutableMap<WindowId, SqlDocumentFilterConverterDecorator> windowId2SqlDocumentFilterConverterDecorator;
    private final ImmutableMap<WindowId, IViewInvalidationAdvisor> viewInvalidationAdvisorsByWindowId;

    @Value
    private static final class SqlViewBindingKey {
        @NonNull
        private final WindowId windowId;
        @Nullable
        private final Characteristic requiredFieldCharacteristic;
        @Nullable
        private final ViewProfileId profileId;
    }

    private final transient CCache<SqlViewBindingKey, ViewLayout> viewLayouts = CCache.newCache("SqlViewLayouts",
            20, 0);
    private final transient CCache<SqlViewBindingKey, SqlViewBinding> viewBindings = CCache
            .newCache("SqlViewBindings", 20, 0);

    private final ImmutableListMultimap<WindowId, ViewProfile> viewProfiles;
    private final ImmutableMap<WindowId, ImmutableMap<ViewProfileId, SqlViewCustomizer>> viewCustomizers;
    private final CompositeDefaultViewProfileIdProvider defaultProfileIdProvider;

    public SqlViewFactory(@NonNull final DocumentDescriptorFactory documentDescriptorFactory,
            @NonNull final DocumentReferencesService documentReferencesService,
            @NonNull final List<SqlViewCustomizer> viewCustomizers,
            @NonNull final List<DefaultViewProfileIdProvider> defaultViewProfileIdProviders,
            @NonNull final List<SqlDocumentFilterConverterDecorator> converterDecorators,
            @NonNull final List<IViewInvalidationAdvisor> viewInvalidationAdvisors) {
        this.documentDescriptorFactory = documentDescriptorFactory;
        this.documentReferencesService = documentReferencesService;

        this.windowId2SqlDocumentFilterConverterDecorator = makeDecoratorsMapAndHandleDuplicates(
                converterDecorators);
        logger.info("Filter converter decorators: {}", windowId2SqlDocumentFilterConverterDecorator);

        this.viewProfiles = makeViewProfilesMap(viewCustomizers);
        logger.info("View profiles: {}", this.viewProfiles);

        this.viewCustomizers = makeViewCustomizersMap(viewCustomizers);
        logger.info("View customizers: {}", this.viewCustomizers);

        this.defaultProfileIdProvider = makeDefaultProfileIdProvider(defaultViewProfileIdProviders,
                viewCustomizers);
        logger.info("Default ProfileId providers: {}", this.defaultProfileIdProvider);

        this.viewInvalidationAdvisorsByWindowId = makeViewInvalidationAdvisorsMap(viewInvalidationAdvisors);
        logger.info("view invalidation advisors: {}", this.viewInvalidationAdvisorsByWindowId);
    }

    private static ImmutableListMultimap<WindowId, ViewProfile> makeViewProfilesMap(
            Collection<SqlViewCustomizer> viewCustomizers) {
        return viewCustomizers.stream().collect(ImmutableListMultimap.toImmutableListMultimap(
                viewCustomizer -> viewCustomizer.getWindowId(), viewCustomizer -> viewCustomizer.getProfile()));
    }

    private static ImmutableMap<WindowId, ImmutableMap<ViewProfileId, SqlViewCustomizer>> makeViewCustomizersMap(
            Collection<SqlViewCustomizer> viewCustomizers) {
        final Map<WindowId, ImmutableMap<ViewProfileId, SqlViewCustomizer>> map = viewCustomizers.stream()
                .sorted(SqlViewCustomizerUtils.ORDERED_COMPARATOR)
                .collect(Collectors.groupingBy(SqlViewCustomizer::getWindowId,
                        ImmutableMap.toImmutableMap(viewCustomizer -> viewCustomizer.getProfile().getProfileId(),
                                viewCustomizer -> viewCustomizer)));
        return ImmutableMap.copyOf(map);
    }

    private static CompositeDefaultViewProfileIdProvider makeDefaultProfileIdProvider(
            final List<DefaultViewProfileIdProvider> providers,
            final List<SqlViewCustomizer> viewCustomizersToExtractFallbacks) {
        final CompositeDefaultViewProfileIdProvider result = CompositeDefaultViewProfileIdProvider.of(providers);

        viewCustomizersToExtractFallbacks.stream().sorted(SqlViewCustomizerUtils.ORDERED_COMPARATOR)
                .forEach(viewCustomizer -> result.setDefaultProfileIdFallbackIfAbsent(viewCustomizer.getWindowId(),
                        viewCustomizer.getProfile().getProfileId()));

        return result;
    }

    private static ImmutableMap<WindowId, SqlDocumentFilterConverterDecorator> makeDecoratorsMapAndHandleDuplicates(
            @NonNull final Collection<SqlDocumentFilterConverterDecorator> providers) {
        try {
            return Maps.uniqueIndex(providers, SqlDocumentFilterConverterDecorator::getWindowId);
        } catch (IllegalArgumentException e) {
            final String message = "The given collection of SqlDocumentFilterConverterDecoratorProvider implementors contains more than one element with the same window-id";
            throw new AdempiereException(message, e)
                    .setParameter("sqlDocumentFilterConverterDecoratorProviders", providers)
                    .appendParametersToMessage();
        }
    }

    private static ImmutableMap<WindowId, IViewInvalidationAdvisor> makeViewInvalidationAdvisorsMap(
            final List<IViewInvalidationAdvisor> viewInvalidationAdvisors) {
        try {
            return Maps.uniqueIndex(viewInvalidationAdvisors, advisor -> {
                final WindowId windowId = advisor.getWindowId();
                Check.assumeNotNull(windowId, "windowId shall not be null for {}", advisor);
                return windowId;
            });
        } catch (IllegalArgumentException e) {
            final String message = "The given collection of " + IViewInvalidationAdvisor.class
                    + " implementors contains more than one element with the same window-id";
            throw new AdempiereException(message, e)
                    .setParameter("viewInvalidationAdvisors", viewInvalidationAdvisors).appendParametersToMessage();
        }
    }

    @Override
    public List<ViewProfile> getAvailableProfiles(final WindowId windowId) {
        return viewProfiles.get(windowId);
    }

    private ViewProfileId getDefaultProfileIdByWindowId(final WindowId windowId) {
        return defaultProfileIdProvider.getDefaultProfileIdByWindowId(windowId);
    }

    public void setDefaultProfileId(@NonNull final WindowId windowId, final ViewProfileId profileId) {
        defaultProfileIdProvider.setDefaultProfileIdOverride(windowId, profileId);
    }

    private SqlViewCustomizer getSqlViewCustomizer(@NonNull final WindowId windowId,
            final ViewProfileId profileId) {
        if (ViewProfileId.isNull(profileId)) {
            return null;
        }

        final ImmutableMap<ViewProfileId, SqlViewCustomizer> viewCustomizersByProfileId = viewCustomizers
                .get(windowId);
        if (viewCustomizersByProfileId == null) {
            return null;
        }

        return viewCustomizersByProfileId.get(profileId);
    }

    @Override
    public ViewLayout getViewLayout(@NonNull final WindowId windowId, @NonNull final JSONViewDataType viewDataType,
            @Nullable final ViewProfileId profileId) {
        final ViewProfileId profileIdEffective = !ViewProfileId.isNull(profileId) ? profileId
                : getDefaultProfileIdByWindowId(windowId);
        final SqlViewBindingKey sqlViewBindingKey = new SqlViewBindingKey(windowId,
                viewDataType.getRequiredFieldCharacteristic(), profileIdEffective);
        return viewLayouts.getOrLoad(sqlViewBindingKey, () -> createViewLayout(sqlViewBindingKey, viewDataType));
    }

    private ViewLayout createViewLayout(final SqlViewBindingKey sqlViewBindingKey,
            final JSONViewDataType viewDataType) {
        final ViewLayout viewLayoutOrig = documentDescriptorFactory
                .getDocumentDescriptor(sqlViewBindingKey.getWindowId()).getViewLayout(viewDataType);

        final SqlViewBinding sqlViewBinding = getViewBinding(sqlViewBindingKey);
        final Collection<DocumentFilterDescriptor> filters = sqlViewBinding.getViewFilterDescriptors().getAll();
        final boolean hasTreeSupport = sqlViewBinding.hasGroupingFields();

        final ViewLayout.ChangeBuilder viewLayoutBuilder = viewLayoutOrig.toBuilder()
                .profileId(sqlViewBindingKey.getProfileId()).filters(filters)
                .treeSupport(hasTreeSupport, true/* treeCollapsible */, ViewLayout.TreeExpandedDepth_AllCollapsed);

        final SqlViewCustomizer sqlViewCustomizer = getSqlViewCustomizer(sqlViewBindingKey.getWindowId(),
                sqlViewBindingKey.getProfileId());
        if (sqlViewCustomizer != null) {
            sqlViewCustomizer.customizeViewLayout(viewLayoutBuilder);
        }

        return viewLayoutBuilder.build();
    }

    @Override
    public IView createView(final CreateViewRequest request) {
        final WindowId windowId = request.getViewId().getWindowId();

        final JSONViewDataType viewType = request.getViewType();
        final ViewProfileId profileId = !ViewProfileId.isNull(request.getProfileId()) ? request.getProfileId()
                : getDefaultProfileIdByWindowId(windowId);
        final SqlViewBindingKey sqlViewBindingKey = new SqlViewBindingKey(windowId,
                viewType.getRequiredFieldCharacteristic(), profileId);

        final SqlViewBinding sqlViewBinding = getViewBinding(sqlViewBindingKey);
        final IViewDataRepository viewDataRepository = new SqlViewDataRepository(sqlViewBinding);

        final DefaultView.Builder viewBuilder = DefaultView.builder(viewDataRepository)
                .setViewId(request.getViewId()).setViewType(viewType).setProfileId(profileId)
                .setReferencingDocumentPaths(request.getReferencingDocumentPaths())
                .setParentViewId(request.getParentViewId()).setParentRowId(request.getParentRowId())
                .addStickyFilters(request.getStickyFilters())
                .addStickyFilter(
                        extractReferencedDocumentFilter(windowId, request.getSingleReferencingDocumentPathOrNull()))
                .viewInvalidationAdvisor(sqlViewBinding.getViewInvalidationAdvisor())
                .applySecurityRestrictions(request.isApplySecurityRestrictions());

        final DocumentFiltersList filters = request.getFilters();
        if (filters.isJson()) {
            viewBuilder.setFiltersFromJSON(filters.getJsonFilters());
        } else {
            viewBuilder.setFilters(filters.getFilters());
        }

        if (request.isUseAutoFilters()) {
            final List<DocumentFilter> autoFilters = createAutoFilters(sqlViewBindingKey);
            viewBuilder.addFiltersIfAbsent(autoFilters);
        }

        if (!request.getFilterOnlyIds().isEmpty()) {
            final String keyColumnName = sqlViewBinding.getSqlViewKeyColumnNamesMap().getSingleKeyColumnName();
            viewBuilder.addStickyFilter(
                    DocumentFilter.inArrayFilter(keyColumnName, keyColumnName, request.getFilterOnlyIds()));
        }

        return viewBuilder.build();
    }

    private final DocumentFilter extractReferencedDocumentFilter(final WindowId targetWindowId,
            final DocumentPath referencedDocumentPath) {
        if (referencedDocumentPath == null) {
            return null;
        } else {
            final DocumentReference reference = documentReferencesService
                    .getDocumentReference(referencedDocumentPath, targetWindowId);
            return reference.getFilter();
        }
    }

    private List<DocumentFilter> createAutoFilters(final SqlViewBindingKey sqlViewBindingKey) {
        final SqlViewBinding sqlViewBinding = getViewBinding(sqlViewBindingKey);
        final Collection<DocumentFilterDescriptor> filters = sqlViewBinding.getViewFilterDescriptors().getAll();

        return filters.stream().filter(DocumentFilterDescriptor::isAutoFilter).map(SqlViewFactory::createAutoFilter)
                .collect(ImmutableList.toImmutableList());
    }

    private static DocumentFilter createAutoFilter(final DocumentFilterDescriptor filterDescriptor) {
        if (!filterDescriptor.isAutoFilter()) {
            throw new AdempiereException("Not an auto filter: " + filterDescriptor);
        }

        final Builder filterBuilder = DocumentFilter.builder().setFilterId(filterDescriptor.getFilterId());

        filterDescriptor.getParameters().stream().filter(DocumentFilterParamDescriptor::isAutoFilter)
                .map(SqlViewFactory::createAutoFilterParam).forEach(filterBuilder::addParameter);

        return filterBuilder.build();
    }

    private static final DocumentFilterParam createAutoFilterParam(
            final DocumentFilterParamDescriptor filterParamDescriptor) {
        final Object value;
        if (filterParamDescriptor.isAutoFilterInitialValueIsDateNow()) {
            final DocumentFieldWidgetType widgetType = filterParamDescriptor.getWidgetType();
            if (widgetType == DocumentFieldWidgetType.Date) {
                value = SystemTime.asDayTimestamp();
            } else {
                value = SystemTime.asTimestamp();
            }
        } else {
            value = filterParamDescriptor.getAutoFilterInitialValue();
        }

        return DocumentFilterParam.builder().setFieldName(filterParamDescriptor.getFieldName())
                .setOperator(Operator.EQUAL).setValue(value).build();
    }

    private SqlViewBinding getViewBinding(@NonNull final SqlViewBindingKey key) {
        return viewBindings.getOrLoad(key, () -> createViewBinding(key));
    }

    private SqlViewBinding createViewBinding(@NonNull final SqlViewBindingKey key) {
        final WindowId windowId = key.getWindowId();
        final DocumentEntityDescriptor entityDescriptor = documentDescriptorFactory
                .getDocumentEntityDescriptor(windowId);
        final Set<String> displayFieldNames = entityDescriptor
                .getFieldNamesWithCharacteristic(key.getRequiredFieldCharacteristic());
        final SqlDocumentEntityDataBindingDescriptor entityBinding = SqlDocumentEntityDataBindingDescriptor
                .cast(entityDescriptor.getDataBinding());
        final DocumentFilterDescriptorsProvider filterDescriptors = entityDescriptor.getFilterDescriptors();

        final SqlViewBinding.Builder builder = createBuilderForEntityBindingAndFieldNames(entityBinding,
                displayFieldNames).filterDescriptors(filterDescriptors)
                        .viewInvalidationAdvisor(getViewInvalidationAdvisor(windowId));

        if (windowId2SqlDocumentFilterConverterDecorator.containsKey(windowId)) {
            builder.filterConverterDecorator(windowId2SqlDocumentFilterConverterDecorator.get(windowId));
        }

        final SqlViewCustomizer sqlViewCustomizer = getSqlViewCustomizer(windowId, key.getProfileId());
        if (sqlViewCustomizer != null) {
            final ViewRowCustomizer rowCustomizer = sqlViewCustomizer;
            builder.rowCustomizer(rowCustomizer);

            sqlViewCustomizer.customizeSqlViewBinding(builder);
        }

        return builder.build();
    }

    private SqlViewBinding.Builder createBuilderForEntityBindingAndFieldNames(
            @NonNull final SqlDocumentEntityDataBindingDescriptor entityBinding,
            @NonNull final Set<String> displayFieldNames) {
        final SqlViewBinding.Builder builder = createBuilderForEntityBinding(entityBinding);

        entityBinding.getFields().stream()
                .map(documentField -> createViewFieldBinding(documentField, displayFieldNames))
                .forEach(builder::field);
        builder.displayFieldNames(displayFieldNames);
        return builder;
    }

    private SqlViewBinding.Builder createBuilderForEntityBinding(
            @NonNull final SqlDocumentEntityDataBindingDescriptor entityBinding) {
        final SqlViewBinding.Builder builder = SqlViewBinding.builder().tableName(entityBinding.getTableName())
                .tableAlias(entityBinding.getTableAlias()).sqlWhereClause(entityBinding.getSqlWhereClause())
                .defaultOrderBys(entityBinding.getDefaultOrderBys());
        return builder;
    }

    private static final SqlViewRowFieldBinding createViewFieldBinding(
            final SqlDocumentFieldDataBindingDescriptor documentField,
            final Collection<String> availableDisplayColumnNames) {
        return createViewFieldBindingBuilder(documentField, availableDisplayColumnNames).build();
    }

    public static final SqlViewRowFieldBinding.SqlViewRowFieldBindingBuilder createViewFieldBindingBuilder(
            @NonNull final SqlDocumentFieldDataBindingDescriptor documentField,
            @NonNull final Collection<String> availableDisplayColumnNames) {
        final String fieldName = documentField.getFieldName();
        final boolean isDisplayColumnAvailable = documentField.isUsingDisplayColumn()
                && availableDisplayColumnNames.contains(fieldName);

        return SqlViewRowFieldBinding.builder().fieldName(fieldName).columnName(documentField.getColumnName())
                .columnSql(documentField.getColumnSql()).keyColumn(documentField.isKeyColumn())
                .widgetType(documentField.getWidgetType()).virtualColumn(documentField.isVirtualColumn())
                //
                .sqlValueClass(documentField.getSqlValueClass()).sqlSelectValue(documentField.getSqlSelectValue())
                .usingDisplayColumn(isDisplayColumnAvailable)
                .sqlSelectDisplayValue(isDisplayColumnAvailable ? documentField.getSqlSelectDisplayValue()
                        : NullStringExpression.instance)
                //
                .sqlOrderBy(documentField.getSqlOrderBy())
                //
                .fieldLoader(new DocumentFieldValueLoaderAsSqlViewRowFieldLoader(
                        documentField.getDocumentFieldValueLoader(), isDisplayColumnAvailable))
        //
        ;
    }

    private IViewInvalidationAdvisor getViewInvalidationAdvisor(final WindowId windowId) {
        return viewInvalidationAdvisorsByWindowId.getOrDefault(windowId, DefaultViewInvalidationAdvisor.instance);
    }

    @Value
    private static final class DocumentFieldValueLoaderAsSqlViewRowFieldLoader implements SqlViewRowFieldLoader {
        private final @NonNull DocumentFieldValueLoader fieldValueLoader;
        private final boolean isDisplayColumnAvailable;

        @Override
        public Object retrieveValueAsJson(@NonNull final ResultSet rs, final String adLanguage)
                throws SQLException {
            final Object fieldValue = fieldValueLoader.retrieveFieldValue(rs, isDisplayColumnAvailable, adLanguage,
                    (LookupDescriptor) null);
            return Values.valueToJsonObject(fieldValue);
        }

    }
}