From 23e5f30a09d94f8a91816c757c5327e97e4b9b81 Mon Sep 17 00:00:00 2001 From: Teo Sarca Date: Wed, 3 May 2017 12:41:27 +0300 Subject: [PATCH] HUEditorViewBuffer_HighVolume prototype https://github.com/metasfresh/metasfresh-webui-api/issues/330 --- .../ui/web/handlingunits/HUEditorView.java | 57 ++- .../HUEditorViewBuffer_HighVolume.java | 369 ++++++++++++++++++ .../handlingunits/HUEditorViewFactory.java | 39 +- .../SqlViewSelectionQueryBuilder.java | 19 + 4 files changed, 467 insertions(+), 17 deletions(-) create mode 100644 src/main/java/de/metas/ui/web/handlingunits/HUEditorViewBuffer_HighVolume.java diff --git a/src/main/java/de/metas/ui/web/handlingunits/HUEditorView.java b/src/main/java/de/metas/ui/web/handlingunits/HUEditorView.java index 3a0d83f48..b54154bb6 100644 --- a/src/main/java/de/metas/ui/web/handlingunits/HUEditorView.java +++ b/src/main/java/de/metas/ui/web/handlingunits/HUEditorView.java @@ -31,6 +31,7 @@ import de.metas.ui.web.view.IView; import de.metas.ui.web.view.ViewId; import de.metas.ui.web.view.ViewResult; +import de.metas.ui.web.view.descriptor.SqlViewBinding; import de.metas.ui.web.view.event.ViewChangesCollector; import de.metas.ui.web.window.datatypes.DocumentId; import de.metas.ui.web.window.datatypes.DocumentPath; @@ -39,6 +40,7 @@ import de.metas.ui.web.window.descriptor.DocumentFieldWidgetType; import de.metas.ui.web.window.model.DocumentQueryOrderBy; import de.metas.ui.web.window.model.filters.DocumentFilter; +import de.metas.ui.web.window.model.filters.DocumentFilterParam.Operator; import lombok.NonNull; /* @@ -65,9 +67,9 @@ public class HUEditorView implements IView { - public static final Builder builder() + public static final Builder builder(final SqlViewBinding sqlViewBinding) { - return new Builder(); + return new Builder(sqlViewBinding); } public static HUEditorView cast(final IView view) @@ -89,8 +91,10 @@ private HUEditorView(final Builder builder) { parentViewId = builder.getParentViewId(); + final boolean isHighVolume = builder.isHighVolume(); + this.huAttributesProvider = HUEditorRowAttributesProvider.builder() - //.readonly(isHighVolume) + .readonly(isHighVolume) .build(); final HUEditorViewRepository huEditorRepo = HUEditorViewRepository.builder() @@ -99,16 +103,23 @@ private HUEditorView(final Builder builder) .attributesProvider(huAttributesProvider) .build(); -// final boolean isHighVolume = false; // TODO: high volume is work in progress -// if (isHighVolume) -// { -// final HUEditorViewBuffer_HighVolume rowsBuffer = new HUEditorViewBuffer_HighVolume(builder.getWindowId(), huEditorRepo); -// this.rowsBuffer = rowsBuffer; -// this.viewId = rowsBuffer.getViewId(); -// } -// else + final Collection huIds = builder.getHUIds(); + + if (isHighVolume) { - this.rowsBuffer = new HUEditorViewBuffer_FullyCached(huEditorRepo, builder.getHUIds()); + final ImmutableList.Builder filters = ImmutableList.builder(); + if(!huIds.isEmpty()) + { + filters.add(DocumentFilter.singleParameterFilter("huIds", I_M_HU.COLUMNNAME_M_HU_ID, Operator.IN_ARRAY, huIds)); + } + + final HUEditorViewBuffer_HighVolume rowsBuffer = new HUEditorViewBuffer_HighVolume(builder.getWindowId(), huEditorRepo, builder.getSqlViewBinding(), filters.build()); + this.rowsBuffer = rowsBuffer; + this.viewId = rowsBuffer.getViewId(); + } + else + { + this.rowsBuffer = new HUEditorViewBuffer_FullyCached(huEditorRepo, huIds); this.viewId = ViewId.random(builder.getWindowId()); } @@ -378,6 +389,7 @@ public SelectViewRowsAction actionSelectHUsByBarcode( // public static final class Builder { + private final SqlViewBinding sqlViewBinding; private ViewId parentViewId; private WindowId windowId; @@ -386,10 +398,11 @@ public static final class Builder private ViewActionDescriptorsList actions = ViewActionDescriptorsList.EMPTY; private Collection huIds; + private boolean highVolume; - private Builder() + private Builder(@NonNull final SqlViewBinding sqlViewBinding) { - super(); + this.sqlViewBinding = sqlViewBinding; } public HUEditorView build() @@ -397,6 +410,11 @@ public HUEditorView build() return new HUEditorView(this); } + private SqlViewBinding getSqlViewBinding() + { + return sqlViewBinding; + } + public Builder setParentViewId(final ViewId parentViewId) { this.parentViewId = parentViewId; @@ -433,6 +451,17 @@ private Collection getHUIds() } return huIds; } + + public Builder setHighVolume(boolean highVolume) + { + this.highVolume = highVolume; + return this; + } + + private boolean isHighVolume() + { + return highVolume; + } public Builder setReferencingDocumentPaths(final String referencingTableName, final Set referencingDocumentPaths) { diff --git a/src/main/java/de/metas/ui/web/handlingunits/HUEditorViewBuffer_HighVolume.java b/src/main/java/de/metas/ui/web/handlingunits/HUEditorViewBuffer_HighVolume.java new file mode 100644 index 000000000..8db0108e7 --- /dev/null +++ b/src/main/java/de/metas/ui/web/handlingunits/HUEditorViewBuffer_HighVolume.java @@ -0,0 +1,369 @@ +package de.metas.ui.web.handlingunits; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.UnaryOperator; +import java.util.stream.Stream; + +import org.adempiere.ad.dao.impl.EqualsQueryFilter; +import org.adempiere.ad.expression.api.IExpressionEvaluator.OnVariableNotFound; +import org.adempiere.ad.expression.api.IStringExpression; +import org.adempiere.ad.trx.api.ITrx; +import org.adempiere.exceptions.DBException; +import org.adempiere.model.PlainContextAware; +import org.adempiere.util.Services; +import org.compiere.util.CCache; +import org.compiere.util.DB; +import org.compiere.util.Env; + +import com.google.common.collect.ImmutableSet; + +import de.metas.handlingunits.IHUQueryBuilder; +import de.metas.handlingunits.IHandlingUnitsDAO; +import de.metas.handlingunits.attribute.Constants; +import de.metas.handlingunits.model.I_M_HU; +import de.metas.printing.esb.base.util.Check; +import de.metas.ui.web.exceptions.EntityNotFoundException; +import de.metas.ui.web.view.SqlViewRowIdsOrderedSelectionFactory; +import de.metas.ui.web.view.ViewEvaluationCtx; +import de.metas.ui.web.view.ViewId; +import de.metas.ui.web.view.ViewRowIdsOrderedSelection; +import de.metas.ui.web.view.descriptor.SqlViewBinding; +import de.metas.ui.web.window.datatypes.DocumentId; +import de.metas.ui.web.window.datatypes.WindowId; +import de.metas.ui.web.window.model.DocumentQueryOrderBy; +import de.metas.ui.web.window.model.filters.DocumentFilter; + +/* + * #%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 + * . + * #L% + */ + +public class HUEditorViewBuffer_HighVolume implements HUEditorViewBuffer +{ + private static final int STREAM_ALL_MAX_SIZE = 100; + + private final HUEditorViewRepository huEditorRepo; + private final IStringExpression sqlSelectHUIdsByPage; + + private final SqlViewRowIdsOrderedSelectionFactory viewSelectionFactory; + private final AtomicReference defaultSelectionRef; + + private final CCache cache_huRowsById = CCache.newLRUCache(I_M_HU.Table_Name + "#HUEditorRows#by#Id", 100, 2); + + HUEditorViewBuffer_HighVolume(final WindowId windowId // + , final HUEditorViewRepository huEditorRepo // + , final SqlViewBinding entityBinding // + , final List filters // + ) + { + this.huEditorRepo = huEditorRepo; + + viewSelectionFactory = SqlViewRowIdsOrderedSelectionFactory.of(entityBinding); + sqlSelectHUIdsByPage = entityBinding.getSqlSelectByPage(); + + final ViewEvaluationCtx viewEvalCtx = ViewEvaluationCtx.of(Env.getCtx()); + + final ViewRowIdsOrderedSelection defaultSelection = viewSelectionFactory.createOrderedSelection(viewEvalCtx, windowId, filters, entityBinding.getDefaultOrderBys()); + defaultSelectionRef = new AtomicReference<>(defaultSelection); + + } + + private ViewRowIdsOrderedSelection getDefaultSelection() + { + return defaultSelectionRef.get(); + } + + private boolean changeDefaultSelection(final UnaryOperator mapper) + { + final ViewRowIdsOrderedSelection defaultSelectionOld = defaultSelectionRef.get(); + final ViewRowIdsOrderedSelection defaultSelectionNew = defaultSelectionRef.updateAndGet(mapper); + return Objects.equals(defaultSelectionOld, defaultSelectionNew); + } + + public ViewId getViewId() + { + return getDefaultSelection().getViewId(); + } + + @Override + public long size() + { + return getDefaultSelection().getSize(); + } + + @Override + public void invalidateAll() + { + cache_huRowsById.clear(); + } + + @Override + public boolean addHUIds(final Collection huIdsToAdd) + { + if (huIdsToAdd == null || huIdsToAdd.isEmpty()) + { + return false; + } + + final Set rowIdsToAdd = HUEditorRow.rowIdsFromM_HU_IDs(huIdsToAdd); + if (rowIdsToAdd.isEmpty()) + { + return false; + } + + return changeDefaultSelection(defaultSelection -> viewSelectionFactory.addRowIdsToSelection(defaultSelection, rowIdsToAdd)); + } + + @Override + public boolean removeHUIds(final Collection huIdsToRemove) + { + if (huIdsToRemove == null || huIdsToRemove.isEmpty()) + { + return false; + } + + final Set rowIdsToRemove = HUEditorRow.rowIdsFromM_HU_IDs(huIdsToRemove); + + rowIdsToRemove.forEach(rowId -> cache_huRowsById.remove(rowId)); + + return changeDefaultSelection(defaultSelection -> viewSelectionFactory.removeRowIdsFromSelection(defaultSelection, rowIdsToRemove)); + } + + @Override + public boolean containsAnyOfHUIds(final Collection huIdsToCheck) + { + final Set rowIds = HUEditorRow.rowIdsFromM_HU_IDs(huIdsToCheck); + return viewSelectionFactory.containsAnyOfRowIds(getDefaultSelection().getSelectionId(), rowIds); + } + + @Override + public Stream streamAllRecursive() throws UnsupportedOperationException + { + final ViewRowIdsOrderedSelection defaultSelection = getDefaultSelection(); + if (defaultSelection.getSize() > STREAM_ALL_MAX_SIZE) + { + throw new UnsupportedOperationException("Streaming all rows when selection is bigger than " + STREAM_ALL_MAX_SIZE + " is not allowed"); + } + + return streamPage(0, STREAM_ALL_MAX_SIZE, defaultSelection.getOrderBys()) + .flatMap(row -> row.streamRecursive()); + } + + @Override + public Stream streamByIdsExcludingIncludedRows(final Collection rowIds) + { + if (rowIds == null || rowIds.isEmpty()) + { + return Stream.empty(); + } + + return streamByIds(rowIds); + } + + private Stream streamByIds(final Collection rowIds) + { + if (rowIds.isEmpty()) + { + return Stream.empty(); + } + + final HUEditorRow[] rows = new HUEditorRow[rowIds.size()]; + final Map rowIdToLoad2index = new HashMap<>(); + { + int idx = 0; + for (final DocumentId rowId : rowIds) + { + final HUEditorRow row = cache_huRowsById.get(rowId); + if (row == null) + { + // to be loaded + rowIdToLoad2index.put(rowId, idx); + } + else + { + rows[idx] = row; + } + + idx++; + } + } + + if (!rowIdToLoad2index.isEmpty()) + { + final Set huIds = HUEditorRow.rowIdsToM_HU_IDs(rowIdToLoad2index.keySet()); + huEditorRepo.retrieveHUEditorRows(huIds) + .forEach(row -> { + final DocumentId rowId = row.getId(); + final Integer idx = rowIdToLoad2index.remove(rowId); + if (idx == null) + { + // wtf?! we got some more then we requested?!? + return; + } + + rows[idx] = row; + + cache_huRowsById.put(rowId, row); + }); + } + + return Stream.of(rows) + .filter(row -> row != null); // just to make sure we won't stream some empty gaps (e.g. missing rows because HU was not a top level one) + } + + @Override + public Stream streamPage(final int firstRow, final int pageLength, final List orderBys) + { + final Set huIds = retrieveHUIdsByPage(firstRow, pageLength, orderBys); + final Set rowIds = HUEditorRow.rowIdsFromM_HU_IDs(huIds); + return streamByIds(rowIds); + } + + private ViewRowIdsOrderedSelection getSelection(final List orderBys) + { + final ViewRowIdsOrderedSelection defaultSelection = getDefaultSelection(); + + if (orderBys == null || orderBys.isEmpty()) + { + return defaultSelection; + } + + if (!Objects.equals(orderBys, defaultSelection.getOrderBys())) + { + throw new UnsupportedOperationException("Sorting is not supported"); + } + + return defaultSelection; + } + + private Set retrieveHUIdsByPage(final int firstRow, final int pageLength, final List orderBys) + { + final ViewRowIdsOrderedSelection selection = getSelection(orderBys); + final String viewSelectionId = selection.getSelectionId(); + final int firstSeqNo = firstRow + 1; // NOTE: firstRow is 0-based while SeqNo are 1-based + final int lastSeqNo = firstRow + pageLength; + + final ViewEvaluationCtx viewEvalCtx = ViewEvaluationCtx.of(Env.getCtx()); + final String sql = sqlSelectHUIdsByPage.evaluate(viewEvalCtx.toEvaluatee(), OnVariableNotFound.Fail); + final Object[] sqlParams = new Object[] { viewSelectionId, firstSeqNo, lastSeqNo }; + + PreparedStatement pstmt = null; + ResultSet rs = null; + try + { + pstmt = DB.prepareStatement(sql, ITrx.TRXNAME_ThreadInherited); + pstmt.setMaxRows(pageLength); + DB.setParameters(pstmt, sqlParams); + + rs = pstmt.executeQuery(); + + final Set huIds = new LinkedHashSet<>(); + while (rs.next()) + { + final int huId = rs.getInt(I_M_HU.COLUMNNAME_M_HU_ID); + if (huId <= 0) + { + continue; + } + huIds.add(huId); + } + + return huIds; + + } + catch (final SQLException ex) + { + throw DBException.wrapIfNeeded(ex) + .setSqlIfAbsent(sql, sqlParams); + } + } + + @Override + public HUEditorRow getById(final DocumentId rowId) throws EntityNotFoundException + { + final int huId = rowId.toInt(); + // FIXME: fails if not top level ... + return huEditorRepo.retrieveForHUId(huId); + } + + @Override + public Set getRowIdsMatchingBarcode(final String barcode) + { + if (Check.isEmpty(barcode, true)) + { + throw new IllegalArgumentException("Invalid barcode"); + } + + // + // Search by SSCC + { + final Set rowIdsBySSCC = createInSelectionHUQueryBuilder() + .addOnlyWithAttribute(Constants.ATTR_SSCC18_Value, barcode) + .createQuery() + .listIds() + .stream() + .map(huId -> HUEditorRow.rowIdFromM_HU_ID(huId)) + .collect(ImmutableSet.toImmutableSet()); + if (!rowIdsBySSCC.isEmpty()) + { + return rowIdsBySSCC; + } + } + + // + // Search by value + { + final Set rowIdsByValue = createInSelectionHUQueryBuilder() + .addFilter(new EqualsQueryFilter<>(I_M_HU.COLUMN_Value, barcode)) + .createQuery() + .listIds() + .stream() + .map(huId -> HUEditorRow.rowIdFromM_HU_ID(huId)) + .collect(ImmutableSet.toImmutableSet()); + if (!rowIdsByValue.isEmpty()) + { + return rowIdsByValue; + } + } + + return ImmutableSet.of(); + } + + private final IHUQueryBuilder createInSelectionHUQueryBuilder() + { + final String selectionId = getDefaultSelection().getSelectionId(); + final IHandlingUnitsDAO handlingUnitsDAO = Services.get(IHandlingUnitsDAO.class); + + return handlingUnitsDAO.createHUQueryBuilder() + .setContext(PlainContextAware.createUsingOutOfTransaction()) + .addFilter(viewSelectionFactory.createQueryFilter(selectionId)); + } + +} diff --git a/src/main/java/de/metas/ui/web/handlingunits/HUEditorViewFactory.java b/src/main/java/de/metas/ui/web/handlingunits/HUEditorViewFactory.java index 22dbe8ade..7acdfaff5 100644 --- a/src/main/java/de/metas/ui/web/handlingunits/HUEditorViewFactory.java +++ b/src/main/java/de/metas/ui/web/handlingunits/HUEditorViewFactory.java @@ -2,14 +2,21 @@ import java.util.Collection; import java.util.Set; +import java.util.function.Supplier; +import org.adempiere.util.lang.ExtendedMemorizingSupplier; import org.compiere.util.CCache; import org.compiere.util.Util.ArrayKey; import org.springframework.beans.factory.annotation.Autowired; +import com.google.common.collect.ImmutableList; + +import de.metas.handlingunits.model.I_M_HU; import de.metas.ui.web.view.IViewFactory; import de.metas.ui.web.view.ViewCreateRequest; import de.metas.ui.web.view.ViewFactory; +import de.metas.ui.web.view.descriptor.SqlViewBinding; +import de.metas.ui.web.view.descriptor.SqlViewRowFieldBinding; import de.metas.ui.web.view.descriptor.ViewLayout; import de.metas.ui.web.view.json.JSONViewDataType; import de.metas.ui.web.window.datatypes.DocumentPath; @@ -20,6 +27,7 @@ import de.metas.ui.web.window.descriptor.factory.DocumentDescriptorFactory; import de.metas.ui.web.window.descriptor.factory.standard.LayoutFactory; import de.metas.ui.web.window.descriptor.filters.DocumentFilterDescriptor; +import de.metas.ui.web.window.model.DocumentQueryOrderBy; /* * #%L @@ -49,8 +57,33 @@ public class HUEditorViewFactory implements IViewFactory @Autowired private DocumentDescriptorFactory documentDescriptorFactory; + private static final Supplier sqlViewBindingSupplier = ExtendedMemorizingSupplier.of(() -> createSqlViewBinding()); private final transient CCache layouts = CCache.newLRUCache("HUEditorViewFactory#Layouts", 10, 0); + private SqlViewBinding getSqlViewBinding() + { + return sqlViewBindingSupplier.get(); + } + + private static SqlViewBinding createSqlViewBinding() + { + return SqlViewBinding.builder() + .setTableName(I_M_HU.Table_Name) + .setDisplayFieldNames(I_M_HU.COLUMNNAME_M_HU_ID) + .setSqlWhereClause("M_HU_Item_Parent_ID is null AND IsActive='Y'") // top levels, active + // + .addField(SqlViewRowFieldBinding.builder() + .fieldName(I_M_HU.COLUMNNAME_M_HU_ID) + .keyColumn(true) + .widgetType(DocumentFieldWidgetType.Integer) + .sqlValueClass(Integer.class) + .fieldLoader((rs, adLanguage) -> null) // shall not be used + .build()) + // + .setOrderBys(ImmutableList.of(DocumentQueryOrderBy.byFieldName(I_M_HU.COLUMNNAME_M_HU_ID, true))) + .build(); + } + @Override public ViewLayout getViewLayout(final WindowId windowId, final JSONViewDataType viewDataType) { @@ -61,9 +94,9 @@ public ViewLayout getViewLayout(final WindowId windowId, final JSONViewDataType @Override public Collection getViewFilterDescriptors(final WindowId windowId, final JSONViewDataType viewType) { - return null; // not supported + return getSqlViewBinding().getViewFilterDescriptors().getAll(); } - + private final ViewLayout createHUViewLayout(final WindowId windowId, JSONViewDataType viewDataType) { if (viewDataType == JSONViewDataType.includedView) @@ -188,7 +221,7 @@ public HUEditorView createView(final ViewCreateRequest request) referencingTableName = null; } - return HUEditorView.builder() + return HUEditorView.builder(getSqlViewBinding()) .setParentViewId(request.getParentViewId()) .setWindowId(windowId) .setHUIds(request.getFilterOnlyIds()) diff --git a/src/main/java/de/metas/ui/web/view/descriptor/SqlViewSelectionQueryBuilder.java b/src/main/java/de/metas/ui/web/view/descriptor/SqlViewSelectionQueryBuilder.java index 08903e31b..f59576b5d 100644 --- a/src/main/java/de/metas/ui/web/view/descriptor/SqlViewSelectionQueryBuilder.java +++ b/src/main/java/de/metas/ui/web/view/descriptor/SqlViewSelectionQueryBuilder.java @@ -5,6 +5,8 @@ import java.util.List; import java.util.Set; +import org.adempiere.ad.dao.IQueryFilter; +import org.adempiere.ad.dao.impl.TypedSqlQueryFilter; import org.adempiere.ad.expression.api.IExpressionEvaluator.OnVariableNotFound; import org.adempiere.ad.expression.api.IStringExpression; import org.adempiere.ad.expression.api.impl.CompositeStringExpression; @@ -14,6 +16,8 @@ import org.adempiere.util.Check; import org.compiere.util.DB; +import com.google.common.collect.ImmutableList; + import de.metas.ui.web.base.model.I_T_WEBUI_ViewSelection; import de.metas.ui.web.view.ViewEvaluationCtx; import de.metas.ui.web.view.ViewId; @@ -311,4 +315,19 @@ public String buildSqlCount(final List sqlParams, final String selection + " AND " + DB.buildSqlList(I_T_WEBUI_ViewSelection.COLUMNNAME_Record_ID, rowIds, sqlParams); } + public IQueryFilter buildInSelectionQueryFilter(final String selectionId) + { + final String tableName = entityBinding.getTableName(); + final String keyColumnName = entityBinding.getKeyColumnName(); + + final String sql = "exists (select 1 from " + I_T_WEBUI_ViewSelection.Table_Name + + " where " + + " " + I_T_WEBUI_ViewSelection.COLUMNNAME_UUID + "=?" + + " and " + I_T_WEBUI_ViewSelection.COLUMNNAME_Record_ID + "=" + tableName + "." + keyColumnName + + ")"; + final List sqlParams = ImmutableList.of(selectionId); + + return new TypedSqlQueryFilter<>(sql, sqlParams); + } + }