diff --git a/api/applib/src/main/java/org/apache/causeway/applib/CausewayModuleApplib.java b/api/applib/src/main/java/org/apache/causeway/applib/CausewayModuleApplib.java index 3784ab15ea2..717aa29cd17 100644 --- a/api/applib/src/main/java/org/apache/causeway/applib/CausewayModuleApplib.java +++ b/api/applib/src/main/java/org/apache/causeway/applib/CausewayModuleApplib.java @@ -40,6 +40,7 @@ import org.apache.causeway.applib.services.bookmark.BookmarkHolder_object; import org.apache.causeway.applib.services.clock.ClockService; import org.apache.causeway.applib.services.columnorder.Object_downloadColumnOrderTxtFilesAsZip; +import org.apache.causeway.applib.services.columnorder.Object_patchColumnOrder; import org.apache.causeway.applib.services.commanddto.conmap.ContentMappingServiceForCommandDto; import org.apache.causeway.applib.services.commanddto.conmap.ContentMappingServiceForCommandsDto; import org.apache.causeway.applib.services.commanddto.processor.spi.CommandDtoProcessorServiceIdentity; @@ -111,6 +112,7 @@ Dto_downloadXml.class, Dto_downloadXsd.class, Object_downloadColumnOrderTxtFilesAsZip.class, + Object_patchColumnOrder.class, Object_downloadLayout.class, Object_patchLayout.class, Object_downloadMetamodelXml.class, diff --git a/api/applib/src/main/java/org/apache/causeway/applib/services/appfeat/ApplicationFeature.java b/api/applib/src/main/java/org/apache/causeway/applib/services/appfeat/ApplicationFeature.java index d0b6f043f5a..16ded0af52d 100644 --- a/api/applib/src/main/java/org/apache/causeway/applib/services/appfeat/ApplicationFeature.java +++ b/api/applib/src/main/java/org/apache/causeway/applib/services/appfeat/ApplicationFeature.java @@ -44,10 +44,10 @@ default String getFullyQualifiedName() { default SortedSet getMembersOfSort(final ApplicationMemberSort memberSort) { return switch (memberSort) { - case PROPERTY -> getProperties(); - case COLLECTION -> getCollections(); - case ACTION -> getActions(); - default -> Collections.emptySortedSet(); + case PROPERTY -> getProperties(); + case COLLECTION -> getCollections(); + case ACTION -> getActions(); + default -> Collections.emptySortedSet(); }; } diff --git a/api/applib/src/main/java/org/apache/causeway/applib/services/appfeat/ApplicationFeatureId.java b/api/applib/src/main/java/org/apache/causeway/applib/services/appfeat/ApplicationFeatureId.java index e748deb42df..421ffbcd11e 100644 --- a/api/applib/src/main/java/org/apache/causeway/applib/services/appfeat/ApplicationFeatureId.java +++ b/api/applib/src/main/java/org/apache/causeway/applib/services/appfeat/ApplicationFeatureId.java @@ -26,6 +26,7 @@ import static java.util.Comparator.naturalOrder; import static java.util.Comparator.nullsFirst; +import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; import org.apache.causeway.applib.Identifier; @@ -45,21 +46,20 @@ import org.apache.causeway.commons.internal.exceptions._Exceptions; import lombok.Getter; -import org.jspecify.annotations.NonNull; import lombok.Synchronized; /** * Value type representing a namespace, type or member. - *

- * This value is {@link Comparable}, the implementation of which considers + * + *

This value is {@link Comparable}, the implementation of which considers * {@link #getSort() (feature) sort}, {@link #getNamespace() namespace}, * {@link #getTypeSimpleName() type simple name} and {@link #getLogicalMemberName() member-logical-name}. - *

- * If the represented member is an action, then {@link #getLogicalMemberName() member-logical-name} + * + *

If the represented member is an action, then {@link #getLogicalMemberName() member-logical-name} * must not include any parameter list or parentheses. * Consequently method overloading is not supported. - *

- * If there is a member name clash involving an action and an association, + * + *

If there is a member name clash involving an action and an association, * then consequently any permissions defined automatically apply to both and one cannot separate * these. * @@ -79,13 +79,16 @@ public class ApplicationFeatureId // -- FACTORY METHODS + /** + * @apiNote the inverse conversion is available via + * {@link ApplicationFeatureRepository#asIdentifier(ApplicationFeatureId)} + */ public static ApplicationFeatureId fromIdentifier(final @NonNull Identifier identifier) { var logicalTypeName = identifier.logicalTypeName(); - if(identifier.type().isClass()) { + if(identifier.type().isClass()) return newType(logicalTypeName); - } return newMember(logicalTypeName, identifier.memberLogicalName()); } @@ -93,15 +96,11 @@ public static ApplicationFeatureId newFeature( final @NonNull ApplicationFeatureSort featureSort, final @NonNull String qualifiedLogicalName) { - switch (featureSort) { - case NAMESPACE: - return newNamespace(qualifiedLogicalName); - case TYPE: - return newType(qualifiedLogicalName); - case MEMBER: - return newMember(qualifiedLogicalName); - } - throw _Exceptions.illegalArgument("Unknown feature sort '%s'", featureSort); + return switch (featureSort) { + case NAMESPACE -> newNamespace(qualifiedLogicalName); + case TYPE -> newType(qualifiedLogicalName); + case MEMBER -> newMember(qualifiedLogicalName); + }; } public static ApplicationFeatureId newFeature( @@ -109,13 +108,11 @@ public static ApplicationFeatureId newFeature( final @Nullable String logicalTypeSimpleName, final @Nullable String memberName) { - if(logicalTypeSimpleName == null) { + if(logicalTypeSimpleName == null) return newNamespace(namespace); - } var logicalTypeName = namespace + "." + logicalTypeSimpleName; - if(memberName == null) { + if(memberName == null) return newType(logicalTypeName); - } return newMember(logicalTypeName, memberName); } @@ -161,9 +158,8 @@ public static ApplicationFeatureId newMember(final String logicalTypeName, final public static ApplicationFeatureId newMember(final String fullyQualifiedLogicalName) { var featureId = new ApplicationFeatureId(ApplicationFeatureSort.MEMBER); var i = fullyQualifiedLogicalName.lastIndexOf("#"); - if(i == -1) { + if(i == -1) throw new IllegalArgumentException("Malformed, expected a '#': " + fullyQualifiedLogicalName); - } var logicalTypeName = fullyQualifiedLogicalName.substring(0, i); var memberName = fullyQualifiedLogicalName.substring(i + 1); initType(featureId, logicalTypeName); @@ -184,10 +180,9 @@ private static void initType(final ApplicationFeatureId featureId, final String } // guard against empty namespace; there should be a meta-model validator that already catched that - if(_Strings.isEmpty(featureId.namespace)) { + if(_Strings.isEmpty(featureId.namespace)) throw _Exceptions.illegalArgument( "fullyQualifiedName '%s' must include a non-empty namespace", fullyQualifiedName); - } featureId.logicalMemberName = null; } @@ -197,9 +192,8 @@ private static void initMember(final ApplicationFeatureId featureId, final @Null } private static String stripOffParamsIfAny(final @Nullable String name) { - if(_Strings.isEmpty(name)) { + if(_Strings.isEmpty(name)) return name; - } final int paramListStartIndex = name.indexOf('('); return paramListStartIndex>-1 ? name.substring(0, paramListStartIndex) @@ -284,9 +278,8 @@ public String getFullyQualifiedName() { @Programmatic public String getLogicalTypeName() { - if (getTypeSimpleName() == null) { + if (getTypeSimpleName() == null) return null; - } var buf = new StringBuilder(); if(!_Strings.isNullOrEmpty(getNamespace())) { buf.append(getNamespace()).append("."); @@ -305,13 +298,12 @@ public ApplicationFeatureId getParentNamespaceFeatureId() { _Assert.assertFalse(sort.isMember()); - if(sort.isType()) { + if(sort.isType()) return ApplicationFeatureId.newNamespace(getNamespace()); - } else { + else { var namespace = getNamespace(); // eg aaa.bbb.ccc - if(!namespace.contains(".")) { + if(!namespace.contains(".")) return null; // parent is root - } final int cutOffPos = namespace.lastIndexOf('.'); final String parentPackageName = namespace.substring(0, cutOffPos); return newNamespace(parentPackageName); @@ -479,15 +471,12 @@ public ApplicationFeatureId withNamespace(final @NonNull String namespace) { * @param logicalTypeName */ public ApplicationFeatureId withLogicalTypeName(final @NonNull String logicalTypeName) { - switch (getSort()) { - case MEMBER: - return newMember(logicalTypeName, this.getLogicalMemberName()); - case TYPE: - return newType(logicalTypeName); - case NAMESPACE: - default: - return this; - } + return switch (getSort()) { + case MEMBER -> newMember(logicalTypeName, this.getLogicalMemberName()); + case TYPE -> newType(logicalTypeName); + case NAMESPACE -> this; + default -> this; + }; } } diff --git a/api/applib/src/main/java/org/apache/causeway/applib/services/appfeat/ApplicationFeatureRepository.java b/api/applib/src/main/java/org/apache/causeway/applib/services/appfeat/ApplicationFeatureRepository.java index dbcde0143c8..21eb8551a9c 100644 --- a/api/applib/src/main/java/org/apache/causeway/applib/services/appfeat/ApplicationFeatureRepository.java +++ b/api/applib/src/main/java/org/apache/causeway/applib/services/appfeat/ApplicationFeatureRepository.java @@ -20,8 +20,12 @@ import java.util.Collection; import java.util.Map; +import java.util.Optional; import java.util.SortedSet; +import org.jspecify.annotations.Nullable; + +import org.apache.causeway.applib.Identifier; import org.apache.causeway.applib.id.LogicalType; /** @@ -52,4 +56,6 @@ public interface ApplicationFeatureRepository { Collection allMembers(); SortedSet propertyIdsFor(LogicalType logicalType); + + Optional asIdentifier(@Nullable ApplicationFeatureId applicationFeatureId); } diff --git a/api/applib/src/main/java/org/apache/causeway/applib/services/columnorder/ColumnOrderTxtFileService.java b/api/applib/src/main/java/org/apache/causeway/applib/services/columnorder/ColumnOrderTxtFileService.java index fb8a647dc87..cd38cdbaa84 100644 --- a/api/applib/src/main/java/org/apache/causeway/applib/services/columnorder/ColumnOrderTxtFileService.java +++ b/api/applib/src/main/java/org/apache/causeway/applib/services/columnorder/ColumnOrderTxtFileService.java @@ -24,28 +24,24 @@ * This is a utility service to support the usage of {@link TableColumnOrderService}, providing the ability to obtain * a zip of each of the Xxx.columnOrder.txt files for the specified domain object. * - *

- * The zip contains: + *

The zip contains: *

- *

* - *

- * These should be unzipped and copied in the domain class' package, and then their contents updated to specify the + *

These should be un-zipped and copied in the domain class' package, and then their contents updated to specify the * order in which the respective object's properties will be shown in the standalone or parented collections. - *

* * @see Object_downloadColumnOrderTxtFilesAsZip + * @see Object_patchColumnOrder * @see TableColumnOrderService * * @since 2.0 {@index} */ public interface ColumnOrderTxtFileService { - byte[] toZip(final Object domainObject); - + byte[] toZip(Object domainObject); } diff --git a/api/applib/src/main/java/org/apache/causeway/applib/services/columnorder/Object_downloadColumnOrderTxtFilesAsZip.java b/api/applib/src/main/java/org/apache/causeway/applib/services/columnorder/Object_downloadColumnOrderTxtFilesAsZip.java index ba974440f3d..24e615b39dc 100644 --- a/api/applib/src/main/java/org/apache/causeway/applib/services/columnorder/Object_downloadColumnOrderTxtFilesAsZip.java +++ b/api/applib/src/main/java/org/apache/causeway/applib/services/columnorder/Object_downloadColumnOrderTxtFilesAsZip.java @@ -74,6 +74,8 @@ @RequiredArgsConstructor public class Object_downloadColumnOrderTxtFilesAsZip { + @Inject ColumnOrderTxtFileService columnOrderTxtFileService; + private final Object domainObject; // mixee public static class ActionDomainEvent @@ -88,6 +90,4 @@ public static class ActionDomainEvent return String.format("%s.columnOrder.zip", domainObject.getClass().getSimpleName()); } - @Inject ColumnOrderTxtFileService columnOrderTxtFileService; - } diff --git a/api/applib/src/main/java/org/apache/causeway/applib/services/columnorder/Object_patchColumnOrder.java b/api/applib/src/main/java/org/apache/causeway/applib/services/columnorder/Object_patchColumnOrder.java new file mode 100644 index 00000000000..118dcdb5949 --- /dev/null +++ b/api/applib/src/main/java/org/apache/causeway/applib/services/columnorder/Object_patchColumnOrder.java @@ -0,0 +1,164 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.applib.services.columnorder; + +import java.util.List; +import java.util.function.UnaryOperator; +import java.util.stream.Stream; + +import jakarta.inject.Inject; + +import org.jspecify.annotations.Nullable; + +import org.apache.causeway.applib.Identifier; +import org.apache.causeway.applib.annotation.Action; +import org.apache.causeway.applib.annotation.ActionLayout; +import org.apache.causeway.applib.annotation.DomainObject; +import org.apache.causeway.applib.annotation.Introspection; +import org.apache.causeway.applib.annotation.MemberSupport; +import org.apache.causeway.applib.annotation.Nature; +import org.apache.causeway.applib.annotation.Parameter; +import org.apache.causeway.applib.annotation.ParameterLayout; +import org.apache.causeway.applib.annotation.PrecedingParamsPolicy; +import org.apache.causeway.applib.annotation.Publishing; +import org.apache.causeway.applib.annotation.RestrictTo; +import org.apache.causeway.applib.annotation.SemanticsOf; +import org.apache.causeway.applib.layout.LayoutConstants; +import org.apache.causeway.applib.services.appfeat.ApplicationFeatureId; +import org.apache.causeway.applib.services.appfeat.ApplicationFeatureRepository; +import org.apache.causeway.applib.services.metamodel.MetaModelService; +import org.apache.causeway.applib.services.metamodel.MetaModelService.AssociationsLookup; +import org.apache.causeway.applib.util.Listing; +import org.apache.causeway.applib.util.Listing.ListingHandler; +import org.apache.causeway.applib.util.Listing.MergePolicy; +import org.apache.causeway.commons.collections.Can; +import org.apache.causeway.commons.internal.exceptions._Exceptions; + +import lombok.RequiredArgsConstructor; + +/** + * Allows uploading of column order definition, that overrules the default lookup for such information. + * + * @since 4.0 {@index} + */ +@Action( + domainEvent = Object_patchColumnOrder.ActionDomainEvent.class, + semantics = SemanticsOf.IDEMPOTENT, + commandPublishing = Publishing.DISABLED, + executionPublishing = Publishing.DISABLED, + restrictTo = RestrictTo.PROTOTYPING) +@ActionLayout( + cssClassFa = "solid file-arrow-up", + describedAs = "Uploads table column order, to be stored in memory for this object type. " + + "It overrules the default column order definition lookup. " + + "On application restart this information is lost.", + fieldSetId = LayoutConstants.FieldSetId.METADATA, + position = ActionLayout.Position.PANEL_DROPDOWN, + sequence = "700.2.4" +) +//framework provided domain objects and mixins should explicitly specify their introspection policy +@DomainObject(nature=Nature.MIXIN, mixinMethod = "act", introspection = Introspection.ANNOTATION_REQUIRED) +@RequiredArgsConstructor +public class Object_patchColumnOrder { + + public static class ActionDomainEvent + extends org.apache.causeway.applib.CausewayModuleApplib.ActionDomainEvent {} + + @Inject MetaModelService metaModelService; + @Inject ApplicationFeatureRepository applicationFeatureRepository; + + private final Object mixee; + + @MemberSupport public Object act( + + @Parameter + @ParameterLayout(describedAs = "The 'Feature', for which the patch is to be applied (in-memory), " + + "that is, " + + "either for a one-to-many relation (a PARENTED Collection), " + + "or a domain-type (applies to all STANDALONE Collections of that element-type). " + + "The Feature either represents a particular one-to-many relation of this domain-type or " + + "represents the domain-type itself or one of the domain-type's super types. " + + "The Apache Causeway Programming Model also supports {parent-type, element-type} scoped " + + "column order definitions, which are not covered by patching yet.") + final ApplicationFeatureId featureId, + + @Parameter(precedingParamsPolicy = PrecedingParamsPolicy.RESET) + @ParameterLayout(multiLine = 20) + final String columnListing) { + + var identifier = applicationFeatureRepository.asIdentifier(featureId) + .orElseThrow(); // not found -> unexpected + + var listing = listingHandler().parseListing(columnListing); + var columns = Can.ofStream(listing.streamEnabled()); + + metaModelService.patchColumnOrder(identifier, columns); + return mixee; + } + + @MemberSupport public List choicesFeatureId() { + return Stream.concat( + metaModelService.streamTypeHierarchy(mixee.getClass()) + .map(ApplicationFeatureId::fromIdentifier), + metaModelService.streamCollections(mixee.getClass()) + .map(ApplicationFeatureId::fromIdentifier)) + .toList(); + } + + @MemberSupport public String defaultColumnListing(final @Nullable ApplicationFeatureId featureId) { + if(featureId==null) + return "# no feature selected"; + + var identifier = applicationFeatureRepository.asIdentifier(featureId) + .orElseThrow(); // not found -> unexpected + + if(identifier.type().isCollection()) + return listing( + metaModelService.parentedAssociationsForColumnRendering(mixee, identifier, AssociationsLookup.AVAILABLE), + metaModelService.parentedAssociationsForColumnRendering(mixee, identifier, AssociationsLookup.ENABLED)) + .toString(); + + if(identifier.type().isClass()) + return listing( + metaModelService.standaloneAssociationsForColumnRendering(identifier.logicalType(), AssociationsLookup.AVAILABLE), + metaModelService.standaloneAssociationsForColumnRendering(identifier.logicalType(), AssociationsLookup.ENABLED)) + .toString(); + + throw _Exceptions.illegalArgument("unsupported feature type %s", identifier.type()); + } + + // -- HELPER + + private Listing listing(final Stream availableIds, final Stream enabledIds) { + // all column candidates + var available = listingHandler() + .createListing(availableIds.map(Identifier::memberLogicalName)); + + // all columns currently rendered + var enabled = listingHandler() + .createListing(enabledIds.map(Identifier::memberLogicalName)); + + return enabled.merge(MergePolicy.ADD_NEW_AS_DISABLED, available); + } + + private final static ListingHandler listingHandler() { + return new ListingHandler<>(String.class, UnaryOperator.identity(), UnaryOperator.identity(), UnaryOperator.identity()); + } + +} diff --git a/api/applib/src/main/java/org/apache/causeway/applib/services/metamodel/MetaModelService.java b/api/applib/src/main/java/org/apache/causeway/applib/services/metamodel/MetaModelService.java index 7fd76afec5a..0b7fa221059 100644 --- a/api/applib/src/main/java/org/apache/causeway/applib/services/metamodel/MetaModelService.java +++ b/api/applib/src/main/java/org/apache/causeway/applib/services/metamodel/MetaModelService.java @@ -20,11 +20,14 @@ import java.util.Optional; import java.util.function.BiPredicate; +import java.util.stream.Stream; import jakarta.inject.Named; +import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; +import org.apache.causeway.applib.Identifier; import org.apache.causeway.applib.annotation.Action; import org.apache.causeway.applib.annotation.DomainObject; import org.apache.causeway.applib.annotation.DomainService; @@ -36,8 +39,6 @@ import org.apache.causeway.commons.collections.Can; import org.apache.causeway.schema.metamodel.v2.MetamodelDto; -import org.jspecify.annotations.NonNull; - /** * This service provides a formal API into the framework's metamodel. * @@ -56,7 +57,7 @@ public interface MetaModelService { * such non-abstract class registered. * (interfaces and abstract types are never added to the lookup table). */ - Optional lookupLogicalTypeByName(final String logicalTypeName); + Optional lookupLogicalTypeByName(String logicalTypeName); /** * Assuming that the {@link LogicalType} passed in actually represents a domain type, then @@ -69,7 +70,7 @@ public interface MetaModelService { * * @param logicalType */ - Can logicalTypeAndAliasesFor(final LogicalType logicalType); + Can logicalTypeAndAliasesFor(LogicalType logicalType); /** * Returns the {@link LogicalType} of a domain class' object type, corresponding to {@link Named#value()}, @@ -80,20 +81,20 @@ public interface MetaModelService { * If there is no such domain type, then an empty {@link Can} will be returned. *

*/ - Can logicalTypeAndAliasesFor(final String logicalTypeName); + Can logicalTypeAndAliasesFor(String logicalTypeName); /** * Provides a lookup by class of a domain class' object type,corresponding to * {@link Named#value()} or {@link DomainService#aliased()} or * {@link DomainObject#aliased()}. */ - Optional lookupLogicalTypeByClass(final Class domainType); + Optional lookupLogicalTypeByClass(Class domainType); /** * Invalidates and rebuilds the internal metadata for the specified domain * type. */ - void rebuild(final Class domainType); + void rebuild(Class domainType); /** * Returns a list of representations of each of member of each domain class. @@ -137,8 +138,7 @@ public interface MetaModelService { * {@link org.apache.causeway.applib.services.conmap.ContentMappingService}. *

*/ - CommandDtoProcessor commandDtoProcessorFor( - String logicalMemberIdentifier); + CommandDtoProcessor commandDtoProcessorFor(String logicalMemberIdentifier); /** * How {@link MetaModelService#sortOf(Class, Mode)} should act if an object @@ -166,13 +166,100 @@ enum Mode { * * @param config - restricts/filters to a subsets of the metamodel. */ - MetamodelDto exportMetaModel(final Config config); + MetamodelDto exportMetaModel(Config config); /** * Can be used to create object relation diagrams (e.g. Plantuml). * * @param filter by {@link BeanSort} and {@link LogicalType} what to include in the resulting graph */ - ObjectGraph exportObjectGraph(final @NonNull BiPredicate filter); + ObjectGraph exportObjectGraph(@NonNull BiPredicate filter); + + /** + * Stream of {@link Identifier} representing the Actions of given domainType, including mixed-in ones, + * but excluding Actions, that are only available for PROTOTYPING. + * @since 4.0 + */ + Stream streamActions(@Nullable Class domainType); + + /** + * Stream of {@link Identifier} representing the Properties of given domainType, including mixed-in ones. + * @since 4.0 + */ + Stream streamProperties(@Nullable Class domainType); + + /** + * Stream of {@link Identifier} representing the Collections of given domainType, including mixed-in ones. + * @since 4.0 + */ + Stream streamCollections(@Nullable Class domainType); + + /** + * Stream of {@link Identifier} representing the type hierarchy of given domainType, starting at given domainType, + * going 'up' in the hierarchy, but excluding {@link Object}. + * + *

Included are all types in the hierarchy, that are recognized by the Metamodel, recognized Interfaces last. + * + * @apiNote Since 4.0, the Metamodel supports Interfaces acting as element-types for Collections. + * This requires explicit {@link DomainObject} annotations on those Interfaces along with the constraint, that within + * a type hierarchy, every type can have at most one super-type. + * + * @since 4.0 + */ + Stream streamTypeHierarchy(@Nullable Class domainType); + + /** + * Parameter to lookup associations for column rendering. + * + * @since 4.0 + */ + public enum AssociationsLookup { + /** + * The Column Order SPI and Column Order Patching have potential to hide otherwise visible columns. + * This query mode shows all columns, that are not permanently hidden while ignoring hiding from above mechanisms. + */ + AVAILABLE, + /** + * This query mode shows all currently visible columns. + */ + ENABLED; + + public boolean isAvailable() { return this==AVAILABLE; } + public boolean isEnabled() { return this==ENABLED; } + } + + /** + * Stream of {@link Identifier} representing the Columns for specified PARENTED collection. + * + *

Columns returned are those that are either in principle AVAILABLE or currently ENABLED, based on given {@link AssociationsLookup}. + * + *

The availability lookup returns {@link Identifier}(s) in no particular order, + * whereas the enablement lookup returns them in same order as rendered. + * + *

If parentDomainObject is null returns an empty {@link Stream}. + * + * @since 4.0 + */ + Stream parentedAssociationsForColumnRendering(Object parentDomainObject, Identifier collectionId, AssociationsLookup lookup); + + /** + * Stream of {@link Identifier} representing the Columns for specified STANDALONE collection. + * + *

Columns returned are those that are either in principle AVAILABLE or currently ENABLED, based on given {@link AssociationsLookup}. + * + *

The availability lookup returns {@link Identifier}(s) in no particular order, + * whereas the enablement lookup returns them in same order as rendered. + * + * @since 4.0 + */ + Stream standaloneAssociationsForColumnRendering(LogicalType logicalType, AssociationsLookup lookup); + + /** + * @param identifier either for a TYPE or a COLLECTION + * @param columnsInOrder to be used instead of the default order + * + * @since 4.0 + */ + void patchColumnOrder(Identifier identifier, Can columnsInOrder); } diff --git a/api/applib/src/main/java/org/apache/causeway/applib/util/Listing.java b/api/applib/src/main/java/org/apache/causeway/applib/util/Listing.java new file mode 100644 index 00000000000..cb3b5d0ca2e --- /dev/null +++ b/api/applib/src/main/java/org/apache/causeway/applib/util/Listing.java @@ -0,0 +1,315 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.applib.util; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Function; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import org.apache.causeway.applib.annotation.Programmatic; +import org.apache.causeway.applib.annotation.ValueSemantics; +import org.apache.causeway.commons.collections.Can; +import org.apache.causeway.commons.functional.Try; +import org.apache.causeway.commons.internal.base._NullSafe; +import org.apache.causeway.commons.io.TextUtils; + +/** + * Represents a list of lines (of text), + * where each {@link Line} has semantics, such as being able to be commented out. + * + *

Each non-comment line can be mapped to a Java class of type {@code T}. + * + *

Typically not used as domain value, as we provide no {@link ValueSemantics}, + * but rather as a (multi-line) {@link String} support class. + * + * @param type each non-comment line can be mapped to (successful parsing) + * @since 4.0 + */ +@Programmatic +public record Listing( + @NonNull ListingHandler handler, + @NonNull Can lines) { + + public record ListingHandler ( + @NonNull Class objectType, + @NonNull Function stringifier, + @NonNull Function destringifier, + @NonNull Function keyExtractor) { + + // -- LISTING FACTORIES + + public Listing emptyListing() { + return createListing(Can.empty()); + } + + public Listing createListing(@Nullable final Iterable enabledElements) { + return createListing(_NullSafe.stream(enabledElements)); + } + public Listing createListing(@Nullable final Stream enabledElementStream) { + if(enabledElementStream==null) return emptyListing(); + return new Listing<>(this, enabledElementStream + .map(t->new LineEnabled<>(t, stringifier().apply(t))) + .collect(Can.toCan())); + } + + public Listing parseListing(@Nullable final String wholeText) { + if(wholeText==null) return emptyListing(); + return parseListing(TextUtils.readLines(wholeText)); + } + public Listing parseListing(@Nullable final Can textLines) { + if(textLines==null) return emptyListing(); + return new Listing<>(this, textLines.map(this::parseLine)); + } + + // -- PARSING + + /** + * Parses the whole line as is, including comments. + * + *

If a line is not a comment, but can also not be mapped to {@code T}, + * then it is commented out. + */ + public Line parseLine(final String wholeLine) { + final var line = wholeLine.trim(); + if(line.isBlank()) + return new LineComment(line); + if(line.startsWith("#")) { + var trimmed = line; + while(trimmed.startsWith("#")) { + trimmed = trimmed.substring(1).trim(); + } + T object = asT(trimmed).getValue().orElse(null); + return object==null + ? new LineComment(wholeLine) + : (line.startsWith("REMOVED ")) + ? new LineRemoved<>(object, trimmed.substring(8)) + : new LineDisabled<>(object, trimmed); + } + Try asT = asT(line); + return asT + .mapSuccessWhenPresent(object->(Line)new LineEnabled(object, line)) + .mapEmptyToFailure() + .mapFailureToSuccess(e->new LineComment(String.format("#ERROR cannot parse ‹%s› as %s (%s)", + line, objectType().getSimpleName(), e))) + .valueAsNonNullElseFail(); + } + + // -- HELPER + + private Try asT(final String stringified) { + return Try.call(()->destringifier().apply(stringified)); + } + + } + + public sealed interface Line + permits MappedLine, LineComment{ + String format(); + } + public sealed interface MappedLine + extends Line + permits LineEnabled, LineDisabled, LineRemoved { + T object(); + } + + /** + * Can be mapped to {@code T}. Not commented out. + */ + public record LineEnabled( + @NonNull T object, + @NonNull String objectStringified) + implements MappedLine { + @Override public String format() { + return objectStringified; + } + public LineDisabled toDisabled() { + return new LineDisabled<>(object, objectStringified); + } + } + /** + * Can be mapped to {@code T}, but commented out. + */ + public record LineDisabled( + @NonNull T object, + @NonNull String objectStringified) + implements MappedLine { + @Override public String format() { + return "#" + objectStringified; + } + public LineEnabled toEnabled() { + return new LineEnabled<>(object, objectStringified); + } + public LineRemoved toRemoved() { + return new LineRemoved<>(object, objectStringified); + } + } + /** + * Can be mapped to {@code T}, but was removed, hence commented out. + */ + public record LineRemoved( + @NonNull T object, + @NonNull String objectStringified) + implements MappedLine { + @Override public String format() { + return "#REMOVED " + objectStringified; + } + public LineDisabled toDisabled() { + return new LineDisabled<>(object, objectStringified); + } + } + /** + * Blank line or arbitrary comment, cannot be mapped to {@code T}. + */ + public record LineComment( + @NonNull String comment) + implements Line { + @Override public String format() { + return comment; + } + } + + // -- STREAMS + + public Stream> streamEnabledLines() { + return lines().stream().filter(LineEnabled.class::isInstance).map(LineEnabled.class::cast); + } + public Stream> streamDisabledLines() { + return lines().stream().filter(LineDisabled.class::isInstance).map(LineDisabled.class::cast); + } + public Stream streamEnabled() { + return streamEnabledLines().map(LineEnabled::object); + } + public Stream streamDisabled() { + return streamDisabledLines().map(LineDisabled::object); + } + public Stream streamComments() { + return lines().stream().filter(LineComment.class::isInstance).map(LineComment.class::cast); + } + + // -- FORMAT + + @Override + public final String toString() { + return lines().map(Line::format).join("\n"); + } + + // -- MERGE + + public enum MergePolicy { + ADD_NEW_AS_ENABLED, + ADD_NEW_AS_DISABLED + } + + /** + * Say this listing was edited by a human, + * but needs to be synchronized with a newer version originating from some system process, + * then we'd like to merge in this new information, + * without loosing any information that is already present in this listing such as: + *

    + *
  • comments
  • + *
  • line ordering
  • + *
+ * + *

We do this by adding a comment line {@code #MERGED} followed by any lines that are new. + * + *

Any lines already existing are kept as they are, + * unless the {@code newerVersion} no longer contains the referenced object of type {@code T}, + * in which case, the {@link Line} will be commented out (if not already) with a marker + * {@code #REMOVED}. + * + *

This requires the merge algorithm to evaluate whether 2 referenced objects are equal, + * which it does by checking object keys as given by {@link ListingHandler#keyExtractor} + * for equality. We could have done the same by directly checking referenced objects for equality, + * but - worst case - that would involve entire objects graphs to be assembled, + * while for our use case its convenient to work with simple object facades. + */ + public Listing merge(@NonNull final MergePolicy policy, @Nullable final Listing newerVersion) { + if(newerVersion==null) return this; + final Map> incomingByKey = newerVersion.streamEnabledLines() + .collect(Collectors.toMap( + line->handler().keyExtractor().apply(line.object()), + UnaryOperator.identity(), + (a, b)->a, + LinkedHashMap::new)); + if(incomingByKey.isEmpty()) return this; + + var mergedLines = new ArrayList(); + + // if in but NOT in -> add to merged (honor LineMergePolicy) + // if NOT in but in -> mark #REMOVED if not already + // if in both all AND b -> add to merged (keep enabled-state as defined by b) + lines().stream() + .forEach((final Line line)->{ + @SuppressWarnings("unchecked") + var key = (line instanceof MappedLine mappedLine) + ? handler().keyExtractor().apply((T) mappedLine.object()) + : null; + final boolean incomingContainsKey = key!=null + ? incomingByKey.remove(key)!=null + : false; + + if(line instanceof LineComment comment) { + mergedLines.add(comment); // identity operation + } else if(line instanceof LineEnabled lineEnabled) { + if(incomingContainsKey) { + mergedLines.add(lineEnabled); // identity operation + } else { + mergedLines.add(lineEnabled.toDisabled().toRemoved()); // mark REMOVED + } + incomingByKey.remove(key); + } else if(line instanceof LineDisabled lineDisabled) { + if(incomingContainsKey) { + mergedLines.add(lineDisabled); // identity operation + } else { + mergedLines.add(lineDisabled.toRemoved()); // mark REMOVED + } + } else if(line instanceof LineRemoved lineRemoved) { + if(incomingContainsKey) { + mergedLines.add(lineRemoved); // identity operation + } else { + mergedLines.add(lineRemoved.toDisabled()); // unmark REMOVED + } + } + }); + + // process the remaining incoming lines + if(!incomingByKey.isEmpty()) { + if(!mergedLines.isEmpty()) { + mergedLines.add(new LineComment("#MERGED")); // skip if we are filling an empty listing + } + if(MergePolicy.ADD_NEW_AS_ENABLED==policy) { + incomingByKey.values() + .forEach(mergedLines::add); + } else { + incomingByKey.values() + .forEach(lineEnabled->mergedLines.add(lineEnabled.toDisabled())); + } + } + + return new Listing<>(handler, Can.ofCollection(mergedLines)); + } + +} diff --git a/api/applib/src/main/java/org/apache/causeway/applib/value/semantics/ValueSemanticsAbstract.java b/api/applib/src/main/java/org/apache/causeway/applib/value/semantics/ValueSemanticsAbstract.java index 70902a46fec..f718556dfdf 100644 --- a/api/applib/src/main/java/org/apache/causeway/applib/value/semantics/ValueSemanticsAbstract.java +++ b/api/applib/src/main/java/org/apache/causeway/applib/value/semantics/ValueSemanticsAbstract.java @@ -43,6 +43,7 @@ import org.apache.causeway.applib.annotation.TimePrecision; import org.apache.causeway.applib.exceptions.recoverable.TextEntryParseException; +import org.apache.causeway.applib.fa.FontAwesomeLayers; import org.apache.causeway.applib.locale.UserLocale; import org.apache.causeway.applib.services.bookmark.IdStringifier; import org.apache.causeway.applib.services.i18n.TranslationContext; @@ -227,8 +228,9 @@ protected Optional parseInteger( final ValueSemanticsProvider.@Nullable Context context, final @Nullable String text) { var input = _Strings.blankToNullOrTrim(text); - if(input==null) - return Optional.empty(); + if(input==null) { + return Optional.empty(); + } try { return parseDecimal(context, input, GroupingSeparatorPolicy.ALLOW) .map(BigDecimal::toBigIntegerExact); @@ -248,15 +250,17 @@ protected Optional parseDecimal( final @Nullable String text, final GroupingSeparatorPolicy groupingSeparatorPolicy) { var input = _Strings.blankToNullOrTrim(text); - if(input==null) - return Optional.empty(); + if(input==null) { + return Optional.empty(); + } if (groupingSeparatorPolicy == GroupingSeparatorPolicy.DISALLOW) { var userLocale = getUserLocale(context); var decimalFormatSymbols = new DecimalFormatSymbols(userLocale.numberFormatLocale()); var groupingSeparatorChar = decimalFormatSymbols.getGroupingSeparator(); - if (input.contains(""+groupingSeparatorChar)) - throw new TextEntryParseException("Invalid value '" + input + "'; do not use the '" + groupingSeparatorChar + "' grouping separator"); + if (input.contains(""+groupingSeparatorChar)) { + throw new TextEntryParseException("Invalid value '" + input + "'; do not use the '" + groupingSeparatorChar + "' grouping separator"); + } } var format = getNumberFormat(context, FormatUsageFor.PARSING); @@ -265,17 +269,19 @@ protected Optional parseDecimal( var position = new ParsePosition(0); try { var number = (BigDecimal)format.parse(input, position); - if (position.getErrorIndex() != -1) - throw new ParseException("could not parse input='" + input + "'", position.getErrorIndex()); - else if (position.getIndex() < input.length()) - throw new ParseException("input='" + input + "' was not processed completely", position.getIndex()); + if (position.getErrorIndex() != -1) { + throw new ParseException("could not parse input='" + input + "'", position.getErrorIndex()); + } else if (position.getIndex() < input.length()) { + throw new ParseException("input='" + input + "' was not processed completely", position.getIndex()); + } // check for maxFractionDigits if required ... final int maxFractionDigits = format.getMaximumFractionDigits(); if(maxFractionDigits>-1 - && number.scale()>format.getMaximumFractionDigits()) - throw new TextEntryParseException(String.format( + && number.scale()>format.getMaximumFractionDigits()) { + throw new TextEntryParseException(String.format( "No more than %d digits can be entered after the decimal separator, " + "got %d in '%s'.", maxFractionDigits, number.scale(), input)); + } return Optional.of(number); } catch (final NumberFormatException | ParseException e) { throw new TextEntryParseException(String.format( @@ -402,9 +408,11 @@ protected final String toMonospace(final String html) { /** * Uses Fontawesome. */ - protected final String faIconAndTitle(final String faClasses, final String titleHtml) { + protected final String faIconAndTitle(final FontAwesomeLayers faLayers, final String titleHtml) { return """ - %s""".formatted(faClasses, titleHtml); + %s%s""".formatted(faLayers.toHtml(), titleHtml); } + + } diff --git a/api/applib/src/test/java/org/apache/causeway/applib/util/ListingTest.java b/api/applib/src/test/java/org/apache/causeway/applib/util/ListingTest.java new file mode 100644 index 00000000000..459cfd8c588 --- /dev/null +++ b/api/applib/src/test/java/org/apache/causeway/applib/util/ListingTest.java @@ -0,0 +1,157 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.applib.util; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertLinesMatch; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.springframework.util.StringUtils; + +import org.apache.causeway.applib.util.Listing.ListingHandler; +import org.apache.causeway.applib.util.Listing.MergePolicy; +import org.apache.causeway.commons.collections.Can; +import org.apache.causeway.commons.io.TextUtils; + +class ListingTest { + + record Customer( + String id, + String name) { + Customer(final String id, final String name) { + assertTrue(StringUtils.hasLength(id)); + assertTrue(StringUtils.hasLength(name)); + this.id = id; + this.name = name; + } + String stringify() { + return String.format("[%s] %s", id, name); + } + static Customer destringify(final String input) { + int i0 = input.indexOf('['); + int i1 = input.indexOf(']'); + assertTrue(i0>-1); + assertTrue(i1>i0); + var cutter = TextUtils.cutter(input); + var id = cutter.keepAfter("[") + .keepBefore("]") + .getValue(); + var name = cutter.keepAfter("]") + .getValue() + .trim(); + return new Customer(id, name); + } + } + + private final ListingHandler listingHandler = + new ListingHandler<>(Customer.class, Customer::stringify, Customer::destringify, Customer::id); + private final Listing listing = + sampleListing(listingHandler); + + @Test + void parsing() { + var enabledCustomers = listing.streamEnabled().collect(Can.toCan()); + assertElementsMatch(Can.of( + new Customer("a", "Jeff"), + new Customer("b", "Jane")), + enabledCustomers); + + var disabledCustomers = listing.streamDisabled().collect(Can.toCan()); + assertElementsMatch(Can.of( + new Customer("c", "Henry")), + disabledCustomers); + } + + @Test + void writing() { + String expectedOutputAfterRoundtrip = """ + # this is a regular comment + [a] Jeff + #ERROR cannot parse ‹this is an invalid line› as Customer (org.opentest4j.AssertionFailedError: expected: but was: ) + #ERROR cannot parse ‹also an # invalid line› as Customer (org.opentest4j.AssertionFailedError: expected: but was: ) + #[c] Henry + # the follwing is a blank line + + [b] Jane + """; + //debug + //System.err.printf("%s%n", listing); + assertTextLinesMatch( + expectedOutputAfterRoundtrip, + listing.toString()); + } + + @Test + void merging() { + String upd = """ + # this is a listing of updates we want to apply + [a] Jeff + [c] Henry + [d] Martha + """; + var updateListing = listingHandler.parseListing(upd); + var merged = listing.merge(MergePolicy.ADD_NEW_AS_DISABLED, updateListing); + String expectedOutputAfterMerge = """ + # this is a regular comment + [a] Jeff + #ERROR cannot parse ‹this is an invalid line› as Customer (org.opentest4j.AssertionFailedError: expected: but was: ) + #ERROR cannot parse ‹also an # invalid line› as Customer (org.opentest4j.AssertionFailedError: expected: but was: ) + #[c] Henry + # the follwing is a blank line + + #REMOVED [b] Jane + + #MERGED + #[d] Martha"""; + //debug + //System.err.printf("%s%n", merged); + assertTextLinesMatch( + expectedOutputAfterMerge, + merged.toString()); + } + + // -- HELPER + + private Listing sampleListing(final ListingHandler listingHandler) { + String input = """ + # this is a regular comment + [a] Jeff + this is an invalid line + also an # invalid line + # ## [c] Henry + # the follwing is a blank line + + [b] Jane + """; + return listingHandler.parseListing(input); + } + + private static void assertTextLinesMatch(final String a, final String b) { + assertLinesMatch( + TextUtils.readLines(a).toList(), + TextUtils.readLines(b).toList()); + } + + private static void assertElementsMatch(final Can a, final Can b) { + assertTextLinesMatch( + a.join("\n"), + b.join("\n")); + } +} diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java index f867582cee4..85229a362d5 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java @@ -27,6 +27,7 @@ import org.jspecify.annotations.NonNull; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Primary; @@ -71,7 +72,6 @@ import org.apache.causeway.core.metamodel.services.layout.LayoutServiceDefault; import org.apache.causeway.core.metamodel.services.metamodel.MetaModelServiceDefault; import org.apache.causeway.core.metamodel.services.registry.ServiceRegistryDefault; -import org.apache.causeway.core.metamodel.services.tablecol.TableColumnOrderServiceDefault; import org.apache.causeway.core.metamodel.services.tablecol.TableColumnOrderServiceUsingTxtFile; import org.apache.causeway.core.metamodel.services.title.TitleServiceDefault; import org.apache.causeway.core.metamodel.spec.impl.CausewayModuleCoreMetamodelConfigurationDefault; @@ -194,8 +194,7 @@ MetaModelServiceDefault.class, ServiceInjectorDefault.class, ServiceRegistryDefault.class, - TableColumnOrderServiceDefault.class, - TableColumnOrderServiceUsingTxtFile.class, + // TableColumnOrderServiceUsingTxtFile.class, NOT here ... use @ComponentScan instead, see below TitleServiceDefault.class, // @Repository's @@ -210,6 +209,8 @@ //last MetamodelInitializer.class }) +// in order for Spring to honor service precedence, services need to be autodetected +@ComponentScan(basePackageClasses = { TableColumnOrderServiceUsingTxtFile.class }) public class CausewayModuleCoreMetamodel { public static final String NAMESPACE = "causeway.metamodel"; diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/collections/layout/columnorder/ColumnOrderPatchingFacet.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/collections/layout/columnorder/ColumnOrderPatchingFacet.java new file mode 100644 index 00000000000..f300ba9ee80 --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/collections/layout/columnorder/ColumnOrderPatchingFacet.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.core.metamodel.facets.collections.layout.columnorder; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +import org.jspecify.annotations.Nullable; + +import org.apache.causeway.applib.Identifier; +import org.apache.causeway.commons.collections.Can; +import org.apache.causeway.core.metamodel.facetapi.Facet; +import org.apache.causeway.core.metamodel.facetapi.FacetHolder; + +/** + * Holder of a {@link Map} that is mutable during runtime and collects column order patching information. + * + *

To be installed on collection's element types. + * + * @apiNote Only used when PROTOTYPING. + * + * @since 4.0 + */ +public record ColumnOrderPatchingFacet( + FacetHolder facetHolder, + Map> columnOrder) implements Facet { + + public ColumnOrderPatchingFacet(final FacetHolder facetHolder) { + this(facetHolder, new ConcurrentHashMap<>()); + } + + @Override + public Class facetType() { + return ColumnOrderPatchingFacet.class; + } + + @Override + public Precedence precedence() { + return Precedence.DEFAULT; + } + + /** + * Clears the map entry. + * @param identifier if null, acts as a no-op + */ + public void clearColumnOrder(final @Nullable Identifier identifier) { + columnOrder.remove(identifier); + } + + /** + * @param identifier if null, acts as a no-op + * @param columnListing if null, clears the map entry + */ + public void putColumnOrder(final @Nullable Identifier identifier, @Nullable final Can columnListing) { + if(identifier==null) + return; + if(columnListing==null) { + clearColumnOrder(identifier); + return; + } + columnOrder.put(identifier, columnListing); + } + + public Optional> lookupColumnOrder(final @Nullable Identifier identifier) { + return identifier!=null + ? Optional.ofNullable(columnOrder.get(identifier)) + : Optional.empty(); + } + +} diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/appfeat/ApplicationFeatureRepositoryDefault.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/appfeat/ApplicationFeatureRepositoryDefault.java index d286c6f943a..0ef73c34f53 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/appfeat/ApplicationFeatureRepositoryDefault.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/appfeat/ApplicationFeatureRepositoryDefault.java @@ -33,9 +33,12 @@ import jakarta.inject.Inject; import jakarta.inject.Named; +import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; + import org.springframework.stereotype.Service; +import org.apache.causeway.applib.Identifier; import org.apache.causeway.applib.annotation.SemanticsOf; import org.apache.causeway.applib.events.metamodel.MetamodelListener; import org.apache.causeway.applib.id.LogicalType; @@ -45,7 +48,6 @@ import org.apache.causeway.applib.services.appfeat.ApplicationFeatureSort; import org.apache.causeway.applib.services.appfeat.ApplicationMemberSort; import org.apache.causeway.commons.internal.collections._Maps; -import org.apache.causeway.commons.internal.exceptions._Exceptions; import org.apache.causeway.core.config.CausewayConfiguration; import org.apache.causeway.core.config.metamodel.services.ApplicationFeaturesInitConfiguration; import org.apache.causeway.core.metamodel.CausewayModuleCoreMetamodel; @@ -62,7 +64,6 @@ import org.apache.causeway.core.metamodel.spec.feature.ObjectMember; import org.apache.causeway.core.metamodel.specloader.SpecificationLoader; -import org.jspecify.annotations.NonNull; import lombok.extern.slf4j.Slf4j; /** @@ -123,9 +124,8 @@ enum InitializationState { private InitializationState initializationState = InitializationState.NOT_INITIALIZED; private synchronized void initializeIfRequired() { - if(initializationState == InitializationState.INITIALIZED) { + if(initializationState == InitializationState.INITIALIZED) return; - } initializationState = InitializationState.INITIALIZED; for (var spec : specificationLoader.snapshotSpecifications()) { @@ -147,9 +147,8 @@ private void visitFeatureIdentifierByName( void createApplicationFeaturesFor(final ObjectSpecification spec) { - if (exclude(spec)) { + if (exclude(spec)) return; - } final List properties = spec.streamProperties(MixedIn.INCLUDED) .collect(Collectors.toList()); @@ -158,9 +157,8 @@ void createApplicationFeaturesFor(final ObjectSpecification spec) { final List actions = spec.streamAnyActions(MixedIn.INCLUDED) .collect(Collectors.toList()); - if (properties.isEmpty() && collections.isEmpty() && actions.isEmpty()) { + if (properties.isEmpty() && collections.isEmpty() && actions.isEmpty()) return; - } var logicalType = spec.logicalType(); var logicalTypeName = logicalType.logicalName(); @@ -212,8 +210,9 @@ private static Class correspondingClassFor(final ObjectSpecification objectSp private static Integer valueOf( final FacetHolder facetHolder, final Class cls) { - final SingleIntValueFacet facet = facetHolder.getFacet(cls); - return facet != null ? facet.value() : null; + return facetHolder.lookupFacet(cls) + .map(SingleIntValueFacet::value) + .orElse(null); } ApplicationFeatureId addClassParent(final ApplicationFeatureId classFeatureId) { @@ -226,9 +225,8 @@ ApplicationFeatureId addClassParent(final ApplicationFeatureId classFeatureId) { void addParents(final ApplicationFeatureId classOrPackageId) { final ApplicationFeatureId parentPackageId = classOrPackageId.getParentNamespaceFeatureId(); - if (parentPackageId == null) { + if (parentPackageId == null) return; - } final ApplicationFeatureDefault parentPackage = (ApplicationFeatureDefault)findPackageElseCreate(parentPackageId); @@ -288,9 +286,8 @@ private boolean newMember( final Integer maxLength, final Integer typicalLength, final SemanticsOf actionSemantics) { - if (objectMember.isAlwaysHidden()) { + if (objectMember.isAlwaysHidden()) return false; - } newMember(classFeatureId, objectMember.getId(), memberSort, returnType, derived, maxLength, typicalLength, actionSemantics); return true; } @@ -325,14 +322,11 @@ private void newMember( } private SortedMap featuresMapFor(final ApplicationMemberSort memberSort) { - switch (memberSort) { - case PROPERTY: - return propertyFeatures; - case COLLECTION: - return collectionFeatures; - default: // case ACTION: - return actionFeatures; - } + return switch (memberSort) { + case PROPERTY->propertyFeatures; + case COLLECTION->collectionFeatures; + default->actionFeatures; // case ACTION: + }; } protected boolean exclude(final ObjectSpecification spec) { @@ -379,15 +373,11 @@ public ApplicationFeature newApplicationFeature(final ApplicationFeatureId featI @Override public ApplicationFeature findFeature(final ApplicationFeatureId featureId) { initializeIfRequired(); - switch (featureId.getSort()) { - case NAMESPACE: - return findNamespace(featureId); - case TYPE: - return findLogicalType(featureId); - case MEMBER: - return findMember(featureId); - } - throw _Exceptions.illegalArgument("Feature of unknown sort '%s'", featureId.getSort()); + return switch (featureId.getSort()) { + case NAMESPACE -> findNamespace(featureId); + case TYPE -> findLogicalType(featureId); + case MEMBER -> findMember(featureId); + }; } public ApplicationFeature findNamespace(final ApplicationFeatureId featureId) { @@ -409,18 +399,13 @@ public ApplicationFeature findMember(final ApplicationFeatureId featureId) { public Collection allFeatures(final ApplicationFeatureSort featureType) { initializeIfRequired(); - if (featureType == null) { + if (featureType == null) return Collections.emptyList(); - } - switch (featureType) { - case NAMESPACE: - return allNamespaces(); - case TYPE: - return allTypes(); - case MEMBER: - return allMembers(); - } - throw new IllegalArgumentException("Unknown feature type " + featureType); + return switch (featureType) { + case NAMESPACE -> allNamespaces(); + case TYPE -> allTypes(); + case MEMBER -> allMembers(); + }; } @Override @@ -445,9 +430,8 @@ public Collection allMembers() { public SortedSet propertyIdsFor(final LogicalType logicalType) { initializeIfRequired(); ApplicationFeatureId typeFeatureId = typeFeatureIdByLogicalType.get(logicalType); - if (typeFeatureId == null) { + if (typeFeatureId == null) return Collections.emptySortedSet(); - } ApplicationFeature applicationFeature = typeFeatures.get(typeFeatureId); return applicationFeature.getProperties(); } @@ -476,4 +460,32 @@ public Map getFeatureIdentifiersByName() { return featureIdentifiersByName; } + @Override + public Optional asIdentifier(final @Nullable ApplicationFeatureId applicationFeatureId) { + return applicationFeatureId!=null + ? specificationLoader.specForLogicalTypeName(applicationFeatureId.getLogicalTypeName()) + .map(ObjectSpecification::logicalType) + .map(logicalType->toIdentifier(applicationFeatureId, logicalType)) + : Optional.empty(); + } + + // -- HELPER + + private Identifier toIdentifier(final ApplicationFeatureId featureId, final LogicalType logicalType) { + return switch (featureId.getSort()) { + case MEMBER -> Optional.ofNullable(findMember(featureId)) + .flatMap(ApplicationFeature::getMemberSort) + .map(memberSort->switch (memberSort) { + case PROPERTY -> Identifier.propertyIdentifier(logicalType, featureId.getLogicalMemberName()); + case COLLECTION -> Identifier.collectionIdentifier(logicalType, featureId.getLogicalMemberName()); + case ACTION -> Identifier.actionIdentifier(logicalType, featureId.getLogicalMemberName()); + default -> null; + }) + .orElse(null); + case TYPE -> Identifier.classIdentifier(logicalType); + case NAMESPACE -> null; + default -> null; + }; + } + } diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/columnorder/ColumnOrderTxtFileServiceDefault.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/columnorder/ColumnOrderTxtFileServiceDefault.java index a428dc4ef4f..f624bec9882 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/columnorder/ColumnOrderTxtFileServiceDefault.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/columnorder/ColumnOrderTxtFileServiceDefault.java @@ -19,7 +19,6 @@ package org.apache.causeway.core.metamodel.services.columnorder; import jakarta.annotation.Priority; -import jakarta.inject.Inject; import jakarta.inject.Named; import org.springframework.beans.factory.annotation.Qualifier; @@ -38,8 +37,6 @@ import org.apache.causeway.core.metamodel.spec.feature.OneToManyAssociation; import org.apache.causeway.core.metamodel.specloader.SpecificationLoader; -import lombok.RequiredArgsConstructor; - /** * Default implementation of {@link ColumnOrderTxtFileService}. * @@ -49,42 +46,39 @@ @Named(CausewayModuleCoreMetamodel.NAMESPACE + ".ColumnOrderTxtFileServiceDefault") @Priority(PriorityPrecedence.LATE) @Qualifier("Default") -@RequiredArgsConstructor(onConstructor_ = {@Inject}) -public class ColumnOrderTxtFileServiceDefault implements ColumnOrderTxtFileService { - - final SpecificationLoader specificationLoader; +public record ColumnOrderTxtFileServiceDefault( + SpecificationLoader specificationLoader) implements ColumnOrderTxtFileService { @Override @Programmatic public byte[] toZip(final Object domainObject) { + var objSpec = specificationLoader.loadSpecification(domainObject.getClass()); var zipBuilder = ZipUtils.zipEntryBuilder(); - addStandaloneEntry(domainObject, zipBuilder); - addCollectionEntries(domainObject, zipBuilder); + addStandaloneEntry(objSpec, zipBuilder); + addCollectionEntries(objSpec, zipBuilder); return zipBuilder.toBytes(); } - // HELPERS + // -- HELPER - private void addStandaloneEntry(final Object domainObject, final ZipUtils.EntryBuilder zipBuilder) { - var parentSpec = specificationLoader.loadSpecification(domainObject.getClass()); + private void addStandaloneEntry(final ObjectSpecification objSpec, final ZipUtils.EntryBuilder zipBuilder) { var buf = new StringBuilder(); - parentSpec.streamAssociations(MixedIn.INCLUDED) + objSpec.streamAssociations(MixedIn.INCLUDED) .map(ObjectFeature::getId) .forEach(assocId -> buf.append(assocId).append("\n")); var fileContents = buf.toString(); - var fileName = String.format("%s.columnOrder.txt", parentSpec.getShortIdentifier()); + var fileName = "%s.columnOrder.txt".formatted(objSpec.getShortIdentifier()); zipBuilder.addAsUtf8(fileName, fileContents); } - private void addCollectionEntries(final Object domainObject, final ZipUtils.EntryBuilder zipBuilder) { - var parentSpec = specificationLoader.loadSpecification(domainObject.getClass()); - parentSpec.streamCollections(MixedIn.INCLUDED) - .forEach(collection -> addCollection(collection, parentSpec, zipBuilder)); + private void addCollectionEntries(final ObjectSpecification objSpec, final ZipUtils.EntryBuilder zipBuilder) { + objSpec.streamCollections(MixedIn.INCLUDED) + .forEach(collection -> addCollection(collection, objSpec, zipBuilder)); } private void addCollection( @@ -94,6 +88,7 @@ private void addCollection( var buf = new StringBuilder(); + //TODO this does not account for any SPI from _MembersAsColumns - problem? collection.getElementType() .streamAssociations(MixedIn.INCLUDED) .filter(ObjectAssociation.Predicates.visibleAccordingToHiddenFacet(Where.PARENTED_TABLES)) @@ -101,7 +96,7 @@ private void addCollection( .map(ObjectFeature::getId) .forEach(assocId -> buf.append(assocId).append("\n")); - var fileName = String.format("%s#%s.columnOrder.txt", parentSpec.getShortIdentifier(), collection.getId()); + var fileName = "%s#%s.columnOrder.txt".formatted(parentSpec.getShortIdentifier(), collection.getId()); var fileContents = buf.toString(); zipBuilder.addAsUtf8(fileName, fileContents); diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/metamodel/MetaModelServiceDefault.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/metamodel/MetaModelServiceDefault.java index b2232bbd58f..bc8e8284742 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/metamodel/MetaModelServiceDefault.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/metamodel/MetaModelServiceDefault.java @@ -25,8 +25,8 @@ import java.util.TreeSet; import java.util.function.BiPredicate; import java.util.stream.Collectors; +import java.util.stream.Stream; -import jakarta.annotation.Priority; import jakarta.inject.Named; import jakarta.inject.Provider; @@ -36,7 +36,7 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; -import org.apache.causeway.applib.annotation.PriorityPrecedence; +import org.apache.causeway.applib.Identifier; import org.apache.causeway.applib.id.LogicalType; import org.apache.causeway.applib.services.appfeat.ApplicationFeatureId; import org.apache.causeway.applib.services.appfeat.ApplicationFeatureSort; @@ -54,11 +54,18 @@ import org.apache.causeway.commons.internal.collections._Lists; import org.apache.causeway.commons.internal.exceptions._Exceptions; import org.apache.causeway.core.metamodel.CausewayModuleCoreMetamodel; +import org.apache.causeway.core.metamodel.facetapi.FacetUtil; +import org.apache.causeway.core.metamodel.facets.collections.layout.columnorder.ColumnOrderPatchingFacet; import org.apache.causeway.core.metamodel.facets.members.publish.command.CommandPublishingFacet; +import org.apache.causeway.core.metamodel.object.ManagedObject; +import org.apache.causeway.core.metamodel.object.ManagedObjects; import org.apache.causeway.core.metamodel.services.metamodel.MetaModelAnnotator.ExporterConfig; +import org.apache.causeway.core.metamodel.spec.ActionScope; import org.apache.causeway.core.metamodel.spec.ObjectSpecification; import org.apache.causeway.core.metamodel.spec.feature.MixedIn; import org.apache.causeway.core.metamodel.spec.feature.ObjectAction; +import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociation; +import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociationContainer.ColumnQuery; import org.apache.causeway.core.metamodel.spec.feature.ObjectMember; import org.apache.causeway.core.metamodel.spec.feature.OneToManyAssociation; import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation; @@ -72,7 +79,6 @@ */ @Service @Named(CausewayModuleCoreMetamodel.NAMESPACE + ".MetaModelServiceDefault") -@Priority(PriorityPrecedence.MIDPOINT) @Qualifier("Default") public record MetaModelServiceDefault( Provider specificationLoaderProvider, @@ -172,23 +178,19 @@ protected boolean isBuiltIn(final ObjectSpecification spec) { @Override public BeanSort sortOf( final @Nullable Class domainType, final Mode mode) { - if(domainType == null) { - return null; - } + if(domainType == null) + return null; final ObjectSpecification objectSpec = specificationLoader().specForType(domainType).orElse(null); - if(objectSpec == null) { - return BeanSort.UNKNOWN; - } + if(objectSpec == null) + return BeanSort.UNKNOWN; if(objectSpec.getBeanSort().isUnknown() - && !(mode == Mode.RELAXED)) { - - throw new IllegalArgumentException(String.format( + && !(mode == Mode.RELAXED)) + throw new IllegalArgumentException(String.format( "Unable to determine what sort of domain object this is: '%s'. Originating domainType: '%s'", objectSpec.getFullIdentifier(), domainType.getName() )); - } return objectSpec.getBeanSort(); @@ -196,28 +198,24 @@ public BeanSort sortOf( @Override public BeanSort sortOf(final Bookmark bookmark, final Mode mode) { - if(bookmark == null) { - return null; - } - - final Class domainType; - switch (mode) { - case RELAXED: - domainType = specificationLoader().specForBookmark(bookmark) - .map(ObjectSpecification::getCorrespondingClass) - .orElse(null); - break; - - case STRICT: - // fall through to... - default: - domainType = specificationLoader().specForBookmark(bookmark) - .map(ObjectSpecification::getCorrespondingClass) - .orElseThrow(()->_Exceptions - .noSuchElement("Cannot resolve logical type name %s to a java class", - bookmark.logicalTypeName())); - break; - } + if(bookmark == null) + return null; + + final Class domainType = switch (mode) { + case RELAXED -> specificationLoader().specForBookmark(bookmark) + .map(ObjectSpecification::getCorrespondingClass) + .orElse(null); + case STRICT -> specificationLoader().specForBookmark(bookmark) + .map(ObjectSpecification::getCorrespondingClass) + .orElseThrow(()->_Exceptions + .noSuchElement("Cannot resolve logical type name %s to a java class", + bookmark.logicalTypeName())); + default -> specificationLoader().specForBookmark(bookmark) + .map(ObjectSpecification::getCorrespondingClass) + .orElseThrow(()->_Exceptions + .noSuchElement("Cannot resolve logical type name %s to a java class", + bookmark.logicalTypeName())); + }; return sortOf(domainType, mode); } @@ -227,22 +225,19 @@ public CommandDtoProcessor commandDtoProcessorFor(final String memberIdentifier) .newFeature(ApplicationFeatureSort.MEMBER, memberIdentifier); final String logicalTypeName = featureId.getLogicalTypeName(); - if(_Strings.isNullOrEmpty(logicalTypeName)) { - return null; - } + if(_Strings.isNullOrEmpty(logicalTypeName)) + return null; final ObjectSpecification spec = specificationLoader().specForLogicalTypeName(logicalTypeName).orElse(null); - if(spec == null) { - return null; - } + if(spec == null) + return null; final ObjectMember objectMemberIfAny = spec.getMember(featureId.getLogicalMemberName()).orElse(null); - if (objectMemberIfAny == null) { - return null; - } - final CommandPublishingFacet commandPublishingFacet = objectMemberIfAny.getFacet(CommandPublishingFacet.class); - if(commandPublishingFacet == null) { - return null; - } + if (objectMemberIfAny == null) + return null; + final CommandPublishingFacet commandPublishingFacet = objectMemberIfAny.lookupFacet(CommandPublishingFacet.class) + .orElse(null); + if(commandPublishingFacet == null) + return null; return commandPublishingFacet.getProcessor(); } @@ -274,4 +269,104 @@ public ObjectGraph exportObjectGraph(final @NonNull BiPredicate streamActions(@Nullable final Class domainType) { + return specificationLoader() + .specForType(domainType) + .stream() + .flatMap(spec->spec.streamActions(ActionScope.PRODUCTION_ONLY, MixedIn.INCLUDED)) + .map(ObjectAction::getFeatureIdentifier); + } + + @Override + public Stream streamProperties(@Nullable final Class domainType) { + return specificationLoader() + .specForType(domainType) + .stream() + .flatMap(spec->spec.streamProperties(MixedIn.INCLUDED)) + .map(OneToOneAssociation::getFeatureIdentifier); + } + + @Override + public Stream streamCollections(@Nullable final Class domainType) { + return specificationLoader() + .specForType(domainType) + .stream() + .flatMap(spec->spec.streamCollections(MixedIn.INCLUDED)) + .map(OneToManyAssociation::getFeatureIdentifier); + } + + @Override + public Stream streamTypeHierarchy(@Nullable final Class domainType) { + return specificationLoader() + .specForType(domainType) + .stream() + .flatMap(ObjectSpecification::streamTypeHierarchyAndInterfaces) + .filter(spec->!spec.getCorrespondingClass().equals(Object.class)) + .map(ObjectSpecification::getFeatureIdentifier); + } + + @Override + public Stream parentedAssociationsForColumnRendering( + final Object parentDomainObject, + final Identifier collectionId, + final AssociationsLookup columnQueryMode) { + + if(parentDomainObject==null + || collectionId==null) + return Stream.empty(); + + var parentMo = ManagedObject.adaptSingular(specificationLoader(), parentDomainObject); + if(ManagedObjects.isNullOrUnspecifiedOrEmpty(parentMo)) + return Stream.empty(); + + var elementType = parentMo.objSpec().getCollection(collectionId.memberLogicalName()) + .map(OneToManyAssociation::getElementType) + .orElse(null); + if(elementType==null) + return Stream.empty(); + + return elementType + .streamAssociationsForColumnRendering(new ColumnQuery(collectionId, parentMo, columnQueryMode)) + .map(ObjectAssociation::getFeatureIdentifier); + } + + @Override + public Stream standaloneAssociationsForColumnRendering( + final LogicalType logicalType, + final AssociationsLookup columnQueryMode) { + + var elementType = specificationLoader() + .specForLogicalType(logicalType) + .orElse(null); + + if(elementType == null) + return Stream.empty(); + + return elementType + .streamAssociationsForColumnRendering(ColumnQuery.forStandaloneTable(columnQueryMode)) + .map(ObjectAssociation::getFeatureIdentifier); + } + + @Override + public void patchColumnOrder(final Identifier identifier, final Can columnsInOrder) { + final ObjectSpecification elementType = switch (identifier.type()) { + case CLASS -> specificationLoader() + .specForLogicalType(identifier.logicalType()) + .orElseThrow(); + case COLLECTION -> specificationLoader() + .specForLogicalType(identifier.logicalType()) + .map(parentType->parentType.getCollectionElseFail(identifier.memberLogicalName())) + .map(OneToManyAssociation::getElementType) + .orElseThrow(); + case ACTION, ACTION_PARAMETER, PROPERTY -> + throw new UnsupportedOperationException("Unimplemented case: " + identifier.type()); + }; + + var columnOrderPatchingFacet = elementType.lookupFacet(ColumnOrderPatchingFacet.class) + .orElseGet(()->FacetUtil.addFacet(new ColumnOrderPatchingFacet(elementType))); + + columnOrderPatchingFacet.putColumnOrder(identifier, columnsInOrder); + } + } diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/tablecol/TableColumnOrderServiceDefault.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/tablecol/TableColumnOrderServiceDefault.java deleted file mode 100644 index c6acec9ad04..00000000000 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/tablecol/TableColumnOrderServiceDefault.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.causeway.core.metamodel.services.tablecol; - -import java.util.List; - -import jakarta.annotation.Priority; -import jakarta.inject.Named; - -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.stereotype.Service; - -import org.apache.causeway.applib.annotation.PriorityPrecedence; -import org.apache.causeway.applib.services.tablecol.TableColumnOrderService; -import org.apache.causeway.core.metamodel.CausewayModuleCoreMetamodel; - -/** - * The default implementation of {@link TableColumnOrderService}. - * Note though that this implementation has lower priority (later precedence) than - * {@link TableColumnOrderServiceUsingTxtFile}. - * - * @since 1.x {@index} - * - * @see TableColumnOrderServiceUsingTxtFile - */ -@Service -@Named(CausewayModuleCoreMetamodel.NAMESPACE + ".TableColumnOrderServiceDefault") -@Priority(PriorityPrecedence.LATE) -@Qualifier("Default") -public class TableColumnOrderServiceDefault implements TableColumnOrderService { - - /** - * Just returns the propertyIds unchanged. - * - * @param parent - * @param collectionId - * @param elementType - * @param associationIds - */ - @Override - public List orderParented( - final Object parent, - final String collectionId, - final Class elementType, - final List associationIds) { - return associationIds; - } - - /** - * Just returns the propertyIds unchanged. - * - * @param domainType - * @param associationIds - */ - @Override - public List orderStandalone( - final Class domainType, - final List associationIds) { - return associationIds; - } -} diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/tablecol/TableColumnOrderServiceUsingTxtFile.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/tablecol/TableColumnOrderServiceUsingTxtFile.java index 7ee868be64c..dc493a419b4 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/tablecol/TableColumnOrderServiceUsingTxtFile.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/tablecol/TableColumnOrderServiceUsingTxtFile.java @@ -26,8 +26,10 @@ import jakarta.annotation.Priority; import jakarta.inject.Named; -import org.springframework.beans.factory.annotation.Qualifier; +import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; + +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; import org.apache.causeway.applib.annotation.CollectionLayout; @@ -38,7 +40,6 @@ import org.apache.causeway.commons.io.TextUtils; import org.apache.causeway.core.metamodel.CausewayModuleCoreMetamodel; -import org.jspecify.annotations.NonNull; import lombok.extern.slf4j.Slf4j; /** @@ -46,8 +47,7 @@ * file containing the list of the associations (usually properties, but collections are also supported), in the * desired order, one associationId per line. * - *

- * The files are located relative to the class itself. A number of conventions are supported: + *

The files are located relative to the class itself. A number of conventions are supported: * *

    *
  • @@ -74,19 +74,15 @@ *
  • *
* - *

- * Any associations omitted from the file will not be shown as columns of the table. The associationId must also + *

Any associations omitted from the file will not be shown as columns of the table. The associationId must also * be an exact match, so can be ignored by commenting out, eg with "#". - *

* - *

- * Also note that association that have been explicitly hidden from tables using - * {@link PropertyLayout#hidden() @PropertyLayout#hidden} or {@link CollectionLayout#hidden()} are never shown, - * irrespective of whether they are listed in the files. You may therefore prefer to not hide properties - * with annotations, and then rely solely on these external columnOrder.txt files. This has the further - * benefit that files can be modified at runtime and will be automatically picked up without requiring a restart - * of the application. - *

+ *

Also note that association that have been explicitly hidden from tables using + * {@link PropertyLayout#hidden() @PropertyLayout#hidden} or {@link CollectionLayout#hidden()} are never shown, + * irrespective of whether they are listed in the files. You may therefore prefer to not hide properties + * with annotations, and then rely solely on these external columnOrder.txt files. This has the further + * benefit that files can be modified at runtime and will be automatically picked up without requiring a restart + * of the application. * * @see TableColumnOrderServiceDefault * @@ -94,7 +90,7 @@ */ @Service @Named(CausewayModuleCoreMetamodel.NAMESPACE + ".TableColumnOrderServiceUsingTxtFile") -@Priority(PriorityPrecedence.LATE - 100) // before Default +@Priority(PriorityPrecedence.LATE - 100) @Qualifier("UsingFiles") @Slf4j public class TableColumnOrderServiceUsingTxtFile implements TableColumnOrderService { @@ -102,8 +98,7 @@ public class TableColumnOrderServiceUsingTxtFile implements TableColumnOrderServ /** * Reads association Ids of the parented collection from a file. * - *

- * The search algorithm is: + *

The search algorithm is: *

    *
  • ParentClassName#collectionId.columnOrder.txt
  • *
  • ParentClassName#collectionId.columnOrder.fallback.txt
  • @@ -116,11 +111,8 @@ public class TableColumnOrderServiceUsingTxtFile implements TableColumnOrderServ *
  • ElementTypeClassName.columnOrder.txt
  • *
  • ElementTypeClassName.columnOrder.fallback.txt
  • *
- *

* - *

- * Additional files can be provided by overriding {@link #addResourceNames(Class, String, Class, List)} - *

+ *

Additional files can be provided by overriding {@link #addResourceNames(Class, String, Class, List)} * * @Returns {@code null}, if no matching resource was found */ @@ -135,9 +127,9 @@ public List orderParented( var domainClass = domainObject.getClass(); var resourceNames = buildResourceNames(domainClass, collectionId, elementType); addResourceNames(elementType, resourceNames); // fallback to reading the element type's own .txt file. - var contents = tryLoad(domainClass, resourceNames) + var content = tryLoad(domainClass, resourceNames) .orElse(null); - return contentsMatching(contents, associationIds); + return contentMatching(content, associationIds); } private List buildResourceNames( @@ -152,9 +144,7 @@ private List buildResourceNames( /** * Builds the list of file names to be read from. * - *

- * The default implementation provides only a single file name, ClassName#collectionId.columnOrder.txt. - *

+ *

The default implementation provides only a single file name, ClassName#collectionId.columnOrder.txt. * * @param domainClass - the class with the parent collection * @param collectionId - the id of the collection @@ -175,9 +165,8 @@ private static Optional tryLoad(final Class domainClass, final List tryLoad(final Class domainClass, final List - * The search algorithm is: + *

The search algorithm is: *

    *
  • DomainTypeClassName.columnOrder.txt
  • *
  • DomainTypeClassName.columnOrder.fallback.txt
  • *
- *

* - *

- * Additional files can be provided by overriding {@link #addResourceNames(Class, List)}. - *

+ *

Additional files can be provided by overriding {@link #addResourceNames(Class, List)}. * * @Returns {@code null}, if no matching resource was found */ @@ -212,9 +197,9 @@ public List orderStandalone( final Class domainType, final List associationIds) { var resourceNames = buildResourceNames(domainType); - var contents = tryLoad(domainType, resourceNames) + var content = tryLoad(domainType, resourceNames) .orElse(null); - return contentsMatching(contents, associationIds); + return contentMatching(content, associationIds); } private List buildResourceNames(final Class domainClass) { @@ -226,9 +211,7 @@ private List buildResourceNames(final Class domainClass) { /** * Builds the list of file names to be read from. * - *

- * The default implementation provides only a single file name, ClassName#collectionId.columnOrder.txt. - *

+ *

The default implementation provides only a single file name, ClassName#collectionId.columnOrder.txt. * * @param domainClass - the class in the standalone collection * @param addTo - to be added to @@ -241,15 +224,15 @@ protected void addResourceNames( } /** - * if contents is {@code null} returns {@code null} + * if content is {@code null} returns {@code null} */ @Nullable - private static List contentsMatching( - final @Nullable String contents, + private static List contentMatching( + final @Nullable String content, final @NonNull List associationIds) { - return contents==null + return content==null ? null - : TextUtils.readLines(contents).stream() + : TextUtils.readLines(content).stream() .map(String::trim) // ignore any leading or trailing whitespace .filter(line->!line.startsWith("#")) // speed up (not strictly required) .filter(associationIds::contains) diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/ObjectSpecificationRecord.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/ObjectSpecificationRecord.java index fd6e163ee81..aa0d162f85c 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/ObjectSpecificationRecord.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/ObjectSpecificationRecord.java @@ -30,7 +30,6 @@ import org.apache.causeway.core.metamodel.facetapi.FacetHolder; import org.apache.causeway.core.metamodel.facetapi.FeatureType; import org.apache.causeway.core.metamodel.facetapi.HasFacetHolder; -import org.apache.causeway.core.metamodel.object.ManagedObject; import org.apache.causeway.core.metamodel.spec.feature.MixedIn; import org.apache.causeway.core.metamodel.spec.feature.ObjectAction; import org.apache.causeway.core.metamodel.spec.feature.ObjectActionContainer; @@ -107,8 +106,8 @@ public record ObjectSpecificationRecord( @Override public Stream streamAssociations(final MixedIn mixedIn) { return associationContainer.streamAssociations(mixedIn); } - @Override public Stream streamAssociationsForColumnRendering(final Identifier memberIdentifier, final ManagedObject parentObject) { - return associationContainer.streamAssociationsForColumnRendering(memberIdentifier, parentObject); + @Override public Stream streamAssociationsForColumnRendering(final ColumnQuery columnQuery) { + return associationContainer.streamAssociationsForColumnRendering(columnQuery); } @Override public Stream streamDeclaredAssociations(final MixedIn mixedIn) { return associationContainer.streamDeclaredAssociations(mixedIn); diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/feature/ObjectAssociationContainer.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/feature/ObjectAssociationContainer.java index 2424377dfcf..e45c3391e9a 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/feature/ObjectAssociationContainer.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/feature/ObjectAssociationContainer.java @@ -21,8 +21,13 @@ import java.util.Optional; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; + import org.apache.causeway.applib.Identifier; +import org.apache.causeway.applib.annotation.Where; +import org.apache.causeway.applib.services.metamodel.MetaModelService.AssociationsLookup; import org.apache.causeway.commons.internal.exceptions._Exceptions; +import org.apache.causeway.core.metamodel.interactions.managed.ManagedMember; import org.apache.causeway.core.metamodel.object.ManagedObject; import org.apache.causeway.core.metamodel.spec.ObjectSpecificationException; @@ -130,13 +135,45 @@ default Stream streamCollections(final MixedIn mixedIn){ .map(OneToManyAssociation.class::cast); } + + /** + * Bundles parameters to query for Properties and Collections visible as columns. + * + * @param memberIdentifier not used for standalone tables + * @param parentObject not used for standalone tables and allowed to be empty for parented ones + * + * @since 4.0 + */ + record ColumnQuery( + @Nullable Identifier memberIdentifier, + @Nullable ManagedObject parentObject, + AssociationsLookup mode) { + public static ColumnQuery forStandaloneTable(final AssociationsLookup mode) { + return new ColumnQuery(null, null, mode); + } + public ColumnQuery(final ManagedMember managedMember, final AssociationsLookup enabled) { + this(managedMember.getIdentifier(), managedMember.getOwner(), enabled); + } + /** + * The collection variant (standalone or parented). + */ + public Where where() { + return isStandalone() + ? Where.STANDALONE_TABLES + : Where.PARENTED_TABLES; + } + public boolean isStandalone() { + return memberIdentifier==null + || memberIdentifier.type().isClass() + || memberIdentifier.type().isAction(); + } + } + /** * Properties and Collections visible as columns, honoring order and visibility. * @param parentObject not used for standalone tables and allowed to be empty for parented ones */ - Stream streamAssociationsForColumnRendering( - Identifier memberIdentifier, - ManagedObject parentObject); + Stream streamAssociationsForColumnRendering(ColumnQuery columnQuery); // -- ASSOCIATION STREAMS (INHERITANCE NOT CONSIDERED) diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectSpecificationDefault.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectSpecificationDefault.java index 635d7310d68..f751a94eb08 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectSpecificationDefault.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectSpecificationDefault.java @@ -132,6 +132,7 @@ final class ObjectSpecificationDefault private final FacetedMethodsBuilder facetedMethodsBuilder; private final ClassSubstitutorRegistry classSubstitutorRegistry; + private final _MembersAsColumns columnHelper; @Getter(onMethod_={@Override}) private final IntrospectionPolicy introspectionPolicy; @@ -167,11 +168,13 @@ public ObjectSpecificationDefault( // naturally supports attribute inheritance from the type's hierarchy this.introspectionPolicy = this.lookupFacet(IntrospectionPolicyFacet.class) - .map(introspectionPolicyFacet->introspectionPolicyFacet.getIntrospectionPolicy()) + .map(IntrospectionPolicyFacet::getIntrospectionPolicy) .orElseGet(()->mmc.getConfiguration().core().metaModel().introspector().policy()); this.facetedMethodsBuilder = new FacetedMethodsBuilder(this, facetProcessor, classSubstitutorRegistry); + + this.columnHelper = new _MembersAsColumns(mmc); } // -- CONTRACT @@ -250,13 +253,12 @@ private Stream createAssociations() { } private ObjectAssociation createAssociation(final FacetedMethod facetMethod) { - if (facetMethod.featureType().isCollection()) { - return OneToManyAssociationDefault.forMethod(facetMethod); - } else if (facetMethod.featureType().isProperty()) { - return OneToOneAssociationDefault.forMethod(facetMethod); - } else { - return null; - } + if (facetMethod.featureType().isCollection()) + return OneToManyAssociationDefault.forMethod(facetMethod); + else if (facetMethod.featureType().isProperty()) + return OneToOneAssociationDefault.forMethod(facetMethod); + else + return null; } private Stream createActions() { @@ -279,9 +281,8 @@ private ObjectAction createAction(final FacetedMethod facetedMethod) { return this.isMixin() ? ObjectActionDefault.forMixinMain(facetedMethod) : ObjectActionDefault.forMethod(facetedMethod); - } else { - return null; - } + } else + return null; } // -- getObjectAction @@ -352,7 +353,7 @@ private void catalogueActions(final BiConsumer onM private final _Lazy> elementSpecification = _Lazy.threadSafe(()->lookupFacet(TypeOfFacet.class) - .map(typeOfFacet -> typeOfFacet.elementSpec())); + .map(TypeOfFacet::elementSpec)); @Override public Optional explicitElementSpec() { @@ -361,19 +362,14 @@ public Optional explicitElementSpec() { // -- TABLE COLUMN RENDERING - @Override - public final Stream streamAssociationsForColumnRendering( - final Identifier memberIdentifier, - final ManagedObject parentObject) { - return new _MembersAsColumns(getMetaModelContext()) - .streamAssociationsForColumnRendering(this, memberIdentifier, parentObject); + @Override public Stream streamAssociationsForColumnRendering(final ColumnQuery columnQuery) { + return columnHelper.streamAssociationsForColumnRendering(this, columnQuery); } @Override public Stream streamActionsForColumnRendering( final Identifier memberIdentifier) { - return new _MembersAsColumns(getMetaModelContext()) - .streamActionsForColumnRendering(this, memberIdentifier); + return columnHelper.streamActionsForColumnRendering(this, memberIdentifier); } // -- DETERMINE INJECTABILITY @@ -505,7 +501,7 @@ public final String getFullIdentifier() { } @Override - public void introspect(IntrospectionRequest request) { + public void introspect(final IntrospectionRequest request) { switch (request) { case REGISTER -> introspectUpTo(IntrospectionState.NOT_INTROSPECTED, ()->"introspect(%s)".formatted(request)); @@ -542,8 +538,9 @@ enum IntrospectionState { /** * @param introspectionContextProvider keeps track of the causal chain of introspection requests */ - private void introspectUpTo(final IntrospectionState upTo, Supplier introspectionContextProvider) { - if(!isLessThan(upTo)) return; // optimization + private void introspectUpTo(final IntrospectionState upTo, final Supplier introspectionContextProvider) { + if(!isLessThan(upTo)) + return; // optimization if(log.isDebugEnabled()) { log.debug("introspectingUpTo: {}, {}", getFullIdentifier(), upTo); @@ -596,21 +593,21 @@ private boolean isLessThan(final IntrospectionState upTo) { protected void loadSpecOfSuperclass(final Class superclass) { if (superclass == null) - return; - + return; + this.superclassSpec = specLoaderInternal().loadSpecification(superclass); - if (superclassSpec != null + if (superclassSpec != null && log.isDebugEnabled()) { log.debug(" Superclass {}", superclass.getName()); } } - + protected void loadSpecOfInterfaces(final Class[] interfaces) { if(interfaces==null) - return; - + return; + var classCache = _ClassCache.getInstance(); - + final List interfaceSpecList = Stream.of(interfaces) // pre-filter common interfaces (performance) .filter(interfaceType->!interfaceType.getName().startsWith("java.")) @@ -628,23 +625,23 @@ protected void loadSpecOfInterfaces(final Class[] interfaces) { .map(specLoaderInternal()::loadSpecification) .filter(Objects::nonNull) .toList(); - + if(!interfaceSpecList.isEmpty()) { if(interfaceSpecList.size()>1) { ValidationFailure.raiseFormatted(facetHolder, - "Cannot use @DomainObject on more than one interface, as inherited by: %s", - getCorrespondingClass().getName()); + "Cannot use @DomainObject on more than one interface, as inherited by: %s", + getCorrespondingClass().getName()); } if (superclassSpec != null) { var superType = superclassSpec.getCorrespondingClass(); if(classCache.head(superType).hasAnnotation(DomainObject.class)) { ValidationFailure.raiseFormatted(facetHolder, - "Cannot use @DomainObject on both, abstract super class and one interface, as inherited by: %s", + "Cannot use @DomainObject on both, abstract super class and one interface, as inherited by: %s", getCorrespondingClass().getName()); } } - -//debug + +//debug // System.err.println("%s".formatted(getCorrespondingClass().getName())); // interfaceSpecList.forEach(i->{ // System.err.println("- %s".formatted(i.getCorrespondingClass().getName())); @@ -653,7 +650,7 @@ protected void loadSpecOfInterfaces(final Class[] interfaces) { this.interfaces.clear(); this.interfaces.addAll(interfaceSpecList); unmodifiableInterfaces.clear(); - } + } } } @@ -747,7 +744,8 @@ public String getTitle(final TitleRenderRequest titleRenderRequest) { private void notifySubscribersIfEntity( final TitleRenderRequest titleRenderRequest, final String titleString) { - if (!isEntity()) return; + if (!isEntity()) + return; var managedObject = titleRenderRequest.object(); managedObject.getBookmark().ifPresent(bookmark -> { @@ -779,7 +777,7 @@ public Can getAliases() { // -- ICON @Override - public Optional getIcon(final ManagedObject domainObject, ObjectSupport.IconSize iconSize) { + public Optional getIcon(final ManagedObject domainObject, final ObjectSupport.IconSize iconSize) { if(ManagedObjects.isSpecified(domainObject)) { _Assert.assertEquals(domainObject.objSpec(), this); } @@ -825,7 +823,7 @@ public boolean isOfTypeResolvePrimitive(final ObjectSpecification other) { @Override public String getSingularName() { return lookupFacet(ObjectNamedFacet.class) - .flatMap(textFacet->textFacet.translated()) + .flatMap(ObjectNamedFacet::translated) // unexpected code reach, however keep for JUnit testing .orElseGet(()->String.format( "(%s has neither title- nor object-named-facet)", @@ -896,12 +894,10 @@ private static class NotANoopFacetFilter implements Predicate getMember(final String memberId) { introspectUpTo(IntrospectionState.FULLY_INTROSPECTED, ()->"getMember %s of %s".formatted(memberId, this.getFeatureIdentifier())); - if(_Strings.isEmpty(memberId)) return Optional.empty(); + if(_Strings.isEmpty(memberId)) + return Optional.empty(); var objectAction = getAction(memberId); - if(objectAction.isPresent()) return objectAction; + if(objectAction.isPresent()) + return objectAction; var association = getAssociation(memberId); - if(association.isPresent()) return association; + if(association.isPresent()) + return association; return Optional.empty(); } @@ -967,7 +966,8 @@ public Optional getDeclaredAssociation(final String id, final introspectUpTo(IntrospectionState.FULLY_INTROSPECTED, ()->"getDeclaredAssociation %s of %s".formatted(id, this.getFeatureIdentifier())); - if(_Strings.isEmpty(id)) return Optional.empty(); + if(_Strings.isEmpty(id)) + return Optional.empty(); return streamDeclaredAssociations(mixedIn) .filter(objectAssociation->objectAssociation.getId().equals(id)) @@ -999,9 +999,8 @@ public Stream streamDeclaredActions( * Creates all mixed in properties and collections for this spec. */ private Stream createMixedInAssociations() { - if (isInjectable() || isValue()) { - return Stream.empty(); - } + if (isInjectable() || isValue()) + return Stream.empty(); return getCausewayBeanTypeRegistry().streamMixinTypes() .flatMap(this::createMixedInAssociation); } @@ -1010,17 +1009,14 @@ private Stream createMixedInAssociation(final Class mixinT var mixinSpec = specLoaderInternal().loadSpecification(mixinType, IntrospectionRequest.FULL); if (mixinSpec == null - || mixinSpec == this) { - return Stream.empty(); - } + || mixinSpec == this) + return Stream.empty(); var mixinFacet = mixinSpec.mixinFacet().orElse(null); - if(mixinFacet == null) { - // this shouldn't happen; to be covered by meta-model validation later - return Stream.empty(); - } - if(!mixinFacet.isMixinFor(getCorrespondingClass())) { + if(mixinFacet == null) + // this shouldn't happen; to be covered by meta-model validation later return Stream.empty(); - } + if(!mixinFacet.isMixinFor(getCorrespondingClass())) + return Stream.empty(); var mixinMethodName = mixinFacet.getMainMethodName(); return mixinSpec.streamActions(ActionScope.ANY, MixedIn.EXCLUDED) @@ -1044,22 +1040,18 @@ private Stream createMixedInAction(final Class mixinType var mixinSpec = specLoaderInternal().loadSpecification(mixinType, IntrospectionRequest.FULL); if (mixinSpec == null - || mixinSpec == this) { - return Stream.empty(); - } + || mixinSpec == this) + return Stream.empty(); var mixinFacet = mixinSpec.mixinFacet().orElse(null); - if(mixinFacet == null) { - // this shouldn't happen; to be covered by meta-model validation later - return Stream.empty(); - } - if(!mixinFacet.isMixinFor(getCorrespondingClass())) { + if(mixinFacet == null) + // this shouldn't happen; to be covered by meta-model validation later return Stream.empty(); - } + if(!mixinFacet.isMixinFor(getCorrespondingClass())) + return Stream.empty(); // don't mixin Object_ mixins to domain services if(getBeanSort().isManagedBeanContributing() - && mixinFacet.isMixinFor(java.lang.Object.class)) { - return Stream.empty(); - } + && mixinFacet.isMixinFor(java.lang.Object.class)) + return Stream.empty(); var mixinMethodName = mixinFacet.getMainMethodName(); @@ -1143,14 +1135,12 @@ private void createMixedInActionsAndResort() { || getBeanSort().isManagedBeanContributing() // in support of composite value-type constructor mixins || getBeanSort().isValue(); - if(!include) { - return; - } + if(!include) + return; var mixedInActions = createMixedInActions() .collect(Collectors.toList()); - if(mixedInActions.isEmpty()) { - return; // nothing to do (this spec has no mixed-in actions, regular actions have already been added) - } + if(mixedInActions.isEmpty()) + return; // nothing to do (this spec has no mixed-in actions, regular actions have already been added) var regularActions = _Lists.newArrayList(objectActions); // defensive copy @@ -1166,14 +1156,12 @@ private void createMixedInActionsAndResort() { * one-shot: must be no-op, if already created */ private void createMixedInAssociationsAndResort() { - if(!isEntityOrViewModelOrAbstract()) { - return; - } + if(!isEntityOrViewModelOrAbstract()) + return; var mixedInAssociations = createMixedInAssociations() .collect(Collectors.toList()); - if(mixedInAssociations.isEmpty()) { - return; // nothing to do (this spec has no mixed-in associations, regular associations have already been added) - } + if(mixedInAssociations.isEmpty()) + return; // nothing to do (this spec has no mixed-in associations, regular associations have already been added) var regularAssociations = _Lists.newArrayList(associations); // defensive copy diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/_MembersAsColumns.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/_MembersAsColumns.java index a4995f6acfd..916a6f1ee8d 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/_MembersAsColumns.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/_MembersAsColumns.java @@ -18,99 +18,113 @@ */ package org.apache.causeway.core.metamodel.spec.impl; +import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.stream.Stream; +import org.jspecify.annotations.NonNull; + import org.apache.causeway.applib.Identifier; import org.apache.causeway.applib.annotation.Where; import org.apache.causeway.applib.layout.component.PropertyLayoutData; import org.apache.causeway.applib.services.tablecol.TableColumnOrderService; import org.apache.causeway.applib.services.tablecol.TableColumnVisibilityService; -import org.apache.causeway.commons.internal.base._NullSafe; -import org.apache.causeway.commons.internal.collections._Lists; -import org.apache.causeway.commons.internal.collections._Maps; -import org.apache.causeway.core.metamodel.context.HasMetaModelContext; +import org.apache.causeway.commons.collections.Can; +import org.apache.causeway.commons.internal.functions._Predicates; import org.apache.causeway.core.metamodel.context.MetaModelContext; +import org.apache.causeway.core.metamodel.facets.collections.layout.columnorder.ColumnOrderPatchingFacet; import org.apache.causeway.core.metamodel.facets.object.grid.GridFacet; -import org.apache.causeway.core.metamodel.object.ManagedObject; import org.apache.causeway.core.metamodel.spec.ObjectSpecification; import org.apache.causeway.core.metamodel.spec.feature.MixedIn; import org.apache.causeway.core.metamodel.spec.feature.ObjectAction; import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociation; -import org.apache.causeway.core.metamodel.util.WhereContexts; +import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociationContainer.ColumnQuery; -import static org.apache.causeway.applib.annotation.Where.PARENTED_TABLES; -import static org.apache.causeway.applib.annotation.Where.STANDALONE_TABLES; - -import lombok.Getter; -import org.jspecify.annotations.NonNull; -import lombok.RequiredArgsConstructor; +record _MembersAsColumns( + boolean isColumnOrderPatchingEnabled, + Can tableColumnVisibilityServices, + Can tableColumnOrderServices) { -@RequiredArgsConstructor -class _MembersAsColumns implements HasMetaModelContext { + _MembersAsColumns(final MetaModelContext mmc) { + this( + mmc.getSystemEnvironment().isPrototyping(), + mmc.getServiceRegistry().select(TableColumnVisibilityService.class), + mmc.getServiceRegistry().select(TableColumnOrderService.class)); + } - @Getter(onMethod_ = {@Override}) - private final MetaModelContext metaModelContext; - - public final Stream streamActionsForColumnRendering( + public Stream streamActionsForColumnRendering( final ObjectSpecification elementType, final Identifier memberIdentifier) { - if(elementType.isValue()) return Stream.empty(); + if(elementType.isValue()) + return Stream.empty(); return elementType.streamRuntimeActions(MixedIn.INCLUDED) - .filter(ObjectAction.Predicates.visibleAccordingToHiddenFacet(WhereContexts.collectionVariant(memberIdentifier))) + .filter(ObjectAction.Predicates.visibleAccordingToHiddenFacet(Where.STANDALONE_TABLES)) .sorted((a, b)->a.getCanonicalFriendlyName().compareTo(b.getCanonicalFriendlyName())); } /** * @param parentObject not used for standalone tables and allowed to be empty for parented ones */ - public final Stream streamAssociationsForColumnRendering( - final ObjectSpecification elementType, - final Identifier memberIdentifier, - final ManagedObject parentObject) { - - // the type that has the properties and collections that make up this table's columns - var elementClass = elementType.getCorrespondingClass(); + public Stream streamAssociationsForColumnRendering( + // the type that has the properties and collections that make up this table's columns + final ObjectSpecification elementType, + final ColumnQuery columnQuery) { - var parentSpecIfAny = parentObject.objSpec(); - - var assocById = _Maps.newLinkedHashMap(); - - elementType.streamAssociations(MixedIn.INCLUDED) - .filter(ObjectAssociation.Predicates.visibleAccordingToHiddenFacet(WhereContexts.collectionVariant(memberIdentifier))) - .filter(ObjectAssociation.Predicates.referencesParent(parentSpecIfAny).negate()) - .filter(assoc->filterColumnsUsingSpi(assoc, elementClass)) // optional SPI to filter columns; - .forEach(assoc->assocById.put(assoc.getId(), assoc)); - - var assocIdsInOrder = _Lists.newArrayList(assocById.keySet()); + var assocById = assembleAvailableColumns(elementType, columnQuery); + var assocIdsInOrder = new ArrayList<>(assocById.keySet()); // sort by order of occurrence within associated layout, if any propertyIdComparator(elementType) .ifPresent(assocIdsInOrder::sort); - // optional SPI to reorder columns - sortColumnsUsingSpi(memberIdentifier, parentObject, assocIdsInOrder, elementClass); + // when querying for AVAILABLE columns, we skip the column sorting SPI and also the column-patching (PROTOTYPING feature) + if(columnQuery.mode().isEnabled()) { + if(!sortColumnsUsingPatch(columnQuery, assocIdsInOrder, elementType)) { + // SPI to reorder columns, where TableColumnOrderServiceUsingTxtFile is a built-in one + // apply only, if not patched + sortColumnsUsingSpi(columnQuery, assocIdsInOrder, elementType); + } + } - // add all ordered columns to the table + // stream columns in final order return assocIdsInOrder.stream() .map(assocById::get) - .filter(_NullSafe::isPresent); + .filter(Objects::nonNull); } // -- HELPER - private boolean filterColumnsUsingSpi( + private Map assembleAvailableColumns( + final ObjectSpecification elementType, + final ColumnQuery columnQuery) { + + final var assocById = new LinkedHashMap(); + + elementType.streamAssociations(MixedIn.INCLUDED) + .filter(ObjectAssociation.Predicates.visibleAccordingToHiddenFacet(columnQuery.where())) + .filter(columnQuery.isStandalone() + ? _Predicates.alwaysTrue() + : ObjectAssociation.Predicates.referencesParent(columnQuery.parentObject().objSpec()).negate()) + .filter(assoc->hideColumnUsingSpi(assoc, elementType.getCorrespondingClass())) + .forEach(assoc->assocById.put(assoc.getId(), assoc)); + + return assocById; + } + + private boolean hideColumnUsingSpi( final ObjectAssociation assoc, final Class elementType) { - return getServiceRegistry() - .select(TableColumnVisibilityService.class) + return tableColumnVisibilityServices .stream() - .noneMatch(x -> x.hides(elementType, assoc.getId())); + .noneMatch(it -> it.hides(elementType, assoc.getId())); } // comparator based on grid facet, that is by order of occurrence within associated layout @@ -120,16 +134,18 @@ private Optional> propertyIdComparator( // same code also appears in DomainObjectPage. // we need to do this here otherwise any tables will render the columns in the wrong order until at least // one object of that type has been rendered via DomainObjectPage. - var elementTypeGridFacet = elementTypeSpec.getFacet(GridFacet.class); + var elementTypeGridFacet = elementTypeSpec.lookupFacet(GridFacet.class).orElse(null); - if(elementTypeGridFacet == null) return Optional.empty(); + if(elementTypeGridFacet == null) + return Optional.empty(); // the facet should always exist, in fact // just enough to ask for the metadata. // don't pass in any object, just need the meta-data var elementTypeGrid = elementTypeGridFacet.getGrid(null); - if(elementTypeGrid ==null) return Optional.empty(); + if(elementTypeGrid ==null) + return Optional.empty(); final Map propertyIdOrderWithinGrid = new HashMap<>(); elementTypeGrid.streamPropertyLayoutData() @@ -148,45 +164,67 @@ private Optional> propertyIdComparator( .thenComparing(Comparator.naturalOrder())); } - private void sortColumnsUsingSpi( - final Identifier memberIdentifier, - // not used for standalone tables, and allowed to be empty in parented ones - final ManagedObject parentObject, - final List propertyIdsInOrder, - final Class elementType) { - var tableColumnOrderServices = getServiceRegistry().select(TableColumnOrderService.class); - if(tableColumnOrderServices.isEmpty()) { - return; - } + /** + * @return whether a column-order patch was found and applied + */ + private boolean sortColumnsUsingPatch( + final ColumnQuery columnQuery, + final List assocIdsInOrder, //mutable + final ObjectSpecification elementType) { + + if(!isColumnOrderPatchingEnabled) + return false; + + var identifier = columnQuery.isStandalone() + ? elementType.getFeatureIdentifier() + : columnQuery.memberIdentifier(); + Objects.requireNonNull(identifier, ()->"framework bug"); + + var patchedColumnOrder = elementType + .lookupFacet(ColumnOrderPatchingFacet.class) + .flatMap(it->it.lookupColumnOrder(columnQuery.memberIdentifier())) + .orElse(null); + if(patchedColumnOrder==null) + return false; + + // intersect 'assocIdsInOrder' with 'patchedColumnOrder' while preserving order as given by the latter + var available = new HashSet<>(assocIdsInOrder); + assocIdsInOrder.clear(); + patchedColumnOrder.stream() + .filter(available::contains) + .forEach(assocIdsInOrder::add); + + return true; + } - var whereContext = whereContextFor(memberIdentifier); + private void sortColumnsUsingSpi( + final ColumnQuery columnQuery, + final List assocIdsInOrder, //mutable + final ObjectSpecification elementType) { - tableColumnOrderServices.stream() - .map(tableColumnOrderService-> - whereContext.inStandaloneTable() - ? tableColumnOrderService.orderStandalone( - elementType, - propertyIdsInOrder) - : tableColumnOrderService.orderParented( - parentObject.getPojo(), - memberIdentifier.memberLogicalName(), - elementType, - propertyIdsInOrder)) - .filter(_NullSafe::isPresent) - .findFirst() - .filter(propertyReorderedIds->propertyReorderedIds!=propertyIdsInOrder) // skip if its the same object - .ifPresent(propertyReorderedIds->{ - propertyIdsInOrder.clear(); - propertyIdsInOrder.addAll(propertyReorderedIds); - }); + if(tableColumnOrderServices.isEmpty()) + return; - } + tableColumnOrderServices.stream() + .map(tableColumnOrderService-> + columnQuery.isStandalone() + ? tableColumnOrderService.orderStandalone( + elementType.getCorrespondingClass(), + assocIdsInOrder) + : tableColumnOrderService.orderParented( + columnQuery.parentObject().getPojo(), + columnQuery.memberIdentifier().memberLogicalName(), + elementType.getCorrespondingClass(), + assocIdsInOrder)) + .filter(Objects::nonNull) + .findFirst() + .filter(assocReorderedIds->assocReorderedIds!=assocIdsInOrder) // skip if its the same object + .ifPresent(assocReorderedIds->{ + assocIdsInOrder.clear(); + assocIdsInOrder.addAll(assocReorderedIds); + }); - static Where whereContextFor(final Identifier memberIdentifier) { - return memberIdentifier.type().isAction() - ? STANDALONE_TABLES - : PARENTED_TABLES; } } diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/tabular/internal/DataTableInternal.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/tabular/internal/DataTableInternal.java index ac4e2564da0..71d88ddd937 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/tabular/internal/DataTableInternal.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/tabular/internal/DataTableInternal.java @@ -33,6 +33,7 @@ import org.apache.causeway.applib.annotation.TableDecorator; import org.apache.causeway.applib.annotation.Where; import org.apache.causeway.applib.services.filter.CollectionFilterService; +import org.apache.causeway.applib.services.metamodel.MetaModelService.AssociationsLookup; import org.apache.causeway.commons.binding.Bindable; import org.apache.causeway.commons.collections.Can; import org.apache.causeway.commons.functional.IndexedFunction; @@ -50,6 +51,7 @@ import org.apache.causeway.core.metamodel.object.ManagedObjects; import org.apache.causeway.core.metamodel.object.PackedManagedObject; import org.apache.causeway.core.metamodel.spec.ObjectSpecification; +import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociationContainer.ColumnQuery; import org.apache.causeway.core.metamodel.spec.feature.ObjectMember; import org.apache.causeway.core.metamodel.tabular.DataColumn; import org.apache.causeway.core.metamodel.tabular.DataRow; @@ -87,9 +89,8 @@ public static DataTableInternal forAction( if(actionResult==null) { new DataTableInternal(managedAction, managedAction.getWhere(), Can.empty()); } - if(!(actionResult instanceof PackedManagedObject)) { - throw _Exceptions.unexpectedCodeReach(); - } + if(!(actionResult instanceof PackedManagedObject)) + throw _Exceptions.unexpectedCodeReach(); var elements = ((PackedManagedObject)actionResult).unpack(); elements.forEach(ManagedObject::getBookmark); @@ -179,7 +180,7 @@ private DataTableInternal( this.dataColumnsObservable = _Observables.lazy(()-> managedMember.getElementType() - .streamAssociationsForColumnRendering(managedMember.getIdentifier(), managedMember.getOwner()) + .streamAssociationsForColumnRendering(new ColumnQuery(managedMember, AssociationsLookup.ENABLED)) .map(assoc->new DataColumnInternal(this, assoc)) .collect(Can.toCan())); @@ -359,18 +360,16 @@ public Set getSelectedRowIndexes() { public ActionInteraction startAssociatedActionInteraction(final String actionId, final Where where) { if(managedMember.getOwner().specialization().isEmpty() - || managedMember.getOwner().getEntityState().isTransientOrRemoved()) { - throw _Exceptions.illegalArgument("cannot start action interaction on missing or deleted action owner"); - } + || managedMember.getOwner().getEntityState().isTransientOrRemoved()) + throw _Exceptions.illegalArgument("cannot start action interaction on missing or deleted action owner"); var featureId = managedMember.getIdentifier(); - if(!featureId.type().isPropertyOrCollection()) { - return ActionInteraction.empty(String.format("[no such collection %s; instead got %s;" + if(!featureId.type().isPropertyOrCollection()) + return ActionInteraction.empty(String.format("[no such collection %s; instead got %s;" + "(while searching for an associated action %s)]", featureId, featureId.type(), actionId)); - } return ActionInteraction.startWithMultiselect(managedMember.getOwner(), actionId, where, this); } @@ -385,7 +384,7 @@ public DataTable export() { .map(DataColumn::associationMetaModel), dataRowsFilteredAndSortedObservable().getValue() .stream() - .map(dr->dr.rowElement()) + .map(DataRow::rowElement) .collect(Can.toCan())); } @@ -484,7 +483,7 @@ public void setupBindings(final DataTableInteractive tableInteractive) { this.selectedRowIndexes = tableInteractive.getSelectedRowIndexes(); }); } - + @Override public String toString() { return "Memento[featureId=%s,dataTable.rowCount=%d]".formatted( diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/WhereContexts.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/WhereContexts.java deleted file mode 100644 index 42b7fb7b96b..00000000000 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/WhereContexts.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.causeway.core.metamodel.util; - -import org.apache.causeway.applib.Identifier; -import org.apache.causeway.applib.annotation.Where; - -import lombok.experimental.UtilityClass; - -@UtilityClass -public class WhereContexts { - - /** - * Utility, that detects the collection variant (standalone or parented), - * based on the feature {@link Identifier} of the originating feature, - * that is, the feature is either a plural member or a plural action result. - */ - public Where collectionVariant(final Identifier featureId) { - var whereContext = featureId.type().isAction() - ? Where.STANDALONE_TABLES - : Where.PARENTED_TABLES; - return whereContext; - } - -} diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/valuesemantics/ApplicationFeatureIdValueSemantics.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/valuesemantics/ApplicationFeatureIdValueSemantics.java index f500c398fff..b49a7c4c865 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/valuesemantics/ApplicationFeatureIdValueSemantics.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/valuesemantics/ApplicationFeatureIdValueSemantics.java @@ -24,6 +24,7 @@ import org.springframework.stereotype.Component; import org.apache.causeway.applib.annotation.PriorityPrecedence; +import org.apache.causeway.applib.fa.FontAwesomeLayers; import org.apache.causeway.applib.services.appfeat.ApplicationFeatureId; import org.apache.causeway.applib.value.semantics.Parser; import org.apache.causeway.applib.value.semantics.Renderer; @@ -79,13 +80,13 @@ public String htmlPresentation(final ValueSemanticsProvider.Context context, fin // uses font-awesome private String iconify(final ApplicationFeatureId featureId) { - var fa = switch (featureId.getSort()) { - case NAMESPACE -> "fa-solid fa-sitemap"; - case MEMBER -> "fa-solid fa-circle-plus .col-indigo"; - case TYPE -> "fa-solid fa-circle .col-indigo"; + var faQuickNotation = switch (featureId.getSort()) { + case NAMESPACE -> "solid border-all .col-chocolate"; // resembling the package symbol from Eclipse IDE + case TYPE -> "solid copyright .col-green"; // resembling the class symbol from Eclipse IDE + case MEMBER -> "solid cube .col-gold"; // resembling member symbol as used in Eclipse Theia }; - - return faIconAndTitle(fa, toMonospace(featureId.stringify())); + var faLayers = FontAwesomeLayers.fromQuickNotation(faQuickNotation); + return faIconAndTitle(faLayers, toMonospace(featureId.stringify())); } // -- PARSER diff --git a/extensions/vw/pdfjs/metamodel/src/test/java/org/apache/causeway/extensions/pdfjs/metamodel/PdfjsViewer_MixinDomainWithPdfJsViewer_IntegTest.dump_facets.approved.xml b/extensions/vw/pdfjs/metamodel/src/test/java/org/apache/causeway/extensions/pdfjs/metamodel/PdfjsViewer_MixinDomainWithPdfJsViewer_IntegTest.dump_facets.approved.xml index 1b5947078a3..84cb83bd6b1 100644 --- a/extensions/vw/pdfjs/metamodel/src/test/java/org/apache/causeway/extensions/pdfjs/metamodel/PdfjsViewer_MixinDomainWithPdfJsViewer_IntegTest.dump_facets.approved.xml +++ b/extensions/vw/pdfjs/metamodel/src/test/java/org/apache/causeway/extensions/pdfjs/metamodel/PdfjsViewer_MixinDomainWithPdfJsViewer_IntegTest.dump_facets.approved.xml @@ -1026,6 +1026,216 @@ org.apache.causeway.applib.value.LocalResourcePath + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.Object + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.causeway.applib.services.appfeat.ApplicationFeatureId + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.String + + + diff --git a/extensions/vw/pdfjs/metamodel/src/test/java/org/apache/causeway/extensions/pdfjs/metamodel/PdfjsViewer_MixinDomain_IntegTest.dump_facets.approved.xml b/extensions/vw/pdfjs/metamodel/src/test/java/org/apache/causeway/extensions/pdfjs/metamodel/PdfjsViewer_MixinDomain_IntegTest.dump_facets.approved.xml index d75e9bd529d..4e9f79c403c 100644 --- a/extensions/vw/pdfjs/metamodel/src/test/java/org/apache/causeway/extensions/pdfjs/metamodel/PdfjsViewer_MixinDomain_IntegTest.dump_facets.approved.xml +++ b/extensions/vw/pdfjs/metamodel/src/test/java/org/apache/causeway/extensions/pdfjs/metamodel/PdfjsViewer_MixinDomain_IntegTest.dump_facets.approved.xml @@ -1019,6 +1019,216 @@ org.apache.causeway.applib.value.LocalResourcePath + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.Object + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.causeway.applib.services.appfeat.ApplicationFeatureId + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.String + + + diff --git a/extensions/vw/pdfjs/metamodel/src/test/java/org/apache/causeway/extensions/pdfjs/metamodel/PdfjsViewer_PropDomainWithPdfjsViewer_IntegTest.dump_facets.approved.xml b/extensions/vw/pdfjs/metamodel/src/test/java/org/apache/causeway/extensions/pdfjs/metamodel/PdfjsViewer_PropDomainWithPdfjsViewer_IntegTest.dump_facets.approved.xml index 56829cdfd55..6516805d8bf 100644 --- a/extensions/vw/pdfjs/metamodel/src/test/java/org/apache/causeway/extensions/pdfjs/metamodel/PdfjsViewer_PropDomainWithPdfjsViewer_IntegTest.dump_facets.approved.xml +++ b/extensions/vw/pdfjs/metamodel/src/test/java/org/apache/causeway/extensions/pdfjs/metamodel/PdfjsViewer_PropDomainWithPdfjsViewer_IntegTest.dump_facets.approved.xml @@ -1015,6 +1015,216 @@ org.apache.causeway.applib.value.LocalResourcePath + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.Object + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.causeway.applib.services.appfeat.ApplicationFeatureId + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.String + + + diff --git a/extensions/vw/pdfjs/metamodel/src/test/java/org/apache/causeway/extensions/pdfjs/metamodel/PdfjsViewer_PropDomain_IntegTest.dump_facets.approved.xml b/extensions/vw/pdfjs/metamodel/src/test/java/org/apache/causeway/extensions/pdfjs/metamodel/PdfjsViewer_PropDomain_IntegTest.dump_facets.approved.xml index 00fdfd88e71..1b6236accf0 100644 --- a/extensions/vw/pdfjs/metamodel/src/test/java/org/apache/causeway/extensions/pdfjs/metamodel/PdfjsViewer_PropDomain_IntegTest.dump_facets.approved.xml +++ b/extensions/vw/pdfjs/metamodel/src/test/java/org/apache/causeway/extensions/pdfjs/metamodel/PdfjsViewer_PropDomain_IntegTest.dump_facets.approved.xml @@ -1008,6 +1008,216 @@ org.apache.causeway.applib.value.LocalResourcePath + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.Object + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.causeway.applib.services.appfeat.ApplicationFeatureId + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.String + + + diff --git a/regressiontests/domainmodel/src/test/java/org/apache/causeway/testdomain/domainmodel/MetaModelRegressionTest.verify.approved.xml b/regressiontests/domainmodel/src/test/java/org/apache/causeway/testdomain/domainmodel/MetaModelRegressionTest.verify.approved.xml index d7c43900f51..25814a79ff7 100644 --- a/regressiontests/domainmodel/src/test/java/org/apache/causeway/testdomain/domainmodel/MetaModelRegressionTest.verify.approved.xml +++ b/regressiontests/domainmodel/src/test/java/org/apache/causeway/testdomain/domainmodel/MetaModelRegressionTest.verify.approved.xml @@ -883,6 +883,182 @@ org.apache.causeway.applib.value.LocalResourcePath + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.Object + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.causeway.applib.services.appfeat.ApplicationFeatureId + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.String + + + @@ -1923,6 +2099,182 @@ org.apache.causeway.applib.value.LocalResourcePath + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.Object + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.causeway.applib.services.appfeat.ApplicationFeatureId + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.String + + + @@ -3208,6 +3560,182 @@ org.apache.causeway.applib.value.LocalResourcePath + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.Object + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.causeway.applib.services.appfeat.ApplicationFeatureId + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.String + + + @@ -4320,6 +4848,182 @@ org.apache.causeway.applib.value.LocalResourcePath + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.Object + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.causeway.applib.services.appfeat.ApplicationFeatureId + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.String + + + @@ -5420,6 +6124,182 @@ org.apache.causeway.applib.value.LocalResourcePath + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.Object + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.causeway.applib.services.appfeat.ApplicationFeatureId + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.String + + + @@ -6833,6 +7713,182 @@ org.apache.causeway.applib.value.LocalResourcePath + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.Object + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.causeway.applib.services.appfeat.ApplicationFeatureId + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.String + + + @@ -8384,6 +9440,182 @@ org.apache.causeway.applib.value.LocalResourcePath + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.Object + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.causeway.applib.services.appfeat.ApplicationFeatureId + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.String + + + @@ -10127,6 +11359,182 @@ org.apache.causeway.applib.value.LocalResourcePath + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.Object + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.causeway.applib.services.appfeat.ApplicationFeatureId + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.String + + + @@ -11353,6 +12761,182 @@ org.apache.causeway.applib.value.LocalResourcePath + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.Object + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.causeway.applib.services.appfeat.ApplicationFeatureId + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.String + + + @@ -12563,6 +14147,182 @@ org.apache.causeway.applib.value.LocalResourcePath + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.Object + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.causeway.applib.services.appfeat.ApplicationFeatureId + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.String + + + @@ -13795,6 +15555,182 @@ org.apache.causeway.applib.value.LocalResourcePath + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.Object + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.causeway.applib.services.appfeat.ApplicationFeatureId + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.String + + + @@ -15052,6 +16988,182 @@ org.apache.causeway.applib.value.LocalResourcePath + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.Object + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.causeway.applib.services.appfeat.ApplicationFeatureId + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.String + + + @@ -16372,6 +18484,182 @@ org.apache.causeway.applib.value.LocalResourcePath + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.Object + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.causeway.applib.services.appfeat.ApplicationFeatureId + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.String + + + @@ -17852,6 +20140,182 @@ org.apache.causeway.applib.value.LocalResourcePath + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.Object + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.causeway.applib.services.appfeat.ApplicationFeatureId + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.String + + + @@ -19261,6 +21725,182 @@ org.apache.causeway.applib.value.LocalResourcePath + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.Object + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.causeway.applib.services.appfeat.ApplicationFeatureId + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.String + + + @@ -20381,6 +23021,182 @@ org.apache.causeway.applib.value.LocalResourcePath + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.Object + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.causeway.applib.services.appfeat.ApplicationFeatureId + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.String + + + @@ -21501,6 +24317,182 @@ org.apache.causeway.applib.value.LocalResourcePath + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.Object + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.causeway.applib.services.appfeat.ApplicationFeatureId + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.String + + + @@ -25437,6 +28429,182 @@ org.apache.causeway.applib.value.LocalResourcePath + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.Object + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.causeway.applib.services.appfeat.ApplicationFeatureId + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.String + + + @@ -26688,6 +29856,182 @@ org.apache.causeway.applib.value.LocalResourcePath + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.Object + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.causeway.applib.services.appfeat.ApplicationFeatureId + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.String + + + @@ -28171,6 +31515,182 @@ org.apache.causeway.applib.value.LocalResourcePath + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.Object + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.causeway.applib.services.appfeat.ApplicationFeatureId + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.String + + + @@ -29658,6 +33178,182 @@ org.apache.causeway.applib.value.LocalResourcePath + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.Object + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.causeway.applib.services.appfeat.ApplicationFeatureId + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.String + + + @@ -34867,6 +38563,182 @@ org.apache.causeway.applib.value.LocalResourcePath + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.Object + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.causeway.applib.services.appfeat.ApplicationFeatureId + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.String + + + @@ -37608,6 +41480,182 @@ org.apache.causeway.applib.value.LocalResourcePath + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.Object + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.causeway.applib.services.appfeat.ApplicationFeatureId + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.String + + + @@ -38723,6 +42771,182 @@ org.apache.causeway.applib.value.LocalResourcePath + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.Object + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.causeway.applib.services.appfeat.ApplicationFeatureId + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.String + + + @@ -39927,6 +44151,182 @@ org.apache.causeway.applib.value.LocalResourcePath + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.Object + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.causeway.applib.services.appfeat.ApplicationFeatureId + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.String + + + @@ -41469,6 +45869,182 @@ org.apache.causeway.applib.value.LocalResourcePath + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.Object + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.causeway.applib.services.appfeat.ApplicationFeatureId + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.String + + + @@ -42933,6 +47509,182 @@ org.apache.causeway.applib.value.LocalResourcePath + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.Object + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.causeway.applib.services.appfeat.ApplicationFeatureId + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.String + + + @@ -44397,6 +49149,182 @@ org.apache.causeway.applib.value.LocalResourcePath + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.Object + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.causeway.applib.services.appfeat.ApplicationFeatureId + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.String + + + @@ -45907,6 +50835,182 @@ org.apache.causeway.applib.value.LocalResourcePath + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.Object + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.causeway.applib.services.appfeat.ApplicationFeatureId + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.String + + + @@ -47183,6 +52287,182 @@ org.apache.causeway.applib.value.LocalResourcePath + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.Object + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.causeway.applib.services.appfeat.ApplicationFeatureId + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang.String + + + diff --git a/viewers/commons/model/src/main/java/org/apache/causeway/viewer/commons/model/css/causeway-supplemental.css b/viewers/commons/model/src/main/java/org/apache/causeway/viewer/commons/model/css/causeway-supplemental.css index 02724d0e45f..b2a0ad9a5b7 100644 --- a/viewers/commons/model/src/main/java/org/apache/causeway/viewer/commons/model/css/causeway-supplemental.css +++ b/viewers/commons/model/src/main/java/org/apache/causeway/viewer/commons/model/css/causeway-supplemental.css @@ -58,30 +58,153 @@ i.ov-bottom-85 { top: 0.85em; } -i.col-black { - color: black; -} -i.col-grey { - color: grey; -} -i.col-green { - color: green; -} -i.col-red { - color: red; -} -i.col-orange { - color: orange; -} -i.col-blue { - color: blue; -} -i.col-dodgerblue { - color: dodgerblue; -} -i.col-gold { - color: gold; -} -i.col-indigo { - color: indigo; -} +/* named colors */ + +i.col-aliceblue { color: aliceblue; } +i.col-antiquewhite { color: antiquewhite; } +i.col-aqua { color: aqua; } +i.col-aquamarine { color: aquamarine; } +i.col-azure { color: azure; } +i.col-beige { color: beige; } +i.col-bisque { color: bisque; } +i.col-black { color: black; } +i.col-blanchedalmond { color: blanchedalmond; } +i.col-blue { color: blue; } +i.col-blueviolet { color: blueviolet; } +i.col-brown { color: brown; } +i.col-burlywood { color: burlywood; } +i.col-cadetblue { color: cadetblue; } +i.col-chartreuse { color: chartreuse; } +i.col-chocolate { color: chocolate; } +i.col-coral { color: coral; } +i.col-cornflowerblue { color: cornflowerblue; } +i.col-cornsilk { color: cornsilk; } +i.col-crimson { color: crimson; } +i.col-cyan { color: cyan; } +i.col-darkblue { color: darkblue; } +i.col-darkcyan { color: darkcyan; } +i.col-darkgoldenrod { color: darkgoldenrod; } +i.col-darkgray { color: darkgray; } +i.col-darkgreen { color: darkgreen; } +i.col-darkgrey { color: darkgrey; } +i.col-darkkhaki { color: darkkhaki; } +i.col-darkmagenta { color: darkmagenta; } +i.col-darkolivegreen { color: darkolivegreen; } +i.col-darkorange { color: darkorange; } +i.col-darkorchid { color: darkorchid; } +i.col-darkred { color: darkred; } +i.col-darksalmon { color: darksalmon; } +i.col-darkseagreen { color: darkseagreen; } +i.col-darkslateblue { color: darkslateblue; } +i.col-darkslategray { color: darkslategray; } +i.col-darkslategrey { color: darkslategrey; } +i.col-darkturquoise { color: darkturquoise; } +i.col-darkviolet { color: darkviolet; } +i.col-deeppink { color: deeppink; } +i.col-deepskyblue { color: deepskyblue; } +i.col-dimgray { color: dimgray; } +i.col-dimgrey { color: dimgrey; } +i.col-dodgerblue { color: dodgerblue; } +i.col-firebrick { color: firebrick; } +i.col-floralwhite { color: floralwhite; } +i.col-forestgreen { color: forestgreen; } +i.col-fuchsia { color: fuchsia; } +i.col-gainsboro { color: gainsboro; } +i.col-ghostwhite { color: ghostwhite; } +i.col-gold { color: gold; } +i.col-goldenrod { color: goldenrod; } +i.col-gray { color: gray; } +i.col-green { color: green; } +i.col-greenyellow { color: greenyellow; } +i.col-grey { color: grey; } +i.col-honeydew { color: honeydew; } +i.col-hotpink { color: hotpink; } +i.col-indianred { color: indianred; } +i.col-indigo { color: indigo; } +i.col-ivory { color: ivory; } +i.col-khaki { color: khaki; } +i.col-lavender { color: lavender; } +i.col-lavenderblush { color: lavenderblush; } +i.col-lawngreen { color: lawngreen; } +i.col-lemonchiffon { color: lemonchiffon; } +i.col-lightblue { color: lightblue; } +i.col-lightcoral { color: lightcoral; } +i.col-lightcyan { color: lightcyan; } +i.col-lightgoldenrodyellow { color: lightgoldenrodyellow; } +i.col-lightgray { color: lightgray; } +i.col-lightgreen { color: lightgreen; } +i.col-lightgrey { color: lightgrey; } +i.col-lightpink { color: lightpink; } +i.col-lightsalmon { color: lightsalmon; } +i.col-lightseagreen { color: lightseagreen; } +i.col-lightskyblue { color: lightskyblue; } +i.col-lightslategray { color: lightslategray; } +i.col-lightslategrey { color: lightslategrey; } +i.col-lightsteelblue { color: lightsteelblue; } +i.col-lightyellow { color: lightyellow; } +i.col-lime { color: lime; } +i.col-limegreen { color: limegreen; } +i.col-linen { color: linen; } +i.col-magenta { color: magenta; } +i.col-maroon { color: maroon; } +i.col-mediumaquamarine { color: mediumaquamarine; } +i.col-mediumblue { color: mediumblue; } +i.col-mediumorchid { color: mediumorchid; } +i.col-mediumpurple { color: mediumpurple; } +i.col-mediumseagreen { color: mediumseagreen; } +i.col-mediumslateblue { color: mediumslateblue; } +i.col-mediumspringgreen { color: mediumspringgreen; } +i.col-mediumturquoise { color: mediumturquoise; } +i.col-mediumvioletred { color: mediumvioletred; } +i.col-midnightblue { color: midnightblue; } +i.col-mintcream { color: mintcream; } +i.col-mistyrose { color: mistyrose; } +i.col-moccasin { color: moccasin; } +i.col-navajowhite { color: navajowhite; } +i.col-navy { color: navy; } +i.col-oldlace { color: oldlace; } +i.col-olive { color: olive; } +i.col-olivedrab { color: olivedrab; } +i.col-orange { color: orange; } +i.col-orangered { color: orangered; } +i.col-orchid { color: orchid; } +i.col-palegoldenrod { color: palegoldenrod; } +i.col-palegreen { color: palegreen; } +i.col-paleturquoise { color: paleturquoise; } +i.col-palevioletred { color: palevioletred; } +i.col-papayawhip { color: papayawhip; } +i.col-peachpuff { color: peachpuff; } +i.col-peru { color: peru; } +i.col-pink { color: pink; } +i.col-plum { color: plum; } +i.col-powderblue { color: powderblue; } +i.col-purple { color: purple; } +i.col-rebeccapurple { color: rebeccapurple; } +i.col-red { color: red; } +i.col-rosybrown { color: rosybrown; } +i.col-royalblue { color: royalblue; } +i.col-saddlebrown { color: saddlebrown; } +i.col-salmon { color: salmon; } +i.col-sandybrown { color: sandybrown; } +i.col-seagreen { color: seagreen; } +i.col-seashell { color: seashell; } +i.col-sienna { color: sienna; } +i.col-silver { color: silver; } +i.col-skyblue { color: skyblue; } +i.col-slateblue { color: slateblue; } +i.col-slategray { color: slategray; } +i.col-slategrey { color: slategrey; } +i.col-snow { color: snow; } +i.col-springgreen { color: springgreen; } +i.col-steelblue { color: steelblue; } +i.col-tan { color: tan; } +i.col-teal { color: teal; } +i.col-thistle { color: thistle; } +i.col-tomato { color: tomato; } +i.col-turquoise { color: turquoise; } +i.col-violet { color: violet; } +i.col-wheat { color: wheat; } +i.col-white { color: white; } +i.col-whitesmoke { color: whitesmoke; } +i.col-yellow { color: yellow; } +i.col-yellowgreen { color: yellowgreen; } diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collection/present/ajaxtable/CollectionContentsAsAjaxTablePanel.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collection/present/ajaxtable/CollectionContentsAsAjaxTablePanel.java index 8c91965679a..3cebd649d92 100644 --- a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collection/present/ajaxtable/CollectionContentsAsAjaxTablePanel.java +++ b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collection/present/ajaxtable/CollectionContentsAsAjaxTablePanel.java @@ -29,10 +29,11 @@ import org.apache.wicket.model.Model; import org.apache.wicket.request.resource.CssResourceReference; +import org.apache.causeway.applib.services.metamodel.MetaModelService.AssociationsLookup; import org.apache.causeway.core.config.CausewayConfiguration.Viewer.Wicket; -import org.apache.causeway.core.metamodel.object.ManagedObject; import org.apache.causeway.core.metamodel.spec.ObjectSpecification; import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociation; +import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociationContainer.ColumnQuery; import org.apache.causeway.core.metamodel.spec.feature.OneToManyAssociation; import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation; import org.apache.causeway.core.metamodel.tabular.DataTableInteractive; @@ -193,13 +194,15 @@ private void addPropertyColumnsIfRequired( var collectionModel = getModel(); var elementType = collectionModel.getElementType(); - if(elementType == null) return; - - final ManagedObject parentObject = collectionModel.getParentObject(); - var memberIdentifier = collectionModel.getIdentifier(); + if(elementType == null) + return; // add all ordered columns to the table - elementType.streamAssociationsForColumnRendering(memberIdentifier, parentObject) + elementType + .streamAssociationsForColumnRendering(new ColumnQuery( + collectionModel.getIdentifier(), + collectionModel.getParentObject(), + AssociationsLookup.ENABLED)) .map(ObjectAssociation::getSpecialization) .map(spez->spez.fold( this::createSingularColumn,