/* * 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.specloader.specimpl; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.collect.Collections2; import com.google.common.collect.FluentIterable; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.isis.applib.AppManifest; import org.apache.isis.applib.Identifier; import org.apache.isis.applib.annotation.NotPersistable; import org.apache.isis.applib.annotation.When; import org.apache.isis.applib.annotation.Where; import org.apache.isis.applib.filter.Filter; import org.apache.isis.applib.filter.Filters; import org.apache.isis.core.commons.authentication.AuthenticationSession; import org.apache.isis.core.commons.exceptions.UnknownTypeException; import org.apache.isis.core.commons.lang.ClassExtensions; import org.apache.isis.core.commons.util.ToString; import org.apache.isis.core.metamodel.adapter.ObjectAdapter; import org.apache.isis.core.metamodel.consent.Consent; import org.apache.isis.core.metamodel.consent.InteractionInitiatedBy; import org.apache.isis.core.metamodel.consent.InteractionResult; import org.apache.isis.core.metamodel.deployment.DeploymentCategory; import org.apache.isis.core.metamodel.facetapi.Facet; import org.apache.isis.core.metamodel.facetapi.FacetHolder; import org.apache.isis.core.metamodel.facetapi.FacetHolderImpl; import org.apache.isis.core.metamodel.facetapi.FeatureType; import org.apache.isis.core.metamodel.facets.actions.notcontributed.NotContributedFacet; import org.apache.isis.core.metamodel.facets.all.describedas.DescribedAsFacet; import org.apache.isis.core.metamodel.facets.all.help.HelpFacet; import org.apache.isis.core.metamodel.facets.all.hide.HiddenFacet; import org.apache.isis.core.metamodel.facets.all.named.NamedFacet; import org.apache.isis.core.metamodel.facets.collections.modify.CollectionFacet; import org.apache.isis.core.metamodel.facets.members.cssclass.CssClassFacet; import org.apache.isis.core.metamodel.facets.object.domainservice.DomainServiceFacet; import org.apache.isis.core.metamodel.facets.object.encodeable.EncodableFacet; 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.notpersistable.NotPersistableFacet; 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; import org.apache.isis.core.metamodel.facets.object.plural.PluralFacet; import org.apache.isis.core.metamodel.facets.object.title.TitleFacet; import org.apache.isis.core.metamodel.facets.object.value.ValueFacet; import org.apache.isis.core.metamodel.interactions.InteractionContext; import org.apache.isis.core.metamodel.interactions.InteractionUtils; import org.apache.isis.core.metamodel.interactions.ObjectTitleContext; import org.apache.isis.core.metamodel.interactions.ObjectValidityContext; import org.apache.isis.core.metamodel.layout.DeweyOrderSet; import org.apache.isis.core.metamodel.services.ServicesInjector; import org.apache.isis.core.metamodel.spec.ActionType; import org.apache.isis.core.metamodel.spec.ObjectSpecId; import org.apache.isis.core.metamodel.spec.ObjectSpecification; import org.apache.isis.core.metamodel.spec.ObjectSpecificationException; import org.apache.isis.core.metamodel.spec.Persistability; import org.apache.isis.core.metamodel.spec.feature.Contributed; import org.apache.isis.core.metamodel.spec.feature.ObjectAction; import org.apache.isis.core.metamodel.spec.feature.ObjectActionParameter; import org.apache.isis.core.metamodel.spec.feature.ObjectAssociation; import org.apache.isis.core.metamodel.spec.feature.ObjectMember; 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.objectstore.jdo.metamodel.facets.object.persistencecapable.JdoPersistenceCapableFacet; public abstract class ObjectSpecificationAbstract extends FacetHolderImpl implements ObjectSpecification { private final static Logger LOG = LoggerFactory.getLogger(ObjectSpecificationAbstract.class); private static class SubclassList { private final List<ObjectSpecification> classes = Lists.newArrayList(); public void addSubclass(final ObjectSpecification subclass) { if(classes.contains(subclass)) { return; } classes.add(subclass); } public boolean hasSubclasses() { return !classes.isEmpty(); } public List<ObjectSpecification> toList() { return Collections.unmodifiableList(classes); } } //region > fields protected final ServicesInjector servicesInjector; private final DeploymentCategory deploymentCategory; private final SpecificationLoader specificationLoader; private final FacetProcessor facetProcessor; /** * Only populated once {@link #introspectTypeHierarchyAndMembers()} is called. */ protected Properties metadataProperties; private final List<ObjectAssociation> associations = Lists.newArrayList(); private final List<ObjectAction> objectActions = Lists.newArrayList(); // partitions and caches objectActions by type; updated in sortCacheAndUpdateActions() private final Map<ActionType, List<ObjectAction>> objectActionsByType = createObjectActionsByType(); private static Map<ActionType, List<ObjectAction>> createObjectActionsByType() { final Map<ActionType, List<ObjectAction>> map = Maps.newHashMap(); for (final ActionType type : ActionType.values()) { map.put(type, Lists.<ObjectAction>newArrayList()); } return map; } private boolean contributeeAndMixedInAssociationsAdded; private boolean contributeeAndMixedInActionsAdded; private final List<ObjectSpecification> interfaces = Lists.newArrayList(); private final SubclassList subclasses = new SubclassList(); private final Class<?> correspondingClass; private final String fullName; private final String shortName; private final Identifier identifier; private final boolean isAbstract; // derived lazily, cached since immutable private ObjectSpecId specId; private ObjectSpecification superclassSpec; private Persistability persistability = Persistability.USER_PERSISTABLE; private TitleFacet titleFacet; private IconFacet iconFacet; private CssClassFacet cssClassFacet; private IntrospectionState introspected = IntrospectionState.NOT_INTROSPECTED; //endregion //region > Constructor public ObjectSpecificationAbstract( final Class<?> introspectedClass, final String shortName, final ServicesInjector servicesInjector, final FacetProcessor facetProcessor) { this.correspondingClass = introspectedClass; this.fullName = introspectedClass.getName(); this.shortName = shortName; this.isAbstract = ClassExtensions.isAbstract(introspectedClass); this.identifier = Identifier.classIdentifier(introspectedClass); this.servicesInjector = servicesInjector; this.facetProcessor = facetProcessor; this.specificationLoader = servicesInjector.getSpecificationLoader(); this.deploymentCategory = servicesInjector.getDeploymentCategoryProvider().getDeploymentCategory(); } //endregion //region > Stuff immediately derivable from class @Override public FeatureType getFeatureType() { return FeatureType.OBJECT; } @Override public ObjectSpecId getSpecId() { if(specId == null) { final ObjectSpecIdFacet facet = getFacet(ObjectSpecIdFacet.class); if(facet == null) { throw new IllegalStateException("could not find an ObjectSpecIdFacet for " + this.getFullIdentifier()); } if(facet != null) { specId = facet.value(); } } return specId; } /** * As provided explicitly within the constructor. * * <p> * Not API, but <tt>public</tt> so that {@link FacetedMethodsBuilder} can * call it. */ @Override public Class<?> getCorrespondingClass() { return correspondingClass; } @Override public String getShortIdentifier() { return shortName; } /** * The {@link Class#getName() (full) name} of the * {@link #getCorrespondingClass() class}. */ @Override public String getFullIdentifier() { return fullName; } public enum IntrospectionState { NOT_INTROSPECTED, BEING_INTROSPECTED, INTROSPECTED, } /** * Only if {@link #setIntrospectionState(org.apache.isis.core.metamodel.specloader.specimpl.ObjectSpecificationAbstract.IntrospectionState)} * has been called (should be called within {@link #updateFromFacetValues()}. */ public IntrospectionState getIntrospectionState() { return introspected; } public void setIntrospectionState(IntrospectionState introspectationState) { this.introspected = introspectationState; } protected boolean isNotIntrospected() { return !(getIntrospectionState() == IntrospectionState.INTROSPECTED); } //endregion //region > Introspection (part 1) public abstract void introspectTypeHierarchyAndMembers(); /** * Intended to be called within {@link #introspectTypeHierarchyAndMembers()} * . */ protected void updateSuperclass(final Class<?> superclass) { if (superclass == null) { return; } superclassSpec = getSpecificationLoader().loadSpecification(superclass); if (superclassSpec != null) { if (LOG.isDebugEnabled()) { LOG.debug(" Superclass " + superclass.getName()); } updateAsSubclassTo(superclassSpec); } } /** * Intended to be called within {@link #introspectTypeHierarchyAndMembers()} * . */ protected void updateInterfaces(final List<ObjectSpecification> interfaces) { this.interfaces.clear(); this.interfaces.addAll(interfaces); } /** * Intended to be called within {@link #introspectTypeHierarchyAndMembers()} * . */ protected void updateAsSubclassTo(final ObjectSpecification supertypeSpec) { if (!(supertypeSpec instanceof ObjectSpecificationAbstract)) { return; } // downcast required because addSubclass is (deliberately) not public // API final ObjectSpecificationAbstract introspectableSpec = (ObjectSpecificationAbstract) supertypeSpec; introspectableSpec.updateSubclasses(this); } /** * Intended to be called within {@link #introspectTypeHierarchyAndMembers()} * . */ protected void updateAsSubclassTo(final List<ObjectSpecification> supertypeSpecs) { for (final ObjectSpecification supertypeSpec : supertypeSpecs) { updateAsSubclassTo(supertypeSpec); } } private void updateSubclasses(final ObjectSpecification subclass) { this.subclasses.addSubclass(subclass); } protected void sortAndUpdateAssociations(final List<ObjectAssociation> associations) { final List<ObjectAssociation> orderedAssociations = sortAssociations(associations); synchronized (this.associations) { this.associations.clear(); this.associations.addAll(orderedAssociations); } } protected void sortCacheAndUpdateActions(final List<ObjectAction> objectActions) { final List<ObjectAction> orderedActions = sortActions(objectActions); synchronized (this.objectActions){ this.objectActions.clear(); this.objectActions.addAll(orderedActions); for (final ActionType type : ActionType.values()) { final List<ObjectAction> objectActionForType = objectActionsByType.get(type); objectActionForType.clear(); objectActionForType.addAll(Collections2.filter(objectActions, ObjectAction.Predicates.ofType(type))); } } } //endregion //region > Introspection (part 2) public void updateFromFacetValues() { titleFacet = getFacet(TitleFacet.class); iconFacet = getFacet(IconFacet.class); cssClassFacet = getFacet(CssClassFacet.class); this.persistability = determinePersistability(); } private Persistability determinePersistability() { final NotPersistableFacet notPersistableFacet = getFacet(NotPersistableFacet.class); if (notPersistableFacet == null) { return Persistability.USER_PERSISTABLE; } final NotPersistable.By initiatedBy = notPersistableFacet.value(); if (initiatedBy == NotPersistable.By.USER_OR_PROGRAM) { return Persistability.TRANSIENT; } else if (initiatedBy == NotPersistable.By.USER) { return Persistability.PROGRAM_PERSISTABLE; } else { return Persistability.USER_PERSISTABLE; } } //endregion //region > Title, Icon @Override public String getTitle(final ObjectAdapter targetAdapter) { return getTitle(null, targetAdapter); } @Override public String getTitle( ObjectAdapter contextAdapterIfAny, ObjectAdapter targetAdapter) { if (titleFacet != null) { final String titleString = titleFacet.title(contextAdapterIfAny, targetAdapter); if (titleString != null && !titleString.equals("")) { return titleString; } } return (this.isService() ? "" : "Untitled ") + getSingularName(); } @Override public String getIconName(final ObjectAdapter reference) { return iconFacet == null ? null : iconFacet.iconName(reference); } @Deprecated @Override public String getCssClass() { return getCssClass(null); } @Override public String getCssClass(final ObjectAdapter reference) { return cssClassFacet == null ? null : cssClassFacet.cssClass(reference); } //endregion //region > Hierarchical /** * Determines if this class represents the same class, or a subclass, of the * specified class. * * <p> * cf {@link Class#isAssignableFrom(Class)}, though target and parameter are * the opposite way around, ie: * * <pre> * cls1.isAssignableFrom(cls2); * </pre> * <p> * is equivalent to: * * <pre> * spec2.isOfType(spec1); * </pre> * * <p> * Callable after {@link #introspectTypeHierarchyAndMembers()} has been * called. */ @Override public boolean isOfType(final ObjectSpecification specification) { // do the comparison using value types because of a possible aliasing/race condition // in matchesParameterOf when building up contributed associations if (specification.getSpecId().equals(this.getSpecId())) { return true; } for (final ObjectSpecification interfaceSpec : interfaces()) { if (interfaceSpec.isOfType(specification)) { return true; } } final ObjectSpecification superclassSpec = superclass(); return superclassSpec != null && superclassSpec.isOfType(specification); } //endregion //region > Name, Description, Persistability /** * The name according to any available {@link org.apache.isis.core.metamodel.facets.all.named.NamedFacet}, * but falling back to {@link #getFullIdentifier()} otherwise. */ @Override public String getSingularName() { final NamedFacet namedFacet = getFacet(NamedFacet.class); return namedFacet != null? namedFacet.value() : this.getFullIdentifier(); } /** * The pluralized name according to any available {@link org.apache.isis.core.metamodel.facets.object.plural.PluralFacet}, * else <tt>null</tt>. */ @Override public String getPluralName() { final PluralFacet pluralFacet = getFacet(PluralFacet.class); return pluralFacet.value(); } /** * The description according to any available {@link org.apache.isis.core.metamodel.facets.object.plural.PluralFacet}, * else empty string (<tt>""</tt>). */ @Override public String getDescription() { final DescribedAsFacet describedAsFacet = getFacet(DescribedAsFacet.class); final String describedAs = describedAsFacet.value(); return describedAs == null ? "" : describedAs; } /* * help is typically a reference (eg a URL) and so should not default to a * textual value if not set up */ @Override public String getHelp() { final HelpFacet helpFacet = getFacet(HelpFacet.class); return helpFacet == null ? null : helpFacet.value(); } @Override public Persistability persistability() { return persistability; } //endregion //region > Facet Handling @Override public <Q extends Facet> Q getFacet(final Class<Q> facetType) { final Q facet = super.getFacet(facetType); Q noopFacet = null; if (isNotANoopFacet(facet)) { return facet; } else { noopFacet = facet; } if (interfaces() != null) { final List<ObjectSpecification> interfaces = interfaces(); for (int i = 0; i < interfaces.size(); i++) { final ObjectSpecification interfaceSpec = interfaces.get(i); if (interfaceSpec == null) { // HACK: shouldn't happen, but occurring on occasion when // running // XATs under JUnit4. Some sort of race condition? continue; } final Q interfaceFacet = interfaceSpec.getFacet(facetType); if (isNotANoopFacet(interfaceFacet)) { return interfaceFacet; } else { if (noopFacet == null) { noopFacet = interfaceFacet; } } } } // search up the inheritance hierarchy final ObjectSpecification superSpec = superclass(); if (superSpec != null) { final Q superClassFacet = superSpec.getFacet(facetType); if (isNotANoopFacet(superClassFacet)) { return superClassFacet; } } return noopFacet; } private boolean isNotANoopFacet(final Facet facet) { return facet != null && !facet.isNoop(); } //endregion //region > DefaultValue - unused /** * @deprecated - never called. * @return - always returns <tt>null</tt> */ @Deprecated @Override public Object getDefaultValue() { return null; } //endregion //region > Identifier @Override public Identifier getIdentifier() { return identifier; } //endregion //region > createTitleInteractionContext @Override public ObjectTitleContext createTitleInteractionContext(final AuthenticationSession session, final InteractionInitiatedBy interactionMethod, final ObjectAdapter targetObjectAdapter) { return new ObjectTitleContext(targetObjectAdapter, getIdentifier(), targetObjectAdapter.titleString(null), interactionMethod); } //endregion //region > Superclass, Interfaces, Subclasses, isAbstract @Override public ObjectSpecification superclass() { return superclassSpec; } @Override public List<ObjectSpecification> interfaces() { return Collections.unmodifiableList(interfaces); } @Override public List<ObjectSpecification> subclasses() { return subclasses.toList(); } @Override public boolean hasSubclasses() { return subclasses.hasSubclasses(); } @Override public final boolean isAbstract() { return isAbstract; } //endregion //region > Associations @Override public List<ObjectAssociation> getAssociations(final Contributed contributed) { // the "contributed.isIncluded()" guard is required because we cannot do this too early; // there must be a session available if(contributed.isIncluded() && !contributeeAndMixedInAssociationsAdded) { synchronized (this.associations) { List<ObjectAssociation> associations = Lists.newArrayList(this.associations); associations.addAll(createContributeeAssociations()); associations.addAll(createMixedInAssociations()); sortAndUpdateAssociations(associations); contributeeAndMixedInAssociationsAdded = true; } } final List<ObjectAssociation> associations = Lists.newArrayList(this.associations); return Lists.newArrayList(Iterables.filter( associations, ContributeeMember.Predicates.regularElse(contributed))); } private static ThreadLocal<Boolean> invalidatingCache = new ThreadLocal<Boolean>() { protected Boolean initialValue() { return Boolean.FALSE; }; }; /** * The association with the given {@link ObjectAssociation#getId() id}. * * <p> * This is overridable because {@link org.apache.isis.core.metamodel.specloader.specimpl.standalonelist.ObjectSpecificationOnStandaloneList} * simply returns <tt>null</tt>. * * <p> * TODO put fields into hash. * * <p> * TODO: could this be made final? (ie does the framework ever call this * method for an {@link org.apache.isis.core.metamodel.specloader.specimpl.standalonelist.ObjectSpecificationOnStandaloneList}) */ @Override public ObjectAssociation getAssociation(final String id) { ObjectAssociation oa = getAssociationWithId(id); if(oa != null) { return oa; } if(! deploymentCategory.isProduction()) { // automatically refresh if not in production // (better support for jrebel) LOG.warn("Could not find association with id '" + id + "'; invalidating cache automatically"); if(!invalidatingCache.get()) { // make sure don't go into an infinite loop, though. try { invalidatingCache.set(true); getSpecificationLoader().invalidateCache(getCorrespondingClass()); } finally { invalidatingCache.set(false); } } else { LOG.warn("... already invalidating cache earlier in stacktrace, so skipped this time"); } oa = getAssociationWithId(id); if(oa != null) { return oa; } } throw new ObjectSpecificationException( String.format("No association called '%s' in '%s'", id, getSingularName())); } private ObjectAssociation getAssociationWithId(final String id) { for (final ObjectAssociation objectAssociation : getAssociations(Contributed.INCLUDED)) { if (objectAssociation.getId().equals(id)) { return objectAssociation; } } return null; } @Deprecated @Override public List<ObjectAssociation> getAssociations(Filter<ObjectAssociation> filter) { return getAssociations(Contributed.INCLUDED, filter); } @Override public List<ObjectAssociation> getAssociations(Contributed contributed, final Filter<ObjectAssociation> filter) { final List<ObjectAssociation> allAssociations = getAssociations(contributed); return Lists.newArrayList( FluentIterable.from(allAssociations) .filter(Filters.asPredicate(filter)) .toSortedList(ObjectMember.Comparators.byMemberOrderSequence()) ); } @SuppressWarnings({ "rawtypes", "unchecked" }) @Override public List<OneToOneAssociation> getProperties(Contributed contributed) { final List list = getAssociations(contributed, ObjectAssociation.Filters.PROPERTIES); return list; } @Override @SuppressWarnings({ "unchecked", "rawtypes" }) public List<OneToManyAssociation> getCollections(Contributed contributed) { final List list = getAssociations(contributed, ObjectAssociation.Filters.COLLECTIONS); return list; } //endregion //region > getObjectActions @Override public List<ObjectAction> getObjectActions( final List<ActionType> types, final Contributed contributed, final Filter<ObjectAction> filter) { // update our list of actions if requesting for contributed actions // and they have not yet been added // the "contributed.isIncluded()" guard is required because we cannot do this too early; // there must be a session available if(contributed.isIncluded() && !contributeeAndMixedInActionsAdded) { synchronized (this.objectActions) { final List<ObjectAction> actions = Lists.newArrayList(this.objectActions); final boolean containsMixin = containsDoOpFacet(MixinFacet.class); final boolean containsDomainService = containsDoOpFacet(DomainServiceFacet.class); final boolean isService = isService(); if (containsMixin || containsDomainService || isService) { // don't contribute to mixins themselves! // don't contribute to services either // - isService() is sufficient check for internal services registered directly with ServicesInjector // - checking for DomainServiceFacet is for application services (isService() may not have been called, for these) } else { actions.addAll(createContributeeActions()); actions.addAll(createMixedInActions()); } sortCacheAndUpdateActions(actions); contributeeAndMixedInActionsAdded = true; } } final List<ObjectAction> actions = Lists.newArrayList(); for (final ActionType type : types) { final Collection<ObjectAction> filterActions = Collections2.filter(objectActionsByType.get(type), Filters.asPredicate(filter)); actions.addAll(filterActions); } return Lists.newArrayList( Iterables.filter( actions, ContributeeMember.Predicates.regularElse(contributed))); } @Override public List<ObjectAction> getObjectActions( final Contributed contributed) { return getObjectActions(ActionType.ALL, contributed, Filters.<ObjectAction>any()); } @Override public List<ObjectAction> getObjectActions( final ActionType type, final Contributed contributed, final Filter<ObjectAction> filter) { return getObjectActions(Collections.singletonList(type), contributed, filter); } //endregion //region > sorting protected List<ObjectAssociation> sortAssociations(final List<ObjectAssociation> associations) { final DeweyOrderSet orderSet = DeweyOrderSet.createOrderSet(associations); final MemberGroupLayoutFacet memberGroupLayoutFacet = this.getFacet(MemberGroupLayoutFacet.class); if(memberGroupLayoutFacet != null) { final List<String> groupOrder = Lists.newArrayList(); groupOrder.addAll(memberGroupLayoutFacet.getLeft()); groupOrder.addAll(memberGroupLayoutFacet.getMiddle()); groupOrder.addAll(memberGroupLayoutFacet.getRight()); orderSet.reorderChildren(groupOrder); } final List<ObjectAssociation> orderedAssociations = Lists.newArrayList(); sortAssociations(orderSet, orderedAssociations); return orderedAssociations; } private static void sortAssociations(final DeweyOrderSet orderSet, final List<ObjectAssociation> associationsToAppendTo) { for (final Object element : orderSet) { if (element instanceof OneToManyAssociation) { associationsToAppendTo.add((ObjectAssociation) element); } else if (element instanceof OneToOneAssociation) { associationsToAppendTo.add((ObjectAssociation) element); } else if (element instanceof DeweyOrderSet) { // just flatten. DeweyOrderSet childOrderSet = (DeweyOrderSet) element; sortAssociations(childOrderSet, associationsToAppendTo); } else { throw new UnknownTypeException(element); } } } protected static List<ObjectAction> sortActions(final List<ObjectAction> actions) { final DeweyOrderSet orderSet = DeweyOrderSet.createOrderSet(actions); final List<ObjectAction> orderedActions = Lists.newArrayList(); sortActions(orderSet, orderedActions); return orderedActions; } private static void sortActions(final DeweyOrderSet orderSet, final List<ObjectAction> actionsToAppendTo) { for (final Object element : orderSet) { if(element instanceof ObjectAction) { final ObjectAction objectAction = (ObjectAction) element; actionsToAppendTo.add(objectAction); } else if (element instanceof DeweyOrderSet) { final DeweyOrderSet set = ((DeweyOrderSet) element); final List<ObjectAction> actions = Lists.newArrayList(); sortActions(set, actions); actionsToAppendTo.addAll(actions); } else { throw new UnknownTypeException(element); } } } private Iterable<Object> getServicePojos() { return getServicesInjector().getRegisteredServices(); } //endregion //region > contributee associations (properties and collections) private List<ObjectAssociation> createContributeeAssociations() { if (isService() || isValue()) { return Collections.emptyList(); } final List<ObjectAssociation> contributeeAssociations = Lists.newArrayList(); for (final Object servicePojo : getServicePojos()) { addContributeeAssociationsIfAny(servicePojo, contributeeAssociations); } return contributeeAssociations; } private void addContributeeAssociationsIfAny( final Object servicePojo, final List<ObjectAssociation> contributeeAssociationsToAppendTo) { final Class<?> serviceClass = servicePojo.getClass(); final ObjectSpecification specification = specificationLoader.loadSpecification(serviceClass); if (specification == this) { return; } final List<ObjectAssociation> contributeeAssociations = createContributeeAssociations(servicePojo); contributeeAssociationsToAppendTo.addAll(contributeeAssociations); } private List<ObjectAssociation> createContributeeAssociations(final Object servicePojo) { final Class<?> serviceClass = servicePojo.getClass(); final ObjectSpecification specification = specificationLoader.loadSpecification(serviceClass); final List<ObjectAction> serviceActions = specification.getObjectActions(ActionType.USER, Contributed.INCLUDED, Filters .<ObjectAction>any()); final List<ObjectActionDefault> contributedActions = Lists.newArrayList(); for (final ObjectAction serviceAction : serviceActions) { if (isAlwaysHidden(serviceAction)) { continue; } final NotContributedFacet notContributed = serviceAction.getFacet(NotContributedFacet.class); if(notContributed != null && notContributed.toAssociations()) { continue; } if(!serviceAction.hasReturn()) { continue; } if (serviceAction.getParameterCount() != 1 || contributeeParameterMatchOf(serviceAction) == -1) { continue; } if(!(serviceAction instanceof ObjectActionDefault)) { continue; } if(!serviceAction.getSemantics().isSafeInNature()) { continue; } contributedActions.add((ObjectActionDefault) serviceAction); } return Lists.newArrayList( Iterables.transform( contributedActions, createContributeeAssociationFunctor(servicePojo, this) )); } private Function<ObjectActionDefault, ObjectAssociation> createContributeeAssociationFunctor( final Object servicePojo, final ObjectSpecification contributeeType) { return new Function<ObjectActionDefault, ObjectAssociation>(){ @Override public ObjectAssociation apply(ObjectActionDefault input) { final ObjectSpecification returnType = input.getReturnType(); final ObjectAssociationAbstract association = createObjectAssociation(input, returnType); facetProcessor.processMemberOrder(metadataProperties, association); return association; } ObjectAssociationAbstract createObjectAssociation( final ObjectActionDefault input, final ObjectSpecification returnType) { if (returnType.isNotCollection()) { return new OneToOneAssociationContributee(servicePojo, input, contributeeType, servicesInjector); } else { return new OneToManyAssociationContributee(servicePojo, input, contributeeType, servicesInjector); } } }; } //endregion //region > mixin associations (properties and collections) private List<ObjectAssociation> createMixedInAssociations() { if (isService() || isValue()) { return Collections.emptyList(); } final Set<Class<?>> mixinTypes = AppManifest.Registry.instance().getMixinTypes(); if(mixinTypes == null) { return Collections.emptyList(); } final List<ObjectAssociation> mixedInAssociations = Lists.newArrayList(); for (final Class<?> mixinType : mixinTypes) { addMixedInAssociationsIfAny(mixinType, mixedInAssociations); } return mixedInAssociations; } private void addMixedInAssociationsIfAny( final Class<?> mixinType, final List<ObjectAssociation> toAppendTo) { final ObjectSpecification specification = getSpecificationLoader().loadSpecification(mixinType); if (specification == this) { return; } final MixinFacet mixinFacet = specification.getFacet(MixinFacet.class); if(mixinFacet == null) { // this shouldn't happen; perhaps it would be more correct to throw an exception? return; } if(!mixinFacet.isMixinFor(getCorrespondingClass())) { return; } final List<ObjectActionDefault> mixinActions = objectActionsOf(specification); final List<ObjectAssociation> mixedInAssociations = Lists.newArrayList( Iterables.transform( Iterables.filter(mixinActions, new Predicate<ObjectActionDefault>() { @Override public boolean apply(final ObjectActionDefault input) { final NotContributedFacet notContributedFacet = input.getFacet(NotContributedFacet.class); if (notContributedFacet == null || !notContributedFacet.toActions()) { return false; } if(input.getParameterCount() != 0) { return false; } if(!input.getSemantics().isSafeInNature()) { return false; } return true; } }), createMixedInAssociationFunctor(this, mixinType, mixinFacet.value()) )); toAppendTo.addAll(mixedInAssociations); } private List objectActionsOf(final ObjectSpecification specification) { return specification.getObjectActions(ActionType.ALL, Contributed.INCLUDED, Filters.<ObjectAction>any()); } private Function<ObjectActionDefault, ObjectAssociation> createMixedInAssociationFunctor( final ObjectSpecification mixedInType, final Class<?> mixinType, final String mixinMethodName) { return new Function<ObjectActionDefault, ObjectAssociation>(){ @Override public ObjectAssociation apply(final ObjectActionDefault mixinAction) { final ObjectAssociationAbstract association = createObjectAssociation(mixinAction); facetProcessor.processMemberOrder(metadataProperties, association); return association; } ObjectAssociationAbstract createObjectAssociation( final ObjectActionDefault mixinAction) { final ObjectSpecification returnType = mixinAction.getReturnType(); if (returnType.isNotCollection()) { return new OneToOneAssociationMixedIn( mixinAction, mixedInType, mixinType, mixinMethodName, servicesInjector); } else { return new OneToManyAssociationMixedIn( mixinAction, mixedInType, mixinType, mixinMethodName, servicesInjector); } } }; } //endregion //region > contributee actions /** * All contributee actions (each wrapping a service's contributed action) for this spec. * * <p> * If this specification {@link #isService() is actually for} a service, * then returns an empty list. */ protected List<ObjectAction> createContributeeActions() { if (isService() || isValue()) { return Collections.emptyList(); } final List<ObjectAction> contributeeActions = Lists.newArrayList(); for (final Object servicePojo : getServicePojos()) { addContributeeActionsIfAny(servicePojo, contributeeActions); } return contributeeActions; } private void addContributeeActionsIfAny( final Object servicePojo, final List<ObjectAction> contributeeActionsToAppendTo) { final Class<?> serviceType = servicePojo.getClass(); final ObjectSpecification specification = getSpecificationLoader().loadSpecification(serviceType); if (specification == this) { return; } final List<ObjectAction> contributeeActions = Lists.newArrayList(); final List<ObjectAction> serviceActions = specification.getObjectActions(ActionType.ALL, Contributed.INCLUDED, Filters .<ObjectAction>any()); for (final ObjectAction serviceAction : serviceActions) { if (isAlwaysHidden(serviceAction)) { continue; } final NotContributedFacet notContributed = serviceAction.getFacet(NotContributedFacet.class); if(notContributed != null && notContributed.toActions()) { continue; } if(!(serviceAction instanceof ObjectActionDefault)) { continue; } final ObjectActionDefault contributedAction = (ObjectActionDefault) serviceAction; // see if qualifies by inspecting all parameters final int contributeeParam = contributeeParameterMatchOf(contributedAction); if (contributeeParam != -1) { ObjectActionContributee contributeeAction = new ObjectActionContributee(servicePojo, contributedAction, contributeeParam, this, servicesInjector); facetProcessor.processMemberOrder(metadataProperties, contributeeAction); contributeeActions.add(contributeeAction); } } contributeeActionsToAppendTo.addAll(contributeeActions); } private boolean isAlwaysHidden(final FacetHolder holder) { final HiddenFacet hiddenFacet = holder.getFacet(HiddenFacet.class); return hiddenFacet != null && hiddenFacet.when() == When.ALWAYS && hiddenFacet.where() == Where.ANYWHERE; } /** * @param serviceAction - number of the parameter that matches, or -1 if none. */ private int contributeeParameterMatchOf(final ObjectAction serviceAction) { final List<ObjectActionParameter> params = serviceAction.getParameters(); for (final ObjectActionParameter param : params) { if (isOfType(param.getSpecification())) { return param.getNumber(); } } return -1; } //endregion //region > mixin actions /** * All contributee actions (each wrapping a service's contributed action) for this spec. * * <p> * If this specification {@link #isService() is actually for} a service, * then returns an empty list. */ protected List<ObjectAction> createMixedInActions() { if (isService() || isValue()) { return Collections.emptyList(); } final Set<Class<?>> mixinTypes = AppManifest.Registry.instance().getMixinTypes(); if(mixinTypes == null) { return Collections.emptyList(); } final List<ObjectAction> mixedInActions = Lists.newArrayList(); for (final Class<?> mixinType : mixinTypes) { addMixedInActionsIfAny(mixinType, mixedInActions); } return mixedInActions; } private void addMixedInActionsIfAny( final Class<?> mixinType, final List<ObjectAction> mixedInActionsToAppendTo) { final ObjectSpecification mixinSpec = getSpecificationLoader().loadSpecification(mixinType); if (mixinSpec == this) { return; } final MixinFacet mixinFacet = mixinSpec.getFacet(MixinFacet.class); if(mixinFacet == null) { // this shouldn't happen; perhaps it would be more correct to throw an exception? return; } if(!mixinFacet.isMixinFor(getCorrespondingClass())) { return; } final List<ObjectAction> actions = Lists.newArrayList(); final List<ObjectAction> mixinActions = mixinSpec.getObjectActions(ActionType.ALL, Contributed.INCLUDED, Filters .<ObjectAction>any()); for (final ObjectAction mixinTypeAction : mixinActions) { if (isAlwaysHidden(mixinTypeAction)) { continue; } if(!(mixinTypeAction instanceof ObjectActionDefault)) { continue; } final ObjectActionDefault mixinAction = (ObjectActionDefault) mixinTypeAction; final NotContributedFacet notContributedFacet = mixinAction.getFacet(NotContributedFacet.class); if(notContributedFacet != null && notContributedFacet.toActions()) { continue; } ObjectActionMixedIn mixedInAction = new ObjectActionMixedIn(mixinType, mixinFacet.value(), mixinAction, this, servicesInjector); facetProcessor.processMemberOrder(metadataProperties, mixedInAction); actions.add(mixedInAction); } mixedInActionsToAppendTo.addAll(actions); } //endregion //region > validity @Override public Consent isValid(final ObjectAdapter targetAdapter, final InteractionInitiatedBy interactionInitiatedBy) { return isValidResult(targetAdapter, interactionInitiatedBy).createConsent(); } @Override public InteractionResult isValidResult( final ObjectAdapter targetAdapter, final InteractionInitiatedBy interactionInitiatedBy) { final ObjectValidityContext validityContext = createValidityInteractionContext( targetAdapter, interactionInitiatedBy); return InteractionUtils.isValidResult(this, validityContext); } /** * Create an {@link InteractionContext} representing an attempt to save the * object. */ @Override public ObjectValidityContext createValidityInteractionContext( final ObjectAdapter targetAdapter, final InteractionInitiatedBy interactionInitiatedBy) { return new ObjectValidityContext(targetAdapter, getIdentifier(), interactionInitiatedBy); } //endregion //region > convenience isXxx (looked up from facets) @Override public boolean isImmutable() { return containsFacet(ImmutableFacet.class); } @Override public boolean isHidden() { return containsFacet(HiddenFacet.class); } @Override public boolean isParseable() { return containsFacet(ParseableFacet.class); } @Override public boolean isEncodeable() { return containsFacet(EncodableFacet.class); } @Override public boolean isValue() { return containsFacet(ValueFacet.class); } @Override public boolean isParented() { return containsFacet(ParentedCollectionFacet.class); } @Override public boolean isParentedOrFreeCollection() { return containsFacet(CollectionFacet.class); } @Override public boolean isNotCollection() { return !isParentedOrFreeCollection(); } @Override public boolean isValueOrIsParented() { return isValue() || isParented(); } @Override public boolean isPersistenceCapable() { return containsFacet(JdoPersistenceCapableFacet.class); } @Override public boolean isPersistenceCapableOrViewModel() { return isViewModel() || isPersistenceCapable(); } //endregion //region > toString @Override public String toString() { final ToString str = new ToString(this); str.append("class", getFullIdentifier()); return str.toString(); } //endregion //region > Dependencies (injected in constructor) private ServicesInjector getServicesInjector() { return servicesInjector; } protected SpecificationLoader getSpecificationLoader() { return specificationLoader; } //endregion }