Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@
package org.apache.causeway.commons.semantics;

import java.beans.Introspector;
import java.lang.annotation.Annotation;
import java.util.Map;
import java.util.function.Predicate;

import org.jspecify.annotations.Nullable;

import org.apache.causeway.commons.internal.base._Strings;
import org.apache.causeway.commons.internal.reflection._Annotations;
import org.apache.causeway.commons.internal.reflection._GenericResolver.ResolvedMethod;

import lombok.Getter;
Expand All @@ -38,10 +40,13 @@ public enum AccessorSemantics {
SET("set");
private final String prefix;

public static String associationIdentifierFor(final ResolvedMethod method) {
public static <A extends Annotation> String associationIdentifierFor(
final ResolvedMethod method,
final Class<A> requiredAnnotationType) {
return AccessorSemantics.isRecordComponentAccessor(method)
? method.name()
: Introspector.decapitalize(_Strings.baseName(method.name()));
|| isFluentGetter(method, requiredAnnotationType)
? method.name()
: Introspector.decapitalize(_Strings.baseName(method.name()));
}

public String prefix(final @Nullable String input) {
Expand All @@ -53,21 +58,28 @@ public String prefix(final @Nullable String input) {
public boolean isPrefixOf(final @Nullable String input) {
return input!=null
? input.startsWith(prefix)
&& input.length()>prefix.length()
: false;
}

// -- HIGH LEVEL PREDICATES

public static boolean isPropertyAccessor(final ResolvedMethod method) {
public static <A extends Annotation> boolean isPropertyAccessor(
final ResolvedMethod method,
final Class<A> requiredAnnotationType) {
return isRecordComponentAccessor(method)
|| isGetter(method)
|| isFluentGetter(method, requiredAnnotationType)
? !hasCollectionSemantics(method.returnType())
: false;
}

public static boolean isCollectionAccessor(final ResolvedMethod method) {
public static <A extends Annotation> boolean isCollectionAccessor(
final ResolvedMethod method,
final Class<A> requiredAnnotationType) {
return isRecordComponentAccessor(method)
|| isNonBooleanGetter(method)
|| isFluentGetter(method, requiredAnnotationType)
? hasCollectionSemantics(method.returnType())
: false;
}
Expand All @@ -83,6 +95,17 @@ public static boolean isRecordComponentAccessor(final ResolvedMethod method) {
return false;
}

public static <A extends Annotation> boolean isFluentGetter(
final ResolvedMethod accessorMethod,
final Class<A> requiredAnnotationType) {
{ // restricted to Java Records (but could be enabled for all classes)
var recordClass = accessorMethod.implementationClass();
if(!recordClass.isRecord()) return false;
}
return !isGetter(accessorMethod)
&& _Annotations.isPresent(accessorMethod.method(), requiredAnnotationType);
}

public static boolean isCandidateGetterName(final @Nullable String name) {
return GET.isPrefixOf(name)
|| IS.isPrefixOf(name);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
package org.apache.causeway.core.metamodel.facetapi;

import org.apache.causeway.applib.Identifier;
import org.apache.causeway.applib.annotation.Collection;
import org.apache.causeway.applib.annotation.Property;
import org.apache.causeway.applib.id.LogicalType;
import org.apache.causeway.commons.collections.ImmutableEnumSet;
import org.apache.causeway.commons.internal.reflection._MethodFacades.MethodFacade;
Expand Down Expand Up @@ -48,14 +50,14 @@ public Identifier identifierFor(final LogicalType typeIdentifier, final MethodFa
@Override
public Identifier identifierFor(final LogicalType typeIdentifier, final MethodFacade method) {
return Identifier.propertyIdentifier(typeIdentifier,
AccessorSemantics.associationIdentifierFor(method.asMethodElseFail())); // expected regular
AccessorSemantics.associationIdentifierFor(method.asMethodElseFail(), Property.class)); // expected regular
}
},
COLLECTION("Collection") {
@Override
public Identifier identifierFor(final LogicalType typeIdentifier, final MethodFacade method) {
return Identifier.collectionIdentifier(typeIdentifier,
AccessorSemantics.associationIdentifierFor(method.asMethodElseFail())); // expected regular
AccessorSemantics.associationIdentifierFor(method.asMethodElseFail(), Collection.class)); // expected regular
}
},
ACTION("Action") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import jakarta.inject.Inject;

import org.apache.causeway.applib.annotation.Collection;
import org.apache.causeway.commons.collections.Can;
import org.apache.causeway.commons.internal.reflection._GenericResolver.ResolvedMethod;
import org.apache.causeway.commons.semantics.AccessorSemantics;
Expand All @@ -42,12 +43,14 @@ public CollectionAccessorFacetViaAccessorFactory(final MetaModelContext mmc) {

@Override
public boolean isAssociationAccessor(final ResolvedMethod method) {
return AccessorSemantics.isCollectionAccessor(method);
return AccessorSemantics.isCollectionAccessor(method, Collection.class);
}

@Override
protected PropertyOrCollectionAccessorFacet createFacet(
final ObjectSpecification typeSpec, final ResolvedMethod accessorMethod, final FacetedMethod facetHolder) {
final ObjectSpecification typeSpec,
final ResolvedMethod accessorMethod,
final FacetedMethod facetHolder) {
return new CollectionAccessorFacetViaAccessor(typeSpec, accessorMethod, facetHolder);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@
*/
package org.apache.causeway.core.metamodel.facets.object.ignore.annotation;

import java.util.function.Consumer;
import java.util.function.Predicate;

import jakarta.inject.Inject;

import org.jspecify.annotations.NonNull;

import org.apache.causeway.commons.internal.functions._Predicates;
import org.apache.causeway.commons.internal.reflection._ClassCache.Attribute;
import org.apache.causeway.commons.internal.reflection._GenericResolver.ResolvedMethod;
Expand All @@ -31,8 +34,6 @@
import org.apache.causeway.core.metamodel.facets.FacetFactoryAbstract;
import org.apache.causeway.core.metamodel.spec.ObjectSpecification;

import org.jspecify.annotations.NonNull;

public class RemoveAnnotatedMethodsFacetFactory
extends FacetFactoryAbstract {

Expand All @@ -43,48 +44,56 @@ public RemoveAnnotatedMethodsFacetFactory(final MetaModelContext mmc) {

@Override
public void process(final ProcessClassContext processClassContext) {
switch (processClassContext.getIntrospectionPolicy()) {
case ENCAPSULATION_ENABLED -> getClassCache()
.streamResolvedMethods(processClassContext.getCls())
/* honor exclude markers (always) */
.filter(filterAndRemoveExclusions(processClassContext))
/* don't throw away mixin main methods,
* those we keep irrespective of IntrospectionPolicy */
.filter(_Predicates.not(isMixinMainMethod(processClassContext)))
.forEach(removeNonInclusions(processClassContext));
case ANNOTATION_REQUIRED -> getClassCache()
.streamPublicMethods(processClassContext.getCls())
/* honor exclude markers (always) */
.filter(filterAndRemoveExclusions(processClassContext))
/* don't throw away mixin main methods,
* those we keep irrespective of IntrospectionPolicy */
.filter(_Predicates.not(isMixinMainMethod(processClassContext)))
.forEach(removeNonInclusions(processClassContext));
case ANNOTATION_OPTIONAL -> getClassCache()
.streamPublicMethods(processClassContext.getCls())
.forEach(removeExclusions(processClassContext));
}
}

var policy = getMetaModelContext().getConfiguration().core().metaModel().introspector().policy();
switch (policy) {
case ENCAPSULATION_ENABLED:
getClassCache()
.streamResolvedMethods(processClassContext.getCls())
/* honor exclude markers (always) */
.filter(method->{
if(ProgrammingModelConstants.MethodExcludeMarker.anyMatchOn(method)) {
processClassContext.removeMethod(method);
return false; // stop processing
}
return true; // continue processing
})
/* don't throw away mixin main methods,
* those we keep irrespective of IntrospectionPolicy */
.filter(_Predicates.not(isMixinMainMethod(processClassContext)))
.forEach(method -> {
if (!ProgrammingModelConstants.MethodIncludeMarker.anyMatchOn(method)) {
processClassContext.removeMethod(method);
}
});
break;

case ANNOTATION_REQUIRED:
// TODO: this could probably be more precise and insist on @Domain.Include for members.

case ANNOTATION_OPTIONAL:
// -- HELPER

getClassCache()
.streamPublicMethods(processClassContext.getCls())
.forEach(method->{
if(ProgrammingModelConstants.MethodExcludeMarker.anyMatchOn(method)) {
processClassContext.removeMethod(method);
}
});
private Predicate<? super ResolvedMethod> filterAndRemoveExclusions(final ProcessClassContext processClassContext) {
return method->{
if(ProgrammingModelConstants.MethodExcludeMarker.anyMatchOn(method)) {
processClassContext.removeMethod(method);
return false; // stop processing
}
return true; // continue processing
};
}

break;
}
private Consumer<? super ResolvedMethod> removeExclusions(final ProcessClassContext processClassContext) {
return method->{
if(ProgrammingModelConstants.MethodExcludeMarker.anyMatchOn(method)) {
processClassContext.removeMethod(method);
}
};
}

// -- HELPER
private Consumer<? super ResolvedMethod> removeNonInclusions(final ProcessClassContext processClassContext) {
return method->{
if(!ProgrammingModelConstants.MethodIncludeMarker.anyMatchOn(method)) {
processClassContext.removeMethod(method);
}
};
}

/**
* We have no MixinFacet yet, so we need to revert to low level introspection tactics.
Expand All @@ -94,9 +103,8 @@ private Predicate<ResolvedMethod> isMixinMainMethod(final @NonNull ProcessClassC
// shortcut, when we already know the class is not a mixin
if(processClassContext.getFacetHolder() instanceof ObjectSpecification) {
var spec = (ObjectSpecification) processClassContext.getFacetHolder();
if(!spec.getBeanSort().isMixin()) {
if(!spec.getBeanSort().isMixin())
return method->false;
}
}
// lookup attribute from class-cache as it should have been already processed by the BeanTypeClassifier
var cls = processClassContext.getCls();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,17 @@
import java.lang.reflect.Constructor;
import java.lang.reflect.RecordComponent;
import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;

import org.jspecify.annotations.NonNull;

import org.apache.causeway.commons.collections.Can;
import org.apache.causeway.core.metamodel.consent.InteractionInitiatedBy;
import org.springframework.util.Assert;

import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
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.ObjectAssociation;
import org.apache.causeway.core.metamodel.util.hmac.Memento;
import org.apache.causeway.core.metamodel.util.hmac.MementoHmacContext;

Expand Down Expand Up @@ -75,14 +74,9 @@ protected Object createViewmodelPojo(
// throws on de-marshalling failure
var memento = mementoContext.parseTrustedMemento(trustedBookmarkIdAsBytes);

var recordComponentPojos = streamRecordComponents(viewmodelSpec)
.map(association->{
var associationPojo = association.isProperty()
? memento.get(association.getId(), association.getElementType().getCorrespondingClass())
//TODO collection values not yet supported by memento (as workaround use Serializable record)
: null;
return associationPojo;
}).toArray();
var recordComponentPojos = streamRecordComponents(viewmodelSpec.getCorrespondingClass())
.map(recComp->memento.get(recComp.getName(), recComp.getType()))
.toArray();

return canonicalConstructor.newInstance(recordComponentPojos);
}
Expand All @@ -92,50 +86,44 @@ public byte[] encodeState(final ManagedObject viewModel) {

final Memento memento = mementoContext.newMemento();

var viewmodelSpec = viewModel.objSpec();

streamRecordComponents(viewmodelSpec)
.forEach(association->{

final ManagedObject associationValue =
association.get(viewModel, InteractionInitiatedBy.PASS_THROUGH);

if(association != null
//TODO collection values not yet supported by memento (as workaround use Serializable record)
&& association.isProperty()
&& associationValue.getPojo()!=null) {
memento.put(association.getId(), associationValue.getPojo());
}
});
Arrays.stream(snapshotRecordComponents(viewModel.getPojo()))
.forEach(arg->memento.put(arg.name(), arg.pojo()));

return memento.stateAsBytes();
}

// -- HELPER

private Can<ObjectAssociation> recordComponentsAsAssociations;
private Stream<ObjectAssociation> streamRecordComponents(
final @NonNull ObjectSpecification viewmodelSpec) {
if(recordComponentsAsAssociations==null) {
this.recordComponentsAsAssociations = recordComponentsAsAssociations(viewmodelSpec);
}
return recordComponentsAsAssociations.stream();
}

private static Can<ObjectAssociation> recordComponentsAsAssociations(
final @NonNull ObjectSpecification viewmodelSpec) {
return Arrays.stream(viewmodelSpec.getCorrespondingClass().getRecordComponents())
.map(RecordComponent::getName)
.map(memberId->viewmodelSpec.getAssociationElseFail(memberId, MixedIn.EXCLUDED))
.collect(Can.toCan());
@SneakyThrows
private static Stream<RecordComponent> streamRecordComponents(final @NonNull Class<?> recordClass) {
Assert.isTrue(recordClass.isRecord(), ()->"Illegal Argument: not a Java record");
return Arrays.stream(recordClass.getRecordComponents());
}

@SneakyThrows
private static <T> Constructor<T> canonicalConstructor(final @NonNull Class<T> recordClass) {
var constructorParamTypes = Arrays.stream(recordClass.getRecordComponents())
var constructorParamTypes = streamRecordComponents(recordClass)
.map(RecordComponent::getType)
.toArray(Class[]::new);
return recordClass.getDeclaredConstructor(constructorParamTypes);
}

private record NamedArg(String name, Object pojo) {
}

@SneakyThrows
private static NamedArg[] snapshotRecordComponents(final Object recordInstance) {
final @NonNull Class<?> recordClass = Objects.requireNonNull(recordInstance).getClass();
Assert.isTrue(recordClass.isRecord(), ()->"Illegal Argument: not a Java record");

RecordComponent[] components = recordClass.getRecordComponents();
NamedArg[] result = new NamedArg[components.length];
for (int i = 0; i < components.length; i++) {
result[i] = new NamedArg(
components[i].getName(),
components[i].getAccessor().invoke(recordInstance));
}
return result;
}

}
Loading