diff --git a/src/main/java/de/metas/ui/web/picking/PickingRow.java b/src/main/java/de/metas/ui/web/picking/PickingRow.java index ae298a1ed..abd8927e2 100644 --- a/src/main/java/de/metas/ui/web/picking/PickingRow.java +++ b/src/main/java/de/metas/ui/web/picking/PickingRow.java @@ -13,6 +13,7 @@ import de.metas.ui.web.view.IViewRow; import de.metas.ui.web.view.IViewRowAttributes; import de.metas.ui.web.view.IViewRowType; +import de.metas.ui.web.view.ViewId; import de.metas.ui.web.view.descriptor.annotation.ViewColumn; import de.metas.ui.web.view.descriptor.annotation.ViewColumn.ViewColumnLayout; import de.metas.ui.web.view.descriptor.annotation.ViewColumnHelper; @@ -50,6 +51,7 @@ @ToString(exclude = "_fieldNameAndJsonValues") public final class PickingRow implements IViewRow { + private final ViewId viewId; private final DocumentId id; private final IViewRowType type; private final boolean processed; @@ -75,12 +77,15 @@ public final class PickingRow implements IViewRow @ViewColumnLayout(when = JSONViewDataType.grid, seqNo = 50) }) private final java.util.Date preparationDate; + + private final ViewId includedViewId; private transient ImmutableMap _fieldNameAndJsonValues; @Builder private PickingRow( @NonNull final DocumentId id, + @NonNull final ViewId viewId, final IViewRowType type, final boolean processed, @NonNull final DocumentPath documentPath, @@ -92,6 +97,7 @@ private PickingRow( final Date preparationDate) { this.id = id; + this.viewId = viewId; this.type = type; this.processed = processed; this.documentPath = documentPath; @@ -101,6 +107,8 @@ private PickingRow( this.qtyToDeliver = qtyToDeliver; this.deliveryDate = deliveryDate; this.preparationDate = preparationDate; + + this.includedViewId = PickingSlotViewsIndexStorage.createViewId(viewId, id); } @Override @@ -160,4 +168,10 @@ public boolean hasIncludedView() { return true; } + + @Override + public ViewId getIncludedViewId() + { + return includedViewId; + } } diff --git a/src/main/java/de/metas/ui/web/picking/PickingSlotView.java b/src/main/java/de/metas/ui/web/picking/PickingSlotView.java index 8d2898578..88fbdf112 100644 --- a/src/main/java/de/metas/ui/web/picking/PickingSlotView.java +++ b/src/main/java/de/metas/ui/web/picking/PickingSlotView.java @@ -39,22 +39,27 @@ * 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 + * 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 + * License along with this program. If not, see * . * #L% */ public class PickingSlotView implements IView { + public static PickingSlotView cast(final IView pickingSlotView) + { + return (PickingSlotView)pickingSlotView; + } + private final ViewId viewId; - private ITranslatableString description; + private final ITranslatableString description; private final Map rows; @Builder @@ -212,5 +217,4 @@ public void notifyRecordsChanged(final Set recordRefs) // TODO Auto-generated method stub } - } diff --git a/src/main/java/de/metas/ui/web/picking/PickingSlotViewFactory.java b/src/main/java/de/metas/ui/web/picking/PickingSlotViewFactory.java index bd7e8876e..3a3d076f1 100644 --- a/src/main/java/de/metas/ui/web/picking/PickingSlotViewFactory.java +++ b/src/main/java/de/metas/ui/web/picking/PickingSlotViewFactory.java @@ -70,10 +70,13 @@ public PickingSlotView createView(final CreateViewRequest request) final Set rowIds = request.getFilterOnlyIds().stream().map(DocumentId::of).collect(ImmutableSet.toImmutableSet()); final List rows = pickingSlotRepo.retrieveRowsByIds(rowIds); + final ViewId pickingViewId = request.getParentViewId(); + final DocumentId pickingRowId = request.getSingleReferencingDocumentPathOrNull().getDocumentId(); + final ViewId pickingSlotViewId = PickingSlotViewsIndexStorage.createViewId(pickingViewId, pickingRowId); + return PickingSlotView.builder() - .viewId(ViewId.random(request.getWindowId())) + .viewId(pickingSlotViewId) .rows(rows) .build(); } - } diff --git a/src/main/java/de/metas/ui/web/picking/PickingSlotViewRepository.java b/src/main/java/de/metas/ui/web/picking/PickingSlotViewRepository.java index fd9ccb27d..3a409e31a 100644 --- a/src/main/java/de/metas/ui/web/picking/PickingSlotViewRepository.java +++ b/src/main/java/de/metas/ui/web/picking/PickingSlotViewRepository.java @@ -6,8 +6,10 @@ import org.adempiere.ad.trx.api.ITrx; import org.adempiere.util.Services; +import org.compiere.Adempiere; import org.compiere.util.DisplayType; import org.compiere.util.Env; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.google.common.collect.ImmutableList; @@ -51,7 +53,8 @@ public class PickingSlotViewRepository private final LookupDataSource bpartnerLookup; private final LookupDataSource bpartnerLocationLookup; - public PickingSlotViewRepository() + @Autowired + public PickingSlotViewRepository(final Adempiere databaseAccess) { warehouseLookup = LookupDataSourceFactory.instance.getLookupDataSource(SqlLookupDescriptor.builder() .setColumnName(I_M_PickingSlot.COLUMNNAME_M_Warehouse_ID) diff --git a/src/main/java/de/metas/ui/web/picking/PickingSlotViewsIndexStorage.java b/src/main/java/de/metas/ui/web/picking/PickingSlotViewsIndexStorage.java new file mode 100644 index 000000000..e59845b78 --- /dev/null +++ b/src/main/java/de/metas/ui/web/picking/PickingSlotViewsIndexStorage.java @@ -0,0 +1,133 @@ +package de.metas.ui.web.picking; + +import java.util.stream.Stream; + +import org.adempiere.exceptions.AdempiereException; +import org.springframework.stereotype.Component; + +import de.metas.ui.web.view.IView; +import de.metas.ui.web.view.IViewsIndexStorage; +import de.metas.ui.web.view.IViewsRepository; +import de.metas.ui.web.view.ViewId; +import de.metas.ui.web.window.datatypes.DocumentId; +import de.metas.ui.web.window.datatypes.WindowId; +import lombok.NonNull; + +/* + * #%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% + */ + +/** + * {@link PickingSlotView}s index storage. + * + * It's not actually a storage. It just forwards all calls to {@link PickingView} where the {@link PickingSlotView}s are storaged, one per each row. + * + * @author metas-dev + * + */ +@Component +public class PickingSlotViewsIndexStorage implements IViewsIndexStorage +{ + // NOTE: avoid using @Autowired because might introduce cyclic dependency. + // We have a setter which will be called when this instance will be registered. + private IViewsRepository viewsRepository; + + @Override + public void setViewsRepository(@NonNull final IViewsRepository viewsRepository) + { + this.viewsRepository = viewsRepository; + } + + @NonNull + private IViewsRepository getViewsRepository() + { + return viewsRepository; + } + + @Override + public WindowId getWindowId() + { + return PickingConstants.WINDOWID_PickingSlotView; + } + + @Override + public void put(final IView pickingSlotView) + { + final ViewId pickingSlotViewId = pickingSlotView.getViewId(); + final PickingView pickingView = getPickingViewBySlotId(pickingSlotViewId); + + final DocumentId rowId = extractRowId(pickingSlotViewId); + + pickingView.setPickingSlotView(rowId, PickingSlotView.cast(pickingSlotView)); + } + + public static ViewId createViewId(final ViewId pickingViewId, final DocumentId pickingRowId) + { + if (!PickingConstants.WINDOWID_PickingView.equals(pickingViewId.getWindowId())) + { + throw new AdempiereException("Invalid pickingViewId '" + pickingViewId + "'. WindowId not matching.") + .setParameter("expectedWindowId", PickingConstants.WINDOWID_PickingView); + } + + return ViewId.ofParts(PickingConstants.WINDOWID_PickingSlotView, pickingViewId.getViewIdPart(), pickingRowId.toJson()); + } + + private ViewId extractPickingViewId(final ViewId pickingSlotViewId) + { + final String viewIdPart = pickingSlotViewId.getViewIdPart(); + return ViewId.ofParts(PickingConstants.WINDOWID_PickingView, viewIdPart); + } + + private DocumentId extractRowId(final ViewId pickingSlotViewId) + { + final String rowIdStr = pickingSlotViewId.getPart(2); + return DocumentId.of(rowIdStr); + } + + private PickingView getPickingViewBySlotId(final ViewId pickingSlotViewId) + { + final ViewId pickingViewId = extractPickingViewId(pickingSlotViewId); + final PickingView view = PickingView.cast(getViewsRepository().getView(pickingViewId)); + return view; + } + + @Override + public PickingSlotView getByIdOrNull(final ViewId pickingSlotViewId) + { + final DocumentId rowId = extractRowId(pickingSlotViewId); + return getPickingViewBySlotId(pickingSlotViewId).getPickingSlotViewOrNull(rowId); + } + + @Override + public void removeById(final ViewId pickingSlotViewId) + { + final DocumentId rowId = extractRowId(pickingSlotViewId); + getPickingViewBySlotId(pickingSlotViewId).removePickingSlotView(rowId); + } + + @Override + public Stream streamAllViews() + { + // Do we really have to implement this? + return Stream.empty(); + } + +} diff --git a/src/main/java/de/metas/ui/web/picking/PickingView.java b/src/main/java/de/metas/ui/web/picking/PickingView.java index 699b5450a..a24dfe6a7 100644 --- a/src/main/java/de/metas/ui/web/picking/PickingView.java +++ b/src/main/java/de/metas/ui/web/picking/PickingView.java @@ -3,6 +3,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Stream; import org.adempiere.ad.dao.IQueryBL; @@ -61,9 +62,16 @@ public class PickingView implements IView { + public static PickingView cast(final IView view) + { + return (PickingView)view; + } + private final ViewId viewId; private final ITranslatableString description; private final Map rows; + + private final ConcurrentHashMap pickingSlotsViewByRowId = new ConcurrentHashMap<>(); @Builder private PickingView(@NonNull final ViewId viewId, @@ -231,6 +239,23 @@ public void notifyRecordsChanged(final Set recordRefs) // TODO Auto-generated method stub } + + /* package */ void setPickingSlotView(@NonNull final DocumentId rowId, @NonNull final PickingSlotView pickingSlotView) + { + pickingSlotsViewByRowId.put(rowId, pickingSlotView); + } + + /* package */ void removePickingSlotView(@NonNull final DocumentId rowId) + { + pickingSlotsViewByRowId.remove(rowId); + } + + /* package */ PickingSlotView getPickingSlotViewOrNull(@NonNull final DocumentId rowId) + { + return pickingSlotsViewByRowId.get(rowId); + } + + @ViewAction(caption = "picking slots", defaultAction = true) public CreateAndOpenIncludedViewAction openPickingSlots(final PickingView pickingView, final DocumentIdsSelection selectedRowIds) @@ -243,14 +268,15 @@ public CreateAndOpenIncludedViewAction openPickingSlots(final PickingView pickin // TODO: fetch the picking slots eligible for selected picking row final DocumentId pickingRowId = selectedRowIds.getSingleDocumentId(); final PickingRow pickingRow = pickingView.getById(pickingRowId); + final PickingSlotViewRepository pickingSlotRepo = Adempiere.getBean(PickingSlotViewRepository.class); final Set pickingSlotRowIds = pickingSlotRepo.retrieveAllRowIds(); final CreateViewRequest createViewRequest = CreateViewRequest.builder(PickingConstants.WINDOWID_PickingSlotView, JSONViewDataType.includedView) .setParentViewId(viewId) + .setReferencingDocumentPath(pickingRow.getDocumentPath()) .setFilterOnlyIds(pickingSlotRowIds) .build(); return CreateAndOpenIncludedViewAction.of(createViewRequest); } - } diff --git a/src/main/java/de/metas/ui/web/picking/PickingViewFactory.java b/src/main/java/de/metas/ui/web/picking/PickingViewFactory.java index e328f76bb..ff2a1f056 100644 --- a/src/main/java/de/metas/ui/web/picking/PickingViewFactory.java +++ b/src/main/java/de/metas/ui/web/picking/PickingViewFactory.java @@ -63,6 +63,7 @@ public ViewLayout getViewLayout(final WindowId windowId, final JSONViewDataType .setHasAttributesSupport(false) .setHasTreeSupport(false) .setHasIncludedViewSupport(true) + .setHasIncludedViewOnSelectSupport(true) // .addElementsFromViewRowClass(PickingRow.class, viewDataType) // @@ -84,11 +85,13 @@ public IView createView(final CreateViewRequest request) throw new IllegalArgumentException("Invalid request's windowId: " + request); } + final ViewId viewId = ViewId.random(PickingConstants.WINDOWID_PickingView); + final Set rowIds = request.getFilterOnlyIds().stream().map(DocumentId::of).collect(ImmutableSet.toImmutableSet()); - final List rows = pickingViewRepo.retrieveRowsByIds(rowIds); + final List rows = pickingViewRepo.retrieveRowsByIds(viewId, rowIds); return PickingView.builder() - .viewId(ViewId.random(PickingConstants.WINDOWID_PickingView)) + .viewId(viewId) .description(ITranslatableString.empty()) .rows(rows) .build(); diff --git a/src/main/java/de/metas/ui/web/picking/PickingViewRepository.java b/src/main/java/de/metas/ui/web/picking/PickingViewRepository.java index 9524629c3..3bfb94c8d 100644 --- a/src/main/java/de/metas/ui/web/picking/PickingViewRepository.java +++ b/src/main/java/de/metas/ui/web/picking/PickingViewRepository.java @@ -13,6 +13,7 @@ import com.google.common.collect.ImmutableSet; import de.metas.inoutcandidate.model.I_M_Packageable_V; +import de.metas.ui.web.view.ViewId; import de.metas.ui.web.view.ViewRow.DefaultRowType; import de.metas.ui.web.window.datatypes.DocumentId; import de.metas.ui.web.window.datatypes.DocumentPath; @@ -67,7 +68,7 @@ public PickingViewRepository() .provideForScope(LookupScope.DocumentField)); } - public List retrieveRowsByIds(final Collection rowIds) + public List retrieveRowsByIds(final ViewId viewId, final Collection rowIds) { final Set shipmentScheduleIds = rowIds.stream().map(DocumentId::toInt).collect(ImmutableSet.toImmutableSet()); if (shipmentScheduleIds.isEmpty()) @@ -80,17 +81,18 @@ public List retrieveRowsByIds(final Collection rowIds) .addInArrayFilter(I_M_Packageable_V.COLUMN_M_ShipmentSchedule_ID, shipmentScheduleIds) .create() .stream(I_M_Packageable_V.class) - .map(packageable -> createPickingRow(packageable)) + .map(packageable -> createPickingRow(viewId, packageable)) .collect(ImmutableList.toImmutableList()); } - private PickingRow createPickingRow(final I_M_Packageable_V packageable) + private PickingRow createPickingRow(final ViewId viewId, final I_M_Packageable_V packageable) { final DocumentId rowId = DocumentId.of(packageable.getM_ShipmentSchedule_ID()); final DocumentPath documentPath = DocumentPath.rootDocumentPath(PickingConstants.WINDOWID_PickingView, rowId); return PickingRow.builder() .documentPath(documentPath) + .viewId(viewId) .id(rowId) .type(DefaultRowType.Row) .processed(false) diff --git a/src/main/java/de/metas/ui/web/view/CreateViewRequest.java b/src/main/java/de/metas/ui/web/view/CreateViewRequest.java index 3e5463793..0cc286c38 100644 --- a/src/main/java/de/metas/ui/web/view/CreateViewRequest.java +++ b/src/main/java/de/metas/ui/web/view/CreateViewRequest.java @@ -192,20 +192,26 @@ public Builder setReferencingDocumentPaths(final Set referencingDo return this; } + public Builder setReferencingDocumentPath(final DocumentPath referencingDocumentPath) + { + setReferencingDocumentPaths(ImmutableSet.of(referencingDocumentPath)); + return this; + } + private Set getReferencingDocumentPaths() { return referencingDocumentPaths == null ? ImmutableSet.of() : ImmutableSet.copyOf(referencingDocumentPaths); } - public Builder setStickyFilters(List stickyFilters) + public Builder setStickyFilters(final List stickyFilters) { this.stickyFilters = stickyFilters; return this; } - + public Builder addStickyFilters(@NonNull final DocumentFilter stickyFilter) { - if(stickyFilters == null) + if (stickyFilters == null) { stickyFilters = new ArrayList<>(); } @@ -263,7 +269,7 @@ public Builder addActionsFromUtilityClass(final Class utilityClass) public Builder addActions(final ViewActionDescriptorsList actionsToAdd) { - this.actions = this.actions.mergeWith(actionsToAdd); + actions = actions.mergeWith(actionsToAdd); return this; } diff --git a/src/main/java/de/metas/ui/web/view/DefaultViewsRepositoryStorage.java b/src/main/java/de/metas/ui/web/view/DefaultViewsRepositoryStorage.java new file mode 100644 index 000000000..87c156a1e --- /dev/null +++ b/src/main/java/de/metas/ui/web/view/DefaultViewsRepositoryStorage.java @@ -0,0 +1,85 @@ +package de.metas.ui.web.view; + +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.RemovalNotification; + +import de.metas.ui.web.window.datatypes.WindowId; +import lombok.NonNull; + +/* + * #%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% + */ + +// NOTE: don't add it to spring context! i.e. don't annotate it with @Component or similar +/* package */final class DefaultViewsRepositoryStorage implements IViewsIndexStorage +{ + private final Cache views = CacheBuilder.newBuilder() + .expireAfterAccess(1, TimeUnit.HOURS) + .removalListener(notification -> onViewRemoved(notification)) + .build(); + + @Override + public WindowId getWindowId() + { + throw new UnsupportedOperationException("windowId not available"); + } + + @Override + public void setViewsRepository(final IViewsRepository viewsRepository) + { + // nothing + } + + private final void onViewRemoved(final RemovalNotification notification) + { + final IView view = (IView)notification.getValue(); + view.close(); + } + + @Override + public void put(@NonNull final IView view) + { + views.put(view.getViewId().getViewId(), view); + } + + @Override + public IView getByIdOrNull(@NonNull final ViewId viewId) + { + return views.getIfPresent(viewId.getViewId()); + } + + @Override + public void removeById(@NonNull final ViewId viewId) + { + views.invalidate(viewId.getViewId()); + } + + @Override + public Stream streamAllViews() + { + return views.asMap().values().stream(); + } + +} diff --git a/src/main/java/de/metas/ui/web/view/IViewRow.java b/src/main/java/de/metas/ui/web/view/IViewRow.java index f691e575d..ef64af198 100644 --- a/src/main/java/de/metas/ui/web/view/IViewRow.java +++ b/src/main/java/de/metas/ui/web/view/IViewRow.java @@ -70,6 +70,7 @@ public interface IViewRow // Attributes // @formatter:off boolean hasIncludedView(); + default ViewId getIncludedViewId() { return null; } // @formatter:on // diff --git a/src/main/java/de/metas/ui/web/view/IViewsIndexStorage.java b/src/main/java/de/metas/ui/web/view/IViewsIndexStorage.java new file mode 100644 index 000000000..7a99ae2a1 --- /dev/null +++ b/src/main/java/de/metas/ui/web/view/IViewsIndexStorage.java @@ -0,0 +1,54 @@ +package de.metas.ui.web.view; + +import java.util.stream.Stream; + +import de.metas.ui.web.window.datatypes.WindowId; + +/* + * #%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% + */ + +/** + * Implementations of this interface are responsible of storing {@link IView} references for a particular window ID identified by {@link #getWindowId()}. + * + * @author metas-dev + * + */ +public interface IViewsIndexStorage +{ + /** @return the window ID for whom this storge is storing the {@link IView} reference */ + WindowId getWindowId(); + + /** Don't call it directly. Will be called by API. */ + void setViewsRepository(IViewsRepository viewsRepository); + + /** Adds given view to the index. If the view already exists, it will be overridden. */ + void put(IView view); + + /** @return the {@link IView} identified by viewId or null if not found. */ + IView getByIdOrNull(ViewId viewId); + + /** Removes the view identified by given viewId. If the view does not exist, the method will do nothing, i.e. not failing. */ + void removeById(ViewId viewId); + + Stream streamAllViews(); + +} diff --git a/src/main/java/de/metas/ui/web/view/ViewId.java b/src/main/java/de/metas/ui/web/view/ViewId.java index f574baa4c..b92d95f50 100644 --- a/src/main/java/de/metas/ui/web/view/ViewId.java +++ b/src/main/java/de/metas/ui/web/view/ViewId.java @@ -1,5 +1,6 @@ package de.metas.ui.web.view; +import java.util.List; import java.util.Objects; import java.util.UUID; @@ -9,7 +10,10 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; +import com.google.common.base.Joiner; import com.google.common.base.Preconditions; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; import de.metas.ui.web.window.datatypes.WindowId; import lombok.EqualsAndHashCode; @@ -50,54 +54,98 @@ public static final ViewId of(@Nullable final String windowIdStr, @NonNull final /** @return ViewId from given viewId string; the WindowId will be extracted from viewId string */ @JsonCreator - public static ViewId ofViewIdString(@NonNull String viewIdStr) + public static ViewId ofViewIdString(@NonNull final String viewIdStr) { final WindowId windowId = null; // N/A return ofViewIdString(viewIdStr, windowId); } - public static ViewId ofViewIdString(@NonNull String viewIdStr, @Nullable WindowId expectedWindowId) + public static ViewId ofViewIdString(@NonNull final String viewIdStr, @Nullable final WindowId expectedWindowId) { - final WindowId windowId = extractWindowIdFromViewId(viewIdStr); + final List parts = SPLITTER.splitToList(viewIdStr); + if (parts.size() < 2) + { + throw new AdempiereException("Invalid viewId: " + viewIdStr); + } + + final WindowId windowId = WindowId.fromJson(parts.get(0)); if (expectedWindowId != null) { Preconditions.checkArgument(Objects.equals(windowId, expectedWindowId), "Invalid windowId: %s (viewId=%s)", windowId, viewIdStr); } - return new ViewId(windowId, viewIdStr); + return new ViewId(viewIdStr, parts, windowId); } public static ViewId random(@NonNull final WindowId windowId) { // TODO: find a way to generate smaller viewIds - final String viewIdStr = windowId.toJson() + SEPARATOR_AFTER_WindowId + UUID.randomUUID().toString(); - return new ViewId(windowId, viewIdStr); + final String viewIdPart = toString(UUID.randomUUID()); + final List parts = ImmutableList.of(windowId.toJson(), viewIdPart); + final String viewIdStr = JOINER.join(parts); + return new ViewId(viewIdStr, parts, windowId); } - private static final WindowId extractWindowIdFromViewId(@NonNull final String viewIdStr) + private static final String toString(final UUID uuid) { - try - { - final int idx = viewIdStr.indexOf(SEPARATOR_AFTER_WindowId); - final String windowIdStr = viewIdStr.substring(0, idx); - return WindowId.fromJson(windowIdStr); - } - catch (Exception ex) + final long mostSigBits = uuid.getMostSignificantBits(); + final long leastSigBits = uuid.getLeastSignificantBits(); + + // copy/paste from java.util.UUID.toString(), with our changes + return (digits(mostSigBits >> 32, 8) + // "-" + + digits(mostSigBits >> 16, 4) + // "-" + + digits(mostSigBits, 4) + // "-" + + digits(leastSigBits >> 48, 4) + // "-" + + digits(leastSigBits, 12)); + } + + /** + * @author java.util.UUID.digits(long, int) + */ + private static String digits(long val, int digits) + { + long hi = 1L << (digits * 4); + return Long.toHexString(hi | (val & (hi - 1))).substring(1); + } + + /** + * Creates a ViewId from parts. + * + * @param windowId + * @param viewIdPart viewId part (without WindowId!) + * @param otherParts optional other parts + * @return ViewId + */ + public static ViewId ofParts(@NonNull final WindowId windowId, @NonNull final String viewIdPart, @NonNull final String... otherParts) + { + final ImmutableList.Builder partsBuilder = ImmutableList. builder() + .add(windowId.toJson()) // 0 + .add(viewIdPart); // 1 + + if (otherParts != null && otherParts.length > 0) { - throw new AdempiereException("Invalid viewId: " + viewIdStr, ex); + partsBuilder.add(otherParts); // 2.. } + + final ImmutableList parts = partsBuilder.build(); + final String viewIdStr = JOINER.join(parts); + return new ViewId(viewIdStr, parts, windowId); } - private static final String SEPARATOR_AFTER_WindowId = "-"; + private static final String SEPARATOR = "-"; + private static final Splitter SPLITTER = Splitter.on(SEPARATOR).trimResults(); + private static final Joiner JOINER = Joiner.on(SEPARATOR); private final WindowId windowId; private final String viewId; + private final List parts; - private ViewId(@NonNull final WindowId windowId, @NonNull final String viewId) + private ViewId(@NonNull final String viewIdStr, @NonNull final List parts, @NonNull final WindowId windowId) { super(); this.windowId = windowId; - this.viewId = viewId; + viewId = viewIdStr; + this.parts = parts; } public WindowId getWindowId() @@ -105,6 +153,7 @@ public WindowId getWindowId() return windowId; } + /** @return full viewId string (including WindowId, including other parts etc) */ public String getViewId() { return viewId; @@ -115,4 +164,15 @@ public String toJson() { return viewId; } + + public String getPart(final int index) + { + return parts.get(index); + } + + /** @return just the viewId part (without the leading WindowId, without other parts etc) */ + public String getViewIdPart() + { + return parts.get(1); + } } diff --git a/src/main/java/de/metas/ui/web/view/ViewsRepository.java b/src/main/java/de/metas/ui/web/view/ViewsRepository.java index 8f8c05d55..bcdf85c75 100644 --- a/src/main/java/de/metas/ui/web/view/ViewsRepository.java +++ b/src/main/java/de/metas/ui/web/view/ViewsRepository.java @@ -4,29 +4,26 @@ import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; import javax.annotation.PostConstruct; import org.adempiere.ad.trx.api.ITrx; import org.adempiere.exceptions.AdempiereException; +import org.adempiere.util.Check; +import org.adempiere.util.lang.MutableInt; import org.adempiere.util.lang.impl.TableRecordReference; -import org.compiere.Adempiere; import org.compiere.util.DB; import org.compiere.util.Util.ArrayKey; import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.ApplicationContext; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Repository; -import com.google.common.base.Preconditions; import com.google.common.base.Stopwatch; -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; -import com.google.common.cache.RemovalNotification; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Streams; import de.metas.logging.LogManager; import de.metas.ui.web.base.model.I_T_WEBUI_ViewSelection; @@ -40,6 +37,7 @@ import de.metas.ui.web.view.json.JSONViewDataType; import de.metas.ui.web.window.controller.DocumentPermissionsHelper; import de.metas.ui.web.window.datatypes.WindowId; +import lombok.NonNull; /* * #%L @@ -78,35 +76,8 @@ public class ViewsRepository implements IViewsRepository @Value("${metasfresh.webui.view.truncateOnStartUp:true}") private boolean truncateSelectionOnStartUp; - private final Cache views = CacheBuilder.newBuilder() - .expireAfterAccess(1, TimeUnit.HOURS) - .removalListener(notification -> onViewRemoved(notification)) - .build(); - - @Autowired - public ViewsRepository(final ApplicationContext context, final Adempiere adempiere_NOTUSED) - { - // - // Discover context factories - for (final Object factoryObj : context.getBeansWithAnnotation(ViewFactory.class).values()) - { - final IViewFactory factory = (IViewFactory)factoryObj; - final ViewFactory annotation = factoryObj.getClass().getAnnotation(ViewFactory.class); - final WindowId windowId = WindowId.fromJson(annotation.windowId()); - - JSONViewDataType[] viewTypes = annotation.viewTypes(); - if (viewTypes.length == 0) - { - viewTypes = JSONViewDataType.values(); - } - - for (final JSONViewDataType viewType : viewTypes) - { - factories.put(mkFactoryKey(windowId, viewType), factory); - logger.info("Registered {} for windowId={}, viewType={}", factory, windowId, viewTypes); - } - } - } + private final ConcurrentHashMap viewsIndexStorages = new ConcurrentHashMap<>(); + private final IViewsIndexStorage defaultViewsIndexStorage = new DefaultViewsRepositoryStorage(); @PostConstruct private void truncateTempTablesIfAllowed() @@ -132,6 +103,34 @@ private static void truncateTable(final String tableName) } } + @Autowired + private void registerAnnotatedFactories(final Collection viewFactories) + { + for (final IViewFactory factory : viewFactories) + { + final ViewFactory annotation = factory.getClass().getAnnotation(ViewFactory.class); + if (annotation == null) + { + logger.info("Skip registering {} because it's not annotated with {}", factory, ViewFactory.class); + continue; + } + + final WindowId windowId = WindowId.fromJson(annotation.windowId()); + JSONViewDataType[] viewTypes = annotation.viewTypes(); + if (viewTypes.length == 0) + { + viewTypes = JSONViewDataType.values(); + } + + for (final JSONViewDataType viewType : viewTypes) + { + factories.put(mkFactoryKey(windowId, viewType), factory); + logger.info("Registered {} for windowId={}, viewType={}", factory, windowId, viewTypes); + } + } + + } + private final IViewFactory getFactory(final WindowId windowId, final JSONViewDataType viewType) { IViewFactory factory = factories.get(mkFactoryKey(windowId, viewType)); @@ -154,6 +153,50 @@ private static final ArrayKey mkFactoryKey(final WindowId windowId, final JSONVi return ArrayKey.of(windowId, viewType); } + @Autowired + private void registerViewsIndexStorages(final Collection viewsIndexStorages) + { + if (viewsIndexStorages.isEmpty()) + { + logger.info("No {} discovered", IViewsIndexStorage.class); + return; + } + + for (final IViewsIndexStorage viewsIndexStorage : viewsIndexStorages) + { + if (viewsIndexStorage instanceof DefaultViewsRepositoryStorage) + { + logger.warn("Skipping {} because it shall not be in spring context", viewsIndexStorage); + continue; + } + + final WindowId windowId = viewsIndexStorage.getWindowId(); + Check.assumeNotNull(windowId, "Parameter windowId is not null"); + + viewsIndexStorage.setViewsRepository(this); + + this.viewsIndexStorages.put(windowId, viewsIndexStorage); + logger.info("Registered {} for windowId={}", viewsIndexStorage, windowId); + } + } + + private IViewsIndexStorage getViewsStorageFor(@NonNull final ViewId viewId) + { + final IViewsIndexStorage viewIndexStorage = viewsIndexStorages.get(viewId.getWindowId()); + if (viewIndexStorage != null) + { + return viewIndexStorage; + } + + return defaultViewsIndexStorage; + } + + private Stream streamAllViews() + { + return Streams.concat(viewsIndexStorages.values().stream(), Stream.of(defaultViewsIndexStorage)) + .flatMap(IViewsIndexStorage::streamAllViews); + } + @Override public ViewLayout getViewLayout(final WindowId windowId, final JSONViewDataType viewDataType) { @@ -170,7 +213,7 @@ public ViewLayout getViewLayout(final WindowId windowId, final JSONViewDataType @Override public List getViews() { - return ImmutableList.copyOf(views.asMap().values()); + return streamAllViews().collect(ImmutableList.toImmutableList()); } @Override @@ -187,7 +230,7 @@ public IView createView(final CreateViewRequest request) .setParameter("factory", factory.toString()); } - views.put(view.getViewId().getViewId(), view); + getViewsStorageFor(view.getViewId()).put(view); return view; } @@ -215,8 +258,7 @@ public IView filterView(final ViewId viewId, final JSONFilterViewRequest jsonReq // NOTE: avoid adding if the factory returned the same view. if (view != newView) { - final ViewId newViewId = newView.getViewId(); - views.put(newViewId.getViewId(), newView); + getViewsStorageFor(newView.getViewId()).put(newView); } // Return the newly created view @@ -226,22 +268,21 @@ public IView filterView(final ViewId viewId, final JSONFilterViewRequest jsonReq @Override public IView getViewIfExists(final ViewId viewId) { - Preconditions.checkNotNull(viewId, "viewId cannot be null"); - return views.getIfPresent(viewId.getViewId()); + return getViewsStorageFor(viewId).getByIdOrNull(viewId); } @Override - public IView getView(final String viewId) + public IView getView(@NonNull final String viewIdStr) { - Preconditions.checkNotNull(viewId, "viewId cannot be null"); - - final IView view = views.getIfPresent(viewId); + final ViewId viewId = ViewId.ofViewIdString(viewIdStr); + final IView view = getViewsStorageFor(viewId).getByIdOrNull(viewId); if (view == null) { throw new EntityNotFoundException("No view found for viewId=" + viewId); } - DocumentPermissionsHelper.assertWindowAccess(view.getViewId().getWindowId(), viewId, UserSession.getCurrentPermissions()); + final String windowName = viewId.getViewId(); // used only for error reporting + DocumentPermissionsHelper.assertWindowAccess(viewId.getWindowId(), windowName, UserSession.getCurrentPermissions()); return view; } @@ -249,14 +290,7 @@ public IView getView(final String viewId) @Override public void deleteView(final ViewId viewId) { - Preconditions.checkNotNull(viewId, "viewId cannot be null"); - views.invalidate(viewId.getViewId()); - } - - private final void onViewRemoved(final RemovalNotification notification) - { - final IView view = (IView)notification.getValue(); - view.close(); + getViewsStorageFor(viewId).removeById(viewId); } @Override @@ -268,13 +302,17 @@ public void notifyRecordsChanged(final Set recordRefs) return; } - final Collection views = this.views.asMap().values(); + final MutableInt notifiedCount = MutableInt.zero(); + streamAllViews() + .forEach(view -> { + view.notifyRecordsChanged(recordRefs); + notifiedCount.incrementAndGet(); + }); if (logger.isDebugEnabled()) { - logger.debug("Notifing {} views about changed records: {}", views.size(), recordRefs); + logger.debug("Notified {} views about changed records: {}", notifiedCount, recordRefs); } - views.forEach(view -> view.notifyRecordsChanged(recordRefs)); } } diff --git a/src/main/java/de/metas/ui/web/view/descriptor/ViewLayout.java b/src/main/java/de/metas/ui/web/view/descriptor/ViewLayout.java index 5ebae7a69..f483d5157 100644 --- a/src/main/java/de/metas/ui/web/view/descriptor/ViewLayout.java +++ b/src/main/java/de/metas/ui/web/view/descriptor/ViewLayout.java @@ -75,6 +75,7 @@ public static final Builder builder() private final boolean hasAttributesSupport; private final boolean hasIncludedViewSupport; + private final boolean hasIncludedViewOnSelectSupport; private final String allowNewCaption; private final boolean hasTreeSupport; @@ -111,6 +112,8 @@ private ViewLayout(final Builder builder) treeExpandedDepth = builder.treeExpandedDepth; hasIncludedViewSupport = builder.hasIncludedViewSupport; + hasIncludedViewOnSelectSupport = builder.hasIncludedViewOnSelectSupport; + allowNewCaption = null; eTag = ETag.of(nextETagVersionSupplier.getAndIncrement(), extractETagAttributes(filters, allowNewCaption)); @@ -141,7 +144,10 @@ private ViewLayout(final ViewLayout from, this.hasTreeSupport = hasTreeSupport; this.treeCollapsible = treeCollapsible; this.treeExpandedDepth = treeExpandedDepth; + hasIncludedViewSupport = from.hasIncludedViewSupport; + hasIncludedViewOnSelectSupport = from.hasIncludedViewOnSelectSupport; + this.allowNewCaption = allowNewCaption; eTag = from.eTag.overridingAttributes(extractETagAttributes(filters, allowNewCaption)); @@ -266,6 +272,11 @@ public boolean isIncludedViewSupport() { return hasIncludedViewSupport; } + + public boolean isIncludedViewOnSelectSupport() + { + return hasIncludedViewOnSelectSupport; + } public boolean isAllowNew() { @@ -363,6 +374,7 @@ public static final class Builder private boolean hasAttributesSupport = false; private boolean hasIncludedViewSupport = false; + private boolean hasIncludedViewOnSelectSupport = false; private boolean hasTreeSupport = false; private boolean treeCollapsible = false; @@ -524,6 +536,12 @@ public Builder setHasIncludedViewSupport(final boolean hasIncludedViewSupport) this.hasIncludedViewSupport = hasIncludedViewSupport; return this; } + + public Builder setHasIncludedViewOnSelectSupport(boolean hasIncludedViewOnSelectSupport) + { + this.hasIncludedViewOnSelectSupport = hasIncludedViewOnSelectSupport; + return this; + } public Builder setHasTreeSupport(final boolean hasTreeSupport) { diff --git a/src/main/java/de/metas/ui/web/view/json/JSONViewLayout.java b/src/main/java/de/metas/ui/web/view/json/JSONViewLayout.java index 2c2e1d853..cd45a3888 100644 --- a/src/main/java/de/metas/ui/web/view/json/JSONViewLayout.java +++ b/src/main/java/de/metas/ui/web/view/json/JSONViewLayout.java @@ -103,6 +103,8 @@ public static JSONViewLayout of( // @JsonProperty("supportIncludedView") private final boolean supportIncludedView; + @JsonProperty("supportIncludedViewOnSelect") + private final Boolean supportIncludedViewOnSelect; // // New record support @@ -139,7 +141,9 @@ private JSONViewLayout(final ViewLayout layout, final JSONOptions jsonOpts) this.filters = JSONDocumentFilterDescriptor.ofCollection(layout.getFilters(), jsonOpts); supportAttributes = layout.isAttributesSupport(); + supportIncludedView = layout.isIncludedViewSupport(); + supportIncludedViewOnSelect = layout.isIncludedViewOnSelectSupport() ? Boolean.TRUE : null; // // Tree diff --git a/src/main/java/de/metas/ui/web/view/json/JSONViewRow.java b/src/main/java/de/metas/ui/web/view/json/JSONViewRow.java index dab04200f..ba84c972d 100644 --- a/src/main/java/de/metas/ui/web/view/json/JSONViewRow.java +++ b/src/main/java/de/metas/ui/web/view/json/JSONViewRow.java @@ -7,13 +7,18 @@ import org.adempiere.util.GuavaCollectors; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import de.metas.ui.web.view.IViewRow; +import de.metas.ui.web.view.ViewId; import de.metas.ui.web.window.datatypes.DocumentId; +import de.metas.ui.web.window.datatypes.WindowId; import de.metas.ui.web.window.datatypes.json.JSONDocumentBase; import de.metas.ui.web.window.datatypes.json.JSONDocumentField; +import lombok.Value; /* * #%L @@ -114,6 +119,12 @@ private static JSONViewRow ofRow(final IViewRow row, final String adLanguage) if (row.hasIncludedView()) { jsonRow.supportIncludedViews = true; + + final ViewId includedViewId = row.getIncludedViewId(); + if(includedViewId != null) + { + jsonRow.includedView = new JSONIncludedViewId(includedViewId.getWindowId(), includedViewId.getViewId()); + } } // @@ -144,9 +155,12 @@ private static JSONViewRow ofRow(final IViewRow row, final String adLanguage) @JsonInclude(JsonInclude.Include.NON_NULL) private Boolean supportAttributes; - @JsonProperty(value = "supportIncludedViews") + @JsonProperty("supportIncludedViews") @JsonInclude(JsonInclude.Include.NON_NULL) private Boolean supportIncludedViews; + @JsonProperty("includedView") + @JsonInclude(JsonInclude.Include.NON_NULL) + private JSONIncludedViewId includedView; @JsonProperty("includedDocuments") @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -165,4 +179,12 @@ private JSONViewRow(final DocumentId documentId) { super(documentId); } + + @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, isGetterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) + @Value + private static final class JSONIncludedViewId + { + private final WindowId windowId; + private final String viewId; + } }