diff --git a/platform-dao/src/test/java/ua/com/fielden/platform/test_config/H2OrPostgreSqlOrSqlServerContextSelector.java b/platform-dao/src/test/java/ua/com/fielden/platform/test_config/H2OrPostgreSqlOrSqlServerContextSelector.java index c5663392e57..c914e8ef055 100644 --- a/platform-dao/src/test/java/ua/com/fielden/platform/test_config/H2OrPostgreSqlOrSqlServerContextSelector.java +++ b/platform-dao/src/test/java/ua/com/fielden/platform/test_config/H2OrPostgreSqlOrSqlServerContextSelector.java @@ -1,30 +1,27 @@ package ua.com.fielden.platform.test_config; -import static org.apache.commons.lang3.StringUtils.isEmpty; - -import java.util.Optional; -import java.util.Properties; - import ua.com.fielden.platform.test.DbCreator; import ua.com.fielden.platform.test.IDomainDrivenTestCaseConfiguration; import ua.com.fielden.platform.test.db_creators.H2DbCreator; import ua.com.fielden.platform.test.runners.AbstractDomainDrivenTestCaseRunner; -import ua.com.fielden.platform.test.runners.H2DomainDrivenTestCaseRunner; -import ua.com.fielden.platform.test.runners.PostgresqlDomainDrivenTestCaseRunner; -import ua.com.fielden.platform.test.runners.SqlServerDomainDrivenTestCaseRunner; - -/** - * A test runner that selects a test configuration {@link ITestContext} from either {@link H2DomainDrivenTestCaseRunner} or {@link PostgresqlDomainDrivenTestCaseRunner} for running unit test. - * The criteria for selecting the appropriate test runner is based on runtime settings. - * - * @author TG Team - */ +import ua.com.fielden.platform.test.runners.H2DomainDrivenTestCaseRunner.H2TestContext; +import ua.com.fielden.platform.test.runners.PostgresqlDomainDrivenTestCaseRunner.PostgresqlTestContext; +import ua.com.fielden.platform.test.runners.SqlServerDomainDrivenTestCaseRunner.SqlServerTestContext; + +import java.util.Optional; +import java.util.Properties; + +import static org.apache.commons.lang3.StringUtils.isEmpty; + +/// A test runner that selects a test configuration [ITestContext] from [SqlServerTestContext] or [PostgresqlTestContext]. +/// The criteria for selecting the appropriate test runner is based on runtime settings. +/// public class H2OrPostgreSqlOrSqlServerContextSelector extends AbstractDomainDrivenTestCaseRunner { // Note: This assumes PostgreSQL is listening on port 5432 (the default). // There is not much else in the URI that would uniquely identify that we are connecting to a PostgreSQL database. - private static final boolean POSTGRESQL = !isEmpty(System.getProperty("databaseUri")) && System.getProperty("databaseUri").contains("5432"); - private static final boolean SQL_SERVER = !isEmpty(System.getProperty("databaseUri")) && System.getProperty("databaseUri").contains("database"); + protected static final boolean POSTGRESQL = !isEmpty(System.getProperty("databaseUri")) && System.getProperty("databaseUri").contains("5432"); + protected static final boolean SQL_SERVER = !isEmpty(System.getProperty("databaseUri")) && System.getProperty("databaseUri").contains("database"); public H2OrPostgreSqlOrSqlServerContextSelector(final Class klass) throws Exception { super(klass, POSTGRESQL ? PostgresqlDbCreator.class : (SQL_SERVER ? SqlServerDbCreator.class : H2DbCreator.class), Optional.empty()); @@ -45,10 +42,10 @@ public void dbCleanUp() { selectRunnerConfig().dbCleanUp(); } - private ITestContext selectRunnerConfig() { + protected ITestContext selectRunnerConfig() { return POSTGRESQL - ? new PostgresqlDomainDrivenTestCaseRunner.PostgresqlTestContext() - : (SQL_SERVER ? new SqlServerDomainDrivenTestCaseRunner.SqlServerTestContext() : new H2DomainDrivenTestCaseRunner.H2TestContext(this)); + ? new PostgresqlTestContext() + : (SQL_SERVER ? new SqlServerTestContext() : new H2TestContext(this)); } } \ No newline at end of file diff --git a/platform-pojo-bl/src/main/java/ua/com/fielden/platform/entity/functional/centre/CentreContextHolder.java b/platform-pojo-bl/src/main/java/ua/com/fielden/platform/entity/functional/centre/CentreContextHolder.java index 4b1d99727c6..26efa81cf45 100644 --- a/platform-pojo-bl/src/main/java/ua/com/fielden/platform/entity/functional/centre/CentreContextHolder.java +++ b/platform-pojo-bl/src/main/java/ua/com/fielden/platform/entity/functional/centre/CentreContextHolder.java @@ -11,15 +11,15 @@ import static java.util.Collections.unmodifiableList; import static java.util.Collections.unmodifiableMap; -/** - * The entity holder for centre context and criteria entity's modified props. - * - * @author TG Team - * - */ +/// The entity holder for centre context and criteria entity's modified props. +/// @KeyType(String.class) @KeyTitle(value = "Key", desc = "Some key description") @CompanionObject(ICentreContextHolder.class) +// GraphQL Web API schema may not find `CentreContextHolder` type in case if it is used as property in allowed for introspection types +// (e.g. those that are persistent / synthetic and have no @DenyIntrospection). +// That's why it is explicitly denied for introspection and such properties become `AbstractDomainTreeRepresentation.isExcluded`. +@DenyIntrospection public class CentreContextHolder extends AbstractEntity { @IsProperty(Object.class) diff --git a/platform-pojo-bl/src/main/java/ua/com/fielden/platform/entity/functional/centre/SavingInfoHolder.java b/platform-pojo-bl/src/main/java/ua/com/fielden/platform/entity/functional/centre/SavingInfoHolder.java index 4265eb22d55..de78e5c7632 100644 --- a/platform-pojo-bl/src/main/java/ua/com/fielden/platform/entity/functional/centre/SavingInfoHolder.java +++ b/platform-pojo-bl/src/main/java/ua/com/fielden/platform/entity/functional/centre/SavingInfoHolder.java @@ -1,31 +1,27 @@ package ua.com.fielden.platform.entity.functional.centre; -import static java.util.Collections.unmodifiableList; -import static java.util.Collections.unmodifiableMap; -import static ua.com.fielden.platform.entity.NoKey.NO_KEY; +import ua.com.fielden.platform.entity.AbstractEntity; +import ua.com.fielden.platform.entity.IContinuationData; +import ua.com.fielden.platform.entity.NoKey; +import ua.com.fielden.platform.entity.annotation.*; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import ua.com.fielden.platform.entity.AbstractEntity; -import ua.com.fielden.platform.entity.IContinuationData; -import ua.com.fielden.platform.entity.NoKey; -import ua.com.fielden.platform.entity.annotation.CompanionObject; -import ua.com.fielden.platform.entity.annotation.IsProperty; -import ua.com.fielden.platform.entity.annotation.KeyType; -import ua.com.fielden.platform.entity.annotation.Observable; -import ua.com.fielden.platform.entity.annotation.Title; +import static java.util.Collections.unmodifiableList; +import static java.util.Collections.unmodifiableMap; +import static ua.com.fielden.platform.entity.NoKey.NO_KEY; -/** - * The entity holder for saving information: entity's modified props + centre context, if any. - * - * @author TG Team - * - */ +/// The entity holder for saving information: entity's modified props + centre context, if any. +/// @KeyType(NoKey.class) @CompanionObject(ISavingInfoHolder.class) +// GraphQL Web API schema may not find `CentreContextHolder` type in case if it is used as property in allowed for introspection types +// (e.g. those that are persistent / synthetic and have no @DenyIntrospection). +// That's why it is explicitly denied for introspection and such properties become `AbstractDomainTreeRepresentation.isExcluded`. +@DenyIntrospection public class SavingInfoHolder extends AbstractEntity { @IsProperty(Object.class) diff --git a/platform-pojo-bl/src/main/java/ua/com/fielden/platform/entity_centre/review/criteria/EntityQueryCriteria.java b/platform-pojo-bl/src/main/java/ua/com/fielden/platform/entity_centre/review/criteria/EntityQueryCriteria.java index 0a116537d03..162135cbadf 100644 --- a/platform-pojo-bl/src/main/java/ua/com/fielden/platform/entity_centre/review/criteria/EntityQueryCriteria.java +++ b/platform-pojo-bl/src/main/java/ua/com/fielden/platform/entity_centre/review/criteria/EntityQueryCriteria.java @@ -325,6 +325,12 @@ private Map enhanceQueryParams(final Map buildPa return buildParametersMap; } + /// Executes the configured Entity Centre query to find count of entities. + /// + public int runCount() { + return count(generateQueryWithSummaries().getKey()); + } + /// Executes the configured entity query. /// public final IPage run(final int pageSize) { diff --git a/platform-pojo-bl/src/main/java/ua/com/fielden/platform/entity_centre/review/criteria/EntityQueryCriteriaUtils.java b/platform-pojo-bl/src/main/java/ua/com/fielden/platform/entity_centre/review/criteria/EntityQueryCriteriaUtils.java index 822c00f53f7..e11c9adbab8 100644 --- a/platform-pojo-bl/src/main/java/ua/com/fielden/platform/entity_centre/review/criteria/EntityQueryCriteriaUtils.java +++ b/platform-pojo-bl/src/main/java/ua/com/fielden/platform/entity_centre/review/criteria/EntityQueryCriteriaUtils.java @@ -68,7 +68,7 @@ public static boolean isPropertyAuthorised(final Class root, final String pro // Root property (aka "entity itself") is always authorised. return property.isEmpty() // Non-root property access is governed by *_CanRead_property_* tokens, where the `property` part is never empty. - || authorisePropertyReading(root, property, authorisationModel, securityTokenProvider).orElseGet(Result::successful).isSuccessful(); + || authorisePropertyIfGuarded(root, property, authorisationModel).orElseGet(Result::successful).isSuccessful(); } /// Creates an Entity Centre query for a list of [QueryProperty] and other parameters. @@ -91,7 +91,7 @@ public static > EntityQueryProgressiveInterfaces.ICo final IDates dates) { authoriseReading(type.getSimpleName(), READ, authorisationModel, securityTokenProvider).ifFailure(Result::throwRuntime); - authoriseCriteria(queryProperties, authorisationModel, securityTokenProvider).ifFailure(Result::throwRuntime); + authoriseCriteria(queryProperties, authorisationModel).ifFailure(Result::throwRuntime); if (createdByUserConstraint.isPresent()) { final QueryProperty queryProperty = EntityQueryCriteriaUtils.createNotInitialisedQueryProperty(managedType, "createdBy"); final List createdByCriteria = new ArrayList<>(); diff --git a/platform-pojo-bl/src/main/java/ua/com/fielden/platform/sample/domain/TgGeneratedEntity.java b/platform-pojo-bl/src/main/java/ua/com/fielden/platform/sample/domain/TgGeneratedEntity.java index 966d8e5104d..e1a588e4d29 100644 --- a/platform-pojo-bl/src/main/java/ua/com/fielden/platform/sample/domain/TgGeneratedEntity.java +++ b/platform-pojo-bl/src/main/java/ua/com/fielden/platform/sample/domain/TgGeneratedEntity.java @@ -3,31 +3,25 @@ import ua.com.fielden.platform.data.generator.WithCreatedByUser; import ua.com.fielden.platform.entity.AbstractEntity; import ua.com.fielden.platform.entity.DynamicEntityKey; -import ua.com.fielden.platform.entity.annotation.CompanionObject; -import ua.com.fielden.platform.entity.annotation.CompositeKeyMember; -import ua.com.fielden.platform.entity.annotation.CritOnly; +import ua.com.fielden.platform.entity.annotation.*; import ua.com.fielden.platform.entity.annotation.CritOnly.Type; -import ua.com.fielden.platform.entity.annotation.DescTitle; -import ua.com.fielden.platform.entity.annotation.IsProperty; -import ua.com.fielden.platform.entity.annotation.KeyType; -import ua.com.fielden.platform.entity.annotation.MapEntityTo; -import ua.com.fielden.platform.entity.annotation.MapTo; -import ua.com.fielden.platform.entity.annotation.Observable; -import ua.com.fielden.platform.entity.annotation.Title; +import ua.com.fielden.platform.reflection.TitlesDescsGetter; import ua.com.fielden.platform.security.user.User; +import ua.com.fielden.platform.utils.Pair; -/** - * Represents a typical example of an entity to be generated. This particular entity is generated by generator {@link TgGeneratedEntityGenerator}. - * - * @author TG Team - * - */ +import static ua.com.fielden.platform.entity.annotation.CritOnly.Type.SINGLE; + +/// Represents a typical example of an entity to be generated. This particular entity is generated by generator `TgGeneratedEntityGenerator`. +/// @KeyType(DynamicEntityKey.class) @CompanionObject(ITgGeneratedEntity.class) @MapEntityTo @DescTitle("Desc") public class TgGeneratedEntity extends AbstractEntity implements WithCreatedByUser { - private static final long serialVersionUID = 1L; + + private static final Pair entityTitleAndDesc = TitlesDescsGetter.getEntityTitleAndDesc(TgGeneratedEntity.class); + public static final String ENTITY_TITLE = entityTitleAndDesc.getKey(); + public static final String ENTITY_DESC = entityTitleAndDesc.getValue(); @IsProperty(assignBeforeSave = true) @MapTo @@ -47,10 +41,25 @@ public class TgGeneratedEntity extends AbstractEntity implemen private User critOnlyMultiProp; @IsProperty - @CritOnly(Type.SINGLE) + @CritOnly(SINGLE) @Title(value = "CritOnly Single", desc = "CritOnly single property") private User critOnlySingleProp; + @IsProperty + @CritOnly(SINGLE) + @Required + private Integer requiredProp; + + public Integer getRequiredProp() { + return requiredProp; + } + + @Observable + public TgGeneratedEntity setRequiredProp(Integer requiredProp) { + this.requiredProp = requiredProp; + return this; + } + @Observable public TgGeneratedEntity setEntityKey(final String entityKey) { this.entityKey = entityKey; diff --git a/platform-pojo-bl/src/main/java/ua/com/fielden/platform/security/tokens/TokenUtils.java b/platform-pojo-bl/src/main/java/ua/com/fielden/platform/security/tokens/TokenUtils.java index a81dabf0173..4aaefed06ce 100644 --- a/platform-pojo-bl/src/main/java/ua/com/fielden/platform/security/tokens/TokenUtils.java +++ b/platform-pojo-bl/src/main/java/ua/com/fielden/platform/security/tokens/TokenUtils.java @@ -61,14 +61,14 @@ public static Result authoriseReading(final String entityTypeSimpleName, final T /// Validates the criteria values to determine whether they can be applied under the authorisation model. /// - public static Result authoriseCriteria(final List queryProperties, final IAuthorisationModel authorisation, final ISecurityTokenProvider securityTokenProvider) { + public static Result authoriseCriteria(final List queryProperties, final IAuthorisationModel authorisation) { return queryProperties.stream() .map(queryProperty -> { // Criterion without any conditions is authorised. // Root property (aka "entity itself") is always authorised. if (!queryProperty.isEmptyWithoutMnemonics() && !"".equals(queryProperty.getPropertyName())) { final var originalType = getOriginalType(stripIfNeeded(queryProperty.getEntityClass())); - return authorisePropertyReading(originalType, queryProperty.getPropertyName(), authorisation, securityTokenProvider); + return authorisePropertyIfGuarded(originalType, queryProperty.getPropertyName(), authorisation); } return Optional.empty(); }) @@ -78,7 +78,9 @@ public static Result authoriseCriteria(final List queryProperties .orElseGet(Result::successful); } - public static Optional authorisePropertyReading(final Class entityType, final String propertyName, final IAuthorisationModel authorisation, final ISecurityTokenProvider securityTokenProvider) { + /// Finds authorisation annotation for entity property and performs authorisation, if present. + /// + public static Optional authorisePropertyIfGuarded(final Class entityType, final String propertyName, final IAuthorisationModel authorisation) { return getPropertyAnnotationOptionally(Authorise.class, entityType, propertyName) .map(annot -> (Class) annot.value()) .map(authorisation::authorise); diff --git a/platform-pojo-bl/src/main/java/ua/com/fielden/platform/security/tokens/persistent/TgGeneratedEntity_CanRead_Token.java b/platform-pojo-bl/src/main/java/ua/com/fielden/platform/security/tokens/persistent/TgGeneratedEntity_CanRead_Token.java new file mode 100644 index 00000000000..3cef0f3b144 --- /dev/null +++ b/platform-pojo-bl/src/main/java/ua/com/fielden/platform/security/tokens/persistent/TgGeneratedEntity_CanRead_Token.java @@ -0,0 +1,15 @@ +package ua.com.fielden.platform.security.tokens.persistent; + +import ua.com.fielden.platform.sample.domain.TgGeneratedEntity; +import ua.com.fielden.platform.sample.domain.TgVehicleModel; +import ua.com.fielden.platform.security.ISecurityToken; +import ua.com.fielden.platform.security.tokens.Template; + +import static java.lang.String.format; + +/// A security token for entity [TgGeneratedEntity] to guard Read. +/// +public class TgGeneratedEntity_CanRead_Token implements ISecurityToken { + public final static String TITLE = format(Template.READ.forTitle(), TgGeneratedEntity.ENTITY_TITLE); + public final static String DESC = format(Template.READ.forDesc(), TgGeneratedEntity.ENTITY_TITLE); +} diff --git a/platform-pojo-bl/src/main/java/ua/com/fielden/platform/ui/config/MainMenuItem.java b/platform-pojo-bl/src/main/java/ua/com/fielden/platform/ui/config/MainMenuItem.java index cdf7211aa22..766ae34dbdc 100644 --- a/platform-pojo-bl/src/main/java/ua/com/fielden/platform/ui/config/MainMenuItem.java +++ b/platform-pojo-bl/src/main/java/ua/com/fielden/platform/ui/config/MainMenuItem.java @@ -1,25 +1,16 @@ package ua.com.fielden.platform.ui.config; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; - import ua.com.fielden.platform.algorithm.search.ITreeNode; import ua.com.fielden.platform.entity.AbstractEntity; -import ua.com.fielden.platform.entity.annotation.CompanionObject; -import ua.com.fielden.platform.entity.annotation.DenyIntrospection; -import ua.com.fielden.platform.entity.annotation.DescTitle; -import ua.com.fielden.platform.entity.annotation.IsProperty; -import ua.com.fielden.platform.entity.annotation.KeyTitle; -import ua.com.fielden.platform.entity.annotation.KeyType; -import ua.com.fielden.platform.entity.annotation.MapEntityTo; -import ua.com.fielden.platform.entity.annotation.MapTo; -import ua.com.fielden.platform.entity.annotation.Observable; -import ua.com.fielden.platform.error.Result; +import ua.com.fielden.platform.entity.annotation.*; import ua.com.fielden.platform.reflection.ClassesRetriever; import ua.com.fielden.platform.reflection.PropertyTypeDeterminator; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + import static ua.com.fielden.platform.error.Result.failure; /** diff --git a/platform-pojo-bl/src/main/java/ua/com/fielden/platform/web/centre/WebApiUtils.java b/platform-pojo-bl/src/main/java/ua/com/fielden/platform/web/centre/WebApiUtils.java index 4c5aad89926..a531e7b2d75 100644 --- a/platform-pojo-bl/src/main/java/ua/com/fielden/platform/web/centre/WebApiUtils.java +++ b/platform-pojo-bl/src/main/java/ua/com/fielden/platform/web/centre/WebApiUtils.java @@ -1,11 +1,11 @@ package ua.com.fielden.platform.web.centre; -import static ua.com.fielden.platform.domaintree.ICalculatedProperty.CalculatedPropertyCategory.AGGREGATED_EXPRESSION; -import static ua.com.fielden.platform.domaintree.impl.AbstractDomainTreeRepresentation.isCalculatedAndOfTypes; - import java.util.List; import java.util.stream.Collectors; +import static ua.com.fielden.platform.domaintree.ICalculatedProperty.CalculatedPropertyCategory.AGGREGATED_EXPRESSION; +import static ua.com.fielden.platform.domaintree.impl.AbstractDomainTreeRepresentation.isCalculatedAndOfTypes; + /** * Utilities for Web API (centre, master) and its implementation. * @@ -22,6 +22,9 @@ public class WebApiUtils { * The surrogate title of centre 'link' configuration. This is used when link with centre parameters opens. */ public static final String LINK_CONFIG_TITLE = "_______________________link"; + public static final String CONFIG_DOES_NOT_EXIST = "Configuration does not exist."; + public static final String SAVE_MSG = "Please save and try again."; + public static final String DUPLICATE_SAVE_MSG = "Please duplicate, save and try again."; /** Private default constructor to prevent instantiation. */ private WebApiUtils() { diff --git a/platform-pojo-bl/src/main/java/ua/com/fielden/platform/web/utils/CentreExecutionMode.java b/platform-pojo-bl/src/main/java/ua/com/fielden/platform/web/utils/CentreExecutionMode.java new file mode 100644 index 00000000000..45a44378380 --- /dev/null +++ b/platform-pojo-bl/src/main/java/ua/com/fielden/platform/web/utils/CentreExecutionMode.java @@ -0,0 +1,15 @@ +package ua.com.fielden.platform.web.utils; + +/// Distinguishes the two execution modes of `CriteriaResource#executeEntityCentreConfiguration`. +/// +/// Sealed deliberately so that any future Entity Centre evolution that introduces a new mode forces +/// reconciliation with the central execution method (the compiler will flag a non-exhaustive switch). +/// This protects programmatic callers (e.g., `DefaultEntityCentreProcessor`) from breaking when the +/// UI flow changes. +/// +/// Permitted implementations: +/// - [HeadlessCentreExecution] -- programmatic API; data only, no UI augmentation. +/// - [UiCentreExecution] -- UI-driven; rendering hints, action indices, dynamic-column metadata, +/// and (when running) the criteria-changed indication are produced. +/// +public sealed interface CentreExecutionMode permits HeadlessCentreExecution, UiCentreExecution {} \ No newline at end of file diff --git a/platform-pojo-bl/src/main/java/ua/com/fielden/platform/web/utils/ConfigSettings.java b/platform-pojo-bl/src/main/java/ua/com/fielden/platform/web/utils/ConfigSettings.java new file mode 100644 index 00000000000..83838f35ee3 --- /dev/null +++ b/platform-pojo-bl/src/main/java/ua/com/fielden/platform/web/utils/ConfigSettings.java @@ -0,0 +1,24 @@ +package ua.com.fielden.platform.web.utils; + +import ua.com.fielden.platform.security.user.User; +import ua.com.fielden.platform.ui.config.EntityCentreConfig; +import ua.com.fielden.platform.ui.menu.MiWithConfigurationSupport; +import ua.com.fielden.platform.web.interfaces.DeviceProfile; + +import java.util.Optional; + +/// Convenient record holding Entity Centre configuration settings. +/// +/// @param saveAsName optional "save-as" name for named configuration or empty [Optional] for default one +/// @param owner a [User] that created the configuration (or own it through inheritance process) +/// @param device indicates whether the configuration belongs to [DeviceProfile#DESKTOP] namespace or [DeviceProfile#MOBILE] +/// @param miType menu item type for the configuration +/// @param centreConfig Entity Centre configuration entity instance +/// +public record ConfigSettings( + Optional saveAsName, + User owner, + DeviceProfile device, + Class> miType, + EntityCentreConfig centreConfig +) {} diff --git a/platform-pojo-bl/src/main/java/ua/com/fielden/platform/web/utils/EntityCentreProcessor.java b/platform-pojo-bl/src/main/java/ua/com/fielden/platform/web/utils/EntityCentreProcessor.java new file mode 100644 index 00000000000..73f3c93fd04 --- /dev/null +++ b/platform-pojo-bl/src/main/java/ua/com/fielden/platform/web/utils/EntityCentreProcessor.java @@ -0,0 +1,157 @@ +package ua.com.fielden.platform.web.utils; + +import ua.com.fielden.platform.data.generator.IGenerator; +import ua.com.fielden.platform.entity.AbstractEntity; +import ua.com.fielden.platform.error.Result; +import ua.com.fielden.platform.pagination.IPage; +import ua.com.fielden.platform.types.either.Either; +import ua.com.fielden.platform.types.either.Left; +import ua.com.fielden.platform.types.either.Right; +import ua.com.fielden.platform.web.centre.IQueryEnhancer; +import ua.com.fielden.platform.web.interfaces.DeviceProfile; + +import java.util.function.Function; + +/// An interface for Entity Centre processing API. +/// +/// The intended main usage is to execute named Entity Centre configurations on behalf of their owners as part of business logic. +/// +/// Configuration to be executed may belong to [DeviceProfile#DESKTOP] or [DeviceProfile#MOBILE] namespaces. +/// It should correspond to some standalone Entity Centre on module menus of the web application. +/// Embedded Centres are not currently supported due to the need to specify their context through some additional API. +/// +/// The next extension to this API may include: +/// - create custom named configurations; +/// - modify existing named configurations with custom criteria values, ordering, page capacity etc. +/// - cover operations for default configurations +/// - cover operations for embedded Entity Centres with some master context (see [IQueryEnhancer]). +/// +public interface EntityCentreProcessor { + String ERR_EXECUTION_FAILED_PREFIX = "Entity Centre configuration execution failed. "; + String ERR_DEFAULT_CONFIG_WITH_UUID_SHOULD_NOT_EXIST = ERR_EXECUTION_FAILED_PREFIX + "[%s] configuration with blank name [%s] shouldn't exist (default configuration should never have UUID)."; + String ERR_LINK_CONFIG_IS_NOT_AVAILABLE_FOR_RUNNING = ERR_EXECUTION_FAILED_PREFIX + "[%s] link config ([%s]) is not available for API running."; + String ERR_CONFIG_MENU_ITEM_TYPE_CANT_BE_FOUND = ERR_EXECUTION_FAILED_PREFIX + "[%s] config's menu item type [%s] can not be found."; + String ERR_CONFIG_DOES_NOT_EXIST = ERR_EXECUTION_FAILED_PREFIX + "Config with [%s] UUID does not exist."; + String ERR_CONFIG_UUID_IS_BLANK = ERR_EXECUTION_FAILED_PREFIX + "Config UUID [%s] is blank."; + String ERR_CONFIG_COULD_NOT_BE_EXECUTED = "Entity Centre configuration with [%s] UUID could not be executed."; + + /// Executes named Entity Centre configuration, defined by UUID, similarly as the owner may have run it through Web UI. + /// Takes into account all unsaved changes in that configuration. + /// + /// Returns [Left] with invalid [Result] for the cases where + /// - UUID is blank (e.g. for default configurations) + /// - there is no configuration with that UUID (or there are multiple ones for some reason) + /// - there are only "orphan" inherited-from-shared configurations with no original one (i.e. if it was deleted) + /// - UUID represents so-called "link" configuration that originates from parameters like "?poCrit=PO001" + /// - UUID represents configuration with validation errors (e.g. requiredness or others) + /// - UUID represents configuration with authorisation errors (either Can Read or Can Read Property for non-empty criterion) + /// - UUID represents configuration with generator errors. + /// + /// Returns [Right] with [IPage] of entities corresponding to configuration criteria, ordering and page capacity. + /// Entity Centre with [IGenerator] performs generation of new data during execution through this API. + /// + /// Entity Centres with dynamic properties are supported (see `IResultSetBuilderDynamicProps#addProps` API). + /// + /// Example: + /// ``` + /// final IPage workOrders = entityCentreProcessor + /// .getResult(configUuid) + /// .orElseThrow(Result::throwRuntime); + /// ``` + /// + /// **Important**: running of this method in context of `@SessionRequired` scope may roll back active transaction. + /// This means that API users must exercise caution if [Left] is returned (use [Either#orElseThrow(Function)]). + /// + /// @param corresponds to the root entity type of the executed Entity Centre configuration. + /// + > Either> getResult( + String configUuid + ); + + /// Finds out whether named Entity Centre configuration, defined by UUID, has non-empty result (similarly to Web UI running). + /// Takes into account all unsaved changes in that configuration. + /// + /// Returns [Left] with invalid [Result] for the cases where + /// - UUID is blank (e.g. for default configurations) + /// - there is no configuration with that UUID (or there are multiple ones for some reason) + /// - there are only "orphan" inherited-from-shared configurations with no original one (i.e. if it was deleted) + /// - UUID represents so-called "link" configuration that originates from parameters like "?poCrit=PO001" + /// - UUID represents configuration with validation errors (e.g. requiredness or others) + /// - UUID represents configuration with authorisation errors (either Can Read or Can Read Property for non-empty criterion) + /// - UUID represents configuration with generator errors. + /// + /// Returns [Right] with [Boolean] indicator for presence of entities corresponding to configuration criteria. + /// Entity Centre with [IGenerator] still performs fresh data generation during execution through this API. + /// + /// Example: + /// ``` + /// final boolean workOrdersPresent = entityCentreProcessor + /// .resultExists(configUuid) + /// .orElseThrow(Result::throwRuntime); + /// ``` + /// + /// **Important**: running of this method in context of `@SessionRequired` scope may roll back active transaction. + /// This means that API users must exercise caution if [Left] is returned (use [Either#orElseThrow(Function)]). + /// + Either resultExists( + String configUuid + ); + + /// Finds out a count of entities in named Entity Centre configuration, defined by UUID (similarly to Web UI running). + /// Takes into account all unsaved changes in that configuration. + /// + /// Returns [Left] with invalid [Result] for the cases where + /// - UUID is blank (e.g. for default configurations) + /// - there is no configuration with that UUID (or there are multiple ones for some reason) + /// - there are only "orphan" inherited-from-shared configurations with no original one (i.e. if it was deleted) + /// - UUID represents so-called "link" configuration that originates from parameters like "?poCrit=PO001" + /// - UUID represents configuration with validation errors (e.g. requiredness or others) + /// - UUID represents configuration with authorisation errors (either Can Read or Can Read Property for non-empty criterion) + /// - UUID represents configuration with generator errors. + /// + /// Returns [Right] with [Integer] for a count of entities corresponding to configuration criteria. + /// Entity Centre with [IGenerator] still performs fresh data generation during execution through this API. + /// + /// Example: + /// ``` + /// final int workOrdersCount = entityCentreProcessor + /// .resultCount(configUuid) + /// .orElseThrow(Result::throwRuntime); + /// ``` + /// + /// **Important**: running of this method in context of `@SessionRequired` scope may roll back active transaction. + /// This means that API users must exercise caution if [Left] is returned (use [Either#orElseThrow(Function)]). + /// + Either resultCount( + String configUuid + ); + + /// Finds out whether named Entity Centre configuration, defined by UUID, is valid for API execution. + /// + /// Returns [Left] with invalid [Result] for the cases where + /// - UUID is blank (e.g. for default configurations) + /// - there is no configuration with that UUID (or there are multiple ones for some reason) + /// - there are only "orphan" inherited-from-shared configurations with no original one (i.e. if it was deleted) + /// - UUID represents so-called "link" configuration that originates from parameters like "?poCrit=PO001". + /// + /// Other unlikely cases where this method returns [Left] with invalid [Result] are if + /// - miType for some reason does not exist, but configuration still refers to it (perhaps, renamed or deleted) + /// - default configuration was found with UUID (default configs should never have UUIDs). + /// + /// Returns [Right] with [ConfigSettings] in case of valid configuration. + /// + /// Example: + /// ``` + /// final ConfigSettings configSettings = entityCentreProcessor + /// .validate(configUuid) + /// .orElseThrow(Result::throwRuntime); + /// ``` + /// + /// **Important**: running of this method in context of `@SessionRequired` scope may roll back active transaction. + /// This means that API users must exercise caution if [Left] is returned (use [Either#orElseThrow(Function)]). + /// + Either validate( + String configUuid + ); + +} diff --git a/platform-pojo-bl/src/main/java/ua/com/fielden/platform/web/utils/HeadlessCentreExecution.java b/platform-pojo-bl/src/main/java/ua/com/fielden/platform/web/utils/HeadlessCentreExecution.java new file mode 100644 index 00000000000..34787c328ac --- /dev/null +++ b/platform-pojo-bl/src/main/java/ua/com/fielden/platform/web/utils/HeadlessCentreExecution.java @@ -0,0 +1,8 @@ +package ua.com.fielden.platform.web.utils; + +/// Headless execution invoked from `DefaultEntityCentreProcessor` for the programmatic API. +/// UI-only output (rendering hints, primary/secondary/property action indices, dynamic-column metadata, +/// criteria-changed indication, leading custom-object entry in the result list) is suppressed. +/// The result list contains only entities. +/// +public record HeadlessCentreExecution() implements CentreExecutionMode {} \ No newline at end of file diff --git a/platform-pojo-bl/src/main/java/ua/com/fielden/platform/web/utils/UiCentreExecution.java b/platform-pojo-bl/src/main/java/ua/com/fielden/platform/web/utils/UiCentreExecution.java new file mode 100644 index 00000000000..58427bdadb0 --- /dev/null +++ b/platform-pojo-bl/src/main/java/ua/com/fielden/platform/web/utils/UiCentreExecution.java @@ -0,0 +1,14 @@ +package ua.com.fielden.platform.web.utils; + +import ua.com.fielden.platform.domaintree.centre.ICentreDomainTreeManager.ICentreDomainTreeManagerAndEnhancer; + +/// UI-driven execution from `CriteriaResource`. All UI-specific output is produced. +/// +/// `updatedFreshCentre` and `previouslyRunCentre` are consumed only when `isRunning` is `true`, +/// to compute the criteria-changed indication. They are unused when `isRunning` is `false` +/// (sort / navigate / refresh) but must still be supplied for symmetry with the UI request flow. +/// +public record UiCentreExecution( + ICentreDomainTreeManagerAndEnhancer updatedFreshCentre, + ICentreDomainTreeManagerAndEnhancer previouslyRunCentre +) implements CentreExecutionMode {} \ No newline at end of file diff --git a/platform-web-resources/src/main/java/ua/com/fielden/platform/web/ioc/IBasicWebApplicationServerModule.java b/platform-web-resources/src/main/java/ua/com/fielden/platform/web/ioc/IBasicWebApplicationServerModule.java index c73ed2297ce..5d2bc1eb2d6 100644 --- a/platform-web-resources/src/main/java/ua/com/fielden/platform/web/ioc/IBasicWebApplicationServerModule.java +++ b/platform-web-resources/src/main/java/ua/com/fielden/platform/web/ioc/IBasicWebApplicationServerModule.java @@ -19,6 +19,8 @@ import ua.com.fielden.platform.web.uri.EntityMasterUrlProvider; import ua.com.fielden.platform.web.utils.AuditMenuItemInitialiser; import ua.com.fielden.platform.web.utils.CriteriaEntityRestorer; +import ua.com.fielden.platform.web.utils.EntityCentreProcessor; +import ua.com.fielden.platform.web.utils.DefaultEntityCentreProcessor; import ua.com.fielden.platform.web.utils.ICriteriaEntityRestorer; import static ua.com.fielden.platform.basic.config.Workflows.deployment; @@ -63,6 +65,7 @@ default void bindWebAppResources(final IWebUiConfig webApp) { // Dependent on IWebUiConfig, IUserProvider and other Web UI infrastructure. bindType(ICriteriaEntityRestorer.class).to(CriteriaEntityRestorer.class); + bindType(EntityCentreProcessor.class).to(DefaultEntityCentreProcessor.class); // Required to load entity centre data into Audit Menu Item entity. bindType(IAuditMenuItemInitialiser.class).to(AuditMenuItemInitialiser.class); diff --git a/platform-web-resources/src/main/java/ua/com/fielden/platform/web/resources/webui/CentreResourceUtils.java b/platform-web-resources/src/main/java/ua/com/fielden/platform/web/resources/webui/CentreResourceUtils.java index 50f046f588c..3eb50b7c267 100644 --- a/platform-web-resources/src/main/java/ua/com/fielden/platform/web/resources/webui/CentreResourceUtils.java +++ b/platform-web-resources/src/main/java/ua/com/fielden/platform/web/resources/webui/CentreResourceUtils.java @@ -30,15 +30,14 @@ import ua.com.fielden.platform.reflection.ClassesRetriever; import ua.com.fielden.platform.reflection.Finder; import ua.com.fielden.platform.reflection.PropertyTypeDeterminator; -import ua.com.fielden.platform.security.user.IUser; import ua.com.fielden.platform.security.user.User; import ua.com.fielden.platform.serialisation.jackson.DefaultValueContract; import ua.com.fielden.platform.types.either.Either; import ua.com.fielden.platform.types.either.Left; import ua.com.fielden.platform.types.either.Right; +import ua.com.fielden.platform.types.tuples.T3; import ua.com.fielden.platform.ui.config.EntityCentreConfig; import ua.com.fielden.platform.ui.config.EntityCentreConfigCo; -import ua.com.fielden.platform.ui.config.MainMenuItemCo; import ua.com.fielden.platform.ui.menu.MiWithConfigurationSupport; import ua.com.fielden.platform.utils.EntityUtils; import ua.com.fielden.platform.utils.Pair; @@ -48,6 +47,7 @@ import ua.com.fielden.platform.web.centre.api.actions.EntityActionConfig; import ua.com.fielden.platform.web.centre.api.context.CentreContextConfig; import ua.com.fielden.platform.web.interfaces.DeviceProfile; +import ua.com.fielden.platform.web.utils.ConfigSettings; import ua.com.fielden.platform.web.utils.EntityResourceUtils; import java.lang.reflect.Field; @@ -75,6 +75,7 @@ import static ua.com.fielden.platform.types.either.Either.left; import static ua.com.fielden.platform.types.either.Either.right; import static ua.com.fielden.platform.types.tuples.T2.t2; +import static ua.com.fielden.platform.types.tuples.T3.t3; import static ua.com.fielden.platform.utils.EntityUtils.areEqual; import static ua.com.fielden.platform.utils.EntityUtils.equalsEx; import static ua.com.fielden.platform.web.centre.AbstractCentreConfigAction.APPLIED_CRITERIA_ENTITY_NAME; @@ -82,6 +83,8 @@ import static ua.com.fielden.platform.web.centre.CentreContext.CHOSENENTITY_PROPERTY_NAME; import static ua.com.fielden.platform.web.centre.CentreContext.INSTANCEBASEDCONTINUATION_PROPERTY_NAME; import static ua.com.fielden.platform.web.centre.CentreUpdaterUtils.*; +import static ua.com.fielden.platform.web.centre.WebApiUtils.*; +import static ua.com.fielden.platform.web.resources.webui.CentreResourceUtils.RunActions.*; import static ua.com.fielden.platform.web.resources.webui.CriteriaIndication.CHANGED; import static ua.com.fielden.platform.web.resources.webui.CriteriaIndication.NONE; import static ua.com.fielden.platform.web.resources.webui.CriteriaResource.*; @@ -92,10 +95,7 @@ /// public class CentreResourceUtils> extends CentreUtils { private static final Logger logger = LogManager.getLogger(CentreResourceUtils.class); - public static final String CONFIG_DOES_NOT_EXIST = "Configuration does not exist."; - private static final String SAVE_MSG = "Please save and try again."; public static final String SAVE_OWN_COPY_MSG = "Only sharing of your own configurations is supported. Please save as your copy and try again."; - private static final String DUPLICATE_SAVE_MSG = "Please duplicate, save and try again."; /// The key for customObject's value containing save-as name. public static final String SAVE_AS_NAME = "saveAsName"; @@ -124,12 +124,15 @@ public class CentreResourceUtils> extends CentreUtil /// The key for customObject's value containing the config share validation error. static final String SHARE_ERROR = "shareError"; + /// The key for [RunActions]. + public static final String RUN_ACTION_KEY = "@@action"; + /// Private default constructor to prevent instantiation. /// private CentreResourceUtils() { } - private enum RunActions { + public enum RunActions { RUN("run"), REFRESH("refresh"), NAVIGATE("navigate"); @@ -218,7 +221,7 @@ static Map createCriteriaMetaValuesCustomObject(final Map customObject) { - return RunActions.RUN.toString().equals(customObject.get("@@action")); + return RUN.toString().equals(customObject.get(RUN_ACTION_KEY)); } /// A predicate to determine whether `customObject` represents action with `retrieveAll` option. @@ -228,7 +231,7 @@ public static boolean isRetrieveAll(final Map customObject) { /// A predicate to determine whether `customObject` represents action `Refresh`. public static boolean isRefreshing(final Map customObject) { - return RunActions.REFRESH.toString().equals(customObject.get("@@action")); + return REFRESH.toString().equals(customObject.get(RUN_ACTION_KEY)); } /// Returns `true` if `Sorting` action is performed, otherwise `false`. @@ -246,7 +249,7 @@ public static boolean isAutoRunning(final Map customObject) { /// /// @param updatedPreviouslyRunCriteriaEntity criteria entity created from PREVIOUSLY_RUN surrogate centre, which was potentially updated from FRESH (in case of "running" action), but not yet actually used for running /// - static , M extends EnhancedCentreEntityQueryCriteria>> Pair, List>> createCriteriaMetaValuesCustomObjectWithResult( + static , M extends EnhancedCentreEntityQueryCriteria>> T3, List>, IPage> createCriteriaMetaValuesCustomObjectWithResult( final Map customObject, final M updatedPreviouslyRunCriteriaEntity) { final Map resultantCustomObject = new LinkedHashMap<>(); @@ -258,7 +261,7 @@ public static boolean isAutoRunning(final Map customObject) { // For refresh / navigate action this instance was not changed from previous run / refresh / navigate action. // For run action this instance was already updated from FRESH surrogate centre and includes "fresh" pageCapacity for the purposes of running // (the only way to make FRESH pageCapacity different from PREVIOUSLY_RUN is to change it using Customise Columns and press DISCARD on selection criteria). - final String action = (String) customObject.get("@@action"); + final String action = (String) customObject.get(RUN_ACTION_KEY); final int pageNumber = isRunning(customObject) ? 0 : (Integer) customObject.get("@@pageNumber"); final boolean retrieveAll = isRetrieveAll(customObject); if (isRunning(customObject)) { @@ -284,7 +287,7 @@ public static boolean isAutoRunning(final Map customObject) { data = refreshedData.getKey(); resultantCustomObject.put("summary", refreshedData.getValue()); } - } else if (RunActions.NAVIGATE.toString().equals(action)) { + } else if (NAVIGATE.toString().equals(action)) { final int pageCapacity = secondTick.getPageCapacity(); try { page = updatedPreviouslyRunCriteriaEntity.getPage(pageNumber, pageCapacity); @@ -304,7 +307,7 @@ public static boolean isAutoRunning(final Map customObject) { final int pageCount = page != null ? page.numberOfPages() : pageCount(realPageCount(data.size(), secondTick.getPageCapacity())); resultantCustomObject.put("pageCount", pageCount); resultantCustomObject.put("pageNumber", page != null ? page.no() : pageCount <= pageNumber ? pageCount - 1 : pageNumber); - return new Pair<>(resultantCustomObject, (List>) data); + return t3(resultantCustomObject, (List>) data, page); } /// Runs the entity centre with option `retrieveAll`. Returns [Right] with a list of entities of all matching entities and a summary, if `retrieveAll == true`. @@ -472,6 +475,26 @@ private static void applyMetaValues(final ICentreDomainTreeManagerAndEnhancer cd } } + /// Creates criteria validation prototype for `surrogateName` and [ConfigSettings]. + /// + /// @param surrogateName surrogate name of the centre ([CentreUpdater#FRESH_CENTRE_NAME], [CentreUpdater#PREVIOUSLY_RUN_CENTRE_NAME] etc.); + /// + public static , M extends EnhancedCentreEntityQueryCriteria>> M createCriteriaValidationPrototype( + final String surrogateName, + final ConfigSettings configSettings, + final ICompanionObjectFinder companionFinder, + final ICriteriaGenerator critGenerator, + final IWebUiConfig webUiConfig, + final ICentreConfigSharingModel sharingModel + ) { + // Load / update centre manager instance from persistence storage. + final var centreManager = updateCentre(configSettings.owner(), configSettings.miType(), surrogateName, configSettings.saveAsName(), configSettings.device(), webUiConfig, companionFinder); + // Construct criteria validation prototype. + final M validationPrototype = createCriteriaValidationPrototype(configSettings.miType(), configSettings.saveAsName(), centreManager, companionFinder, critGenerator, -1L, configSettings.owner(), configSettings.device(), webUiConfig, sharingModel); + // Apply meta-state resetting. + return resetMetaStateForCriteriaValidationPrototype(validationPrototype, getOriginalManagedType(validationPrototype.getType(), centreManager)); + } + /// Creates the validation prototype for criteria entity of concrete `miType`. /// /// The entity creation process uses rigorous generation of criteria type and the instance every time (based on cdtmae of concrete miType). diff --git a/platform-web-resources/src/main/java/ua/com/fielden/platform/web/resources/webui/CriteriaResource.java b/platform-web-resources/src/main/java/ua/com/fielden/platform/web/resources/webui/CriteriaResource.java index 5033a780d27..e052ee40715 100644 --- a/platform-web-resources/src/main/java/ua/com/fielden/platform/web/resources/webui/CriteriaResource.java +++ b/platform-web-resources/src/main/java/ua/com/fielden/platform/web/resources/webui/CriteriaResource.java @@ -11,6 +11,7 @@ import org.restlet.resource.Put; import ua.com.fielden.platform.criteria.generator.ICriteriaGenerator; import ua.com.fielden.platform.dao.IEntityDao; +import ua.com.fielden.platform.data.generator.IGenerator; import ua.com.fielden.platform.domaintree.centre.ICentreDomainTreeManager.ICentreDomainTreeManagerAndEnhancer; import ua.com.fielden.platform.domaintree.impl.CalculatedProperty; import ua.com.fielden.platform.entity.AbstractEntity; @@ -20,6 +21,7 @@ import ua.com.fielden.platform.entity.meta.MetaProperty; import ua.com.fielden.platform.entity_centre.review.criteria.EnhancedCentreEntityQueryCriteria; import ua.com.fielden.platform.error.Result; +import ua.com.fielden.platform.pagination.IPage; import ua.com.fielden.platform.security.IAuthorisationModel; import ua.com.fielden.platform.security.provider.ISecurityTokenProvider; import ua.com.fielden.platform.security.user.IUserProvider; @@ -44,6 +46,9 @@ import ua.com.fielden.platform.web.interfaces.DeviceProfile; import ua.com.fielden.platform.web.interfaces.IDeviceProvider; import ua.com.fielden.platform.web.resources.RestServerUtil; +import ua.com.fielden.platform.web.utils.CentreExecutionMode; +import ua.com.fielden.platform.web.utils.ConfigSettings; +import ua.com.fielden.platform.web.utils.UiCentreExecution; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -53,16 +58,17 @@ import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; -import java.util.stream.Collectors; import java.util.stream.Stream; import static java.lang.String.format; import static java.util.Optional.*; import static java.util.UUID.randomUUID; import static java.util.concurrent.TimeUnit.SECONDS; +import static java.util.stream.Collectors.toMap; import static ua.com.fielden.platform.data.generator.IGenerator.FORCE_REGENERATION_KEY; import static ua.com.fielden.platform.data.generator.IGenerator.shouldForceRegeneration; import static ua.com.fielden.platform.error.Result.failure; +import static ua.com.fielden.platform.error.Result.successful; import static ua.com.fielden.platform.security.tokens.Template.READ; import static ua.com.fielden.platform.security.tokens.TokenUtils.authoriseCriteria; import static ua.com.fielden.platform.security.tokens.TokenUtils.authoriseReading; @@ -77,9 +83,9 @@ import static ua.com.fielden.platform.web.centre.CentreUpdater.removeCentres; import static ua.com.fielden.platform.web.centre.CentreUpdaterUtils.*; import static ua.com.fielden.platform.web.centre.CentreUtils.isFreshCentreChanged; +import static ua.com.fielden.platform.web.centre.WebApiUtils.CONFIG_DOES_NOT_EXIST; import static ua.com.fielden.platform.web.centre.WebApiUtils.LINK_CONFIG_TITLE; -import static ua.com.fielden.platform.web.factories.webui.ResourceFactoryUtils.extractSaveAsName; -import static ua.com.fielden.platform.web.factories.webui.ResourceFactoryUtils.wasLoadedPreviouslyAndConfigUuid; +import static ua.com.fielden.platform.web.factories.webui.ResourceFactoryUtils.*; import static ua.com.fielden.platform.web.resources.webui.CentreResourceUtils.*; import static ua.com.fielden.platform.web.resources.webui.CriteriaIndication.*; import static ua.com.fielden.platform.web.resources.webui.EntityValidationResource.VALIDATION_COUNTER; @@ -306,7 +312,7 @@ private String determineNonConflictingName(final String preliminaryName, final i name = preliminaryName + (index == -1 ? "" : format(CONFLICTING_TITLE_SUFFIX, index == 0 ? "" : " " + index)); } return findConfigOpt(miType, user, NAME_OF.apply(FRESH_CENTRE_NAME).apply(of(name)).apply(device()), companionFinder, FETCH_CONFIG) - .map(conflictingConfig -> determineNonConflictingName(preliminaryName, index + 1)) + .map(_ -> determineNonConflictingName(preliminaryName, index + 1)) .orElse(name); } @@ -441,7 +447,9 @@ public static Representation createCriteriaRetrievalEnvelope( ); } - public static Representation createCriteriaDiscardEnvelope( + /// Creates resource envelope of type [Representation] for discarded Entity Centre configuration. + /// + static Representation createCriteriaDiscardEnvelope( final ICentreDomainTreeManagerAndEnhancer updatedFreshCentre, final Class> miType, final Optional saveAsName, @@ -473,7 +481,9 @@ public static Representation createCriteriaDiscardEnvelope( ); } - public static , M extends EnhancedCentreEntityQueryCriteria>> CriteriaIndication createCriteriaIndication( + /// Calculates [CriteriaIndication] for the Entity Centre configuration to be returned as part of resultant envelopes to the client. + /// + static CriteriaIndication createCriteriaIndication( final String wasRun, final ICentreDomainTreeManagerAndEnhancer freshCentre, final Class> miType, @@ -494,20 +504,86 @@ public static Representation createCriteriaDiscardEnvelope( return NONE; } - public static CriteriaIndication createChangedCriteriaIndication(final ICentreDomainTreeManagerAndEnhancer freshCentre, final ICentreDomainTreeManagerAndEnhancer savedCentre) { + /// Calculates [CriteriaIndication] for the Entity Centre configuration by comparing "fresh" vs "saved" versions. + /// + static CriteriaIndication createChangedCriteriaIndication(final ICentreDomainTreeManagerAndEnhancer freshCentre, final ICentreDomainTreeManagerAndEnhancer savedCentre) { return !savedCentre.getFirstTick().selectionCriteriaEquals(freshCentre.getFirstTick()) ? CHANGED : NONE; } - private Result authoriseCriteriaEntity(final EnhancedCentreEntityQueryCriteria criteriaEntity) { - final var entityAuthorisationResult = authoriseReading(getEntityType(miType).getSimpleName(), READ, authorisationModel, securityTokenProvider); + /// Authorises criteria entity on data reading for the whole centre and for individual guarded criteria with non-empty values. + /// + public static Result authoriseCriteriaEntity( + final EnhancedCentreEntityQueryCriteria criteriaEntity, + final IAuthorisationModel authorisationModel, + final ISecurityTokenProvider securityTokenProvider + ) { + final var entityAuthorisationResult = authoriseReading(getEntityType(criteriaEntity.miType()).getSimpleName(), READ, authorisationModel, securityTokenProvider); return entityAuthorisationResult.isSuccessful() - ? authoriseCriteria(criteriaEntity.queryProperties.get(), authorisationModel, securityTokenProvider) + ? authoriseCriteria(criteriaEntity.queryProperties.get(), authorisationModel) : entityAuthorisationResult; } - /// Handles `PUT` requests triggered by the `tg-selection-criteria.run()` method. + /// Validates Entity Centre criteria entity prior running, including crit-only single prototype validation and authorisation. + /// + public static Result validateCriteriaBeforeRunning( + final EnhancedCentreEntityQueryCriteria criteriaEntity, + final IAuthorisationModel authorisationModel, + final ISecurityTokenProvider securityTokenProvider + ) { + final Result validationResult = criteriaEntity.isValid(); + if (!validationResult.isSuccessful()) { + return validationResult; + } + + final Result authorisationResult = authoriseCriteriaEntity(criteriaEntity, authorisationModel, securityTokenProvider); + if (!authorisationResult.isSuccessful()) { + return authorisationResult; + } + return successful(); + } + + /// Generates Entity Centre data for the cases, where [IGenerator] is specified. + /// Refresh / navigate / sort actions are not triggering re-generation, only run does. + /// Returns the [Result] of generation, which may be invalid, based on the business logic. /// @SuppressWarnings("unchecked") + public static Result generateDataIfNeeded( + final EnhancedCentreEntityQueryCriteria criteriaEntity, + final Class> miType, + final IWebUiConfig webUiConfig, + final boolean isRunning, + final boolean isSorting, + final Map customObject + ) { + final var centre = getEntityCentre(miType.getName(), webUiConfig); + // If the run() invocation warrants data generation (e.g. it has nothing to do with sorting), + // then check if a generator was provided for an entity centre DSL configuration. + final var createdByConstraintShouldOccur = centre.getGeneratorTypes().isPresent(); + final var generationShouldOccur = isRunning && !isSorting && createdByConstraintShouldOccur; + if (generationShouldOccur) { + // Obtain the type for entities to be generated. + final var generatorEntityType = (Class>) centre.getGeneratorTypes().get().getKey(); + + // Create and execute a generator instance. + final var generator = centre.createGeneratorInstance(centre.getGeneratorTypes().get().getValue()); + final Map> params = criteriaEntity.nonProxiedProperties().collect(toLinkedHashMap( + MetaProperty::getName, + mp -> ofNullable(mp.getValue()) + )); + params.putAll(criteriaEntity.getParameters().entrySet().stream().collect(toMap( + Map.Entry::getKey, + entry -> ofNullable(entry.getValue())) + )); + if (shouldForceRegeneration(customObject)) { + params.put(FORCE_REGENERATION_KEY, of(true)); + } + return generator.gen(generatorEntityType, params); + } + return successful(); + } + + /// Handles `PUT` requests triggered by the `tg-selection-criteria.run()` method. + /// @Put @Override public Representation put(final Representation envelope) { @@ -517,8 +593,9 @@ public Representation put(final Representation envelope) { user = userProvider.getUser(); miType = centre.getMenuItemType(); - // obtain lock for current user and miType of the centre (disregard saveAsName as it is unlikely that self-concurrent running will occur for different configurations of the same centre) - final Lock lock = locks.computeIfAbsent(t2(user, miType), t2 -> new ReentrantLock()); // create Lock if not yet present; atomic action + // Obtain lock for current user and miType of the centre. + // Disregard saveAsName as it is unlikely that self-concurrent running will occur for different configurations of the same centre. + final Lock lock = locks.computeIfAbsent(t2(user, miType), _ -> new ReentrantLock()); // create Lock if not yet present; atomic action final boolean lockAcquired = tryLocking(lock); if (!lockAcquired) { LOGGER.info("The lock could not be acquired for [%s] seconds. Let's continue concurrent running of the [%s] centre and user [%s].".formatted(RUNNING_LOCK_TIMEOUT, miType.getSimpleName(), user)); @@ -558,20 +635,14 @@ public Representation put(final Representation envelope) { updatedFreshCentre = freshCentreAppliedCriteriaEntity.getCentreDomainTreeMangerAndEnhancer(); } - // There is a need to validate criteria entity with the check for 'required' properties. If it is not successful -- immediately return result without query running, fresh centre persistence, data generation etc. - final Result validationResult = freshCentreAppliedCriteriaEntity.isValid(); + // There is a need to validate criteria entity, particularly with the check for 'required' properties. + // If it is not successful -- immediately return result without query running, fresh centre persistence, data generation etc. + final Result validationResult = validateCriteriaBeforeRunning(freshCentreAppliedCriteriaEntity, authorisationModel, securityTokenProvider); if (!validationResult.isSuccessful()) { - LOGGER.debug("CRITERIA_RESOURCE: run finished (validation failed)."); - final var criteriaIndication = createCriteriaIndication((String) centreContextHolder.getModifHolder().get("@@wasRun"), updatedFreshCentre, miType, saveAsName, user, companionFinder, device(), webUiConfig); - return restUtil.rawListJsonRepresentation(freshCentreAppliedCriteriaEntity, updateResultantCustomObject(freshCentreAppliedCriteriaEntity.centreDirtyCalculator(), miType, saveAsName, updatedFreshCentre, new LinkedHashMap<>(), of(criteriaIndication))); - } - - final Result authorisationResult = authoriseCriteriaEntity(freshCentreAppliedCriteriaEntity); - if (!authorisationResult.isSuccessful()) { - LOGGER.debug("CRITERIA_RESOURCE: run failed (authorisation validation failed)."); + LOGGER.debug("CRITERIA_RESOURCE: run failed (validation failed)."); final var criteriaIndication = createCriteriaIndication((String) centreContextHolder.getModifHolder().get("@@wasRun"), updatedFreshCentre, miType, saveAsName, user, companionFinder, device(), webUiConfig); return restUtil.resultJSONRepresentation( - authorisationResult.copyWith(List.of( + validationResult.copyWith(List.of( freshCentreAppliedCriteriaEntity, updateResultantCustomObject(freshCentreAppliedCriteriaEntity.centreDirtyCalculator(), miType, saveAsName, updatedFreshCentre, new LinkedHashMap<>(), of(criteriaIndication)) )) @@ -582,33 +653,18 @@ public Representation put(final Representation envelope) { freshCentreAppliedCriteriaEntity = null; } - // if the run() invocation warrants data generation (e.g. it has nothing to do with sorting) - // then for an entity centre configuration check if a generator was provided - final boolean createdByConstraintShouldOccur = centre.getGeneratorTypes().isPresent(); - final boolean generationShouldOccur = isRunning && !isSorting && createdByConstraintShouldOccur; - if (generationShouldOccur) { - // obtain the type for entities to be generated - final Class> generatorEntityType = (Class>) centre.getGeneratorTypes().get().getKey(); - - // create and execute a generator instance - final var generator = centre.createGeneratorInstance(centre.getGeneratorTypes().get().getValue()); - final Map> params = freshCentreAppliedCriteriaEntity.nonProxiedProperties().collect(toLinkedHashMap( - (final MetaProperty mp) -> mp.getName(), - (final MetaProperty mp) -> ofNullable(mp.getValue()))); - params.putAll(freshCentreAppliedCriteriaEntity.getParameters().entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, entry -> Optional.ofNullable(entry.getValue())))); - if (shouldForceRegeneration(customObject)) { - params.put(FORCE_REGENERATION_KEY, of(true)); - } - final Result generationResult = generator.gen(generatorEntityType, params); - // if the data generation was unsuccessful based on the returned Result value then stop any further logic and return the obtained result - // otherwise, proceed with the request handling further to actually query the data - // in most cases, the generated and queried data would be represented by the same entity and, thus, the final query needs to be enhanced with user related filtering by property 'createdBy' - if (!generationResult.isSuccessful()) { - LOGGER.debug("CRITERIA_RESOURCE: run finished (generation failed)."); - final var criteriaIndication = createCriteriaIndication((String) centreContextHolder.getModifHolder().get("@@wasRun"), updatedFreshCentre, miType, saveAsName, user, companionFinder, device(), webUiConfig); - final Result result = generationResult.copyWith(List.of(freshCentreAppliedCriteriaEntity, updateResultantCustomObject(freshCentreAppliedCriteriaEntity.centreDirtyCalculator(), miType, saveAsName, updatedFreshCentre, new LinkedHashMap<>(), of(criteriaIndication)))); - return restUtil.resultJSONRepresentation(result); - } + // Handle generation logic for Entity Centres with `IGenerator`. + /// Refresh / navigate / sort actions are not triggering re-generation, only run does. + final var generationResult = generateDataIfNeeded(freshCentreAppliedCriteriaEntity, miType, webUiConfig, isRunning, isSorting, customObject); + // If the data generation was unsuccessful based on the returned Result value then stop any further logic and return the obtained result. + // Otherwise, proceed with the request handling further to actually query the data. + // In most cases, the generated and queried data would be represented by the same entity. + // And, in that case, the final query will be enhanced with user-related filtering by `createdBy` property. + if (!generationResult.isSuccessful()) { + LOGGER.debug("CRITERIA_RESOURCE: run finished (generation failed)."); + final var criteriaIndication = createCriteriaIndication((String) centreContextHolder.getModifHolder().get("@@wasRun"), updatedFreshCentre, miType, saveAsName, user, companionFinder, device(), webUiConfig); + final Result result = generationResult.copyWith(List.of(freshCentreAppliedCriteriaEntity, updateResultantCustomObject(freshCentreAppliedCriteriaEntity.centreDirtyCalculator(), miType, saveAsName, updatedFreshCentre, new LinkedHashMap<>(), of(criteriaIndication)))); + return restUtil.resultJSONRepresentation(result); } if (isRunning) { @@ -620,7 +676,7 @@ public Representation put(final Representation envelope) { // Performs criteria validation on centre refresh / navigate. // It is needed if the user changed token role association between run and refresh actions. if (!isRunning) { - final Result authorisationResult = authoriseCriteriaEntity(previouslyRunCriteriaEntity); + final Result authorisationResult = authoriseCriteriaEntity(previouslyRunCriteriaEntity, authorisationModel, securityTokenProvider); if (!authorisationResult.isSuccessful()) { LOGGER.debug("CRITERIA_RESOURCE: refresh failed (authorisation validation failed)."); return restUtil.resultJSONRepresentation( @@ -631,71 +687,38 @@ public Representation put(final Representation envelope) { ); } } - final Pair, List>> pair = createCriteriaMetaValuesCustomObjectWithResult( - customObject, - complementCriteriaEntityBeforeRunning( // complements previouslyRunCriteriaEntity instance - previouslyRunCriteriaEntity, - webUiConfig, - companionFinder, - user, - critGenerator, - entityFactory, - centreContextHolder, - sharingModel - ) + + // Complements criteria entity with IGenerator-related user condition, query enhancer, fetch providers etc. + // I.e. prepares criteria entity for running. + complementCriteriaEntityBeforeRunning( + previouslyRunCriteriaEntity, + webUiConfig, + companionFinder, + user, + critGenerator, + entityFactory, + centreContextHolder, + sharingModel ); - if (isRunning) { - final var updatedSavedCentre = updateCentre(user, miType, SAVED_CENTRE_NAME, saveAsName, device(), webUiConfig, companionFinder); - final var changedCriteriaIndication = createChangedCriteriaIndication(updatedFreshCentre, updatedSavedCentre); - updateResultantCustomObject(previouslyRunCriteriaEntity.centreDirtyCalculatorWithSavedSupplier().apply(() -> updatedSavedCentre), miType, saveAsName, previouslyRunCentre, pair.getKey(), of(changedCriteriaIndication)); - } - // Running the rendering customiser for result set of entities. - pair.getKey().put("renderingHints", createRenderingHints(pair.getValue())); + // Execute actual Entity Centre configuration run / refresh / navigate / sort logic. + final var resultListAndPage = executeEntityCentreConfiguration( + new ConfigSettings(saveAsName, user, device(), miType, null), + new UiCentreExecution(updatedFreshCentre, previouslyRunCentre), + isRunning, - // Apply primary and secondary action selectors - pair.getKey().putAll(linkedMapOf(createPrimaryActionIndicesForCentre(pair.getValue(), centre))); - pair.getKey().putAll(linkedMapOf(createSecondaryActionIndicesForCentre(pair.getValue(), centre))); - pair.getKey().putAll(linkedMapOf(createPropertyActionIndicesForCentre(pair.getValue(), centre))); + customObject, + previouslyRunCriteriaEntity, + webUiConfig, + companionFinder, + critGenerator, + entityFactory, + centreContextHolder, + sharingModel + ); - // Build dynamic properties object - final List>, Optional, ?>>>> resPropsWithContext = getDynamicResultProperties( - centre, - webUiConfig, - companionFinder, - user, - critGenerator, - entityFactory, - centreContextHolder, - previouslyRunCriteriaEntity, - device(), - sharingModel); - - pair.getKey().put("dynamicColumns", createDynamicProperties(resPropsWithContext)); - - Stream> processedEntities = enhanceResultEntitiesWithCustomPropertyValues( - centre, - centre.getCustomPropertiesDefinitions(), - centre.getCustomPropertiesAsignmentHandler(), - pair.getValue().stream()); - - //Enhance entities with values defined with consumer in each dynamic property. - processedEntities = enhanceResultEntitiesWithDynamicPropertyValues(processedEntities, resPropsWithContext); - //Enhance rendering hints with styles for each dynamic column. - processedEntities = enhanceResultEntitiesWithDynamicPropertyRenderingHints(processedEntities, resPropsWithContext, (List) pair.getKey().get("renderingHints")); - - final var list = new ArrayList(); - list.add(isRunning ? previouslyRunCriteriaEntity : null); - list.add(pair.getKey()); - - // TODO It looks like adding values directly to the list outside the map object leads to proper type/serialiser correspondence - // FIXME Need to investigate why this is the case. - processedEntities.forEach(list::add); - - // NOTE: the following line can be the example how 'criteria running' server errors manifest to the client application - // throw new IllegalStateException("Illegal state during criteria running."); LOGGER.debug("CRITERIA_RESOURCE: run finished."); - return restUtil.rawListJsonRepresentation(list.toArray()); + return restUtil.rawListJsonRepresentation(resultListAndPage._1.toArray()); } finally { if (lockAcquired) { // it is necessary to unlock Lock in finally block (exceptions will be handled properly then) @@ -705,6 +728,185 @@ public Representation put(final Representation envelope) { }, restUtil); } + /// Executes Entity Centre configuration with [ConfigSettings]. Uses `criteriaEntity` for execution. + /// + /// @param mode distinguishes UI-driven execution (computes rendering hints, action indices, + /// dynamic-column metadata, and -- when `isRunning` -- the criteria-changed indication) + /// from headless execution (data only). See [CentreExecutionMode]. + /// + public static T2, IPage>> executeEntityCentreConfiguration( + final ConfigSettings configSettings, + final CentreExecutionMode mode, + final boolean isRunning, + final Map customObject, + final EnhancedCentreEntityQueryCriteria, ?> criteriaEntity, + + final IWebUiConfig webUiConfig, + final ICompanionObjectFinder companionFinder, + final ICriteriaGenerator critGenerator, + final EntityFactory entityFactory, + final CentreContextHolder centreContextHolder, + final ICentreConfigSharingModel sharingModel + ) { + final var centre = getEntityCentre(criteriaEntity.miType().getName(), webUiConfig); + + final var resultCustomObjectAndEntities = createCriteriaMetaValuesCustomObjectWithResult(customObject, criteriaEntity); + final var resultCustomObject = resultCustomObjectAndEntities._1; + final var resultEntities = resultCustomObjectAndEntities._2; + + // Pattern-bind once for use across the four UI-mode gates below. + // Equivalent in shape to the previous single-boolean gate, so the original computation order is preserved exactly. + final var ui = (mode instanceof UiCentreExecution u) ? u : null; + if (ui != null) { + if (isRunning) { + final var updatedSavedCentre = updateCentre(configSettings.owner(), configSettings.miType(), SAVED_CENTRE_NAME, configSettings.saveAsName(), configSettings.device(), webUiConfig, companionFinder); + final var changedCriteriaIndication = createChangedCriteriaIndication(ui.updatedFreshCentre(), updatedSavedCentre); + updateResultantCustomObject(criteriaEntity.centreDirtyCalculatorWithSavedSupplier().apply(() -> updatedSavedCentre), configSettings.miType(), configSettings.saveAsName(), ui.previouslyRunCentre(), resultCustomObject, of(changedCriteriaIndication)); + } + + // Running the rendering customiser for result set of entities. + resultCustomObject.put("renderingHints", createRenderingHints(resultEntities, centre)); + + // Apply primary and secondary action selectors. + resultCustomObject.putAll(linkedMapOf(createPrimaryActionIndicesForCentre(resultEntities, centre))); + resultCustomObject.putAll(linkedMapOf(createSecondaryActionIndicesForCentre(resultEntities, centre))); + resultCustomObject.putAll(linkedMapOf(createPropertyActionIndicesForCentre(resultEntities, centre))); + } + + // Build dynamic properties object + final var resPropsWithContext = getDynamicResultProperties( + centre, + webUiConfig, + companionFinder, + configSettings.owner(), + critGenerator, + entityFactory, + centreContextHolder, + criteriaEntity, + configSettings.device(), + sharingModel + ); + + if (ui != null) { + resultCustomObject.put("dynamicColumns", createDynamicProperties(resPropsWithContext, centre)); + } + + // Enhance entities with custom / dynamic property values. + final Stream> enhancedEntities = enhanceEntitiesWithValues(resultEntities.stream(), resPropsWithContext, centre); + final Stream> processedEntities; + if (ui != null) { + // Enhance rendering hints with styles for each dynamic column. + processedEntities = enhanceResultEntitiesWithDynamicPropertyRenderingHints(enhancedEntities, resPropsWithContext, (List) resultCustomObject.get("renderingHints")); + } + else { + processedEntities = enhancedEntities; + } + + final var list = new ArrayList<>(); + if (ui != null) { + list.add(isRunning ? criteriaEntity : null); + list.add(resultCustomObject); + } + + final List> data = new ArrayList<>(); + // TODO It looks like adding values directly to the list outside the map object leads to proper type/serialiser correspondence + // FIXME Need to investigate why this is the case. + processedEntities.forEach(entity -> { + list.add(entity); + data.add(entity); + }); + return t2(list, new Page(resultCustomObjectAndEntities._3, data, resPropsWithContext, centre)); + } + + /// Enhances entities with custom / dynamic property values. + /// + private static Stream> enhanceEntitiesWithValues( + final Stream> entityStream, + final List>, Optional, ?>>>> resPropsWithContext, + final EntityCentre> centre + ) { + // Enhance entities with values defined with consumer in each dynamic property. + return enhanceResultEntitiesWithDynamicPropertyValues( + // Enhance entities with custom values. + enhanceResultEntitiesWithCustomPropertyValues( + centre, + centre.getCustomPropertiesDefinitions(), + centre.getCustomPropertiesAsignmentHandler(), + entityStream + ), + resPropsWithContext + ); + } + + /// A page implementation that takes into account the need to [#enhanceEntitiesWithValues(Stream, List, EntityCentre)]. + /// + private record Page( + IPage> backingPage, + List> data, + List>, Optional, ?>>>> resPropsWithContext, + EntityCentre> centre + ) implements IPage> { + + @Override + public int capacity() { + return backingPage.capacity(); + } + + @Override + public int no() { + return backingPage.no(); + } + + @Override + public int numberOfPages() { + return backingPage.numberOfPages(); + } + + @Override + public boolean hasNext() { + return backingPage.hasNext(); + } + + @Override + public boolean hasPrev() { + return backingPage.hasPrev(); + } + + private Page createPage(final IPage> page) { + return new Page( + page, + enhanceEntitiesWithValues(page.data().stream(), resPropsWithContext, centre).toList(), + resPropsWithContext, + centre + ); + } + + @Override + public IPage> next() { + return createPage(backingPage.next()); + } + + @Override + public IPage> prev() { + return createPage(backingPage.prev()); + } + + @Override + public IPage> last() { + return createPage(backingPage.last()); + } + + @Override + public IPage> first() { + return createPage(backingPage.first()); + } + + @Override + public AbstractEntity summary() { + return backingPage.summary(); + } + } + /// A method to try acquiring a lock for running a centre. /// It should not throw any exceptions, but simply return `true` if the lock was acquired or `false` otherwise. /// Attempts to acquire a lock for running a centre. @@ -727,7 +929,7 @@ private boolean tryLocking(final Lock lock) { /// Calculates rendering hints for the given `entities`. /// - private List createRenderingHints(final List> entities) { + private static List createRenderingHints(final List> entities, final EntityCentre> centre) { final Optional> renderingCustomiser = centre.getRenderingCustomiser(); if (renderingCustomiser.isPresent()) { final IRenderingCustomiser renderer = renderingCustomiser.get(); @@ -749,14 +951,14 @@ public static Stream> enhanceResultEntitiesWithDynamicProperty final List>, Optional, ?>>>> resPropsWithContext) { return stream.map(entity -> { - resPropsWithContext.forEach(resPropWithContext -> { - resPropWithContext.getKey().entityPreProcessor.get().accept(entity, resPropWithContext.getValue()); - }); + resPropsWithContext.forEach(resPropWithContext -> + resPropWithContext.getKey().entityPreProcessor.get().accept(entity, resPropWithContext.getValue()) + ); return entity; }); } - private Stream> enhanceResultEntitiesWithDynamicPropertyRenderingHints( + private static Stream> enhanceResultEntitiesWithDynamicPropertyRenderingHints( final Stream> stream, List>, Optional, ?>>>> resPropsWithContext, final List renderingHints) { @@ -771,11 +973,11 @@ private Stream> enhanceResultEntitiesWithDynamicPropertyRender entityRendHints = renderingHints.get(idx); } if (entityRendHints instanceof Map) { - resPropsWithContext.forEach(resPropWithContext -> { - resPropWithContext.getKey().renderingHintsProvider.ifPresent(hintProvider -> { - ((Map)entityRendHints).putAll(hintProvider.apply(entity, resPropWithContext.getValue())); - }); - }); + resPropsWithContext.forEach(resPropWithContext -> + resPropWithContext.getKey().renderingHintsProvider.ifPresent(hintProvider -> + ((Map) entityRendHints).putAll(hintProvider.apply(entity, resPropWithContext.getValue())) + ) + ); } return entity; }); @@ -794,35 +996,31 @@ private Stream> enhanceResultEntitiesWithDynamicPropertyRender final ICentreConfigSharingModel sharingModel) { final List>, Optional, ?>>>> resList = new ArrayList<>(); - centre.getDynamicProperties().forEach(resProp -> { - resProp.dynamicColBuilderType.ifPresent(propDefinerClass -> { - final Optional, ?>> optionalCentreContext = CentreResourceUtils.createCentreContext( - true, // full context, fully-fledged restoration. This means that IQueryEnhancer descendants (centre query enhancers) could use IContextDecomposer for context decomposition on deep levels. - webUiConfig, - companionFinder, - user, - critGenerator, - entityFactory, - centreContextHolder, - criteriaEntity, - resProp.contextConfig, - null, /* chosenProperty is not applicable in queryEnhancer context */ - device, - sharingModel - ); - resList.add(new Pair<>(resProp, optionalCentreContext)); - }); - }); + centre.getDynamicProperties().forEach(resProp -> resProp.dynamicColBuilderType.ifPresent(_ -> { + final Optional, ?>> optionalCentreContext = CentreResourceUtils.createCentreContext( + true, // full context, fully-fledged restoration. This means that IQueryEnhancer descendants (centre query enhancers) could use IContextDecomposer for context decomposition on deep levels. + webUiConfig, + companionFinder, + user, + critGenerator, + entityFactory, + centreContextHolder, + criteriaEntity, + resProp.contextConfig, + null, /* chosenProperty is not applicable in queryEnhancer context */ + device, + sharingModel + ); + resList.add(new Pair<>(resProp, optionalCentreContext)); + })); return resList; } - private Map>> createDynamicProperties(final List>, Optional, ?>>>> resPropsWithContext) { + private static Map>> createDynamicProperties(final List>, Optional, ?>>>> resPropsWithContext, final EntityCentre> centre) { final Map>> dynamicColumns = new LinkedHashMap<>(); - resPropsWithContext.forEach(resPropWithContext -> { - centre.getDynamicColumnBuilderFor(resPropWithContext.getKey()) - .flatMap(dynColumnBuilder -> dynColumnBuilder.getColumnsConfig(resPropWithContext.getValue())) - .ifPresent(config -> dynamicColumns.put(resPropWithContext.getKey().propName.get() + "Columns", config.build())); - }); + resPropsWithContext.forEach(resPropWithContext -> centre.getDynamicColumnBuilderFor(resPropWithContext.getKey()) + .flatMap(dynColumnBuilder -> dynColumnBuilder.getColumnsConfig(resPropWithContext.getValue())) + .ifPresent(config -> dynamicColumns.put(resPropWithContext.getKey().propName.get() + "Columns", config.build()))); return dynamicColumns; } @@ -861,31 +1059,27 @@ private static Map updateResultantCustomObject( final ICriteriaGenerator critGenerator, final EntityFactory entityFactory, final CentreContextHolder centreContextHolder, - final Optional, Optional>> queryEnhancerConfig, + final Optional, Optional>> maybeQueryEnhancerAndConfig, final EnhancedCentreEntityQueryCriteria criteriaEntity, final DeviceProfile device, final ICentreConfigSharingModel sharingModel) { - if (queryEnhancerConfig.isPresent()) { - return Optional.of(new Pair<>( - queryEnhancerConfig.get().getKey(), - CentreResourceUtils.createCentreContext( - true, // full context, fully-fledged restoration. This means that IQueryEnhancer descendants (centre query enhancers) could use IContextDecomposer for context decomposition on deep levels. - webUiConfig, - companionFinder, - user, - critGenerator, - entityFactory, - centreContextHolder, - criteriaEntity, - queryEnhancerConfig.get().getValue(), - null, /* chosenProperty is not applicable in queryEnhancer context */ - device, - sharingModel - ) - )); - } else { - return empty(); - } + return maybeQueryEnhancerAndConfig.map(queryEnhancerAndConfig -> new Pair<>( + queryEnhancerAndConfig.getKey(), + CentreResourceUtils.createCentreContext( + true, // full context, fully-fledged restoration. This means that IQueryEnhancer descendants (centre query enhancers) could use IContextDecomposer for context decomposition on deep levels. + webUiConfig, + companionFinder, + user, + critGenerator, + entityFactory, + centreContextHolder, + criteriaEntity, + queryEnhancerAndConfig.getValue(), + null, /* chosenProperty is not applicable in queryEnhancer context */ + device, + sharingModel + ) + )); } /// Assigns values to the custom properties. @@ -898,7 +1092,7 @@ public static Stream> enhanceResultEntitiesWithCustomPropertyV { final Optional>> assignedEntitiesOp = customPropertiesAsignmentHandler - .map(handlerType -> centre.createAssignmentHandlerInstance(handlerType)) + .map(centre::createAssignmentHandlerInstance) .map(handler -> entities.map(entity -> {handler.assignValues(entity); return entity;})); final Stream> assignedEntities = assignedEntitiesOp.orElse(entities); @@ -908,9 +1102,7 @@ public static Stream> enhanceResultEntitiesWithCustomPropertyV if (customProp.propDef.isPresent()) { final PropDef propDef = customProp.propDef.get(); final String propertyName = CalculatedProperty.generateNameFrom(propDef.title); - if (propDef.value.isPresent()) { - entity.set(propertyName, propDef.value.get()); - } + propDef.value.ifPresent(o -> entity.set(propertyName, o)); } } return entity; diff --git a/platform-web-resources/src/main/java/ua/com/fielden/platform/web/test/server/DataPopulationConfig.java b/platform-web-resources/src/main/java/ua/com/fielden/platform/web/test/server/DataPopulationConfig.java index 39d0a07586f..3f67a55f134 100644 --- a/platform-web-resources/src/main/java/ua/com/fielden/platform/web/test/server/DataPopulationConfig.java +++ b/platform-web-resources/src/main/java/ua/com/fielden/platform/web/test/server/DataPopulationConfig.java @@ -1,6 +1,7 @@ package ua.com.fielden.platform.web.test.server; import com.google.inject.Injector; +import com.google.inject.Module; import ua.com.fielden.platform.audit.AuditingMode; import ua.com.fielden.platform.ioc.AbstractPlatformIocModule; import ua.com.fielden.platform.ioc.ApplicationInjectorFactory; @@ -15,13 +16,9 @@ import static ua.com.fielden.platform.audit.AuditingIocModule.AUDIT_MODE; import static ua.com.fielden.platform.utils.MiscUtilities.propertiesUnionLeft; -/** - * Provides Web UI Testing Server specific implementation of {@link IDomainDrivenTestCaseConfiguration} - * to be used for creation and population of the target development database. - * - * @author TG Team - */ -public final class DataPopulationConfig implements IDomainDrivenTestCaseConfiguration { +/// Provides Web UI Testing Server specific implementation of [IDomainDrivenTestCaseConfiguration] to be used for creation and population of the target development database. +/// +public class DataPopulationConfig implements IDomainDrivenTestCaseConfiguration { private final Injector injector; @@ -43,20 +40,35 @@ public DataPopulationConfig(final Properties props) { defaultProps.setProperty("email.smtp", "localhost"); defaultProps.setProperty("email.fromAddress", "tg@localhost"); - final var finalProps = propertiesUnionLeft(props, defaultProps); - - final ApplicationDomain appDomain = new ApplicationDomain(); - injector = new ApplicationInjectorFactory() - .add(new TgTestApplicationServerIocModule(appDomain, appDomain.domainTypes(), finalProps)) - .add(new NewUserEmailNotifierTestIocModule()) - .add(new DataFilterTestIocModule()) - .add(new IocModule()) - .getInjector(); + injector = createFactory(propertiesUnionLeft(props, defaultProps)).getInjector(); } catch (final Exception e) { throw new IllegalStateException("Could not create data population configuration.", e); } } + protected ApplicationInjectorFactory createFactory(final Properties properties) { + final ApplicationDomain appDomain = new ApplicationDomain(); + return new ApplicationInjectorFactory() + .add(createApplicationServerModule(appDomain, properties)) + .add(new NewUserEmailNotifierTestIocModule()) + .add(new DataFilterTestIocModule()) + .add(createAdditionalIocModule()); + } + + /// Creates the application-server [Module] used by [#createFactory]. + /// Subclasses can override to substitute a different (e.g. web-aware) server module. + /// + protected TgTestApplicationServerIocModule createApplicationServerModule(final ApplicationDomain appDomain, final Properties properties) { + return new TgTestApplicationServerIocModule(appDomain, appDomain.domainTypes(), properties); + } + + /// Creates an additional [Module] appended at the end of the factory chain. + /// Subclasses can override to substitute a module appropriate for their context (e.g. one that does not duplicate bindings already provided by their server module). + /// + protected Module createAdditionalIocModule() { + return new IocModule(); + } + @Override public T getInstance(final Class type) { return injector.getInstance(type); diff --git a/platform-web-resources/src/main/java/ua/com/fielden/platform/web/test/server/TgTestWebApplicationServerIocModule.java b/platform-web-resources/src/main/java/ua/com/fielden/platform/web/test/server/TgTestWebApplicationServerIocModule.java index cf2cd1a9b43..14664a4f2ad 100644 --- a/platform-web-resources/src/main/java/ua/com/fielden/platform/web/test/server/TgTestWebApplicationServerIocModule.java +++ b/platform-web-resources/src/main/java/ua/com/fielden/platform/web/test/server/TgTestWebApplicationServerIocModule.java @@ -8,16 +8,14 @@ import ua.com.fielden.platform.entity.validation.CanBuildReferenceHierarchyForEveryEntityValidator; import ua.com.fielden.platform.entity.validation.ICanBuildReferenceHierarchyForEntityValidator; import ua.com.fielden.platform.web.ioc.IBasicWebApplicationServerModule; +import ua.com.fielden.platform.web.utils.EntityCentreProcessor; +import ua.com.fielden.platform.web.utils.DefaultEntityCentreProcessor; import java.util.List; import java.util.Properties; -/** - * Guice injector module for TG Testing Server (WebApp-enabled). - * - * @author TG Team - * - */ +/// Guice injector module for TG Testing Server (WebApp-enabled). +/// public class TgTestWebApplicationServerIocModule extends TgTestApplicationServerIocModule implements IBasicWebApplicationServerModule, IModuleWithInjector { private final Properties props; @@ -36,6 +34,7 @@ protected void configure() { super.configure(); bind(ICanBuildReferenceHierarchyForEntityValidator.class).to(CanBuildReferenceHierarchyForEveryEntityValidator.class); bindWebAppResources(new WebUiConfig(props)); + bind(EntityCentreProcessor.class).to(DefaultEntityCentreProcessor.class); } @Override diff --git a/platform-web-resources/src/main/java/ua/com/fielden/platform/web/test/server/WebUiConfig.java b/platform-web-resources/src/main/java/ua/com/fielden/platform/web/test/server/WebUiConfig.java index db9016314ef..3b0fa01926e 100644 --- a/platform-web-resources/src/main/java/ua/com/fielden/platform/web/test/server/WebUiConfig.java +++ b/platform-web-resources/src/main/java/ua/com/fielden/platform/web/test/server/WebUiConfig.java @@ -258,13 +258,7 @@ public void initConfiguration() { .addTopAction(CentreConfigActions.CUSTOMISE_COLUMNS_ACTION.mkAction()).also() .addTopAction(StandardActions.EXPORT_ACTION.mkAction(TgFetchProviderTestEntity.class)) .addCrit("property").asMulti().autocompleter(TgPersistentEntityWithProperties.class).setDefaultValue(multi().string().setValues("KE*").value()).also() - .addCrit("propForValidation").asSingle().autocompleter(TgPersistentEntityWithProperties.class) - .setDefaultValue( - single() - .entity(TgPersistentEntityWithProperties.class) - .setValue(injector().getInstance(ITgPersistentEntityWithProperties.class).findByKey("KEY8")) - .value() - ). + .addCrit("propForValidation").asSingle().autocompleter(TgPersistentEntityWithProperties.class). setLayoutFor(Device.DESKTOP, Optional.empty(), LayoutComposer.mkGridForCentre(1, 2)) .addProp("property") @@ -1579,7 +1573,6 @@ public JsCode build() { .addCrit("critOnlyEntityProp").asSingle().autocompleter(TgPersistentEntityWithProperties.class) .withMatcher(CritOnlySingleEntityPropValueMatcherForCentre.class, context().withSelectedEntities()./*withMasterEntity().*/build()) .lightDesc() - /* */.setDefaultValue(single().entity(TgPersistentEntityWithProperties.class)./* TODO not applicable on query generation level not().*/setValue(injector().getInstance(ITgPersistentEntityWithProperties.class).findByKey("KEY8"))./* TODO not applicable on query generation level canHaveNoValue(). */value()) .also() .addCrit("userParam").asSingle().autocompleter(User.class) .withProps(pair("base", false), pair("basedOnUser", false)) diff --git a/platform-web-resources/src/main/java/ua/com/fielden/platform/web/test/server/config/TgGeneratedEntityWebUiConfig.java b/platform-web-resources/src/main/java/ua/com/fielden/platform/web/test/server/config/TgGeneratedEntityWebUiConfig.java index 6922368a75b..57b95c3f364 100644 --- a/platform-web-resources/src/main/java/ua/com/fielden/platform/web/test/server/config/TgGeneratedEntityWebUiConfig.java +++ b/platform-web-resources/src/main/java/ua/com/fielden/platform/web/test/server/config/TgGeneratedEntityWebUiConfig.java @@ -20,7 +20,10 @@ import ua.com.fielden.platform.web.view.master.api.IMaster; import ua.com.fielden.platform.web.view.master.api.actions.MasterActions; import ua.com.fielden.platform.web.view.master.api.impl.SimpleMasterBuilder; -/** + +import static ua.com.fielden.platform.web.centre.api.crit.defaults.mnemonics.construction.options.DefaultValueOptions.single; + +/** * {@link TgGeneratedEntity} Web UI configuration. * * @author TG Team @@ -49,7 +52,7 @@ private TgGeneratedEntityWebUiConfig(final Injector injector, final IWebUiBuilde * @return created entity centre */ private EntityCentre createCentre(final Injector injector) { - final String layout = LayoutComposer.mkGridForCentre(2, 2); + final String layout = LayoutComposer.mkVarGridForCentre(2, 2, 1); final EntityActionConfig standardNewAction = StandardActions.NEW_ACTION.mkAction(TgGeneratedEntity.class); final EntityActionConfig standardDeleteAction = StandardActions.DELETE_ACTION.mkAction(TgGeneratedEntity.class); @@ -65,7 +68,8 @@ private EntityCentre createCentre(final Injector injector) { .addCrit("this").asMulti().autocompleter(TgGeneratedEntity.class).also() .addCrit("critOnlyMultiProp").asMulti().autocompleter(User.class).also() .addCrit("critOnlySingleProp").asSingle().autocompleter(User.class).also() - .addCrit("createdBy").asMulti().autocompleter(User.class) + .addCrit("createdBy").asMulti().autocompleter(User.class).also() + .addCrit("requiredProp").asSingle().integer().setDefaultValue(single().integer().setValue(1).value()) .setLayoutFor(Device.DESKTOP, Optional.empty(), layout) .setLayoutFor(Device.TABLET, Optional.empty(), layout) .setLayoutFor(Device.MOBILE, Optional.empty(), layout) diff --git a/platform-web-resources/src/main/java/ua/com/fielden/platform/web/utils/DefaultEntityCentreProcessor.java b/platform-web-resources/src/main/java/ua/com/fielden/platform/web/utils/DefaultEntityCentreProcessor.java new file mode 100644 index 00000000000..3fd53b9089f --- /dev/null +++ b/platform-web-resources/src/main/java/ua/com/fielden/platform/web/utils/DefaultEntityCentreProcessor.java @@ -0,0 +1,285 @@ +package ua.com.fielden.platform.web.utils; + +import com.google.inject.Inject; +import ua.com.fielden.platform.criteria.generator.ICriteriaGenerator; +import ua.com.fielden.platform.entity.AbstractEntity; +import ua.com.fielden.platform.entity.factory.EntityFactory; +import ua.com.fielden.platform.entity.factory.ICompanionObjectFinder; +import ua.com.fielden.platform.entity.query.fluent.EntityQueryProgressiveInterfaces.ICompoundCondition0; +import ua.com.fielden.platform.entity_centre.exceptions.EntityCentreExecutionException; +import ua.com.fielden.platform.entity_centre.review.criteria.EnhancedCentreEntityQueryCriteria; +import ua.com.fielden.platform.error.Result; +import ua.com.fielden.platform.pagination.IPage; +import ua.com.fielden.platform.security.IAuthorisationModel; +import ua.com.fielden.platform.security.provider.ISecurityTokenProvider; +import ua.com.fielden.platform.security.user.IUser; +import ua.com.fielden.platform.security.user.IUserProvider; +import ua.com.fielden.platform.security.user.User; +import ua.com.fielden.platform.types.either.Either; +import ua.com.fielden.platform.types.either.Left; +import ua.com.fielden.platform.types.tuples.T3; +import ua.com.fielden.platform.ui.config.EntityCentreConfig; +import ua.com.fielden.platform.ui.config.EntityCentreConfigCo; +import ua.com.fielden.platform.ui.menu.MiWithConfigurationSupport; +import ua.com.fielden.platform.web.app.IWebUiConfig; +import ua.com.fielden.platform.web.centre.ICentreConfigSharingModel; +import ua.com.fielden.platform.web.interfaces.DeviceProfile; + +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + +import static java.lang.Class.forName; +import static java.util.Optional.empty; +import static java.util.Optional.of; +import static org.apache.commons.lang3.StringUtils.isBlank; +import static ua.com.fielden.platform.entity.query.fluent.EntityQueryUtils.from; +import static ua.com.fielden.platform.entity.query.fluent.EntityQueryUtils.select; +import static ua.com.fielden.platform.error.Result.failure; +import static ua.com.fielden.platform.types.either.Either.left; +import static ua.com.fielden.platform.types.either.Either.right; +import static ua.com.fielden.platform.types.tuples.T2.t2; +import static ua.com.fielden.platform.types.tuples.T3.t3; +import static ua.com.fielden.platform.utils.CollectionUtil.mapOf; +import static ua.com.fielden.platform.utils.EntityUtils.fetchWithKeyAndDesc; +import static ua.com.fielden.platform.web.centre.CentreUpdater.PREFIX_OF; +import static ua.com.fielden.platform.web.centre.WebApiUtils.LINK_CONFIG_TITLE; +import static ua.com.fielden.platform.web.interfaces.DeviceProfile.DESKTOP; +import static ua.com.fielden.platform.web.interfaces.DeviceProfile.MOBILE; +import static ua.com.fielden.platform.web.resources.webui.CentreResourceUtils.*; +import static ua.com.fielden.platform.web.resources.webui.CentreResourceUtils.RunActions.RUN; +import static ua.com.fielden.platform.web.resources.webui.CriteriaResource.*; + +/// Default [EntityCentreProcessor] implementation, that uses [IWebUiConfig] Entity Centres registry and persistent storage. +/// +public class DefaultEntityCentreProcessor implements EntityCentreProcessor { + + private static final Function> TITLE_PREFIX_INCL_DEFAULT = + surrogateName -> device -> PREFIX_OF.apply(surrogateName).apply(device).replace("[%", "%"); + + private final ICompanionObjectFinder companionFinder; + private final IUserProvider userProvider; + private final ICriteriaGenerator critGenerator; + private final IWebUiConfig webUiConfig; + private final EntityFactory entityFactory; + private final ICentreConfigSharingModel sharingModel; + private final IAuthorisationModel authorisationModel; + private final ISecurityTokenProvider securityTokenProvider; + + @Inject + public DefaultEntityCentreProcessor( + final ICompanionObjectFinder companionFinder, + final IUserProvider userProvider, + final ICriteriaGenerator critGenerator, + final IWebUiConfig webUiConfig, + final EntityFactory entityFactory, + final ICentreConfigSharingModel sharingModel, + final IAuthorisationModel authorisationModel, + final ISecurityTokenProvider securityTokenProvider + ) { + this.companionFinder = companionFinder; + this.userProvider = userProvider; + this.critGenerator = critGenerator; + this.webUiConfig = webUiConfig; + this.entityFactory = entityFactory; + this.sharingModel = sharingModel; + this.authorisationModel = authorisationModel; + this.securityTokenProvider = securityTokenProvider; + } + + /// Creates centre configuration query for config `uuid` and `surrogateName` regardless of the device profile where it was created. + /// + /// @param maybeAlias alias for [EntityCentreConfig] which can be used for some outer query + /// + private static ICompoundCondition0 centreConfigQueryFor(final String uuid, final String surrogateName, final Optional maybeAlias) { + final var selectStart = select(EntityCentreConfig.class); + return maybeAlias.map(selectStart::as).orElse(selectStart) + .where().begin() + .prop("title").like().val(TITLE_PREFIX_INCL_DEFAULT.apply(surrogateName).apply(DESKTOP)) + .or().prop("title").like().val(TITLE_PREFIX_INCL_DEFAULT.apply(surrogateName).apply(MOBILE)) + .end() + .and().condition(centreConfigCondFor(uuid)); + } + + /// Determines [ConfigSettings] for a configuration, defined by `configUuid`. + /// These include who owns the configuration, it's "save-as" name and [DeviceProfile], where it was created. + /// + /// Returns [Left] for invalid configuration. + /// + private static Either determineConfigurationSettings(final String configUuid, final ICompanionObjectFinder companionFinder) { + // Blank uuid does not represent any centre configuration. + if (isBlank(configUuid)) { + return left(failure(ERR_CONFIG_UUID_IS_BLANK.formatted(configUuid))); + } + + // Find "fresh" persisted configuration instance for which there is a corresponding "saved" instance for the same owner. + final EntityCentreConfigCo eccCompanion = companionFinder.find(EntityCentreConfig.class); + final Optional freshConfigOpt = eccCompanion.getEntityOptional( + from(centreConfigQueryFor(configUuid, FRESH_CENTRE_NAME, of("ecc")) + .and().exists(centreConfigQueryFor(configUuid, SAVED_CENTRE_NAME, empty()) + .and().prop("owner").eq().extProp("ecc.owner") + .model() + ).model() + ) + .with(fetchWithKeyAndDesc(EntityCentreConfig.class, true) + .with("owner", "menuItem", "title") + .fetchModel() + ).model() + ); + + // If there is no such configuration, return invalid `Result`. + // This also covers situation with two or more such configurations, which should not be possible. + if (freshConfigOpt.isEmpty()) { + return left(failure(ERR_CONFIG_DOES_NOT_EXIST.formatted(configUuid))); + } + + final var freshConfig = freshConfigOpt.get(); + // Determine owner. + final IUser coUser = companionFinder.find(User.class, true); + final User owner = coUser.findUser(freshConfig.getOwner().getKey()); + + // Determine menu item type. + final var miTypeName = freshConfig.getMenuItem().getKey(); + final Class> miType; + try { + miType = (Class>) forName(miTypeName); + } catch (final ClassNotFoundException notFoundException) { + return left(failure(new EntityCentreExecutionException(ERR_CONFIG_MENU_ITEM_TYPE_CANT_BE_FOUND.formatted(configUuid, miTypeName), notFoundException))); + } + + // Determine device. + final var device = freshConfig.getTitle().startsWith(MOBILE.name()) ? MOBILE : DESKTOP; + + // Determine "save-as" name. + final var saveAsNameString = freshConfig.getTitle().contains("[") ? obtainTitleFrom(freshConfig.getTitle(), FRESH_CENTRE_NAME, device) : ""; + if (isBlank(saveAsNameString)) { + return left(failure(ERR_DEFAULT_CONFIG_WITH_UUID_SHOULD_NOT_EXIST.formatted(configUuid, saveAsNameString))); + } + if (LINK_CONFIG_TITLE.equals(saveAsNameString)) { + return left(failure(ERR_LINK_CONFIG_IS_NOT_AVAILABLE_FOR_RUNNING.formatted(configUuid, saveAsNameString))); + } + return right(new ConfigSettings(of(saveAsNameString), owner, device, miType, freshConfig)); + } + + @Override + public > Either> getResult(final String configUuid) { + return entityCentreResult(configUuid, empty(), t3 -> executeWithEntities(t3._1, t3._2, t3._3)); + } + + /// Prepares Entity Centre configuration with `configUuid` for running and applies `executor` to the prepared state. + /// `executor` runs inside the same `try/catch/finally` that handles user-provider swap and exception wrapping, + /// so callers need not duplicate exception handling for each kind of execution (running, counting, existence check). + /// + /// @param maybeCustomPageCapacity optional page capacity, which will override the page capacity of the configuration + /// @param executor operation to perform on the prepared configuration state; its result becomes the right value + /// + private Either entityCentreResult( + final String configUuid, + final Optional maybeCustomPageCapacity, + final Function, EnhancedCentreEntityQueryCriteria, ?>>, R> executor + ) { + // Determine current user to be returned back into the user provider once the execution has been performed. + final User currentUser = userProvider.getUser(); + try { + // Find out the settings for configuration. Stop execution if the settings can not be determined or inapplicable. + final var resultOrConfigSettings = determineConfigurationSettings(configUuid, companionFinder); + if (resultOrConfigSettings.isLeft()) { + return left(resultOrConfigSettings.asLeft().value()); + } + final var configSettings = resultOrConfigSettings.asRight().value(); + + // Apply the configuration owner to user provider temporarily. + userProvider.setUser(configSettings.owner()); + + // Create custom object for centre running, containing all settings. + // The only necessary setting is indication that centre should be run (i.e. not a page refresh / navigate). + final Map customObject = mapOf(t2(RUN_ACTION_KEY, RUN.toString())); + + // Create criteria entity for "fresh" surrogate configuration. + final EnhancedCentreEntityQueryCriteria, ?> freshCriteriaEntity = createCriteriaValidationPrototype(FRESH_CENTRE_NAME, configSettings, companionFinder, critGenerator, webUiConfig, sharingModel); + + // Validate the criteria entity. Stop execution if it is invalid. + final Result validationResult = validateCriteriaBeforeRunning(freshCriteriaEntity, authorisationModel, securityTokenProvider); + if (!validationResult.isSuccessful()) { + return left(validationResult); + } + + // Generate entities if the centre has IGenerator defined. Stop execution if generation result is not successful. + // `customObject` does not have parameter for generation forcing -- this parameter is not important. + final Result generationResult = generateDataIfNeeded(freshCriteriaEntity, freshCriteriaEntity.miType(), webUiConfig, true, false, customObject); + if (!generationResult.isSuccessful()) { + return left(generationResult); + } + + // Adjust page capacity to some custom value, if present. + maybeCustomPageCapacity.ifPresent( + customPageCapacity -> freshCriteriaEntity.getCentreDomainTreeMangerAndEnhancer().getSecondTick().setPageCapacity(customPageCapacity) + ); + + // Complements criteria entity with IGenerator-related user condition, query enhancer, fetch providers etc. + // I.e. prepares criteria entity for running. + complementCriteriaEntityBeforeRunning( + freshCriteriaEntity, + webUiConfig, + companionFinder, + configSettings.owner(), + critGenerator, + entityFactory, + null, + sharingModel + ); + + return right(executor.apply(t3(configSettings, customObject, freshCriteriaEntity))); + } catch (final Exception exception) { + return left(failure(new EntityCentreExecutionException(ERR_CONFIG_COULD_NOT_BE_EXECUTED.formatted(configUuid), exception))); + } finally { + // Return original user back to user provider. + userProvider.setUser(currentUser); + } + } + + /// Executes Entity Centre configuration with previously determined `configSettings` and `criteriaEntity`. + /// + private > IPage executeWithEntities( + final ConfigSettings configSettings, + final Map customObject, + final EnhancedCentreEntityQueryCriteria, ?> criteriaEntity + ) { + // Perform actual running of `criteriaEntity` with `configSettings`. + final var resultListAndPage = executeEntityCentreConfiguration( + configSettings, + new HeadlessCentreExecution(), + true, + customObject, + criteriaEntity, + webUiConfig, + companionFinder, + critGenerator, + entityFactory, + null, + sharingModel + ); + return (IPage) resultListAndPage._2; + } + + @Override + public Either resultCount(final String configUuid) { + return entityCentreResult(configUuid, empty(), t3 -> t3._3.runCount()); + } + + @Override + public Either resultExists(final String configUuid) { + return entityCentreResult(configUuid, of(1), t3 -> !executeWithEntities(t3._1, t3._2, t3._3).data().isEmpty()); + } + + @Override + public Either validate(String configUuid) { + try { + // Find out the settings for configuration. Stop if the settings can not be determined or inapplicable. + return determineConfigurationSettings(configUuid, companionFinder); + } catch (final Exception exception) { + return left(failure(new EntityCentreExecutionException(ERR_CONFIG_COULD_NOT_BE_EXECUTED.formatted(configUuid), exception))); + } + } + +} diff --git a/platform-web-resources/src/test/java/ua/com/fielden/platform/test_config/H2OrPostgreSqlOrSqlServerContextSelectorForWebTests.java b/platform-web-resources/src/test/java/ua/com/fielden/platform/test_config/H2OrPostgreSqlOrSqlServerContextSelectorForWebTests.java new file mode 100644 index 00000000000..37c9ce1f36c --- /dev/null +++ b/platform-web-resources/src/test/java/ua/com/fielden/platform/test_config/H2OrPostgreSqlOrSqlServerContextSelectorForWebTests.java @@ -0,0 +1,76 @@ +package ua.com.fielden.platform.test_config; + +import ua.com.fielden.platform.test.DbCreator; +import ua.com.fielden.platform.test.IDomainDrivenTestCaseConfiguration; +import ua.com.fielden.platform.test.runners.AbstractDomainDrivenTestCaseRunner; +import ua.com.fielden.platform.test.runners.H2DomainDrivenTestCaseRunner.H2TestContext; +import ua.com.fielden.platform.test.runners.PostgresqlDomainDrivenTestCaseRunner.PostgresqlTestContext; +import ua.com.fielden.platform.test.runners.SqlServerDomainDrivenTestCaseRunner.SqlServerTestContext; +import ua.com.fielden.platform.web.app.IWebUiConfig; +import ua.com.fielden.platform.web.test.server.DataPopulationConfigForWebTests; + +import java.util.Optional; +import java.util.Properties; + +/// A test runner that selects a test configuration [ITestContext] from [SqlServerTestContextForWebTests] or [PostgresqlTestContextForWebTests]. +/// The criteria for selecting the appropriate test runner is based on runtime settings. +/// +/// This test runner is capable for running tests with Web UI infrastructure such as [IWebUiConfig]. +/// Used for web-server-driven tests in *-web-resources module. +/// +public class H2OrPostgreSqlOrSqlServerContextSelectorForWebTests extends H2OrPostgreSqlOrSqlServerContextSelector { + + public H2OrPostgreSqlOrSqlServerContextSelectorForWebTests(Class klass) throws Exception { + super(klass); + } + + public H2OrPostgreSqlOrSqlServerContextSelectorForWebTests(Class klass, Class dbCreatorType, Optional testConfig) throws Exception { + super(klass, dbCreatorType, testConfig); + } + + /// [PostgresqlTestContext] with Web UI infrastructure capabilities. + /// + private static class PostgresqlTestContextForWebTests extends PostgresqlTestContext { + @Override + public Properties mkDbProps(String dbUri) { + return mkPropsWebCapable(super.mkDbProps(dbUri)); + } + } + + /// [SqlServerTestContext] with Web UI infrastructure capabilities. + /// + private static class SqlServerTestContextForWebTests extends SqlServerTestContext { + @Override + public Properties mkDbProps(String dbUri) { + return mkPropsWebCapable(super.mkDbProps(dbUri)); + } + } + + /// [H2TestContext] with Web UI infrastructure capabilities. + /// + private static class H2TestContextForWebTests extends H2TestContext { + public H2TestContextForWebTests(AbstractDomainDrivenTestCaseRunner runner) { + super(runner); + } + @Override + public Properties mkDbProps(String dbUri) { + return mkPropsWebCapable(super.mkDbProps(dbUri)); + } + } + + private static Properties mkPropsWebCapable(Properties props) { + props.setProperty("config.domain", DataPopulationConfigForWebTests.class.getName()); + props.setProperty("attachments.location", "../platform-web-resources/src/test/resources/attachments"); + props.setProperty("web.domain", "unit-test.com"); + props.setProperty("web.path", "/"); + props.setProperty("port", "1234"); + return props; + } + + protected ITestContext selectRunnerConfig() { + return POSTGRESQL + ? new PostgresqlTestContextForWebTests() + : (SQL_SERVER ? new SqlServerTestContextForWebTests() : new H2TestContextForWebTests(this)); + } + +} diff --git a/platform-web-resources/src/test/java/ua/com/fielden/platform/web/test/server/DataPopulationConfigForWebTests.java b/platform-web-resources/src/test/java/ua/com/fielden/platform/web/test/server/DataPopulationConfigForWebTests.java new file mode 100644 index 00000000000..cdac3508a16 --- /dev/null +++ b/platform-web-resources/src/test/java/ua/com/fielden/platform/web/test/server/DataPopulationConfigForWebTests.java @@ -0,0 +1,41 @@ +package ua.com.fielden.platform.web.test.server; + +import com.google.inject.Module; +import ua.com.fielden.platform.audit.AuditingMode; +import ua.com.fielden.platform.test.IDomainDrivenTestCaseConfiguration; +import ua.com.fielden.platform.web.test.config.ApplicationDomain; + +import java.util.Properties; + +import static ua.com.fielden.platform.audit.AuditingIocModule.AUDIT_MODE; + +/// Provides Web UI Testing Server specific implementation of [IDomainDrivenTestCaseConfiguration] to be used for web unit tests. +/// +public final class DataPopulationConfigForWebTests extends DataPopulationConfig { + + /// Creates a configuration using the provided properties. + /// Some default properties are set by this constructor, but the provided ones take precedence. + /// + public DataPopulationConfigForWebTests(final Properties props) { + super(enableAuditing(props)); + } + + /// Enables auditing, which is required by Web UI in `TgTestWebApplicationServerIocModule`. + /// + private static Properties enableAuditing(final Properties props) { + props.setProperty(AUDIT_MODE, AuditingMode.ENABLED.name()); + props.setProperty("audit.path", "../platform-pojo-bl/target/classes"); + return props; + } + + @Override + protected TgTestApplicationServerIocModule createApplicationServerModule(final ApplicationDomain appDomain, final Properties properties) { + return new TgTestWebApplicationServerIocModule(appDomain, appDomain.domainTypes(), properties); + } + + @Override + protected Module createAdditionalIocModule() { + return new UniversalConstantsTestIocModule(); + } + +} diff --git a/platform-web-resources/src/test/java/ua/com/fielden/platform/web/test/server/UniversalConstantsTestIocModule.java b/platform-web-resources/src/test/java/ua/com/fielden/platform/web/test/server/UniversalConstantsTestIocModule.java new file mode 100644 index 00000000000..d9a98695d0f --- /dev/null +++ b/platform-web-resources/src/test/java/ua/com/fielden/platform/web/test/server/UniversalConstantsTestIocModule.java @@ -0,0 +1,20 @@ +package ua.com.fielden.platform.web.test.server; + +import com.google.inject.Binder; +import com.google.inject.Module; +import ua.com.fielden.platform.test.ioc.DatesForTesting; +import ua.com.fielden.platform.test.ioc.UniversalConstantsForTesting; +import ua.com.fielden.platform.utils.IDates; +import ua.com.fielden.platform.utils.IUniversalConstants; + +/// IoC module for [IUniversalConstants] for unit testing purposes. +/// +public class UniversalConstantsTestIocModule implements Module { + + @Override + public void configure(Binder binder) { + binder.bind(IDates.class).to(DatesForTesting.class); + binder.bind(IUniversalConstants.class).to(UniversalConstantsForTesting.class); + } + +} diff --git a/platform-web-resources/src/test/java/ua/com/fielden/platform/web/utils/EntityCentreProcessorTest.java b/platform-web-resources/src/test/java/ua/com/fielden/platform/web/utils/EntityCentreProcessorTest.java new file mode 100644 index 00000000000..c97f130cabf --- /dev/null +++ b/platform-web-resources/src/test/java/ua/com/fielden/platform/web/utils/EntityCentreProcessorTest.java @@ -0,0 +1,505 @@ +package ua.com.fielden.platform.web.utils; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import fielden.test_app.main.menu.compound.MiTgCompoundEntity; +import org.junit.Test; +import org.junit.runner.RunWith; +import ua.com.fielden.platform.domaintree.centre.ICentreDomainTreeManager.ICentreDomainTreeManagerAndEnhancer; +import ua.com.fielden.platform.entity.factory.ICompanionObjectFinder; +import ua.com.fielden.platform.sample.domain.TgGeneratedEntity; +import ua.com.fielden.platform.sample.domain.compound.TgCompoundEntity; +import ua.com.fielden.platform.security.tokens.persistent.TgGeneratedEntity_CanRead_Token; +import ua.com.fielden.platform.security.user.*; +import ua.com.fielden.platform.test_config.AbstractDaoTestCase; +import ua.com.fielden.platform.test_config.H2OrPostgreSqlOrSqlServerContextSelectorForWebTests; +import ua.com.fielden.platform.ui.config.EntityCentreConfig; +import ua.com.fielden.platform.ui.config.MainMenuItem; +import ua.com.fielden.platform.ui.menu.sample.MiTgGeneratedEntity; +import ua.com.fielden.platform.web.app.IWebUiConfig; +import ua.com.fielden.platform.web.interfaces.DeviceProfile; + +import java.util.Map; +import java.util.function.Consumer; + +import static java.util.Optional.empty; +import static java.util.Optional.of; +import static java.util.UUID.randomUUID; +import static org.junit.Assert.*; +import static ua.com.fielden.platform.entity.meta.MetaProperty.ERR_REQUIRED; +import static ua.com.fielden.platform.entity.query.fluent.EntityQueryUtils.select; +import static ua.com.fielden.platform.types.tuples.T2.t2; +import static ua.com.fielden.platform.utils.CollectionUtil.*; +import static ua.com.fielden.platform.web.centre.CentreUpdater.*; +import static ua.com.fielden.platform.web.centre.WebApiUtils.LINK_CONFIG_TITLE; +import static ua.com.fielden.platform.web.interfaces.DeviceProfile.DESKTOP; +import static ua.com.fielden.platform.web.interfaces.DeviceProfile.MOBILE; +import static ua.com.fielden.platform.web.utils.EntityCentreProcessor.*; + +/// Tests for {@link EntityCentreProcessor}. +/// +/// These can serve as an example of how end-application tests can be implemented. +/// +@RunWith(H2OrPostgreSqlOrSqlServerContextSelectorForWebTests.class) +public class EntityCentreProcessorTest extends AbstractDaoTestCase { + + public static final String NON_EXISTING_MI_TYPE_NAME = "fielden.test_app.main.menu.compound.NON_EXISTING"; + + @Test + public void executing_getResult_method_returns_zero_entities_for_named_configuration_without_data() { + final var uuid = randomUUID().toString(); + initTestData(uuid, () -> {}, _ -> {}); + + final var result = getInstance(EntityCentreProcessor.class).getResult(uuid); + + assertNotNull(result); + assertTrue(result.isRight()); + assertNotNull(result.asRight().value()); + assertEquals(0, result.asRight().value().data().size()); + } + + @Test + public void executing_resultExists_method_returns_false_for_named_configuration_without_data() { + final var uuid = randomUUID().toString(); + initTestData(uuid, () -> {}, _ -> {}); + + final var result = getInstance(EntityCentreProcessor.class).resultExists(uuid); + + assertNotNull(result); + assertTrue(result.isRight()); + assertFalse(result.asRight().value()); + } + + @Test + public void executing_getResult_method_returns_entities_for_named_configuration() { + final var uuid = randomUUID().toString(); + initTestData(uuid, () -> save(new_(TgCompoundEntity.class, "KEY1").setActive(true).setDesc("desc 1")), _ -> {}); + + final var result = getInstance(EntityCentreProcessor.class).getResult(uuid); + + assertNotNull(result); + assertTrue(result.isRight()); + assertNotNull(result.asRight().value()); + assertEquals(1, result.asRight().value().data().size()); + assertEquals("KEY1", result.asRight().value().data().getFirst().getKey()); + } + + @Test + public void executing_resultExists_method_returns_true_for_named_configuration() { + final var uuid = randomUUID().toString(); + initTestData(uuid, () -> save(new_(TgCompoundEntity.class, "KEY1").setActive(true).setDesc("desc 1")), _ -> {}); + + final var result = getInstance(EntityCentreProcessor.class).resultExists(uuid); + + assertNotNull(result); + assertTrue(result.isRight()); + assertTrue(result.asRight().value()); + } + + @Test + public void executing_getResult_method_returns_entities_for_named_configuration_with_custom_criteria() { + final var uuid = randomUUID().toString(); + initTestData(uuid, () -> { + save(new_(TgCompoundEntity.class, "KEY1").setActive(true).setDesc("desc 1")); + save(new_(TgCompoundEntity.class, "KEY2").setActive(true).setDesc("desc 2")); + }, centreManager -> centreManager.getFirstTick().setValue(TgCompoundEntity.class, "", listOf("*2"))); + + final var result = getInstance(EntityCentreProcessor.class).getResult(uuid); + + assertNotNull(result); + assertTrue(result.isRight()); + assertNotNull(result.asRight().value()); + assertEquals(1, result.asRight().value().data().size()); + assertEquals("KEY2", result.asRight().value().data().getFirst().getKey()); + } + + @Test + public void executing_resultExists_method_returns_true_for_named_configuration_with_custom_criteria() { + final var uuid = randomUUID().toString(); + initTestData(uuid, () -> { + save(new_(TgCompoundEntity.class, "KEY1").setActive(true).setDesc("desc 1")); + save(new_(TgCompoundEntity.class, "KEY2").setActive(true).setDesc("desc 2")); + }, centreManager -> centreManager.getFirstTick().setValue(TgCompoundEntity.class, "", listOf("*2"))); + + final var result = getInstance(EntityCentreProcessor.class).resultExists(uuid); + + assertNotNull(result); + assertTrue(result.isRight()); + assertTrue(result.asRight().value()); + } + + @Test + public void executing_getResult_method_returns_entities_for_named_configuration_in_mobile_namespace() { + final var uuid = randomUUID().toString(); + initTestData(uuid, MOBILE, () -> save(new_(TgCompoundEntity.class, "KEY1").setActive(true).setDesc("desc 1")), _ -> {}); + + final var result = getInstance(EntityCentreProcessor.class).getResult(uuid); + + assertNotNull(result); + assertTrue(result.isRight()); + assertNotNull(result.asRight().value()); + assertEquals(1, result.asRight().value().data().size()); + assertEquals("KEY1", result.asRight().value().data().getFirst().getKey()); + } + + @Test + public void executing_resultExists_method_returns_true_for_named_configuration_in_mobile_namespace() { + final var uuid = randomUUID().toString(); + initTestData(uuid, MOBILE, () -> save(new_(TgCompoundEntity.class, "KEY1").setActive(true).setDesc("desc 1")), _ -> {}); + + final var result = getInstance(EntityCentreProcessor.class).resultExists(uuid); + + assertNotNull(result); + assertTrue(result.isRight()); + assertTrue(result.asRight().value()); + } + + @Test + public void executing_resultExists_method_returns_invalid_result_for_blank_uuid() { + final var uuid = ""; + + final var result = getInstance(EntityCentreProcessor.class).resultExists(uuid); + + assertNotNull(result); + assertTrue(result.isLeft()); + assertNotNull(result.asLeft().value()); + assertFalse(result.asLeft().value().isSuccessful()); + assertEquals(ERR_CONFIG_UUID_IS_BLANK.formatted(uuid), result.asLeft().value().getMessage()); + } + + @Test + public void executing_resultExists_method_returns_invalid_result_for_non_existing_named_configuration() { + final var uuid = randomUUID().toString(); + + final var result = getInstance(EntityCentreProcessor.class).resultExists(uuid); + + assertNotNull(result); + assertTrue(result.isLeft()); + assertNotNull(result.asLeft().value()); + assertFalse(result.asLeft().value().isSuccessful()); + assertEquals(ERR_CONFIG_DOES_NOT_EXIST.formatted(uuid), result.asLeft().value().getMessage()); + } + + @Test + public void executing_resultExists_method_returns_invalid_result_for_orphan_inherited_from_shared_named_configuration() { + final var uuid = randomUUID().toString(); + + final var configSettings = new ConfigSettings(of("saveAs"), getUser(), DESKTOP, MiTgCompoundEntity.class, null); + createConfig(configSettings, FRESH_CENTRE_NAME, uuid); + createConfig(configSettings, SAVED_CENTRE_NAME, null); + + final var result = getInstance(EntityCentreProcessor.class).resultExists(uuid); + + assertNotNull(result); + assertTrue(result.isLeft()); + assertNotNull(result.asLeft().value()); + assertFalse(result.asLeft().value().isSuccessful()); + assertEquals(ERR_CONFIG_DOES_NOT_EXIST.formatted(uuid), result.asLeft().value().getMessage()); + } + + @Test + public void executing_resultExists_method_returns_invalid_result_for_unusual_named_configuration_with_no_miType() { + final var uuid = randomUUID().toString(); + + final var configSettings = new ConfigSettings(of("saveAs"), getUser(), DESKTOP, null, null); + createConfig(configSettings, FRESH_CENTRE_NAME, uuid); + createConfig(configSettings, SAVED_CENTRE_NAME, uuid); + + final var result = getInstance(EntityCentreProcessor.class).resultExists(uuid); + + assertNotNull(result); + assertTrue(result.isLeft()); + assertNotNull(result.asLeft().value()); + assertFalse(result.asLeft().value().isSuccessful()); + assertEquals(ERR_CONFIG_MENU_ITEM_TYPE_CANT_BE_FOUND.formatted(uuid, NON_EXISTING_MI_TYPE_NAME), result.asLeft().value().getMessage()); + } + + @Test + public void executing_resultExists_method_returns_invalid_result_for_unusual_default_configuration_with_uuid() { + final var uuid = randomUUID().toString(); + + final var configSettings = new ConfigSettings(empty(), getUser(), DESKTOP, MiTgCompoundEntity.class, null); + createConfig(configSettings, FRESH_CENTRE_NAME, uuid); + createConfig(configSettings, SAVED_CENTRE_NAME, uuid); + + final var result = getInstance(EntityCentreProcessor.class).resultExists(uuid); + + assertNotNull(result); + assertTrue(result.isLeft()); + assertNotNull(result.asLeft().value()); + assertFalse(result.asLeft().value().isSuccessful()); + assertEquals(ERR_DEFAULT_CONFIG_WITH_UUID_SHOULD_NOT_EXIST.formatted(uuid, ""), result.asLeft().value().getMessage()); + } + + @Test + public void executing_resultExists_method_returns_invalid_result_for_link_configuration() { + final var uuid = randomUUID().toString(); + + final var configSettings = new ConfigSettings(of(LINK_CONFIG_TITLE), getUser(), DESKTOP, MiTgCompoundEntity.class, null); + createConfig(configSettings, FRESH_CENTRE_NAME, uuid); + createConfig(configSettings, SAVED_CENTRE_NAME, uuid); + + final var result = getInstance(EntityCentreProcessor.class).resultExists(uuid); + + assertNotNull(result); + assertTrue(result.isLeft()); + assertNotNull(result.asLeft().value()); + assertFalse(result.asLeft().value().isSuccessful()); + assertEquals(ERR_LINK_CONFIG_IS_NOT_AVAILABLE_FOR_RUNNING.formatted(uuid, LINK_CONFIG_TITLE), result.asLeft().value().getMessage()); + } + + @Test + public void executing_resultExists_method_returns_invalid_result_for_invalid_configuration() { + final var uuid = randomUUID().toString(); + + final var configSettings = new ConfigSettings(of("saveAs"), getUser(), DESKTOP, MiTgGeneratedEntity.class, null); + createConfig(configSettings, FRESH_CENTRE_NAME, uuid); + createConfig(configSettings, SAVED_CENTRE_NAME, uuid); + + initTestData(centreManager -> centreManager.getFirstTick().setValue(TgGeneratedEntity.class, "requiredProp", null), configSettings); + + final var result = getInstance(EntityCentreProcessor.class).getResult(uuid); + + assertNotNull(result); + assertTrue(result.isLeft()); + assertNotNull(result.asLeft().value()); + assertFalse(result.asLeft().value().isSuccessful()); + assertEquals(ERR_REQUIRED.formatted("Required Prop", "Centre Selection Criteria"), result.asLeft().value().getMessage()); + } + + @Test + public void executing_resultExists_method_returns_invalid_result_for_unauthorised_configuration() { + final var uuid = randomUUID().toString(); + + final var configSettings = new ConfigSettings(of("saveAs"), getUser(), DESKTOP, MiTgGeneratedEntity.class, null); + createConfig(configSettings, FRESH_CENTRE_NAME, uuid); + createConfig(configSettings, SAVED_CENTRE_NAME, uuid); + + final SecurityRoleAssociationCo co$ = co$(SecurityRoleAssociation.class); + co$.removeAssociations(setOf( + co$.new_() + .setRole(co(UserRole.class).findByKey(UNIT_TEST_ROLE)) + .setSecurityToken(TgGeneratedEntity_CanRead_Token.class) + )); + + final var result = getInstance(EntityCentreProcessor.class).getResult(uuid); + + assertNotNull(result); + assertTrue(result.isLeft()); + assertNotNull(result.asLeft().value()); + assertFalse(result.asLeft().value().isSuccessful()); + assertEquals("Permission denied due to token [%s] restriction.".formatted(TgGeneratedEntity_CanRead_Token.TITLE), result.asLeft().value().getMessage()); + } + + @Test + public void executing_resultExists_method_returns_invalid_result_for_configuration_with_invalid_generation() { + final var uuid = randomUUID().toString(); + + final var configSettings = new ConfigSettings(of("saveAs"), getUser(), DESKTOP, MiTgGeneratedEntity.class, null); + createConfig(configSettings, FRESH_CENTRE_NAME, uuid); + createConfig(configSettings, SAVED_CENTRE_NAME, uuid); + + initTestData(centreManager -> centreManager.getFirstTick().setValue(TgGeneratedEntity.class, "critOnlySingleProp", getUser()), configSettings); + + final var result = getInstance(EntityCentreProcessor.class).getResult(uuid); + + assertNotNull(result); + assertTrue(result.isLeft()); + assertNotNull(result.asLeft().value()); + assertFalse(result.asLeft().value().isSuccessful()); + assertEquals("Can not generate the instance based on current user [%s], choose another user for that.".formatted(getUser()), result.asLeft().value().getMessage()); + } + + @Test + public void executing_getResult_method_returns_page_supporting_next_navigation() { + final var uuid = initPaginatedData(8, 3); + final var firstPage = getInstance(EntityCentreProcessor.class).getResult(uuid).asRight().value(); + assertEquals(3, firstPage.data().size()); + final var secondPage = firstPage.next(); + assertEquals(3, secondPage.data().size()); + final var thirdPage = secondPage.next(); + assertEquals(2, thirdPage.data().size()); + } + + @Test + public void executing_getResult_method_returns_page_supporting_prev_navigation() { + final var uuid = initPaginatedData(8, 3); + final var firstPage = getInstance(EntityCentreProcessor.class).getResult(uuid).asRight().value(); + final var secondPage = firstPage.next(); + final var backToFirst = secondPage.prev(); + assertEquals(3, backToFirst.data().size()); + } + + @Test + public void executing_getResult_method_returns_page_supporting_last_navigation() { + final var uuid = initPaginatedData(8, 3); + final var firstPage = getInstance(EntityCentreProcessor.class).getResult(uuid).asRight().value(); + final var lastPage = firstPage.last(); + assertEquals(2, lastPage.data().size()); + } + + @Test + public void executing_getResult_method_returns_page_supporting_first_navigation() { + final var uuid = initPaginatedData(8, 3); + final var firstPage = getInstance(EntityCentreProcessor.class).getResult(uuid).asRight().value(); + final var lastPage = firstPage.last(); + final var backToFirst = lastPage.first(); + assertEquals(3, backToFirst.data().size()); + } + + @Test + public void executing_resultCount_method_returns_entity_count_below_page_capacity() { + final var uuid = initPaginatedData(2, 3); + final var result = getInstance(EntityCentreProcessor.class).resultCount(uuid); + assertEquals(Integer.valueOf(2), result.asRight().value()); + } + + @Test + public void executing_resultCount_method_returns_entity_count_above_page_capacity() { + final var uuid = initPaginatedData(8, 3); + final var result = getInstance(EntityCentreProcessor.class).resultCount(uuid); + assertEquals(Integer.valueOf(8), result.asRight().value()); + } + + @Test + public void executing_resultCount_method_returns_invalid_result_for_configuration_with_invalid_generation() { + final var uuid = randomUUID().toString(); + + final var configSettings = new ConfigSettings(of("saveAs"), getUser(), DESKTOP, MiTgGeneratedEntity.class, null); + createConfig(configSettings, FRESH_CENTRE_NAME, uuid); + createConfig(configSettings, SAVED_CENTRE_NAME, uuid); + initTestData(centreManager -> centreManager.getFirstTick().setValue(TgGeneratedEntity.class, "critOnlySingleProp", getUser()), configSettings); + + final var result = getInstance(EntityCentreProcessor.class).resultCount(uuid); + + assertNotNull(result); + assertTrue(result.isLeft()); + assertNotNull(result.asLeft().value()); + assertFalse(result.asLeft().value().isSuccessful()); + assertEquals("Can not generate the instance based on current user [%s], choose another user for that.".formatted(getUser()), result.asLeft().value().getMessage()); + } + + @Test + public void executing_resultCount_method_returns_valid_result_for_configuration_with_valid_generation_and_does_not_include_entities_generated_by_other_users() { + final var uuid = randomUUID().toString(); + final var user = save(new_(User.class, "USER").setBase(true).setEmail("USER@unit-test.software").setActive(true)); + save(new_composite(UserAndRoleAssociation.class, user, co(UserRole.class).findByKey(UNIT_TEST_ROLE))); + final var configSettings = new ConfigSettings(of("saveAs"), user, DESKTOP, MiTgGeneratedEntity.class, null); + createConfig(configSettings, FRESH_CENTRE_NAME, uuid); + createConfig(configSettings, SAVED_CENTRE_NAME, uuid); + initTestData(centreManager -> centreManager.getFirstTick().setValue(TgGeneratedEntity.class, "critOnlySingleProp", getUser()), configSettings); + getInstance(EntityCentreProcessor.class).resultCount(uuid); + assertEquals(10, co(TgGeneratedEntity.class).count(select(TgGeneratedEntity.class).model())); + + final var otherUuid = randomUUID().toString(); + final var otherUser = save(new_(User.class, "OTHER_USER").setBase(true).setEmail("OTHER_USER@unit-test.software").setActive(true)); + save(new_composite(UserAndRoleAssociation.class, otherUser, co(UserRole.class).findByKey(UNIT_TEST_ROLE))); + final var otherUserConfigSettings = new ConfigSettings(of("otherUserSaveAs"), otherUser, DESKTOP, MiTgGeneratedEntity.class, null); + createConfig(otherUserConfigSettings, FRESH_CENTRE_NAME, otherUuid); + createConfig(otherUserConfigSettings, SAVED_CENTRE_NAME, otherUuid); + initTestData(centreManager -> centreManager.getFirstTick().setValue(TgGeneratedEntity.class, "critOnlySingleProp", getUser()), otherUserConfigSettings); + final var result = getInstance(EntityCentreProcessor.class).resultCount(otherUuid); + assertEquals(20, co(TgGeneratedEntity.class).count(select(TgGeneratedEntity.class).model())); + + assertNotNull(result); + assertTrue(result.isRight()); + assertNotNull(result.asRight().value()); + assertEquals(Integer.valueOf(10), result.asRight().value()); + } + + /// Initialise test data for config `uuid` (desktop device profile). + /// + /// @param createData runnable for custom data creation + /// @param enhanceCentreManager mutating function for centre manager to provide custom criteria and other configuration parameters + /// + private void initTestData(final String uuid, final Runnable createData, final Consumer enhanceCentreManager) { + initTestData(uuid, DESKTOP, createData, enhanceCentreManager); + } + + /// Initialise paginated test data: creates `entityCount` entities with the given `pageCapacity`. + /// Returns the config UUID. + /// + private String initPaginatedData(final int entityCount, final int pageCapacity) { + final var uuid = randomUUID().toString(); + initTestData(uuid, () -> { + for (int i = 1; i <= entityCount; i++) { + save(new_(TgCompoundEntity.class, String.format("KEY%02d", i)).setActive(true).setDesc("desc " + i)); + } + }, centreManager -> centreManager.getSecondTick().setPageCapacity(pageCapacity)); + return uuid; + } + + /// Initialise test data for config `uuid` and `device` profile. + /// + /// @param createData runnable for custom data creation + /// @param enhanceCentreManager mutating function for centre manager to provide custom criteria and other configuration parameters + /// + private void initTestData(final String uuid, final DeviceProfile device, final Runnable createData, final Consumer enhanceCentreManager) { + createData.run(); + + final var configSettings = new ConfigSettings(of("saveAs"), getUser(), device, MiTgCompoundEntity.class, null); + createConfig(configSettings, FRESH_CENTRE_NAME, uuid); + createConfig(configSettings, SAVED_CENTRE_NAME, uuid); + + initTestData(enhanceCentreManager, configSettings); + } + + /// Initialise test data for `configSettings`. + /// + /// @param enhanceCentreManager mutating function for centre manager to provide custom criteria and other configuration parameters + /// + private void initTestData(Consumer enhanceCentreManager, ConfigSettings configSettings) { + final IWebUiConfig webUiConfig = getInstance(IWebUiConfig.class); + final ICompanionObjectFinder companionFinder = getInstance(ICompanionObjectFinder.class); + + final var centreManager = updateCentre(configSettings.owner(), configSettings.miType(), FRESH_CENTRE_NAME, configSettings.saveAsName(), configSettings.device(), webUiConfig, companionFinder); + enhanceCentreManager.accept(centreManager); + commitCentreWithoutConflicts(configSettings.owner(), configSettings.miType(), FRESH_CENTRE_NAME, configSettings.saveAsName(), configSettings.device(), centreManager, null /* newDesc */, webUiConfig, companionFinder); + } + + /// Creates Entity Centre persisted configuration for concrete surrogate version of it. + /// + private void createConfig( + final ConfigSettings configSettings, + final String surrogateName, + final String uuid + ) { + final Map diffObject = mapOf( + t2("PROPERTIES" /* CentreUpdater.PROPERTIES */, mapOf()) + ); + try { + save(new_(EntityCentreConfig.class) + .setOwner(configSettings.owner()) + .setMenuItem(getMenuItem(configSettings.miType())) + .setTitle(NAME_OF.apply(surrogateName).apply(configSettings.saveAsName()).apply(configSettings.device())) + .setConfigBody(new ObjectMapper().writeValueAsBytes(diffObject)) + .setConfigUuid(uuid) + ); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + /// Get (or create) entity instance for `menuItemType`. + /// Use `null` for some non-existing type, that still exists in persistent storage for some reason. + /// + private MainMenuItem getMenuItem(final Class menuItemType) { + if (menuItemType == null) { + return getMenuItemForName(null); + } + return getMenuItemForName(menuItemType.getName()); + } + + /// Get (or create) entity instance for `menuItemTypeName`. + /// Use `null` for some non-existing type, that still exists in persistent storage for some reason. + /// + private MainMenuItem getMenuItemForName(final String menuItemTypeName) { + if (menuItemTypeName == null) { + return getMenuItemForName(NON_EXISTING_MI_TYPE_NAME); + } + return co(MainMenuItem.class) + .findByKeyOptional(menuItemTypeName) + .orElseGet(() -> (MainMenuItem) save(new_(MainMenuItem.class).setKey(menuItemTypeName))); + } + +} \ No newline at end of file diff --git a/platform-web-ui/src/main/java/ua/com/fielden/platform/web/centre/CentreUpdater.java b/platform-web-ui/src/main/java/ua/com/fielden/platform/web/centre/CentreUpdater.java index 11489911ee1..f27288993f0 100644 --- a/platform-web-ui/src/main/java/ua/com/fielden/platform/web/centre/CentreUpdater.java +++ b/platform-web-ui/src/main/java/ua/com/fielden/platform/web/centre/CentreUpdater.java @@ -10,6 +10,7 @@ import ua.com.fielden.platform.entity.factory.ICompanionObjectFinder; import ua.com.fielden.platform.entity.query.fluent.EntityQueryProgressiveInterfaces.ICompoundCondition0; import ua.com.fielden.platform.entity.query.fluent.fetch; +import ua.com.fielden.platform.entity.query.model.ConditionModel; import ua.com.fielden.platform.entity.query.model.EntityResultQueryModel; import ua.com.fielden.platform.entity_centre.mnemonics.DateRangePrefixEnum; import ua.com.fielden.platform.entity_centre.mnemonics.MnemonicEnum; @@ -172,12 +173,17 @@ enum MetaValueType { return ent.getKey().toString(); } }, entity); - /** - * Function to get title of surrogate configuration from surrogate name, save-as name and device. - */ + + /// Function to get title of surrogate configuration from surrogate name, save-as name and device. + /// public static final Function, Function>> NAME_OF = surrogateName -> saveAs -> device -> deviceSpecific(saveAsSpecific(surrogateName, saveAs), device) + DIFFERENCES_SUFFIX; - - /** Protected default constructor to prevent instantiation. */ + + /// Function to get query prefix for title of surrogate configuration from surrogate name and device. + /// + public static final Function> PREFIX_OF = surrogateName -> device -> deviceSpecific(surrogateName, device) + "[%"; + + /// Protected default constructor to prevent instantiation. + /// protected CentreUpdater() { } @@ -780,7 +786,7 @@ public static String obtainTitleFrom(final String title, final String surrogateN /** * Receives actual title from surrogate name persisted inside {@link EntityCentreConfig#getTitle()}. - * + * * @param title * @param surrogateNamePrefix * @return @@ -789,51 +795,82 @@ private static String obtainTitleFrom(final String title, final String surrogate final String surrogateWithSuffix = title.replaceFirst(surrogateNamePrefix, ""); return surrogateWithSuffix.substring(1, surrogateWithSuffix.lastIndexOf("]")); } - - /** - * Creates a function that returns a query to find centre configurations persisted. - *

- * Looks only for named / link configurations, default configurations are avoided. - * - * @param miType - * @param device -- the device for which centre configurations are looked for - * @param surrogateName -- surrogate name of the centre (fresh, previouslyRun etc.) - * @return - */ + + /// Creates composable centre configuration query for `device` and `surrogateName`. + /// + /// @param device the device for which centre configurations are looked for + /// @param surrogateName surrogate name of the centre (fresh, previouslyRun etc.) + /// + private static ICompoundCondition0 centreConfigQueryFor(final DeviceProfile device, final String surrogateName) { + return select(EntityCentreConfig.class) + .where().prop("title").like().val(PREFIX_OF.apply(surrogateName).apply(device)) + .and().prop("title").notLike().val(PREFIX_OF.apply(surrogateName).apply(opposite(device))); + } + + /// Creates composable centre configuration query for `uuid`, `device` and `surrogateName`. + /// + /// @param device the device for which centre configurations are looked for + /// @param surrogateName surrogate name of the centre (fresh, previouslyRun etc.) + /// + private static ICompoundCondition0 centreConfigQueryFor(final String uuid, final DeviceProfile device, final String surrogateName) { + return centreConfigQueryFor(device, surrogateName) + .and().condition(centreConfigCondFor(uuid)); + } + + /// Creates composable centre configuration query for `uuid`, `miType`, `device` and `surrogateName`. + /// + /// @param device the device for which centre configurations are looked for + /// @param surrogateName surrogate name of the centre (fresh, previouslyRun etc.) + /// + static ICompoundCondition0 centreConfigQueryFor(final String uuid, final Class> miType, final DeviceProfile device, final String surrogateName) { + return centreConfigQueryFor(uuid, device, surrogateName) + .and().condition(centreConfigCondFor(miType)); + } + + /// Creates composable centre configuration condition for `uuid`. + /// + public static ConditionModel centreConfigCondFor(final String uuid) { + return cond().prop("configUuid").eq().val(uuid).model(); + } + + /// Creates a function that returns a query to find centre persisted configurations. + /// + /// Looks only for named / link configurations, default configurations are skipped. + /// + /// @param device -- the device for which centre configurations are looked for + /// @param surrogateName -- surrogate name of the centre (fresh, previouslyRun etc.) + /// static ICompoundCondition0 centreConfigQueryFor(final Class> miType, final DeviceProfile device, final String surrogateName) { - return select(EntityCentreConfig.class).where() - .prop("title").like().val(deviceSpecific(surrogateName, device) + "[%") - .and().prop("title").notLike().val(deviceSpecific(surrogateName, opposite(device)) + "[%") - .and().prop("menuItem.key").eq().val(miType.getName()); + return centreConfigQueryFor(device, surrogateName) + .and().condition(centreConfigCondFor(miType)); } - - /** - * Creates a function that returns a query to find centre configurations persisted for user. - *

- * Looks only for named / link configurations, default configurations are avoided. - * - * @param user - * @param miType - * @param device -- the device for which centre configurations are looked for - * @param surrogateName -- surrogate name of the centre (fresh, previouslyRun etc.) - * @return - */ + + /// Creates composable centre configuration condition for `miType`. + /// + private static ConditionModel centreConfigCondFor(Class> miType) { + return cond().prop("menuItem.key").eq().val(miType.getName()).model(); + } + + /// Creates a function that returns a query to find persisted centre configurations for `user`. + /// + /// Looks only for named / link configurations, default configurations are skipped. + /// + /// @param device -- the device for which centre configurations are looked for + /// @param surrogateName -- surrogate name of the centre (fresh, previouslyRun etc.) + /// static ICompoundCondition0 centreConfigQueryFor(final User user, final Class> miType, final DeviceProfile device, final String surrogateName) { return centreConfigQueryFor(miType, device, surrogateName) - .and().prop("owner").eq().val(user); + .and().condition(centreConfigCondFor(user)); + } + + /// Creates composable centre configuration condition for `owner`. + /// + static ConditionModel centreConfigCondFor(final User owner) { + return cond().prop("owner").eq().val(owner).model(); } - /** - * Loads centre through the following chain: 'default centre' + 'differences' := 'centre'. - * - * @param user - * @param miType - * @param saveAsName -- user-defined title of 'saveAs' centre configuration or empty {@link Optional} for unnamed centre - * @param updatedDiff -- updated differences - * @param companionFinder - * - * @return - */ + /// Loads centre through the following chain: 'default centre' + 'differences' => 'centre'. + /// private static ICentreDomainTreeManagerAndEnhancer loadCentreFromDefaultAndDiff( final Class> miType, final Map updatedDiff, @@ -844,32 +881,24 @@ private static ICentreDomainTreeManagerAndEnhancer loadCentreFromDefaultAndDiff( return applyDifferences(defaultCentre, updatedDiff, getEntityType(miType), companionFinder); } - /** - * Creates user-specific (!) default centre manager from Centre DSL configuration. - *

- * IMPORTANT: this 'default centre' is used for constructing 'fresh centre', 'previouslyRun centre' and their 'diff centres', that is why it is very important to make it suitable for Web UI default values. - * All other centres will reuse such Web UI specific default values. - *

- * Please note that 'default' centre is specific to the user on current thread ({@link IUserProvider}). All injector-based default values will be user-specific - * if they are defined as user-specific in domain logic. - * - * @param gdtm - * @param miType - * @return - */ + /// Creates user-specific (!) default centre manager from Centre DSL configuration. + /// + /// IMPORTANT: this 'default centre' is used for constructing 'fresh centre', 'previouslyRun centre' and their 'diff centres'. + /// That's why it is very important to make it suitable for Web UI default values. + /// All other centres will reuse such Web UI specific default values. + /// + /// Please note that 'default' centre is specific to the user on current thread ([IUserProvider]). + /// All injector-based default values will be user-specific if they are defined as user-specific in domain logic. + /// public static ICentreDomainTreeManagerAndEnhancer getDefaultCentre(final Class> miType, final IWebUiConfig webUiConfig) { return applyWebUIDefaultValues(createDefaultCentre(miType, webUiConfig), getEntityType(miType)); } - /** - * Returns {@code runAutomatically} parameter for the Centre DSL configuration defined by {@code miType}. - *

- * Centres defined as {@code runAutomatically} not only runs automatically on loading; criteria for such centres will be cleared before auto-running (see {@link CriteriaResource#put} for more details). - * - * @param miType - * @param webUiConfig - * @return - */ + /// Returns `runAutomatically` parameter for the Centre DSL configuration defined by `miType`. + /// + /// Centres defined as `runAutomatically` not only runs automatically on loading. + /// Criteria for such centres will be cleared before auto-running (see `CriteriaResource#put` for more details). + /// public static boolean defaultRunAutomatically(final Class> miType, final IWebUiConfig webUiConfig) { return ofNullable(webUiConfig.getCentres().get(miType)) // additional safety in case if for some reason there is no EntityCentre instance for miType .map(EntityCentre::isRunAutomatically) @@ -1238,15 +1267,10 @@ private static void processValue(final Map diff, final String va } } - /** - * Applies the differences from 'differences centre' on top of 'target centre'. - * - * @param targetCentre - * @param differencesCentre - * @param root - * @param companionFinder -- to process crit-only single entity-typed values - * @return - */ + /// Applies the differences from 'differences centre' on top of 'target centre'. + /// + /// @param companionFinder to process crit-only single entity-typed values + /// static ICentreDomainTreeManagerAndEnhancer applyDifferences(final ICentreDomainTreeManagerAndEnhancer targetCentre, final Map differences, final Class> root, final ICompanionObjectFinder companionFinder) { final Supplier> managedTypeSupplier = () -> targetCentre.getEnhancer().getManagedType(root); final Map> propertiesDiff = (Map>) differences.get(PROPERTIES); @@ -1339,25 +1363,16 @@ static ICentreDomainTreeManagerAndEnhancer applyDifferences(final ICentreDomainT return targetCentre; } - /** - * Takes property differences from diff. Creates empty property differences inside diff if they are empty. - * - * @param property - * @param diff - * @return - */ + /// Takes property differences from `diff`. Creates empty property differences inside `diff` if they are empty. + /// static Map propDiff(final String property, final Map diff) { final Map> propertiesDiff = (Map>) diff.get(PROPERTIES); return diff(property, propertiesDiff); } - /** - * Takes property differences from propertiesDiff part of overall diff. Creates empty property differences inside propertiesDiff if they are empty. - * - * @param property - * @param propertiesDiff - * @return - */ + /// Takes property differences from `propertiesDiff` part of overall diff. + /// Creates empty property differences inside `propertiesDiff` if they are empty. + /// private static Map diff(final String property, final Map> propertiesDiff) { final Map propertyDiff = propertiesDiff.get(property); if (propertyDiff == null) { @@ -1368,11 +1383,8 @@ private static Map diff(final String property, final Map createEmptyDifferences() { final Map diff = new LinkedHashMap<>(); final Map> propertiesDiff = new LinkedHashMap<>(); diff --git a/platform-web-ui/src/main/java/ua/com/fielden/platform/web/centre/CentreUpdaterUtils.java b/platform-web-ui/src/main/java/ua/com/fielden/platform/web/centre/CentreUpdaterUtils.java index 2fa30bd2533..53d0b94257d 100644 --- a/platform-web-ui/src/main/java/ua/com/fielden/platform/web/centre/CentreUpdaterUtils.java +++ b/platform-web-ui/src/main/java/ua/com/fielden/platform/web/centre/CentreUpdaterUtils.java @@ -137,17 +137,23 @@ public static Long saveNewEntityCentreManager( final ICompanionObjectFinder coFinder, final Function adjustConfig ) { + final MainMenuItem menuItem = getMenuItem(menuItemType, coFinder); + final EntityCentreConfigCo co$EntityCentreConfig = coFinder.find(EntityCentreConfig.class); + final EntityCentreConfig ecc = adjustConfig.apply(co$EntityCentreConfig.new_().setOwner(user).setTitle(newName).setMenuItem(menuItem).setConfigBody(serialisedDifferences).setDesc(newDesc)); + return co$EntityCentreConfig.saveWithRetry(ecc); + } + + /// Gets (or creates) [MainMenuItem] entity for `menuItemType`. + /// + private static MainMenuItem getMenuItem(final Class menuItemType, final ICompanionObjectFinder coFinder) { final var co$MainMenuItem = coFinder.find(MainMenuItem.class); - final MainMenuItem menuItem = co$MainMenuItem.findByKeyOptional(menuItemType.getName()).orElseGet(() -> { + return co$MainMenuItem.findByKeyOptional(menuItemType.getName()).orElseGet(() -> { final MainMenuItem newMainMenuItem = co$MainMenuItem.new_(); newMainMenuItem.setKey(menuItemType.getName()); return co$MainMenuItem.save(newMainMenuItem); }); - final EntityCentreConfigCo co$EntityCentreConfig = coFinder.find(EntityCentreConfig.class); - final EntityCentreConfig ecc = adjustConfig.apply(co$EntityCentreConfig.new_().setOwner(user).setTitle(newName).setMenuItem(menuItem).setConfigBody(serialisedDifferences).setDesc(newDesc)); - return co$EntityCentreConfig.saveWithRetry(ecc); } - + /// Overrides existing [EntityCentreConfig] instance with new serialised diff. /// Otherwise, in case where there is no such instance in database, creates and saves new [EntityCentreConfig] instance with serialised diff inside. /// @@ -198,38 +204,29 @@ public static Optional findConfigOpt( ); } - /// Finds optional configuration for `model` and `uuid` with predefined fetch model, sufficient for most situations. + /// Finds optional configuration for `model` with predefined fetch model, sufficient for most situations. /// - private static Optional findConfigOptByUuid(final ICompoundCondition0 model, final String uuid, final ICompanionObjectFinder companionFinder) { + private static Optional findConfigOptByModel(final ICompoundCondition0 model, final ICompanionObjectFinder companionFinder) { final EntityCentreConfigCo coEntityCentreConfig = companionFinder.find(EntityCentreConfig.class, true); - return coEntityCentreConfig.getEntityOptional(from(model - .and().prop("configUuid").eq().val(uuid).model() - ).with(fetchWithKeyAndDesc(EntityCentreConfig.class).with("preferred").with("configUuid").with("owner.base").with("configBody").with("runAutomatically").fetchModel()).model()); + return coEntityCentreConfig.getEntityOptional( + from(model.model()) + .with(fetchWithKeyAndDesc(EntityCentreConfig.class) + .with("preferred", "configUuid", "owner.base", "configBody", "runAutomatically") + .fetchModel() + ).model() + ); } /// Finds optional configuration for `uuid`, `miType`, `device` and `surrogateName` with predefined fetch model, sufficient for most situations. /// - public static Optional findConfigOptByUuid( - final String uuid, - final Class> miType, - final DeviceProfile device, - final String surrogateName, - final ICompanionObjectFinder companionFinder - ) { - return findConfigOptByUuid(centreConfigQueryFor(miType, device, surrogateName), uuid, companionFinder); + public static Optional findConfigOptByUuid(final String uuid, final Class> miType, final DeviceProfile device, final String surrogateName, final ICompanionObjectFinder companionFinder) { + return findConfigOptByModel(centreConfigQueryFor(uuid, miType, device, surrogateName), companionFinder); } /// Finds optional configuration for `uuid`, `user`, `miType`, `device` and `surrogateName` with predefined fetch model, sufficient for most situations. /// - public static Optional findConfigOptByUuid( - final String uuid, - final User user, - final Class> miType, - final DeviceProfile device, - final String surrogateName, - final ICompanionObjectFinder companionFinder - ) { - return findConfigOptByUuid(centreConfigQueryFor(user, miType, device, surrogateName), uuid, companionFinder); + public static Optional findConfigOptByUuid(final String uuid, final User user, final Class> miType, final DeviceProfile device, final String surrogateName, final ICompanionObjectFinder companionFinder) { + return findConfigOptByModel(centreConfigQueryFor(uuid, miType, device, surrogateName).and().condition(centreConfigCondFor(user)), companionFinder); } /// Removes centre configurations from persistent storage. diff --git a/platform-web-ui/src/main/web/ua/com/fielden/platform/web/actions/tg-ui-action.js b/platform-web-ui/src/main/web/ua/com/fielden/platform/web/actions/tg-ui-action.js index feb5037aab0..bf11291ebb8 100644 --- a/platform-web-ui/src/main/web/ua/com/fielden/platform/web/actions/tg-ui-action.js +++ b/platform-web-ui/src/main/web/ua/com/fielden/platform/web/actions/tg-ui-action.js @@ -801,6 +801,13 @@ Polymer({ self._reflector.setCustomProperty(context, '@@sharedUri', self._sharedUri); } } + // Customly enhance context by applying `customiseContext` function, that can be implemented, e.g., in `preAction`. + self.customiseContext?.( + context, // The `context` to be customised. + self, // `tg-ui-action` itself. + // Convenient `setCustomProperty` function. + (customPropertyName, customPropertyValue) => self._reflector.setCustomProperty(context, customPropertyName, customPropertyValue) + ); return context; }, diff --git a/platform-web-ui/src/main/web/ua/com/fielden/platform/web/centre/test/tg-entity-centre.html b/platform-web-ui/src/main/web/ua/com/fielden/platform/web/centre/test/tg-entity-centre.html index 794fb9b5219..cf9933ad1e5 100644 --- a/platform-web-ui/src/main/web/ua/com/fielden/platform/web/centre/test/tg-entity-centre.html +++ b/platform-web-ui/src/main/web/ua/com/fielden/platform/web/centre/test/tg-entity-centre.html @@ -63,7 +63,7 @@ oldPostRetrieved(entity, bindingEntity, customObject); centre.postRetrieved = oldPostRetrieved; - assert.strictEqual(bindingEntity.get('tgFetchProviderTestEntity_propForValidation'), 'KEY8'); + assert.strictEqual(bindingEntity.get('tgFetchProviderTestEntity_propForValidation'), null); assert.strictEqual(centre.$.selection_criteria._centreDirty, true, '_centreDirty flag should be defined and should be equal to true.'); // this is because we use default configuration which is always dirty assert.ok(entity, 'Criteria entity should arrive from the server.'); assert.strictEqual(entity.isValid(), true, 'Criteria entity should be valid.'); diff --git a/platform-web-ui/src/main/web/ua/com/fielden/platform/web/centre/tg-entity-centre-behavior.js b/platform-web-ui/src/main/web/ua/com/fielden/platform/web/centre/tg-entity-centre-behavior.js index 55dd8f94912..8b6fd235258 100644 --- a/platform-web-ui/src/main/web/ua/com/fielden/platform/web/centre/tg-entity-centre-behavior.js +++ b/platform-web-ui/src/main/web/ua/com/fielden/platform/web/centre/tg-entity-centre-behavior.js @@ -1038,6 +1038,7 @@ const TgEntityCentreBehaviorImpl = { slf._actionInProgress = false; }, function (error) { slf._actionInProgress = false; + throw error; } ); }.bind(self); @@ -1116,6 +1117,7 @@ const TgEntityCentreBehaviorImpl = { self._actionInProgress = false; }, function (error) { self._actionInProgress = false; + throw error; }); }).bind(self); diff --git a/platform-web-ui/src/main/web/ua/com/fielden/platform/web/centre/tg-entity-centre-template.js b/platform-web-ui/src/main/web/ua/com/fielden/platform/web/centre/tg-entity-centre-template.js index 1cc14813e54..b6aa40a8eec 100644 --- a/platform-web-ui/src/main/web/ua/com/fielden/platform/web/centre/tg-entity-centre-template.js +++ b/platform-web-ui/src/main/web/ua/com/fielden/platform/web/centre/tg-entity-centre-template.js @@ -28,6 +28,9 @@ import '/resources/centre/tg-selection-criteria-styles.js'; import { TgEntityCentreTemplateBehavior } from '/resources/centre/tg-entity-centre-template-behavior.js'; import '/resources/centre/tg-entity-centre-insertion-point.js'; import { TgReflector } from '/app/tg-reflector.js'; +// The following import statement are useful for pre/PostActions on Entity Centre actions. +// This will be eliminated by pre/PostActions Imports API in #2407 (>= 3.0.0). +import { getParentAnd } from '/resources/reflection/tg-polymer-utils.js'; const selectionCritTemplate = html`