This is an automated email from the ASF dual-hosted git repository. danhaywood pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/isis.git
commit 266a4e9964e5483b53456895641f49c1a580fcab Author: Andi Huber <ahu...@apache.org> AuthorDate: Fri Jan 12 16:11:31 2018 +0100 ISIS-1740 new Facet: NavigableParentFacet + major rework to integrate with Isis' meta-model --- .../services/navparent/NavigableParentService.java | 20 +++ .../isis/core/commons/reflection/Reflect.java | 174 +++++++++++++++++++++ .../isis/core/metamodel/facets/Annotations.java | 2 +- .../object/navparent/NavigableParentFacet.java | 44 ++++++ .../navparent/NavigableParentFacetAbstract.java | 36 +++++ .../NavigableParentAnnotationFacetFactory.java | 163 +++++++++++++++++++ .../method/NavigableParentFacetMethod.java | 68 ++++++++ .../method/NavigableParentFacetMethodFactory.java | 64 ++++++++ .../core/metamodel/spec/ObjectSpecification.java | 12 ++ .../specimpl/ObjectSpecificationAbstract.java | 12 ++ .../core/metamodel/util/pchain/ParentChain.java | 63 +++----- ...ingParentChain.java => ParentChainDefault.java} | 43 ++--- .../metamodel/util/pchain/SimpleParentChain.java | 75 --------- .../isis/core/metamodel/util/pchain/SoftCache.java | 124 --------------- .../dflt/ProgrammingModelFacetsJava5.java | 4 + .../NavigableParentFacetMethodFactoryTest.java | 70 +++++++++ .../navparent/NavigableParentFacetMethodTest.java | 83 ++++++++++ .../NavigableParentAnnotationFacetFactoryTest.java | 120 ++++++++++++++ .../annotation/NavigableParentTestSamples.java | 49 ++++++ .../testspec/ObjectSpecificationStub.java | 5 + .../model/models/whereami/WhereAmIModel.java | 2 +- .../models/whereami/WhereAmIModelDefault.java | 18 ++- 22 files changed, 968 insertions(+), 283 deletions(-) diff --git a/core/applib/src/main/java/org/apache/isis/applib/services/navparent/NavigableParentService.java b/core/applib/src/main/java/org/apache/isis/applib/services/navparent/NavigableParentService.java new file mode 100644 index 0000000..129122a --- /dev/null +++ b/core/applib/src/main/java/org/apache/isis/applib/services/navparent/NavigableParentService.java @@ -0,0 +1,20 @@ +package org.apache.isis.applib.services.navparent; + +import org.apache.isis.applib.annotation.Programmatic; + +/** + * + * @author ahu...@apache.org + * @since 2.0.0 + */ +public interface NavigableParentService { + + /** + * Return the navigable parent (a domain-object or a domain-view-model) of the object, + * used to build a navigable parent chain as required by the 'where-am-I' feature. + * + */ + @Programmatic + public Object navigableParentOf(Object domainObject); + +} diff --git a/core/metamodel/src/main/java/org/apache/isis/core/commons/reflection/Reflect.java b/core/metamodel/src/main/java/org/apache/isis/core/commons/reflection/Reflect.java new file mode 100644 index 0000000..59bc1c8 --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/isis/core/commons/reflection/Reflect.java @@ -0,0 +1,174 @@ +/* + * 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.isis.core.commons.reflection; + +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Stream; + +/** + * + * Provides shortcuts for common java.lang.reflect idioms. + * + * @author ahu...@apache.org + * @since 2.0.0 + * + */ +public class Reflect { + + public static Object[] emptyObjects = {}; + public static Class<?>[] emptyClasses = {}; + + // -- CLASS REFLECTION + + /** + * Returns declared methods of this class/interface and all super classes/interfaces. + * @param type + * @return + */ + public static List<Method> getAllDeclaredMethods(Class<?> type) { + final List<Method> methods = new ArrayList<>(); + + Stream.of(type.getDeclaredMethods()).forEach(methods::add); + visitInterfaces(type,c->Stream.of(c.getDeclaredMethods()).forEach(methods::add)); + visitSuperclassesOf(type,c->Stream.of(c.getDeclaredMethods()).forEach(methods::add)); + return methods; + } + + /** + * Returns declared fields of this class/interface and all super classes/interfaces. + * @param type + * @return + */ + public static List<Field> getAllDeclaredFields(Class<?> type) { + final List<Field> fields = new ArrayList<>(); + + Stream.of(type.getDeclaredFields()).forEach(fields::add); + visitInterfaces(type,c->Stream.of(c.getDeclaredFields()).forEach(fields::add)); + visitSuperclassesOf(type,c->Stream.of(c.getDeclaredFields()).forEach(fields::add)); + return fields; + } + + public static void visitSuperclassesOf(final Class<?> clazz, final Consumer<Class<?>> visitor){ + final Class<?> superclass = clazz.getSuperclass(); + if(superclass!=null){ + visitor.accept(superclass); + visitSuperclassesOf(superclass, visitor); + } + } + + public static void visitInterfaces(final Class<?> clazz, final Consumer<Class<?>> visitor){ + if(clazz.isInterface()) + visitor.accept(clazz); + + for(Class<?> interf : clazz.getInterfaces()) + visitor.accept(interf); + } + + public static Method getGetter(Class<?> cls, String propertyName) throws IntrospectionException { + final BeanInfo beanInfo = Introspector.getBeanInfo(cls); + for(PropertyDescriptor pd:beanInfo.getPropertyDescriptors()){ + if(!pd.getName().equals(propertyName)) + continue; + return pd.getReadMethod(); + } + return null; + } + + public static Method getGetter(Object bean, String propertyName) throws IntrospectionException { + if(bean==null) + return null; + return getGetter(bean, propertyName); + } + + + // -- PRIMITIVE TYPES + + private static final Set<Class<?>> primitives = new HashSet<>(Arrays.asList( + boolean.class, + byte.class, + char.class, + double.class, + float.class, + int.class, + long.class, + short.class + //void.class //separated out into its own predicate: isVoid(...) + )); + + private static final Set<Class<?>> primitiveWrappers = new HashSet<>(Arrays.asList( + Boolean.class, + Byte.class, + Character.class, + Double.class, + Float.class, + Integer.class, + Long.class, + Short.class + //Void.class //separated out into its own predicate: isVoid(...) + )); + + // -- TYPE PREDICATES + + public static boolean isVoid(Class<?> c) { + Objects.requireNonNull(c); + return c == void.class || c == Void.class; + } + + public static boolean isPrimitive(Class<?> c) { + Objects.requireNonNull(c); + return primitives.contains(c); + } + + public static boolean isPrimitiveWrapper(Class<?> c) { + Objects.requireNonNull(c); + return primitiveWrappers.contains(c); + } + + + // -- METHOD PREDICATES + + public static boolean isNoArg(Method m) { + Objects.requireNonNull(m); + return m.getParameterTypes().length==0; + } + + public static boolean isPublic(Method m) { + Objects.requireNonNull(m); + return Modifier.isPublic(m.getModifiers()); + } + + public static boolean isVoid(Method m) { + Objects.requireNonNull(m); + return isVoid(m.getReturnType()); + } + +} diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/Annotations.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/Annotations.java index b2aaeda..5a4a45e 100644 --- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/Annotations.java +++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/Annotations.java @@ -435,7 +435,7 @@ public final class Annotations { } } - static class FieldEvaluator<T extends Annotation> extends Evaluator<T> { + public static class FieldEvaluator<T extends Annotation> extends Evaluator<T> { private final Field field; FieldEvaluator(final Field field, final T annotation) { diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/navparent/NavigableParentFacet.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/navparent/NavigableParentFacet.java new file mode 100644 index 0000000..252eff3 --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/navparent/NavigableParentFacet.java @@ -0,0 +1,44 @@ +/* + * 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.isis.core.metamodel.facets.object.navparent; + +import org.apache.isis.core.metamodel.facetapi.Facet; + +/** + * + * Mechanism for obtaining the navigable parent (a domain-object or a domain-view-model) + * of an instance of a class, used to build a navigable parent chain as required by the + * 'where-am-I' feature. + * + * @author ahu...@apache.org + * @since 2.0.0 + * + */ +public interface NavigableParentFacet extends Facet { + + /** + * Returns the navigable parent (a domain-object or a domain-view-model) for the target object + * or null if there is no parent. + * @param object + * @return + */ + Object navigableParent(final Object object); + +} diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/navparent/NavigableParentFacetAbstract.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/navparent/NavigableParentFacetAbstract.java new file mode 100644 index 0000000..b3468ac --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/navparent/NavigableParentFacetAbstract.java @@ -0,0 +1,36 @@ +/* + * 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.isis.core.metamodel.facets.object.navparent; + +import org.apache.isis.core.metamodel.facetapi.Facet; +import org.apache.isis.core.metamodel.facetapi.FacetAbstract; +import org.apache.isis.core.metamodel.facetapi.FacetHolder; + +public abstract class NavigableParentFacetAbstract extends FacetAbstract implements NavigableParentFacet { + + public static Class<? extends Facet> type() { + return NavigableParentFacet.class; + } + + public NavigableParentFacetAbstract(final FacetHolder holder) { + super(type(), holder, Derivation.NOT_DERIVED); + } + +} diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/navparent/annotation/NavigableParentAnnotationFacetFactory.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/navparent/annotation/NavigableParentAnnotationFacetFactory.java new file mode 100644 index 0000000..f3b2102 --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/navparent/annotation/NavigableParentAnnotationFacetFactory.java @@ -0,0 +1,163 @@ +/* + * 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.isis.core.metamodel.facets.object.navparent.annotation; + +import java.beans.IntrospectionException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.List; + +import org.apache.isis.applib.annotation.Parent; +import org.apache.isis.core.commons.config.IsisConfiguration; +import org.apache.isis.core.commons.reflection.Reflect; +import org.apache.isis.core.metamodel.facetapi.FacetHolder; +import org.apache.isis.core.metamodel.facetapi.FacetUtil; +import org.apache.isis.core.metamodel.facetapi.FeatureType; +import org.apache.isis.core.metamodel.facetapi.MetaModelValidatorRefiner; +import org.apache.isis.core.metamodel.facets.Annotations; +import org.apache.isis.core.metamodel.facets.FacetFactoryAbstract; +import org.apache.isis.core.metamodel.facets.MethodFinderUtils; +import org.apache.isis.core.metamodel.facets.object.navparent.method.NavigableParentFacetMethod; +import org.apache.isis.core.metamodel.methodutils.MethodScope; +import org.apache.isis.core.metamodel.services.ServicesInjector; +import org.apache.isis.core.metamodel.services.persistsession.PersistenceSessionServiceInternal; +import org.apache.isis.core.metamodel.spec.ObjectSpecification; +import org.apache.isis.core.metamodel.specloader.validator.MetaModelValidatorComposite; +import org.apache.isis.core.metamodel.specloader.validator.MetaModelValidatorVisiting; +import org.apache.isis.core.metamodel.specloader.validator.ValidationFailures; + +/** + * + * @author ahu...@apache.org + * @since 2.0.0 + * + */ +public class NavigableParentAnnotationFacetFactory extends FacetFactoryAbstract implements MetaModelValidatorRefiner { + + private static final String NAVIGABLE_PARENT_METHOD_NAME = "parent"; + + + public NavigableParentAnnotationFacetFactory() { + super(FeatureType.OBJECTS_ONLY); + } + + @Override + public void process(final ProcessClassContext processClassContext) { + final Class<?> cls = processClassContext.getCls(); + final FacetHolder facetHolder = processClassContext.getFacetHolder(); + + final List<Annotations.Evaluator<Parent>> evaluators = Annotations.getEvaluators(cls, Parent.class); + if (evaluators.isEmpty()) { + return; + } else if (evaluators.size()>1) { + throw new RuntimeException("unable to determine navigable parent due to ambiguity"); + } + + final Annotations.Evaluator<Parent> parentEvaluator = evaluators.get(0); + + final Method method; + + // find method that provides the parent ... + if(parentEvaluator instanceof Annotations.MethodEvaluator) { + // we have a @Parent annotated method + method = ((Annotations.MethodEvaluator<Parent>) parentEvaluator).getMethod(); + } else if(parentEvaluator instanceof Annotations.FieldEvaluator) { + // we have a @Parent annotated field (occurs if one uses lombok's @Getter on a field) + final Field field = ((Annotations.FieldEvaluator<Parent>) parentEvaluator).getField(); + try { + method = Reflect.getGetter(cls, field.getName()); + } catch (IntrospectionException e) { + return; + } + } else { + return; + } + + try { + FacetUtil.addFacet(new NavigableParentFacetMethod(method, facetHolder)); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + + + /** + * Violation if there is a class that has both a <tt>parent()</tt> method and also + * any non-inherited method annotated with <tt>@Parent</tt>. + * <p> + * If there are only inherited methods annotated with <tt>@Parent</tt> then this is + * <i>not</i> a violation; but the imperative <tt>parent()</tt> method will take precedence. + * </p> + */ + @Override + public void refineMetaModelValidator(MetaModelValidatorComposite metaModelValidator, IsisConfiguration configuration) { + metaModelValidator.add(new MetaModelValidatorVisiting(new MetaModelValidatorVisiting.Visitor() { + + //TODO [ahuber] code is a copy of the TitleAnnotationFacetFactory, not sure ... + // 1) what the wanted behavior should be (what about annotations in interfaces, ambiguity, etc.) + // 2) what this code fragment does + + @Override + public boolean visit(ObjectSpecification objectSpec, ValidationFailures validationFailures) { + final Class<?> cls = objectSpec.getCorrespondingClass(); + + final Method parentMethod = + MethodFinderUtils.findMethod(cls, MethodScope.OBJECT, NAVIGABLE_PARENT_METHOD_NAME, Object.class, null); + if (parentMethod == null) { + return true; // no conflict + } + + // determine if cls contains a @Parent annotated method, not inherited from superclass + final Class<?> supClass = cls.getSuperclass(); + if (supClass == null) { + return true; // no conflict + } + + final List<Method> methods = methodsWithParentAnnotation(cls); + final List<Method> superClassMethods = methodsWithParentAnnotation(supClass); + if (methods.size() > superClassMethods.size()) { + validationFailures.add( + "%s: conflict for determining a strategy for retrieval of (navigable) parent for class, " + + "contains a method '%s' and an annotation '@%s'", + objectSpec.getIdentifier().getClassName(), + NAVIGABLE_PARENT_METHOD_NAME, + Parent.class.getName()); + } + + return true; + } + + private List<Method> methodsWithParentAnnotation(final Class<?> cls) { + return MethodFinderUtils.findMethodsWithAnnotation(cls, MethodScope.OBJECT, Parent.class); + } + + })); + } + + + @Override + public void setServicesInjector(final ServicesInjector servicesInjector) { + super.setServicesInjector(servicesInjector); + adapterManager = servicesInjector.getPersistenceSessionServiceInternal(); + } + + PersistenceSessionServiceInternal adapterManager; + +} diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/navparent/method/NavigableParentFacetMethod.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/navparent/method/NavigableParentFacetMethod.java new file mode 100644 index 0000000..167a048 --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/navparent/method/NavigableParentFacetMethod.java @@ -0,0 +1,68 @@ +/* + * 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.isis.core.metamodel.facets.object.navparent.method; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Method; + +import org.apache.isis.core.metamodel.facetapi.FacetHolder; +import org.apache.isis.core.metamodel.facets.object.navparent.NavigableParentFacetAbstract; + +/** + * + * @author ahu...@apache.org + * @since 2.0.0 + * + */ +public class NavigableParentFacetMethod extends NavigableParentFacetAbstract { + + private final MethodHandle methodHandle; + + public NavigableParentFacetMethod(final Method method, final FacetHolder holder) throws IllegalAccessException { + super(holder); + this.methodHandle = handleOf(method); + } + + @Override + public Object navigableParent(Object object) { + try { + return methodHandle.invoke(object); + } catch (final Throwable ex) { + return null; + } + } + + // -- HELPER + + private static MethodHandle handleOf(Method m) throws IllegalAccessException { + + if(!m.isAccessible()) { + m.setAccessible(true); + MethodHandle mh = MethodHandles.publicLookup().unreflect(m); + m.setAccessible(false); + return mh; + } + + return MethodHandles.publicLookup().unreflect(m); + + } + +} diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/navparent/method/NavigableParentFacetMethodFactory.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/navparent/method/NavigableParentFacetMethodFactory.java new file mode 100644 index 0000000..ad0db2a --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/navparent/method/NavigableParentFacetMethodFactory.java @@ -0,0 +1,64 @@ +/* + * 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.isis.core.metamodel.facets.object.navparent.method; + +import java.lang.reflect.Method; + +import org.apache.isis.core.metamodel.facetapi.FacetHolder; +import org.apache.isis.core.metamodel.facetapi.FacetUtil; +import org.apache.isis.core.metamodel.facetapi.FeatureType; +import org.apache.isis.core.metamodel.methodutils.MethodScope; +import org.apache.isis.core.metamodel.facets.MethodFinderUtils; +import org.apache.isis.core.metamodel.facets.MethodPrefixBasedFacetFactoryAbstract; + +/** + * + * @author ahu...@apache.org + * @since 2.0.0 + * + */ +public class NavigableParentFacetMethodFactory extends MethodPrefixBasedFacetFactoryAbstract { + + private static final String NAVIGABLE_PARENT_PREFIX = "parent"; + + private static final String[] PREFIXES = { NAVIGABLE_PARENT_PREFIX, }; + + public NavigableParentFacetMethodFactory() { + super(FeatureType.OBJECTS_ONLY, OrphanValidation.VALIDATE, PREFIXES); + } + + @Override + public void process(final ProcessClassContext processClassContext) { + final Class<?> cls = processClassContext.getCls(); + final FacetHolder facetHolder = processClassContext.getFacetHolder(); + + final Method method = + MethodFinderUtils.findMethod(cls, MethodScope.OBJECT, NAVIGABLE_PARENT_PREFIX, Object.class, NO_PARAMETERS_TYPES); + if (method == null) { + return; + } + processClassContext.removeMethod(method); + try { + FacetUtil.addFacet(new NavigableParentFacetMethod(method, facetHolder)); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } +} diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/spec/ObjectSpecification.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/spec/ObjectSpecification.java index 7d4e842..12833a5 100644 --- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/spec/ObjectSpecification.java +++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/spec/ObjectSpecification.java @@ -54,6 +54,8 @@ import org.apache.isis.core.metamodel.spec.feature.ObjectMember; import org.apache.isis.core.metamodel.specloader.SpecificationLoader; import org.apache.isis.core.metamodel.specloader.classsubstitutor.ClassSubstitutor; +import com.google.common.base.Function; + /** * Represents an entity or value (cf {@link java.lang.Class}) within the * metamodel. @@ -209,6 +211,14 @@ public interface ObjectSpecification extends Specification, ObjectActionContaine * returned by the {@link IconFacet}; is not necessarily immutable. */ String getIconName(ObjectAdapter object); + + /** + * Returns this object's navigable parent, if any. + * @param object + * @return + * @since 2.0.0 + */ + Object getNavigableParent(Object object); /** * @@ -373,4 +383,6 @@ public interface ObjectSpecification extends Specification, ObjectActionContaine boolean isPersistenceCapable(); boolean isPersistenceCapableOrViewModel(); + + } diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/specloader/specimpl/ObjectSpecificationAbstract.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/specloader/specimpl/ObjectSpecificationAbstract.java index d939016..ab0faa9 100644 --- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/specloader/specimpl/ObjectSpecificationAbstract.java +++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/specloader/specimpl/ObjectSpecificationAbstract.java @@ -24,6 +24,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Stream; import com.google.common.base.Function; import com.google.common.collect.Collections2; @@ -66,6 +67,7 @@ import org.apache.isis.core.metamodel.facets.object.icon.IconFacet; import org.apache.isis.core.metamodel.facets.object.immutable.ImmutableFacet; import org.apache.isis.core.metamodel.facets.object.membergroups.MemberGroupLayoutFacet; import org.apache.isis.core.metamodel.facets.object.mixin.MixinFacet; +import org.apache.isis.core.metamodel.facets.object.navparent.NavigableParentFacet; import org.apache.isis.core.metamodel.facets.object.objectspecid.ObjectSpecIdFacet; import org.apache.isis.core.metamodel.facets.object.parented.ParentedCollectionFacet; import org.apache.isis.core.metamodel.facets.object.parseable.ParseableFacet; @@ -91,6 +93,7 @@ import org.apache.isis.core.metamodel.spec.feature.OneToManyAssociation; import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation; import org.apache.isis.core.metamodel.specloader.SpecificationLoader; import org.apache.isis.core.metamodel.specloader.facetprocessor.FacetProcessor; +import org.apache.isis.core.metamodel.util.pchain.ParentChain; import org.apache.isis.objectstore.jdo.metamodel.facets.object.persistencecapable.JdoPersistenceCapableFacet; public abstract class ObjectSpecificationAbstract extends FacetHolderImpl implements ObjectSpecification { @@ -160,6 +163,7 @@ public abstract class ObjectSpecificationAbstract extends FacetHolderImpl implem private TitleFacet titleFacet; private IconFacet iconFacet; + private NavigableParentFacet navigableParentFacet; private CssClassFacet cssClassFacet; private IntrospectionState introspected = IntrospectionState.NOT_INTROSPECTED; @@ -346,6 +350,7 @@ public abstract class ObjectSpecificationAbstract extends FacetHolderImpl implem titleFacet = getFacet(TitleFacet.class); iconFacet = getFacet(IconFacet.class); + navigableParentFacet = getFacet(NavigableParentFacet.class); cssClassFacet = getFacet(CssClassFacet.class); } @@ -377,6 +382,13 @@ public abstract class ObjectSpecificationAbstract extends FacetHolderImpl implem public String getIconName(final ObjectAdapter reference) { return iconFacet == null ? null : iconFacet.iconName(reference); } + + @Override + public Object getNavigableParent(final Object object) { + return navigableParentFacet == null + ? null + : navigableParentFacet.navigableParent(object); + } @Deprecated @Override diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/util/pchain/ParentChain.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/util/pchain/ParentChain.java index 31ae507..8f9471c 100644 --- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/util/pchain/ParentChain.java +++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/util/pchain/ParentChain.java @@ -19,17 +19,15 @@ package org.apache.isis.core.metamodel.util.pchain; -import java.lang.reflect.Method; import java.util.LinkedHashSet; -import java.util.LinkedList; import java.util.Set; +import java.util.function.Function; import java.util.stream.Stream; -import org.apache.isis.applib.annotation.Parent; -import org.apache.isis.core.commons.reflection.Reflect; +import org.apache.isis.core.metamodel.spec.ObjectSpecification; /** - * Represents a unidirectional linked ordered set of Pojos (chain), where the chain + * Represents a unidirectionally linked ordered set of POJOs (chain), where the chain * starts at startNode. Each subsequent node is linked via de-referencing a * singular field (or no-arg method) that is annotated with {@code @Parent}. * <br/> @@ -41,40 +39,26 @@ import org.apache.isis.core.commons.reflection.Reflect; */ public interface ParentChain { - static ParentChain simple() { - return new SimpleParentChain(); - } - - static ParentChain caching() { - return new CachingParentChain(); + public static ParentChain of(Function<Class<?>, ObjectSpecification> specificationLookup){ + return new ParentChainDefault(specificationLookup); } + /** + * Returns the parent node of this {@code node} or {@code null} if {@code node} has no parent. + * @param node + * @return + */ public Object parentOf(Object node); - static boolean providesParent(Method m) { - if(!Reflect.isNoArg(m)) - return false; - if(!Reflect.isPublic(m)) - return false; - if(Reflect.isVoid(m)) - return false; - if(Reflect.isPrimitive(m.getReturnType())) - return false; - - if(m.getName().equals("parent")) - return true; - - if(m.isAnnotationPresent(Parent.class)) - return true; - - return false; - } - - default Stream<Object> streamParentChainOf(Object startNode){ + /** + * Returns a Stream of nodes that are chained together by parent references. + * The startNode is excluded from the Stream. + * @param startNode + * @return + */ + public default Stream<Object> streamParentChainOf(Object startNode){ final Set<Object> chain = new LinkedHashSet<>(); - chain.add(startNode); - Object next = startNode; while((next = parentOf(next))!=null) { @@ -83,17 +67,8 @@ public interface ParentChain { break; } - return chain.stream().skip(1); + return chain.stream(); } - - default Stream<Object> streamReversedParentChainOf(Object startNode){ - final LinkedList<Object> reverseChain = new LinkedList<Object>(); - - streamParentChainOf(startNode) - .forEach(reverseChain::addFirst); - - return reverseChain.stream(); - } - + } diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/util/pchain/CachingParentChain.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/util/pchain/ParentChainDefault.java similarity index 52% rename from core/metamodel/src/main/java/org/apache/isis/core/metamodel/util/pchain/CachingParentChain.java rename to core/metamodel/src/main/java/org/apache/isis/core/metamodel/util/pchain/ParentChainDefault.java index 630e928..2374634 100644 --- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/util/pchain/CachingParentChain.java +++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/util/pchain/ParentChainDefault.java @@ -19,48 +19,31 @@ package org.apache.isis.core.metamodel.util.pchain; -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.reflect.Method; +import java.util.function.Function; -class CachingParentChain extends SimpleParentChain { +import org.apache.isis.core.metamodel.spec.ObjectSpecification; - private final SoftCache<Class<?>, MethodHandle> cache = new SoftCache<>(); +class ParentChainDefault implements ParentChain { + + private final Function<Class<?>, ObjectSpecification> specificationLookup; + + ParentChainDefault(Function<Class<?>, ObjectSpecification> specificationLookup) { + this.specificationLookup = specificationLookup; + } @Override public Object parentOf(Object node) { if(node==null) return null; - final MethodHandle mh = cache.computeIfAbsent(node.getClass(), - key->{ - try { - return methodHandleOf(node); - } catch (IllegalAccessException e) { - e.printStackTrace(); - return null; - } - }); + final Class<?> cls = node.getClass(); - if(mh==null) - return null; + final ObjectSpecification spec = specificationLookup.apply(cls); - try { - return mh.invoke(node); - } catch (Throwable e) { - e.printStackTrace(); + if(spec==null) return null; - } - } - - protected static MethodHandle methodHandleOf(Object node) throws IllegalAccessException{ - final Method getter = parentGetterOf(node); - return getter!=null ? handleOf(getter) : null; - } - - public static MethodHandle handleOf(Method m) throws IllegalAccessException { - return MethodHandles.publicLookup().unreflect(m); + return spec.getNavigableParent(node); } } diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/util/pchain/SimpleParentChain.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/util/pchain/SimpleParentChain.java deleted file mode 100644 index b30e427..0000000 --- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/util/pchain/SimpleParentChain.java +++ /dev/null @@ -1,75 +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.isis.core.metamodel.util.pchain; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -import org.apache.isis.applib.annotation.Parent; -import org.apache.isis.core.commons.lang.NullSafe; -import org.apache.isis.core.commons.reflection.Reflect; - -class SimpleParentChain implements ParentChain { - - @Override - public Object parentOf(Object node) { - if(node==null) - return null; - - final Method getter = parentGetterOf(node); - if(getter==null) - return null; - - try { - return getter.invoke(node, Reflect.emptyObjects); - } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { - e.printStackTrace(); - } - - return null; - } - - protected static Method parentGetterOf(Object node) { - return - NullSafe.stream(Reflect.getAllDeclaredMethods(node.getClass())) - .filter(ParentChain::providesParent) - .findFirst() - .orElse(findGetterForAnnotatedField(node)); - } - - protected static Method findGetterForAnnotatedField(Object node) { - return - NullSafe.stream(Reflect.getAllDeclaredFields(node.getClass())) - .filter(f->f.isAnnotationPresent(Parent.class)) - .findFirst() - .map(f->getterOf(node, f.getName())) - .orElse(null); - } - - private static Method getterOf(Object bean, String propertyName) { - try { - return Reflect.getGetter(bean, propertyName); - } catch (Exception e) { - e.printStackTrace(); - return null; - } - } - -} diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/util/pchain/SoftCache.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/util/pchain/SoftCache.java deleted file mode 100644 index 34c1ff6..0000000 --- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/util/pchain/SoftCache.java +++ /dev/null @@ -1,124 +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.isis.core.metamodel.util.pchain; - -import java.lang.ref.SoftReference; -import java.util.HashMap; -import java.util.Map; -import java.util.function.Function; -import java.util.function.Supplier; - -/** - * Implements a caching {@code Map} where objects are stored referenced by - * a unique key, while {@codeMap.Entries} might be garbage collected any time. - * - * @author ahu...@apache.org - * - * @param <K> - * @param <T> - */ -class SoftCache<K,T> { - - private Map<K,SoftReference<T>> data; - - public SoftCache() { - data=newMap(); - } - - public SoftCache(Supplier<Map<K,SoftReference<T>>> mapFactory) { - data=mapFactory.get(); - } - - /** - * Note: might be overridden to use a different map implementation for storage - * @return - */ - protected Map<K,SoftReference<T>> newMap(){ - return new HashMap<>(); - } - - /** - * Note: call to this method will fool the garbage collector, - * so that last objects in the entry set will be kept longer, - * due to latest access - * @return number of currently usable SoftReferences - */ - public int computeSize(){ - Map<K,SoftReference<T>> keep = newMap(); - for(Map.Entry<K,SoftReference<T>> entry : data.entrySet()){ - if(entry.getValue()!=null) keep.put(entry.getKey(),entry.getValue()); - } - data.clear(); - data=keep; - return data.size(); - } - - // keep private! (result is not guaranteed to be accurate, - // since the garbage collector may change the soft references any time) - @SuppressWarnings("unused") - private boolean contains(K key){ - return get(key)!=null; - } - - public void put(K key, T x){ - data.put(key, new SoftReference<T>(x)); - } - - public T get(K key){ - SoftReference<T> ref = data.get(key); - if(ref==null) { - data.remove(key); - return null; - } - return ref.get(); - } - - public void clear() { - data.clear(); - } - - /** - * Tries to fetch a value from cache and returns it if it's a hit. - * Otherwise stores and returns the value supplied by the mappingFunction. - * @param key - * @param mappingFunction - * @return either the value stored under key or (if there is no such key) the result from the factory - */ - public T computeIfAbsent(K key, Function<? super K,? extends T> mappingFunction){ - return computeIfAbsent(key,()->mappingFunction.apply(key)); - } - - /** - * Tries to fetch a value from cache and returns it if it's a hit. - * Otherwise stores and returns the value supplied by the factory. - * @param key - * @param factory - * @return either the value stored under key or (if there is no such key) the result from the factory - */ - public T computeIfAbsent(K key, Supplier<T> factory) { - T res = get(key); - if(res!=null) - return res; - res = factory.get(); - put(key,res); - return res; - } - -} - diff --git a/core/metamodel/src/main/java/org/apache/isis/progmodels/dflt/ProgrammingModelFacetsJava5.java b/core/metamodel/src/main/java/org/apache/isis/progmodels/dflt/ProgrammingModelFacetsJava5.java index 96e54b9..293dafb 100644 --- a/core/metamodel/src/main/java/org/apache/isis/progmodels/dflt/ProgrammingModelFacetsJava5.java +++ b/core/metamodel/src/main/java/org/apache/isis/progmodels/dflt/ProgrammingModelFacetsJava5.java @@ -83,6 +83,8 @@ import org.apache.isis.core.metamodel.facets.object.ignore.jdo.RemoveJdoPrefixed import org.apache.isis.core.metamodel.facets.object.immutable.immutableannot.CopyImmutableFacetOntoMembersFactory; import org.apache.isis.core.metamodel.facets.object.membergroups.annotprop.MemberGroupLayoutFacetFactory; import org.apache.isis.core.metamodel.facets.object.mixin.MixinFacetForMixinAnnotationFactory; +import org.apache.isis.core.metamodel.facets.object.navparent.annotation.NavigableParentAnnotationFacetFactory; +import org.apache.isis.core.metamodel.facets.object.navparent.method.NavigableParentFacetMethodFactory; import org.apache.isis.core.metamodel.facets.object.objectspecid.classname.ObjectSpecIdFacetDerivedFromClassNameFactory; import org.apache.isis.core.metamodel.facets.object.objectvalidprops.impl.ObjectValidPropertiesFacetImplFactory; import org.apache.isis.core.metamodel.facets.object.parseable.annotcfg.ParseableFacetAnnotationElseConfigurationFactory; @@ -325,6 +327,8 @@ public final class ProgrammingModelFacetsJava5 extends ProgrammingModelAbstract addFactory(new TitleAnnotationFacetFactory()); addFactory(new TitleFacetViaMethodsFactory()); addFactory(new IconFacetMethodFactory()); + addFactory(new NavigableParentAnnotationFacetFactory()); + addFactory(new NavigableParentFacetMethodFactory()); addFactory(new CssClassFacetMethodFactory()); diff --git a/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/object/navparent/NavigableParentFacetMethodFactoryTest.java b/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/object/navparent/NavigableParentFacetMethodFactoryTest.java new file mode 100644 index 0000000..b962bbc --- /dev/null +++ b/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/object/navparent/NavigableParentFacetMethodFactoryTest.java @@ -0,0 +1,70 @@ +/* + * 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.isis.core.metamodel.facets.object.navparent; + +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertThat; + +import java.lang.reflect.Method; + +import org.apache.isis.core.metamodel.facetapi.Facet; +import org.apache.isis.core.metamodel.facets.AbstractFacetFactoryTest; +import org.apache.isis.core.metamodel.facets.FacetFactory.ProcessClassContext; +import org.apache.isis.core.metamodel.facets.object.navparent.method.NavigableParentFacetMethod; +import org.apache.isis.core.metamodel.facets.object.navparent.method.NavigableParentFacetMethodFactory; + +public class NavigableParentFacetMethodFactoryTest extends AbstractFacetFactoryTest { + + private NavigableParentFacetMethodFactory facetFactory; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + facetFactory = new NavigableParentFacetMethodFactory(); + } + + @Override + protected void tearDown() throws Exception { + facetFactory = null; + super.tearDown(); + } + + public void testNavigableParentMethodPickedUpOnClassAndMethodRemoved() { + class Customer { + @SuppressWarnings("unused") + public Object parent() { + return null; + } + } + final Method navigableParentMethod = findMethod(Customer.class, "parent"); + + facetFactory.process(new ProcessClassContext(Customer.class, methodRemover, facetedMethod)); + + final Facet facet = facetedMethod.getFacet(NavigableParentFacet.class); + assertThat(facet, is(notNullValue())); + assertThat(facet, is(instanceOf(NavigableParentFacetMethod.class))); + + assertTrue(methodRemover.getRemovedMethodMethodCalls().contains(navigableParentMethod)); + } + +} diff --git a/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/object/navparent/NavigableParentFacetMethodTest.java b/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/object/navparent/NavigableParentFacetMethodTest.java new file mode 100644 index 0000000..70a1dce --- /dev/null +++ b/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/object/navparent/NavigableParentFacetMethodTest.java @@ -0,0 +1,83 @@ +/* + * 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.isis.core.metamodel.facets.object.navparent; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.assertThat; + +import java.lang.reflect.Method; + +import org.apache.isis.core.metamodel.adapter.ObjectAdapter; +import org.apache.isis.core.metamodel.facetapi.FacetHolder; +import org.apache.isis.core.metamodel.facets.object.navparent.method.NavigableParentFacetMethod; +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.jmock.integration.junit4.JUnit4Mockery; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class NavigableParentFacetMethodTest { + + private final Mockery mockery = new JUnit4Mockery(); + + private NavigableParentFacetMethod facet; + private FacetHolder mockFacetHolder; + + private ObjectAdapter mockOwningAdapter; + + private DomainObjectWithProblemInNavigableParentMethod pojo; + + public static class DomainObjectWithProblemInNavigableParentMethod { + public String parent() { + throw new NullPointerException(); + } + } + + @Before + public void setUp() throws Exception { + + pojo = new DomainObjectWithProblemInNavigableParentMethod(); + mockFacetHolder = mockery.mock(FacetHolder.class); + mockOwningAdapter = mockery.mock(ObjectAdapter.class); + final Method navigableParentMethod = DomainObjectWithProblemInNavigableParentMethod.class.getMethod("parent"); + facet = new NavigableParentFacetMethod(navigableParentMethod, mockFacetHolder); + + mockery.checking(new Expectations() { + { + allowing(mockOwningAdapter).getObject(); + will(returnValue(pojo)); + } + }); + } + + @After + public void tearDown() throws Exception { + facet = null; + } + + @Test + public void testNavigableParentThrowsException() { + final Object parent = facet.navigableParent(mockOwningAdapter.getObject()); + assertThat(parent, is(nullValue())); + } + +} diff --git a/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/object/navparent/annotation/NavigableParentAnnotationFacetFactoryTest.java b/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/object/navparent/annotation/NavigableParentAnnotationFacetFactoryTest.java new file mode 100644 index 0000000..fff951c --- /dev/null +++ b/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/object/navparent/annotation/NavigableParentAnnotationFacetFactoryTest.java @@ -0,0 +1,120 @@ +/* + * 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.isis.core.metamodel.facets.object.navparent.annotation; + +import java.lang.reflect.Method; + +import org.apache.isis.core.commons.authentication.AuthenticationSession; +import org.apache.isis.core.commons.authentication.AuthenticationSessionProvider; +import org.apache.isis.core.commons.reflection.Reflect; +import org.apache.isis.core.metamodel.adapter.ObjectAdapter; +import org.apache.isis.core.metamodel.deployment.DeploymentCategory; +import org.apache.isis.core.metamodel.deployment.DeploymentCategoryProvider; +import org.apache.isis.core.metamodel.facetapi.Facet; +import org.apache.isis.core.metamodel.facets.AbstractFacetFactoryJUnit4TestCase; +import org.apache.isis.core.metamodel.facets.FacetFactory.ProcessClassContext; +import org.apache.isis.core.metamodel.facets.object.navparent.NavigableParentFacet; +import org.apache.isis.core.metamodel.facets.object.navparent.annotation.NavigableParentTestSamples.DomainObjectA; +import org.apache.isis.core.metamodel.facets.object.navparent.method.NavigableParentFacetMethod; +import org.jmock.Expectations; +import org.jmock.auto.Mock; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class NavigableParentAnnotationFacetFactoryTest extends AbstractFacetFactoryJUnit4TestCase { + + private NavigableParentAnnotationFacetFactory facetFactory; + + @Mock + private ObjectAdapter mockObjectAdapter; + @Mock + private AuthenticationSession mockAuthenticationSession; + + @Before + public void setUp() throws Exception { + + context.allowing(mockSpecificationLoader); + + facetFactory = new NavigableParentAnnotationFacetFactory(); + facetFactory.setServicesInjector(mockServicesInjector); + + context.checking(new Expectations() { + { + allowing(mockServicesInjector).lookupService(AuthenticationSessionProvider.class); + will(returnValue(mockAuthenticationSessionProvider)); + + allowing(mockServicesInjector).lookupService(DeploymentCategoryProvider.class); + will(returnValue(mockDeploymentCategoryProvider)); + + allowing(mockDeploymentCategoryProvider).getDeploymentCategory(); + will(returnValue(DeploymentCategory.PRODUCTION)); + + allowing(mockAuthenticationSessionProvider).getAuthenticationSession(); + will(returnValue(mockAuthenticationSession)); + + allowing(mockServicesInjector).getSpecificationLoader(); + will(returnValue(mockSpecificationLoader)); + + allowing(mockServicesInjector).getPersistenceSessionServiceInternal(); + will(returnValue(mockPersistenceSessionServiceInternal)); + } + }); + + facetFactory.setServicesInjector(mockServicesInjector); + + } + + @After + @Override + public void tearDown() throws Exception { + facetFactory = null; + super.tearDown(); + } + + @Test + public void testParentAnnotatedMethod() throws Exception { + testParentMethod(new DomainObjectA(), "root"); + } + + // -- HELPER + + private void testParentMethod(Object domainObject, String parentMethodName) throws Exception { + + final Class<?> domainClass = domainObject.getClass(); + + facetFactory.process(new ProcessClassContext(domainClass, mockMethodRemover, facetedMethod)); + + final Facet facet = facetedMethod.getFacet(NavigableParentFacet.class); + Assert.assertNotNull(facet); + Assert.assertTrue(facet instanceof NavigableParentFacetMethod); + + final NavigableParentFacetMethod navigableParentFacetMethod = (NavigableParentFacetMethod) facet; + final Method parentMethod = domainClass.getMethod(parentMethodName); + + Assert.assertEquals( + parentMethod.invoke(domainObject, Reflect.emptyObjects), + navigableParentFacetMethod.navigableParent(domainObject) ); + + } + + + +} diff --git a/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/object/navparent/annotation/NavigableParentTestSamples.java b/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/object/navparent/annotation/NavigableParentTestSamples.java new file mode 100644 index 0000000..4326b72 --- /dev/null +++ b/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/object/navparent/annotation/NavigableParentTestSamples.java @@ -0,0 +1,49 @@ +package org.apache.isis.core.metamodel.facets.object.navparent.annotation; + +import org.apache.isis.applib.annotation.Parent; + +class NavigableParentTestSamples { + + // has no navigable parent + protected static class DomainObjectRoot { + + @Override + public String toString() { + return "Root"; + } + + } + + // has navigable parent 'Root' specified via Annotation + protected static class DomainObjectA { + + private final static Object myParent = new DomainObjectRoot(); + + @Override + public String toString() { + return "A"; + } + + @Parent + public Object root() { + return myParent; + } + + } + + // has navigable parent 'A' specified via method + protected static class DomainObjectB { + + private final static Object myParent = new DomainObjectA(); + + @Override + public String toString() { + return "B"; + } + + public Object parent() { + return myParent; + } + + } +} diff --git a/core/metamodel/src/test/java/org/apache/isis/core/metamodel/testspec/ObjectSpecificationStub.java b/core/metamodel/src/test/java/org/apache/isis/core/metamodel/testspec/ObjectSpecificationStub.java index 086bb4c..6eb9c84 100644 --- a/core/metamodel/src/test/java/org/apache/isis/core/metamodel/testspec/ObjectSpecificationStub.java +++ b/core/metamodel/src/test/java/org/apache/isis/core/metamodel/testspec/ObjectSpecificationStub.java @@ -189,6 +189,11 @@ public class ObjectSpecificationStub extends FacetHolderImpl implements ObjectSp return null; } + @Override + public Object getNavigableParent(Object object) { + return null; + } + @Override public String getCssClass() { return null; diff --git a/core/viewer-wicket-model/src/main/java/org/apache/isis/viewer/wicket/model/models/whereami/WhereAmIModel.java b/core/viewer-wicket-model/src/main/java/org/apache/isis/viewer/wicket/model/models/whereami/WhereAmIModel.java index fdc11f2..7d4a8ce 100644 --- a/core/viewer-wicket-model/src/main/java/org/apache/isis/viewer/wicket/model/models/whereami/WhereAmIModel.java +++ b/core/viewer-wicket-model/src/main/java/org/apache/isis/viewer/wicket/model/models/whereami/WhereAmIModel.java @@ -26,7 +26,7 @@ import org.apache.isis.viewer.wicket.model.models.EntityModel; /** * Represents a navigable chain of parent nodes starting at the current node. * - * @author a.hu...@corax.at + * @author ahu...@apache.org * * @since 2.0.0 * diff --git a/core/viewer-wicket-model/src/main/java/org/apache/isis/viewer/wicket/model/models/whereami/WhereAmIModelDefault.java b/core/viewer-wicket-model/src/main/java/org/apache/isis/viewer/wicket/model/models/whereami/WhereAmIModelDefault.java index 955c2db..042595f 100644 --- a/core/viewer-wicket-model/src/main/java/org/apache/isis/viewer/wicket/model/models/whereami/WhereAmIModelDefault.java +++ b/core/viewer-wicket-model/src/main/java/org/apache/isis/viewer/wicket/model/models/whereami/WhereAmIModelDefault.java @@ -19,26 +19,28 @@ package org.apache.isis.viewer.wicket.model.models.whereami; -import java.util.ArrayList; -import java.util.List; +import java.util.LinkedList; import java.util.stream.Stream; +import org.apache.isis.core.metamodel.adapter.ObjectAdapter; import org.apache.isis.core.metamodel.util.pchain.ParentChain; +import org.apache.isis.core.runtime.system.context.IsisContext; import org.apache.isis.viewer.wicket.model.models.EntityModel; class WhereAmIModelDefault implements WhereAmIModel { - private final List<Object> reversedChainOfParents = new ArrayList<>(); + private final LinkedList<Object> reversedChainOfParents = new LinkedList<>(); private final EntityModel startOfChain; public WhereAmIModelDefault(EntityModel startOfChain) { this.startOfChain = startOfChain; - final Object startPojo = startOfChain.getObject().getObject(); - - ParentChain.caching() - .streamReversedParentChainOf(startPojo) - .forEach(reversedChainOfParents::add); + final ObjectAdapter adapter = startOfChain.getObject(); + final Object startNode = adapter.getObject(); + + ParentChain.of(IsisContext.getSessionFactory().getSpecificationLoader()::loadSpecification) + .streamParentChainOf(startNode) + .forEach(reversedChainOfParents::addFirst); } @Override -- To stop receiving notification emails like this one, please contact danhayw...@apache.org.