001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one
003 *  or more contributor license agreements.  See the NOTICE file
004 *  distributed with this work for additional information
005 *  regarding copyright ownership.  The ASF licenses this file
006 *  to you under the Apache License, Version 2.0 (the
007 *  "License"); you may not use this file except in compliance
008 *  with the License.  You may obtain a copy of the License at
009 *
010 *        http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *  Unless required by applicable law or agreed to in writing,
013 *  software distributed under the License is distributed on an
014 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *  KIND, either express or implied.  See the License for the
016 *  specific language governing permissions and limitations
017 *  under the License.
018 */
019
020package org.apache.isis.core.metamodel.specloader.specimpl;
021
022import java.util.*;
023import com.google.common.base.Function;
024import com.google.common.collect.Collections2;
025import com.google.common.collect.Iterables;
026import com.google.common.collect.Lists;
027import com.google.common.collect.Maps;
028import org.slf4j.Logger;
029import org.slf4j.LoggerFactory;
030import org.apache.isis.applib.Identifier;
031import org.apache.isis.applib.annotation.NotPersistable;
032import org.apache.isis.applib.annotation.When;
033import org.apache.isis.applib.annotation.Where;
034import org.apache.isis.applib.filter.Filter;
035import org.apache.isis.applib.filter.Filters;
036import org.apache.isis.applib.profiles.Localization;
037import org.apache.isis.core.commons.authentication.AuthenticationSession;
038import org.apache.isis.core.commons.authentication.AuthenticationSessionProvider;
039import org.apache.isis.core.commons.exceptions.UnknownTypeException;
040import org.apache.isis.core.commons.lang.ClassExtensions;
041import org.apache.isis.core.commons.util.ToString;
042import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
043import org.apache.isis.core.metamodel.adapter.ServicesProvider;
044import org.apache.isis.core.metamodel.consent.Consent;
045import org.apache.isis.core.metamodel.consent.InteractionInvocationMethod;
046import org.apache.isis.core.metamodel.consent.InteractionResult;
047import org.apache.isis.core.metamodel.deployment.DeploymentCategory;
048import org.apache.isis.core.metamodel.facetapi.Facet;
049import org.apache.isis.core.metamodel.facetapi.FacetHolder;
050import org.apache.isis.core.metamodel.facetapi.FacetHolderImpl;
051import org.apache.isis.core.metamodel.facetapi.FeatureType;
052import org.apache.isis.core.metamodel.facets.collections.modify.CollectionFacet;
053import org.apache.isis.core.metamodel.facets.describedas.DescribedAsFacet;
054import org.apache.isis.core.metamodel.facets.help.HelpFacet;
055import org.apache.isis.core.metamodel.facets.hide.HiddenFacet;
056import org.apache.isis.core.metamodel.facets.members.cssclass.CssClassFacet;
057import org.apache.isis.core.metamodel.facets.named.NamedFacet;
058import org.apache.isis.core.metamodel.facets.object.aggregated.ParentedFacet;
059import org.apache.isis.core.metamodel.facets.object.dirty.ClearDirtyObjectFacet;
060import org.apache.isis.core.metamodel.facets.object.dirty.IsDirtyObjectFacet;
061import org.apache.isis.core.metamodel.facets.object.dirty.MarkDirtyObjectFacet;
062import org.apache.isis.core.metamodel.facets.object.encodeable.EncodableFacet;
063import org.apache.isis.core.metamodel.facets.object.icon.IconFacet;
064import org.apache.isis.core.metamodel.facets.object.immutable.ImmutableFacet;
065import org.apache.isis.core.metamodel.facets.object.membergroups.MemberGroupLayoutFacet;
066import org.apache.isis.core.metamodel.facets.object.notpersistable.NotPersistableFacet;
067import org.apache.isis.core.metamodel.facets.object.objecttype.ObjectSpecIdFacet;
068import org.apache.isis.core.metamodel.facets.object.parseable.ParseableFacet;
069import org.apache.isis.core.metamodel.facets.object.plural.PluralFacet;
070import org.apache.isis.core.metamodel.facets.object.title.TitleFacet;
071import org.apache.isis.core.metamodel.facets.object.value.ValueFacet;
072import org.apache.isis.core.metamodel.facets.typeof.TypeOfFacet;
073import org.apache.isis.core.metamodel.interactions.InteractionContext;
074import org.apache.isis.core.metamodel.interactions.InteractionUtils;
075import org.apache.isis.core.metamodel.interactions.ObjectTitleContext;
076import org.apache.isis.core.metamodel.interactions.ObjectValidityContext;
077import org.apache.isis.core.metamodel.layout.DeweyOrderSet;
078import org.apache.isis.core.metamodel.spec.*;
079import org.apache.isis.core.metamodel.spec.feature.*;
080import org.apache.isis.core.metamodel.specloader.facetprocessor.FacetProcessor;
081import org.apache.isis.core.metamodel.specloader.specimpl.objectlist.ObjectSpecificationForFreeStandingList;
082import org.apache.isis.core.progmodel.facets.actions.notcontributed.NotContributedFacet;
083
084public abstract class ObjectSpecificationAbstract extends FacetHolderImpl implements ObjectSpecification {
085
086    private final static Logger LOG = LoggerFactory.getLogger(ObjectSpecificationAbstract.class);
087
088    private static class SubclassList {
089        private final List<ObjectSpecification> classes = Lists.newArrayList();
090
091        public void addSubclass(final ObjectSpecification subclass) {
092            if(classes.contains(subclass)) { 
093                return;
094            }
095            classes.add(subclass);
096        }
097
098        public boolean hasSubclasses() {
099            return !classes.isEmpty();
100        }
101
102        public List<ObjectSpecification> toList() {
103            return Collections.unmodifiableList(classes);
104        }
105    }
106
107    private final DeploymentCategory deploymentCategory;
108    private final AuthenticationSessionProvider authenticationSessionProvider;
109    private final ServicesProvider servicesProvider;
110    private final ObjectInstantiator objectInstantiator;
111    private final SpecificationLoader specificationLookup;
112    
113    private final FacetProcessor facetProcessor;
114    
115    /**
116     * Only populated once {@link #introspectTypeHierarchyAndMembers()} is called.
117     */
118    protected Properties metadataProperties;
119
120    protected final ObjectMemberContext objectMemberContext;
121
122
123    private final List<ObjectAssociation> associations = Lists.newArrayList();
124    private final List<ObjectAction> objectActions = Lists.newArrayList();
125    // partitions and caches objectActions by type; updated in sortCacheAndUpdateActions()
126    private final Map<ActionType, List<ObjectAction>> objectActionsByType = createObjectActionsByType();
127
128    private static Map<ActionType, List<ObjectAction>> createObjectActionsByType() {
129        final Map<ActionType, List<ObjectAction>> map = Maps.newHashMap();
130        for (final ActionType type : ActionType.values()) {
131            map.put(type, Lists.<ObjectAction>newArrayList());
132        }
133        return map;
134    }
135
136    private boolean contributeeAssociationsAdded;
137    private boolean contributeeActionsAdded;
138
139
140    private final List<ObjectSpecification> interfaces = Lists.newArrayList();
141    private final SubclassList subclasses = new SubclassList();
142
143    private final Class<?> correspondingClass;
144    private final String fullName;
145    private final String shortName;
146    private final Identifier identifier;
147    private final boolean isAbstract;
148    // derived lazily, cached since immutable
149    private ObjectSpecId specId;
150
151    private ObjectSpecification superclassSpec;
152
153    private Persistability persistability = Persistability.USER_PERSISTABLE;
154
155    private MarkDirtyObjectFacet markDirtyObjectFacet;
156    private ClearDirtyObjectFacet clearDirtyObjectFacet;
157    private IsDirtyObjectFacet isDirtyObjectFacet;
158
159    private TitleFacet titleFacet;
160    private IconFacet iconFacet;
161
162    private IntrospectionState introspected = IntrospectionState.NOT_INTROSPECTED;
163
164    // //////////////////////////////////////////////////////////////////////
165    // Constructor
166    // //////////////////////////////////////////////////////////////////////
167
168    public ObjectSpecificationAbstract(
169            final Class<?> introspectedClass, 
170            final String shortName,
171            final SpecificationContext specificationContext, 
172            final ObjectMemberContext objectMemberContext) {
173
174        this.correspondingClass = introspectedClass;
175        this.fullName = introspectedClass.getName();
176        this.shortName = shortName;
177        
178        this.isAbstract = ClassExtensions.isAbstract(introspectedClass);
179        this.identifier = Identifier.classIdentifier(introspectedClass);
180
181        this.deploymentCategory = specificationContext.getDeploymentCategory();
182        this.authenticationSessionProvider = specificationContext.getAuthenticationSessionProvider();
183        this.servicesProvider = specificationContext.getServicesProvider();
184        this.objectInstantiator = specificationContext.getObjectInstantiator();
185        this.specificationLookup = specificationContext.getSpecificationLookup();
186        this.facetProcessor = specificationContext.getFacetProcessor();
187        
188        this.objectMemberContext = objectMemberContext;
189    }
190
191    
192    protected DeploymentCategory getDeploymentCategory() {
193        return deploymentCategory;
194    }
195
196    // //////////////////////////////////////////////////////////////////////
197    // Stuff immediately derivable from class
198    // //////////////////////////////////////////////////////////////////////
199
200    @Override
201    public FeatureType getFeatureType() {
202        return FeatureType.OBJECT;
203    }
204
205    @Override
206    public ObjectSpecId getSpecId() {
207        if(specId == null) {
208            final ObjectSpecIdFacet facet = getFacet(ObjectSpecIdFacet.class);
209            if(facet == null) {
210                throw new IllegalStateException("could not find an ObjectSpecIdFacet for " + this.getFullIdentifier());
211            }
212            if(facet != null) {
213                specId = facet.value();
214            }
215        }
216        return specId;
217    }
218    
219    /**
220     * As provided explicitly within the
221     * {@link #ObjectSpecificationAbstract(Class, String, org.apache.isis.core.metamodel.spec.SpecificationContext, org.apache.isis.core.metamodel.spec.feature.ObjectMemberContext)}
222     * constructor}.
223     * 
224     * <p>
225     * Not API, but <tt>public</tt> so that {@link FacetedMethodsBuilder} can
226     * call it.
227     */
228    @Override
229    public Class<?> getCorrespondingClass() {
230        return correspondingClass;
231    }
232
233    @Override
234    public String getShortIdentifier() {
235        return shortName;
236    }
237
238    /**
239     * The {@link Class#getName() (full) name} of the
240     * {@link #getCorrespondingClass() class}.
241     */
242    @Override
243    public String getFullIdentifier() {
244        return fullName;
245    }
246
247    
248    public enum IntrospectionState {
249        NOT_INTROSPECTED,
250        BEING_INTROSPECTED,
251        INTROSPECTED,
252    }
253
254    /**
255     * Only if {@link #setIntrospectionState(org.apache.isis.core.metamodel.specloader.specimpl.ObjectSpecificationAbstract.IntrospectionState)}
256     * has been called (should be called within {@link #updateFromFacetValues()}.
257     */
258    public IntrospectionState getIntrospectionState() {
259        return introspected;
260    }
261
262    public void setIntrospectionState(IntrospectionState introspectationState) {
263        this.introspected = introspectationState;
264    }
265    
266    protected boolean isNotIntrospected() {
267        return !(getIntrospectionState() == IntrospectionState.INTROSPECTED);
268    }
269
270
271    // //////////////////////////////////////////////////////////////////////
272    // Introspection (part 1)
273    // //////////////////////////////////////////////////////////////////////
274
275    public abstract void introspectTypeHierarchyAndMembers();
276
277    /**
278     * Intended to be called within {@link #introspectTypeHierarchyAndMembers()}
279     * .
280     */
281    protected void updateSuperclass(final Class<?> superclass) {
282        if (superclass == null) {
283            return;
284        }
285        superclassSpec = getSpecificationLookup().loadSpecification(superclass);
286        if (superclassSpec != null) {
287            if (LOG.isDebugEnabled()) {
288                LOG.debug("  Superclass " + superclass.getName());
289            }
290            updateAsSubclassTo(superclassSpec);
291        }
292    }
293
294    /**
295     * Intended to be called within {@link #introspectTypeHierarchyAndMembers()}
296     * .
297     */
298    protected void updateInterfaces(final List<ObjectSpecification> interfaces) {
299        this.interfaces.clear();
300        this.interfaces.addAll(interfaces);
301    }
302
303    /**
304     * Intended to be called within {@link #introspectTypeHierarchyAndMembers()}
305     * .
306     */
307    protected void updateAsSubclassTo(final ObjectSpecification supertypeSpec) {
308        if (!(supertypeSpec instanceof ObjectSpecificationAbstract)) {
309            return;
310        }
311        // downcast required because addSubclass is (deliberately) not public
312        // API
313        final ObjectSpecificationAbstract introspectableSpec = (ObjectSpecificationAbstract) supertypeSpec;
314        introspectableSpec.updateSubclasses(this);
315    }
316
317    /**
318     * Intended to be called within {@link #introspectTypeHierarchyAndMembers()}
319     * .
320     */
321    protected void updateAsSubclassTo(final List<ObjectSpecification> supertypeSpecs) {
322        for (final ObjectSpecification supertypeSpec : supertypeSpecs) {
323            updateAsSubclassTo(supertypeSpec);
324        }
325    }
326
327    private void updateSubclasses(final ObjectSpecification subclass) {
328        this.subclasses.addSubclass(subclass);
329    }
330
331    protected void sortAndUpdateAssociations(final List<ObjectAssociation> associations) {
332        final List<ObjectAssociation> orderedAssociations = sortAssociations(associations);
333        synchronized (this.associations) {
334            this.associations.clear();
335            this.associations.addAll(orderedAssociations);
336        }
337    }
338
339    protected void sortCacheAndUpdateActions(final List<ObjectAction> objectActions) {
340        final List<ObjectAction> orderedActions = sortActions(objectActions);
341        synchronized (this.objectActions){
342            this.objectActions.clear();
343            this.objectActions.addAll(orderedActions);
344
345            for (final ActionType type : ActionType.values()) {
346                final List<ObjectAction> objectActionForType = objectActionsByType.get(type);
347                objectActionForType.clear();
348                objectActionForType.addAll(Collections2.filter(objectActions, ObjectAction.Predicates.ofType(type)));
349            }
350        }
351    }
352
353    // //////////////////////////////////////////////////////////////////////
354    // Introspection (part 2)
355    // //////////////////////////////////////////////////////////////////////
356
357    public void updateFromFacetValues() {
358        clearDirtyObjectFacet = getFacet(ClearDirtyObjectFacet.class);
359        markDirtyObjectFacet = getFacet(MarkDirtyObjectFacet.class);
360        isDirtyObjectFacet = getFacet(IsDirtyObjectFacet.class);
361
362        titleFacet = getFacet(TitleFacet.class);
363        iconFacet = getFacet(IconFacet.class);
364
365        this.persistability = determinePersistability();
366    }
367
368    private Persistability determinePersistability() {
369        final NotPersistableFacet notPersistableFacet = getFacet(NotPersistableFacet.class);
370        if (notPersistableFacet == null) {
371            return Persistability.USER_PERSISTABLE;
372        }
373        final NotPersistable.By initiatedBy = notPersistableFacet.value();
374        if (initiatedBy == NotPersistable.By.USER_OR_PROGRAM) {
375            return Persistability.TRANSIENT;
376        } else if (initiatedBy == NotPersistable.By.USER) {
377            return Persistability.PROGRAM_PERSISTABLE;
378        } else {
379            return Persistability.USER_PERSISTABLE;
380        }
381    }
382
383    /**
384     * Intended to be called (if at all) within {@link #updateFromFacetValues()}
385     * .
386     */
387    protected void setClearDirtyObjectFacet(final ClearDirtyObjectFacet clearDirtyObjectFacet) {
388        this.clearDirtyObjectFacet = clearDirtyObjectFacet;
389    }
390
391
392    // //////////////////////////////////////////////////////////////////////
393    // Title, Icon
394    // //////////////////////////////////////////////////////////////////////
395
396    @Override
397    public String getTitle(final ObjectAdapter targetAdapter, final Localization localization) {
398        return getTitle(null, targetAdapter, localization);
399    }
400
401    @Override
402    public String getTitle(ObjectAdapter contextAdapterIfAny, ObjectAdapter targetAdapter, Localization localization) {
403        if (titleFacet != null) {
404            final String titleString = titleFacet.title(contextAdapterIfAny, targetAdapter, localization);
405            if (titleString != null && !titleString.equals("")) {
406                return titleString;
407            }
408        }
409        return (this.isService() ? "" : "Untitled ") + getSingularName();
410    }
411
412
413    @Override
414    public String getIconName(final ObjectAdapter reference) {
415        return iconFacet == null ? null : iconFacet.iconName(reference);
416    }
417
418    // //////////////////////////////////////////////////////////////////////
419    // Specification
420    // //////////////////////////////////////////////////////////////////////
421
422    @Override
423    public Instance getInstance(final ObjectAdapter adapter) {
424        return adapter;
425    }
426
427    // //////////////////////////////////////////////////////////////////////
428    // Hierarchical
429    // //////////////////////////////////////////////////////////////////////
430
431    /**
432     * Determines if this class represents the same class, or a subclass, of the
433     * specified class.
434     * 
435     * <p>
436     * cf {@link Class#isAssignableFrom(Class)}, though target and parameter are
437     * the opposite way around, ie:
438     * 
439     * <pre>
440     * cls1.isAssignableFrom(cls2);
441     * </pre>
442     * <p>
443     * is equivalent to:
444     * 
445     * <pre>
446     * spec2.isOfType(spec1);
447     * </pre>
448     * 
449     * <p>
450     * Callable after {@link #introspectTypeHierarchyAndMembers()} has been
451     * called.
452     */
453    @Override
454    public boolean isOfType(final ObjectSpecification specification) {
455        // do the comparison using value types because of a possible aliasing/race condition 
456        // in matchesParameterOf when building up contributed associations
457        if (specification.getSpecId().equals(this.getSpecId())) {
458            return true;
459        }
460        for (final ObjectSpecification interfaceSpec : interfaces()) {
461            if (interfaceSpec.isOfType(specification)) {
462                return true;
463            }
464        }
465        final ObjectSpecification superclassSpec = superclass();
466        return superclassSpec != null ? superclassSpec.isOfType(specification) : false;
467    }
468
469    // //////////////////////////////////////////////////////////////////////
470    // Name, Description, Persistability
471    // //////////////////////////////////////////////////////////////////////
472
473    /**
474     * The name according to any available {@link org.apache.isis.core.metamodel.facets.named.NamedFacet},
475     * but falling back to {@link #getFullIdentifier()} otherwise.
476     */
477    @Override
478    public String getSingularName() {
479        final NamedFacet namedFacet = getFacet(NamedFacet.class);
480        return namedFacet != null? namedFacet.value() : this.getFullIdentifier();
481    }
482
483    /**
484     * The pluralized name according to any available {@link org.apache.isis.core.metamodel.facets.object.plural.PluralFacet},
485     * else <tt>null</tt>.
486     */
487    @Override
488    public String getPluralName() {
489        final PluralFacet pluralFacet = getFacet(PluralFacet.class);
490        return pluralFacet.value();
491    }
492
493    /**
494     * The description according to any available {@link org.apache.isis.core.metamodel.facets.object.plural.PluralFacet},
495     * else empty string (<tt>""</tt>).
496     */
497    @Override
498    public String getDescription() {
499        final DescribedAsFacet describedAsFacet = getFacet(DescribedAsFacet.class);
500        final String describedAs = describedAsFacet.value();
501        return describedAs == null ? "" : describedAs;
502    }
503
504    /*
505     * help is typically a reference (eg a URL) and so should not default to a
506     * textual value if not set up
507     */
508    @Override
509    public String getHelp() {
510        final HelpFacet helpFacet = getFacet(HelpFacet.class);
511        return helpFacet == null ? null : helpFacet.value();
512    }
513
514    @Override
515    public String getCssClass() {
516        final CssClassFacet cssClassFacet = getFacet(CssClassFacet.class);
517        return cssClassFacet == null ? null : cssClassFacet.value();
518    }
519
520    @Override
521    public Persistability persistability() {
522        return persistability;
523    }
524
525    // //////////////////////////////////////////////////////////////////////
526    // Dirty object support
527    // //////////////////////////////////////////////////////////////////////
528
529    @Override
530    public boolean isDirty(final ObjectAdapter object) {
531        return isDirtyObjectFacet == null ? false : isDirtyObjectFacet.invoke(object);
532    }
533
534    @Override
535    public void clearDirty(final ObjectAdapter object) {
536        if (clearDirtyObjectFacet != null) {
537            clearDirtyObjectFacet.invoke(object);
538        }
539    }
540
541    @Override
542    public void markDirty(final ObjectAdapter object) {
543        if (markDirtyObjectFacet != null) {
544            markDirtyObjectFacet.invoke(object);
545        }
546    }
547
548    // //////////////////////////////////////////////////////////////////////
549    // Facet Handling
550    // //////////////////////////////////////////////////////////////////////
551
552    @Override
553    public <Q extends Facet> Q getFacet(final Class<Q> facetType) {
554        final Q facet = super.getFacet(facetType);
555        Q noopFacet = null;
556        if (isNotANoopFacet(facet)) {
557            return facet;
558        } else {
559            noopFacet = facet;
560        }
561        if (interfaces() != null) {
562            final List<ObjectSpecification> interfaces = interfaces();
563            for (int i = 0; i < interfaces.size(); i++) {
564                final ObjectSpecification interfaceSpec = interfaces.get(i);
565                if (interfaceSpec == null) {
566                    // HACK: shouldn't happen, but occurring on occasion when
567                    // running
568                    // XATs under JUnit4. Some sort of race condition?
569                    continue;
570                }
571                final Q interfaceFacet = interfaceSpec.getFacet(facetType);
572                if (isNotANoopFacet(interfaceFacet)) {
573                    return interfaceFacet;
574                } else {
575                    if (noopFacet == null) {
576                        noopFacet = interfaceFacet;
577                    }
578                }
579            }
580        }
581        // search up the inheritance hierarchy
582        final ObjectSpecification superSpec = superclass();
583        if (superSpec != null) {
584            final Q superClassFacet = superSpec.getFacet(facetType);
585            if (isNotANoopFacet(superClassFacet)) {
586                return superClassFacet;
587            }
588        }
589        return noopFacet;
590    }
591
592    private boolean isNotANoopFacet(final Facet facet) {
593        return facet != null && !facet.isNoop();
594    }
595
596    // //////////////////////////////////////////////////////////////////////
597    // DefaultValue
598    // //////////////////////////////////////////////////////////////////////
599
600    @Override
601    public Object getDefaultValue() {
602        return null;
603    }
604
605    // //////////////////////////////////////////////////////////////////////
606    // Identifier
607    // //////////////////////////////////////////////////////////////////////
608
609    @Override
610    public Identifier getIdentifier() {
611        return identifier;
612    }
613
614    // //////////////////////////////////////////////////////////////////
615    // create InteractionContext
616    // //////////////////////////////////////////////////////////////////
617
618    @Override
619    public ObjectTitleContext createTitleInteractionContext(final AuthenticationSession session, final InteractionInvocationMethod interactionMethod, final ObjectAdapter targetObjectAdapter) {
620        return new ObjectTitleContext(getDeploymentCategory(), session, interactionMethod, targetObjectAdapter, getIdentifier(), targetObjectAdapter.titleString());
621    }
622
623    // //////////////////////////////////////////////////////////////////////
624    // Superclass, Interfaces, Subclasses, isAbstract
625    // //////////////////////////////////////////////////////////////////////
626
627    @Override
628    public ObjectSpecification superclass() {
629        return superclassSpec;
630    }
631
632    @Override
633    public List<ObjectSpecification> interfaces() {
634        return Collections.unmodifiableList(interfaces);
635    }
636
637    @Override
638    public List<ObjectSpecification> subclasses() {
639        return subclasses.toList();
640    }
641
642    @Override
643    public boolean hasSubclasses() {
644        return subclasses.hasSubclasses();
645    }
646
647    @Override
648    public final boolean isAbstract() {
649        return isAbstract;
650    }
651
652    // //////////////////////////////////////////////////////////////////////
653    // Associations
654    // //////////////////////////////////////////////////////////////////////
655
656    @Override
657    public List<ObjectAssociation> getAssociations(final Contributed contributed) {
658        // the "contributed.isIncluded()" guard is required because we cannot do this too early;
659        // there must be a session available
660        if(contributed.isIncluded() && !contributeeAssociationsAdded) {
661            synchronized (this.associations) {
662                List<ObjectAssociation> associations = Lists.newArrayList(this.associations);
663                associations.addAll(createContributeeAssociations());
664                sortAndUpdateAssociations(associations);
665                contributeeAssociationsAdded = true;
666            }
667        }
668        final List<ObjectAssociation> associations = Lists.newArrayList(this.associations);
669        return Lists.newArrayList(Iterables.filter(
670                associations, ContributeeMember.Predicates.regularElse(contributed)));
671    }
672
673
674    private static ThreadLocal<Boolean> invalidatingCache = new ThreadLocal<Boolean>() {
675        protected Boolean initialValue() {
676            return Boolean.FALSE;
677        };
678    };
679
680    /**
681     * The association with the given {@link ObjectAssociation#getId() id}.
682     * 
683     * <p>
684     * This is overridable because {@link ObjectSpecificationForFreeStandingList}
685     * simply returns <tt>null</tt>.
686     * 
687     * <p>
688     * TODO put fields into hash.
689     * 
690     * <p>
691     * TODO: could this be made final? (ie does the framework ever call this
692     * method for an {@link ObjectSpecificationForFreeStandingList})
693     */
694    @Override
695    public ObjectAssociation getAssociation(final String id) {
696        ObjectAssociation oa = getAssociationWithId(id);
697        if(oa != null) {
698            return oa;
699        }
700        if(!getDeploymentCategory().isProduction()) {
701            // automatically refresh if not in production
702            // (better support for jrebel)
703            
704            LOG.warn("Could not find association with id '" + id + "'; invalidating cache automatically");
705            if(!invalidatingCache.get()) {
706                // make sure don't go into an infinite loop, though.
707                try {
708                    invalidatingCache.set(true);
709                    getSpecificationLookup().invalidateCache(getCorrespondingClass());
710                } finally {
711                    invalidatingCache.set(false);
712                }
713            } else {
714                LOG.warn("... already invalidating cache earlier in stacktrace, so skipped this time");
715            }
716            oa = getAssociationWithId(id);
717            if(oa != null) {
718                return oa;
719            }
720        }
721        throw new ObjectSpecificationException("No association called '" + id + "' in '" + getSingularName() + "'");
722    }
723
724    private ObjectAssociation getAssociationWithId(final String id) {
725        for (final ObjectAssociation objectAssociation : getAssociations(Contributed.INCLUDED)) {
726            if (objectAssociation.getId().equals(id)) {
727                return objectAssociation;
728            }
729        }
730        return null;
731    }
732
733    @Deprecated
734    @Override
735    public List<ObjectAssociation> getAssociations(Filter<ObjectAssociation> filter) {
736        return getAssociations(Contributed.INCLUDED, filter);
737    }
738
739    @Override
740    public List<ObjectAssociation> getAssociations(Contributed contributed, final Filter<ObjectAssociation> filter) {
741        final List<ObjectAssociation> allAssociations = getAssociations(contributed);
742        return Lists.newArrayList(
743                Iterables.filter(allAssociations, Filters.asPredicate(filter)));
744    }
745
746    @SuppressWarnings({ "rawtypes", "unchecked" })
747    @Override
748    public List<OneToOneAssociation> getProperties(Contributed contributed) {
749        final List list = getAssociations(contributed, ObjectAssociation.Filters.PROPERTIES);
750        return list;
751    }
752
753    @Override
754    @SuppressWarnings({ "unchecked", "rawtypes" })
755    public List<OneToManyAssociation> getCollections(Contributed contributed) {
756        final List list = getAssociations(contributed, ObjectAssociation.Filters.COLLECTIONS);
757        return list;
758    }
759
760    // //////////////////////////////////////////////////////////////////////
761    // getObjectActions
762    // //////////////////////////////////////////////////////////////////////
763
764    @Override
765    public List<ObjectAction> getObjectActions(
766            final List<ActionType> types,
767            final Contributed contributed, 
768            final Filter<ObjectAction> filter) {
769
770        // update our list of actions if requesting for contributed actions
771        // and they have not yet been added
772        // the "contributed.isIncluded()" guard is required because we cannot do this too early;
773        // there must be a session available
774        if(contributed.isIncluded() && !contributeeActionsAdded) {
775            synchronized (this.objectActions) {
776                final List<ObjectAction> actions = Lists.newArrayList(this.objectActions);
777                actions.addAll(createContributeeActions());
778                sortCacheAndUpdateActions(actions);
779                contributeeActionsAdded = true;
780            }
781        }
782
783        final List<ObjectAction> actions = Lists.newArrayList();
784        for (final ActionType type : types) {
785            final Collection<ObjectAction> filterActions =
786                    Collections2.filter(objectActionsByType.get(type), Filters.asPredicate(filter));
787            actions.addAll(filterActions);
788        }
789        return Lists.newArrayList(
790                Iterables.filter(
791                        actions,
792                        ContributeeMember.Predicates.regularElse(contributed)));
793    }
794
795    @Override
796    public List<ObjectAction> getObjectActions(
797            final Contributed contributed) {
798        return getObjectActions(ActionType.ALL, contributed, Filters.<ObjectAction>any());
799    }
800
801    @Override
802    public List<ObjectAction> getObjectActions(
803            final ActionType type, 
804            final Contributed contributed, 
805            final Filter<ObjectAction> filter) {
806        return getObjectActions(Collections.singletonList(type), contributed, filter);
807    }
808
809    // //////////////////////////////////////////////////////////////////////
810    // sorting
811    // //////////////////////////////////////////////////////////////////////
812    
813    protected List<ObjectAssociation> sortAssociations(final List<ObjectAssociation> associations) {
814        final DeweyOrderSet orderSet = DeweyOrderSet.createOrderSet(associations);
815        final MemberGroupLayoutFacet memberGroupLayoutFacet = this.getFacet(MemberGroupLayoutFacet.class);
816        
817        if(memberGroupLayoutFacet != null) {
818            final List<String> groupOrder = Lists.newArrayList();
819            groupOrder.addAll(memberGroupLayoutFacet.getLeft());
820            groupOrder.addAll(memberGroupLayoutFacet.getMiddle());
821            groupOrder.addAll(memberGroupLayoutFacet.getRight());
822            
823            orderSet.reorderChildren(groupOrder);
824        }
825        final List<ObjectAssociation> orderedAssociations = Lists.newArrayList();
826        sortAssociations(orderSet, orderedAssociations);
827        return orderedAssociations;
828    }
829
830    private static void sortAssociations(final DeweyOrderSet orderSet, final List<ObjectAssociation> associationsToAppendTo) {
831        for (final Object element : orderSet) {
832            if (element instanceof OneToManyAssociation) {
833                associationsToAppendTo.add((ObjectAssociation) element);
834            } else if (element instanceof OneToOneAssociation) {
835                associationsToAppendTo.add((ObjectAssociation) element);
836            } else if (element instanceof DeweyOrderSet) {
837                // just flatten.
838                DeweyOrderSet childOrderSet = (DeweyOrderSet) element;
839                sortAssociations(childOrderSet, associationsToAppendTo);
840            } else {
841                throw new UnknownTypeException(element);
842            }
843        }
844    }
845
846    protected static List<ObjectAction> sortActions(final List<ObjectAction> actions) {
847        final DeweyOrderSet orderSet = DeweyOrderSet.createOrderSet(actions);
848        final List<ObjectAction> orderedActions = Lists.newArrayList();
849        sortActions(orderSet, orderedActions);
850        return orderedActions;
851    }
852
853    private static void sortActions(final DeweyOrderSet orderSet, final List<ObjectAction> actionsToAppendTo) {
854        for (final Object element : orderSet) {
855            if(element instanceof ObjectAction) {
856                final ObjectAction objectAction = (ObjectAction) element;
857                actionsToAppendTo.add(objectAction);
858            }
859            else if (element instanceof DeweyOrderSet) {
860                final DeweyOrderSet set = ((DeweyOrderSet) element);
861                final List<ObjectAction> actions = Lists.newArrayList();
862                sortActions(set, actions);
863                actionsToAppendTo.addAll(actions);
864            } else {
865                throw new UnknownTypeException(element);
866            }
867        }
868    }
869
870    // //////////////////////////////////////////////////////////////////////
871    // getServiceActionsReturning
872    // //////////////////////////////////////////////////////////////////////
873
874    @Override
875    public List<ObjectAction> getServiceActionsReturning(final List<ActionType> types) {
876        final List<ObjectAction> serviceActions = Lists.newArrayList();
877        final List<ObjectAdapter> services = getServicesProvider().getServices();
878        for (final ObjectAdapter serviceAdapter : services) {
879            appendServiceActionsReturning(serviceAdapter, types, serviceActions);
880        }
881        return serviceActions;
882    }
883
884    private void appendServiceActionsReturning(final ObjectAdapter serviceAdapter, final List<ActionType> types, final List<ObjectAction> relatedActionsToAppendTo) {
885        final List<ObjectAction> matchingActionsToAppendTo = Lists.newArrayList();
886        for (final ActionType type : types) {
887            final List<ObjectAction> serviceActions = serviceAdapter.getSpecification().getObjectActions(type, Contributed.INCLUDED, Filters.<ObjectAction>any());
888            for (final ObjectAction serviceAction : serviceActions) {
889                addIfReturnsSubtype(serviceAction, matchingActionsToAppendTo);
890            }
891        }
892        relatedActionsToAppendTo.addAll(matchingActionsToAppendTo);
893    }
894
895    private void addIfReturnsSubtype(final ObjectAction serviceAction, final List<ObjectAction> matchingActionsToAppendTo) {
896        final ObjectSpecification returnType = serviceAction.getReturnType();
897        if (returnType == null) {
898            return;
899        }
900        if (returnType.isParentedOrFreeCollection()) {
901            final TypeOfFacet facet = serviceAction.getFacet(TypeOfFacet.class);
902            if (facet != null) {
903                final ObjectSpecification elementType = facet.valueSpec();
904                addIfReturnsSubtype(serviceAction, elementType, matchingActionsToAppendTo);
905            }
906        } else {
907            addIfReturnsSubtype(serviceAction, returnType, matchingActionsToAppendTo);
908        }
909    }
910
911    private void addIfReturnsSubtype(final ObjectAction serviceAction, final ObjectSpecification actionType, final List<ObjectAction> matchingActionsToAppendTo) {
912        if (actionType.isOfType(this)) {
913            matchingActionsToAppendTo.add(serviceAction);
914        }
915    }
916
917    
918    // //////////////////////////////////////////////////////////////////////
919    // contributee associations (properties and collections)
920    // //////////////////////////////////////////////////////////////////////
921
922    private List<ObjectAssociation> createContributeeAssociations() {
923        if (isService()) {
924            return Collections.emptyList();
925        }
926        
927        final List<ObjectAssociation> contributeeAssociations = Lists.newArrayList();
928        final List<ObjectAdapter> services = getServicesProvider().getServices();
929        for (final ObjectAdapter serviceAdapter : services) {
930            addContributeeAssociationsIfAny(serviceAdapter, contributeeAssociations);
931        }
932        return contributeeAssociations;
933    }
934
935    private void addContributeeAssociationsIfAny(
936            final ObjectAdapter serviceAdapter, 
937            final List<ObjectAssociation> contributeeAssociationsToAppendTo) {
938        final ObjectSpecification specification = serviceAdapter.getSpecification();
939        if (specification == this) {
940            return;
941        }
942        final List<ObjectAssociation> contributeeAssociations = createContributeeAssociations(serviceAdapter);
943        contributeeAssociationsToAppendTo.addAll(contributeeAssociations);
944    }
945
946    /**
947     * Synthesises {@link ObjectAssociation}s from matching {@link ObjectAction}s of any of the services
948     * that accept one parameter
949     */
950    private List<ObjectAssociation> createContributeeAssociations(final ObjectAdapter serviceAdapter) {
951        
952        final ObjectSpecification specification = serviceAdapter.getSpecification();
953        final List<ObjectAction> serviceActions = specification.getObjectActions(ActionType.USER, Contributed.INCLUDED, Filters.<ObjectAction>any());
954        
955        final List<ObjectActionImpl> contributedActions = Lists.newArrayList();
956        for (final ObjectAction serviceAction : serviceActions) {
957            if (isAlwaysHidden(serviceAction)) {
958                continue;
959            }
960            final NotContributedFacet notContributed = serviceAction.getFacet(NotContributedFacet.class);
961            if(notContributed != null && notContributed.toAssociations()) {
962                continue;
963            }
964            if(!serviceAction.hasReturn()) {
965                continue;
966            }
967            if (serviceAction.getParameterCount() != 1 || contributeeParameterMatchOf(serviceAction) == -1) {
968                continue;
969            }
970            if(!(serviceAction instanceof ObjectActionImpl)) {
971                continue;
972            }
973            contributedActions.add((ObjectActionImpl) serviceAction);
974        }
975        
976        return Lists.newArrayList(Iterables.transform(contributedActions, createContributeeAssociationFunctor(serviceAdapter, this)));
977    }
978
979
980    private Function<ObjectActionImpl, ObjectAssociation> createContributeeAssociationFunctor(
981            final ObjectAdapter serviceAdapter, final ObjectSpecification contributeeType) {
982        return new Function<ObjectActionImpl, ObjectAssociation>(){
983            @Override
984            public ObjectAssociation apply(ObjectActionImpl input) {
985                final ObjectSpecification returnType = input.getReturnType();
986                final ObjectAssociationAbstract association = returnType.isNotCollection() 
987                        ? new OneToOneAssociationContributee(serviceAdapter, input, contributeeType, objectMemberContext) 
988                        : new OneToManyAssociationContributee(serviceAdapter, input, contributeeType, objectMemberContext);
989                facetProcessor.processMemberOrder(metadataProperties, association);
990                return association;
991            }
992        };
993    }
994
995
996    // //////////////////////////////////////////////////////////////////////
997    // contributee actions
998    // //////////////////////////////////////////////////////////////////////
999
1000    /**
1001     * All contributee actions (each wrapping a service's contributed action) for this spec.
1002     * 
1003     * <p>
1004     * If this specification {@link #isService() is actually for} a service,
1005     * then returns an empty list.
1006     */
1007    protected List<ObjectAction> createContributeeActions() {
1008        if (isService()) {
1009            return Collections.emptyList();
1010        }
1011        final List<ObjectAction> contributeeActions = Lists.newArrayList();
1012            
1013        final List<ObjectAdapter> services = getServicesProvider().getServices();
1014        for (final ObjectAdapter serviceAdapter : services) {
1015            addContributeeActionsIfAny(serviceAdapter, contributeeActions);
1016        }
1017        return contributeeActions;
1018    }
1019
1020    private void addContributeeActionsIfAny(
1021            final ObjectAdapter serviceAdapter, 
1022            final List<ObjectAction> contributeeActionsToAppendTo) {
1023        final ObjectSpecification specification = serviceAdapter.getSpecification();
1024        if (specification == this) {
1025            return;
1026        }
1027        final List<ObjectAction> contributeeActions = Lists.newArrayList();
1028        final List<ObjectAction> serviceActions = specification.getObjectActions(ActionType.ALL, Contributed.INCLUDED, Filters.<ObjectAction>any());
1029        for (final ObjectAction serviceAction : serviceActions) {
1030            if (isAlwaysHidden(serviceAction)) {
1031                continue;
1032            }
1033            final NotContributedFacet notContributed = serviceAction.getFacet(NotContributedFacet.class);
1034            if(notContributed != null && notContributed.toActions()) {
1035                continue;
1036            }
1037            if(!(serviceAction instanceof ObjectActionImpl)) {
1038                continue;
1039            }
1040            final ObjectActionImpl contributedAction = (ObjectActionImpl) serviceAction;
1041        
1042            // see if qualifies by inspecting all parameters
1043            final int contributeeParam = contributeeParameterMatchOf(contributedAction);
1044            if (contributeeParam != -1) {
1045                ObjectActionContributee contributeeAction = 
1046                        new ObjectActionContributee(serviceAdapter, contributedAction, contributeeParam, this, objectMemberContext);
1047                facetProcessor.processMemberOrder(metadataProperties, contributeeAction);
1048                contributeeActions.add(contributeeAction);
1049            }
1050        }
1051        contributeeActionsToAppendTo.addAll(contributeeActions);
1052    }
1053    
1054    private boolean isAlwaysHidden(final FacetHolder holder) {
1055        final HiddenFacet hiddenFacet = holder.getFacet(HiddenFacet.class);
1056        return hiddenFacet != null && hiddenFacet.when() == When.ALWAYS && hiddenFacet.where() == Where.ANYWHERE;
1057    }
1058
1059    
1060
1061    /**
1062     * @param serviceAction - number of the parameter that matches, or -1 if none.
1063     */
1064    private int contributeeParameterMatchOf(final ObjectAction serviceAction) {
1065        final List<ObjectActionParameter> params = serviceAction.getParameters();
1066        for (final ObjectActionParameter param : params) {
1067            if (isOfType(param.getSpecification())) {
1068                return param.getNumber();
1069            }
1070        }
1071        return -1;
1072    }
1073
1074    // //////////////////////////////////////////////////////////////////////
1075    // validity
1076    // //////////////////////////////////////////////////////////////////////
1077
1078    @Override
1079    public Consent isValid(final ObjectAdapter inObject) {
1080        return isValidResult(inObject).createConsent();
1081    }
1082
1083    /**
1084     * TODO: currently this method is hard-coded to assume all interactions are
1085     * initiated {@link InteractionInvocationMethod#BY_USER by user}.
1086     */
1087    @Override
1088    public InteractionResult isValidResult(final ObjectAdapter targetObjectAdapter) {
1089        final ObjectValidityContext validityContext = createValidityInteractionContext(deploymentCategory, getAuthenticationSession(), InteractionInvocationMethod.BY_USER, targetObjectAdapter);
1090        return InteractionUtils.isValidResult(this, validityContext);
1091    }
1092
1093    /**
1094     * Create an {@link InteractionContext} representing an attempt to save the
1095     * object.
1096     */
1097    @Override
1098    public ObjectValidityContext createValidityInteractionContext(DeploymentCategory deploymentCategory, final AuthenticationSession session, final InteractionInvocationMethod interactionMethod, final ObjectAdapter targetObjectAdapter) {
1099        return new ObjectValidityContext(deploymentCategory, session, interactionMethod, targetObjectAdapter, getIdentifier());
1100    }
1101
1102    // //////////////////////////////////////////////////////////////////////
1103    // convenience isXxx (looked up from facets)
1104    // //////////////////////////////////////////////////////////////////////
1105
1106    @Override
1107    public boolean isImmutable() {
1108        return containsFacet(ImmutableFacet.class);
1109    }
1110
1111    @Override
1112    public boolean isHidden() {
1113        return containsFacet(HiddenFacet.class);
1114    }
1115
1116    @Override
1117    public boolean isParseable() {
1118        return containsFacet(ParseableFacet.class);
1119    }
1120
1121    @Override
1122    public boolean isEncodeable() {
1123        return containsFacet(EncodableFacet.class);
1124    }
1125
1126    @Override
1127    public boolean isValue() {
1128        return containsFacet(ValueFacet.class);
1129    }
1130
1131    @Override
1132    public boolean isParented() {
1133        return containsFacet(ParentedFacet.class);
1134    }
1135
1136    @Override
1137    public boolean isParentedOrFreeCollection() {
1138        return containsFacet(CollectionFacet.class);
1139    }
1140
1141    @Override
1142    public boolean isNotCollection() {
1143        return !isParentedOrFreeCollection();
1144    }
1145
1146    @Override
1147    public boolean isValueOrIsParented() {
1148        return isValue() || isParented();
1149    }
1150
1151    // //////////////////////////////////////////////////////////////////////
1152    // misc
1153    // //////////////////////////////////////////////////////////////////////
1154
1155    @Override
1156    public Object createObject() {
1157        throw new UnsupportedOperationException(getFullIdentifier());
1158    }
1159
1160    @Override
1161    public ObjectAdapter initialize(ObjectAdapter objectAdapter) {
1162        return objectAdapter;
1163    }
1164
1165    // //////////////////////////////////////////////////////////////////////
1166    // toString
1167    // //////////////////////////////////////////////////////////////////////
1168
1169    @Override
1170    public String toString() {
1171        final ToString str = new ToString(this);
1172        str.append("class", getFullIdentifier());
1173        return str.toString();
1174    }
1175
1176    // //////////////////////////////////////////////////////////////////////
1177    // Convenience
1178    // //////////////////////////////////////////////////////////////////////
1179
1180    /**
1181     * convenience method to return the current {@link AuthenticationSession}
1182     * from the {@link #getAuthenticationSessionProvider() injected}
1183     * {@link AuthenticationSessionProvider}.
1184     */
1185    protected final AuthenticationSession getAuthenticationSession() {
1186        return getAuthenticationSessionProvider().getAuthenticationSession();
1187    }
1188
1189    // //////////////////////////////////////////////////////////////////////
1190    // Dependencies (injected in constructor)
1191    // //////////////////////////////////////////////////////////////////////
1192
1193    protected AuthenticationSessionProvider getAuthenticationSessionProvider() {
1194        return authenticationSessionProvider;
1195    }
1196
1197    public ServicesProvider getServicesProvider() {
1198        return servicesProvider;
1199    }
1200
1201    public ObjectInstantiator getObjectInstantiator() {
1202        return objectInstantiator;
1203    }
1204
1205    public SpecificationLoader getSpecificationLookup() {
1206        return specificationLookup;
1207    }
1208
1209
1210}