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.dflt;
021
022import java.lang.reflect.Array;
023import java.lang.reflect.Method;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027import java.util.Properties;
028import com.google.common.collect.Lists;
029import com.google.common.collect.Maps;
030import org.slf4j.Logger;
031import org.slf4j.LoggerFactory;
032import org.apache.isis.applib.filter.Filter;
033import org.apache.isis.applib.filter.Filters;
034import org.apache.isis.applib.profiles.Perspective;
035import org.apache.isis.core.commons.debug.DebugBuilder;
036import org.apache.isis.core.commons.debug.DebuggableWithTitle;
037import org.apache.isis.core.commons.exceptions.IsisException;
038import org.apache.isis.core.commons.lang.StringExtensions;
039import org.apache.isis.core.commons.util.ToString;
040import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
041import org.apache.isis.core.metamodel.adapter.mgr.AdapterManager;
042import org.apache.isis.core.metamodel.facetapi.Facet;
043import org.apache.isis.core.metamodel.facetapi.FacetHolder;
044import org.apache.isis.core.metamodel.facets.FacetedMethod;
045import org.apache.isis.core.metamodel.facets.ImperativeFacet;
046import org.apache.isis.core.metamodel.facets.named.NamedFacet;
047import org.apache.isis.core.metamodel.facets.named.NamedFacetInferred;
048import org.apache.isis.core.metamodel.facets.object.callbacks.CallbackUtils;
049import org.apache.isis.core.metamodel.facets.object.callbacks.CreatedCallbackFacet;
050import org.apache.isis.core.metamodel.facets.object.icon.IconFacet;
051import org.apache.isis.core.metamodel.facets.object.plural.PluralFacet;
052import org.apache.isis.core.metamodel.facets.object.plural.PluralFacetInferred;
053import org.apache.isis.core.metamodel.facets.object.title.TitleFacet;
054import org.apache.isis.core.metamodel.facets.object.value.ValueFacet;
055import org.apache.isis.core.metamodel.runtimecontext.ServicesInjector;
056import org.apache.isis.core.metamodel.spec.*;
057import org.apache.isis.core.metamodel.spec.feature.*;
058import org.apache.isis.core.metamodel.specloader.classsubstitutor.ClassSubstitutor;
059import org.apache.isis.core.metamodel.specloader.specimpl.*;
060
061public class ObjectSpecificationDefault extends ObjectSpecificationAbstract implements DebuggableWithTitle, FacetHolder {
062
063    private final static Logger LOG = LoggerFactory.getLogger(ObjectSpecificationDefault.class);
064
065    private static String determineShortName(final Class<?> introspectedClass) {
066        final String name = introspectedClass.getName();
067        return name.substring(name.lastIndexOf('.') + 1);
068    }
069
070    // //////////////////////////////////////////////////////////////
071    // fields
072    // //////////////////////////////////////////////////////////////
073
074    private boolean isService;
075
076    /**
077     * Lazily built by {@link #getMember(Method)}.
078     */
079    private Map<Method, ObjectMember> membersByMethod = null;
080    
081    private final IntrospectionContext introspectionContext;
082    private final CreateObjectContext createObjectContext;
083
084    private final FacetedMethodsBuilder facetedMethodsBuilder;
085
086
087    // //////////////////////////////////////////////////////////////////////
088    // Constructor
089    // //////////////////////////////////////////////////////////////////////
090
091    public ObjectSpecificationDefault(
092            final Class<?> correspondingClass,
093            final FacetedMethodsBuilderContext facetedMethodsBuilderContext, 
094            final IntrospectionContext introspectionContext, 
095            final SpecificationContext specContext, 
096            final ObjectMemberContext objectMemberContext,
097            final CreateObjectContext createObjectContext) {
098        super(correspondingClass, determineShortName(correspondingClass), specContext, objectMemberContext);
099
100        this.facetedMethodsBuilder = new FacetedMethodsBuilder(this, facetedMethodsBuilderContext);
101
102        this.introspectionContext = introspectionContext;
103        this.createObjectContext = createObjectContext;
104    }
105
106    @Override
107    public void introspectTypeHierarchyAndMembers() {
108
109        metadataProperties = null;
110        if(isNotIntrospected()) {
111            metadataProperties = facetedMethodsBuilder.introspectClass();
112        }
113        
114        // name
115        if(isNotIntrospected()) {
116            addNamedFacetAndPluralFacetIfRequired();
117        }
118
119        // go no further if a value
120        if(this.containsFacet(ValueFacet.class)) {
121            if (LOG.isDebugEnabled()) {
122                LOG.debug("skipping full introspection for value type " + getFullIdentifier());
123            }
124            return;
125        }
126
127        // superclass
128        if(isNotIntrospected()) {
129            final Class<?> superclass = getCorrespondingClass().getSuperclass();
130            updateSuperclass(superclass);
131        }
132
133
134        // walk superinterfaces
135
136        //
137        // REVIEW: the processing here isn't quite the same as with
138        // superclasses, in that with superclasses the superclass adds this type as its
139        // subclass, whereas here this type defines itself as the subtype.
140        //
141        // it'd be nice to push the responsibility for adding subclasses to
142        // the interface type... needs some tests around it, though, before
143        // making that refactoring.
144        //
145        final Class<?>[] interfaceTypes = getCorrespondingClass().getInterfaces();
146        final List<ObjectSpecification> interfaceSpecList = Lists.newArrayList();
147        for (final Class<?> interfaceType : interfaceTypes) {
148            final Class<?> substitutedInterfaceType = getClassSubstitutor().getClass(interfaceType);
149            if (substitutedInterfaceType != null) {
150                final ObjectSpecification interfaceSpec = getSpecificationLookup().loadSpecification(substitutedInterfaceType);
151                interfaceSpecList.add(interfaceSpec);
152            }
153        }
154
155        if(isNotIntrospected()) {
156            updateAsSubclassTo(interfaceSpecList);
157        }
158        if(isNotIntrospected()) {
159            updateInterfaces(interfaceSpecList);
160        }
161
162        // associations and actions
163        if(isNotIntrospected()) {
164            final List<ObjectAssociation> associations = createAssociations(metadataProperties);
165            sortAndUpdateAssociations(associations);
166        }
167
168        if(isNotIntrospected()) {
169            final List<ObjectAction> actions = createActions(metadataProperties);
170            sortCacheAndUpdateActions(actions);
171        }
172
173        if(isNotIntrospected()) {
174            facetedMethodsBuilder.introspectClassPostProcessing(metadataProperties);    
175        }
176        
177        if(isNotIntrospected()) {
178            updateFromFacetValues();    
179        }
180    }
181
182    private void addNamedFacetAndPluralFacetIfRequired() {
183        NamedFacet namedFacet = getFacet(NamedFacet.class);
184        if (namedFacet == null) {
185            namedFacet = new NamedFacetInferred(StringExtensions.asNaturalName2(getShortIdentifier()), this);
186            addFacet(namedFacet);
187        }
188
189        PluralFacet pluralFacet = getFacet(PluralFacet.class);
190        if (pluralFacet == null) {
191            pluralFacet = new PluralFacetInferred(StringExtensions.asPluralName(namedFacet.value()), this);
192            addFacet(pluralFacet);
193        }
194    }
195
196    // //////////////////////////////////////////////////////////////////////
197    // create associations and actions
198    // //////////////////////////////////////////////////////////////////////
199
200    private List<ObjectAssociation> createAssociations(Properties properties) {
201        final List<FacetedMethod> associationFacetedMethods = facetedMethodsBuilder.getAssociationFacetedMethods(properties);
202        final List<ObjectAssociation> associations = Lists.newArrayList();
203        for (FacetedMethod facetedMethod : associationFacetedMethods) {
204            final ObjectAssociation association = createAssociation(facetedMethod);
205            if(association != null) {
206                associations.add(association);
207            }
208        }
209        return associations;
210    }
211    
212
213    private ObjectAssociation createAssociation(final FacetedMethod facetMethod) {
214        if (facetMethod.getFeatureType().isCollection()) {
215            return new OneToManyAssociationImpl(facetMethod, objectMemberContext);
216        } else if (facetMethod.getFeatureType().isProperty()) {
217            return new OneToOneAssociationImpl(facetMethod, objectMemberContext);
218        } else {
219            return null;
220        }
221    }
222
223    private List<ObjectAction> createActions(Properties metadataProperties) {
224        final List<FacetedMethod> actionFacetedMethods = facetedMethodsBuilder.getActionFacetedMethods(metadataProperties);
225        final List<ObjectAction> actions = Lists.newArrayList();
226        for (FacetedMethod facetedMethod : actionFacetedMethods) {
227            final ObjectAction action = createAction(facetedMethod);
228            if(action != null) {
229                actions.add(action);
230            }
231        }
232        return actions;
233    }
234
235
236    private ObjectAction createAction(final FacetedMethod facetedMethod) {
237        if (facetedMethod.getFeatureType().isAction()) {
238            return new ObjectActionImpl(facetedMethod, objectMemberContext);
239        } else {
240            return null;
241        }
242    }
243
244
245    // //////////////////////////////////////////////////////////////////////
246    // Whether a service or not
247    // //////////////////////////////////////////////////////////////////////
248
249    @Override
250    public boolean isService() {
251        return isService;
252    }
253
254    /**
255     * TODO: should ensure that service has at least one user action; fix when
256     * specification knows of its hidden methods.
257     * 
258     * <pre>
259     * if (objectActions != null &amp;&amp; objectActions.length == 0) {
260     *     throw new ObjectSpecificationException(&quot;Service object &quot; + getFullName() + &quot; should have at least one user action&quot;);
261     * }
262     * </pre>
263     */
264    @Override
265    public void markAsService() {
266        ensureServiceHasNoAssociations();
267        isService = true;
268    }
269
270    private void ensureServiceHasNoAssociations() {
271        final List<ObjectAssociation> associations = getAssociations(Contributed.EXCLUDED);
272        final StringBuilder buf = new StringBuilder();
273        for (final ObjectAssociation association : associations) {
274            final String name = association.getId();
275            // services are allowed to have one association, called 'id'
276            if (!isValidAssociationForService(name)) {
277                appendAssociationName(buf, name);
278            }
279        }
280        if (buf.length() > 0) {
281            throw new ObjectSpecificationException("Service object " + getFullIdentifier() + " should have no fields, but has: " + buf);
282        }
283    }
284
285    /**
286     * Services are allowed to have one association, called 'id'.
287     * 
288     * <p>
289     * This is used for {@link Perspective}s (user profiles).
290     */
291    private boolean isValidAssociationForService(final String associationId) {
292        return "id".indexOf(associationId) != -1;
293    }
294
295    private void appendAssociationName(final StringBuilder fieldNames, final String name) {
296        fieldNames.append(fieldNames.length() > 0 ? ", " : "");
297        fieldNames.append(name);
298    }
299
300    // //////////////////////////////////////////////////////////////////////
301    // getObjectAction
302    // //////////////////////////////////////////////////////////////////////
303
304    @Override
305    public ObjectAction getObjectAction(final ActionType type, final String id, final List<ObjectSpecification> parameters) {
306        final List<ObjectAction> actions = 
307                getObjectActions(type, Contributed.INCLUDED, Filters.<ObjectAction>any());
308        return firstAction(actions, id, parameters);
309    }
310
311    @Override
312    public ObjectAction getObjectAction(final ActionType type, final String id) {
313        final List<ObjectAction> actions = 
314                getObjectActions(type, Contributed.INCLUDED, Filters.<ObjectAction>any()); 
315        return firstAction(actions, id);
316    }
317
318    @Override
319    public ObjectAction getObjectAction(final String id) {
320        final List<ObjectAction> actions = 
321                getObjectActions(ActionType.ALL, Contributed.INCLUDED, Filters.<ObjectAction>any()); 
322        return firstAction(actions, id);
323    }
324
325    private static ObjectAction firstAction(
326            final List<ObjectAction> candidateActions, 
327            final String actionName, 
328            final List<ObjectSpecification> parameters) {
329        outer: for (int i = 0; i < candidateActions.size(); i++) {
330            final ObjectAction action = candidateActions.get(i);
331            if (actionName != null && !actionName.equals(action.getId())) {
332                continue outer;
333            }
334            if (action.getParameters().size() != parameters.size()) {
335                continue outer;
336            }
337            for (int j = 0; j < parameters.size(); j++) {
338                if (!parameters.get(j).isOfType(action.getParameters().get(j).getSpecification())) {
339                    continue outer;
340                }
341            }
342            return action;
343        }
344        return null;
345    }
346
347    private static ObjectAction firstAction(
348            final List<ObjectAction> candidateActions, 
349            final String id) {
350        if (id == null) {
351            return null;
352        }
353        for (int i = 0; i < candidateActions.size(); i++) {
354            final ObjectAction action = candidateActions.get(i);
355            if (id.equals(action.getIdentifier().toNameParmsIdentityString())) {
356                return action;
357            }
358            if (id.equals(action.getIdentifier().toNameIdentityString())) {
359                return action;
360            }
361            continue;
362        }
363        return null;
364    }
365
366    // //////////////////////////////////////////////////////////////////////
367    // createObject
368    // //////////////////////////////////////////////////////////////////////
369
370    @Override
371    public Object createObject() {
372        if (getCorrespondingClass().isArray()) {
373            return Array.newInstance(getCorrespondingClass().getComponentType(), 0);
374        }
375        
376        try {
377            return getObjectInstantiator().instantiate(getCorrespondingClass());
378        } catch (final ObjectInstantiationException e) {
379            throw new IsisException("Failed to create instance of type " + getFullIdentifier(), e);
380        }
381    }
382
383    /**
384     * REVIEW: does this behaviour live best here?  Not that sure that it does...
385     */
386    @Override
387    public ObjectAdapter initialize(final ObjectAdapter adapter) {
388                        
389        // initialize new object
390        final List<ObjectAssociation> fields = adapter.getSpecification().getAssociations(Contributed.EXCLUDED);
391        for (ObjectAssociation field : fields) {
392            field.toDefault(adapter);
393        }
394        getDependencyInjector().injectServicesInto(adapter.getObject());
395        
396        CallbackUtils.callCallback(adapter, CreatedCallbackFacet.class);
397        
398        return adapter;
399    }
400
401
402    // //////////////////////////////////////////////////////////////////////
403    // getMember, catalog... (not API)
404    // //////////////////////////////////////////////////////////////////////
405
406    public ObjectMember getMember(final Method method) {
407        if (membersByMethod == null) {
408            this.membersByMethod = catalogueMembers();
409        }
410        return membersByMethod.get(method);
411    }
412
413    private HashMap<Method, ObjectMember> catalogueMembers() {
414        final HashMap<Method, ObjectMember> membersByMethod = Maps.newHashMap();
415        cataloguePropertiesAndCollections(membersByMethod);
416        catalogueActions(membersByMethod);
417        return membersByMethod;
418    }
419    
420    private void cataloguePropertiesAndCollections(final Map<Method, ObjectMember> membersByMethod) {
421        final Filter<ObjectAssociation> noop = Filters.anyOfType(ObjectAssociation.class);
422        final List<ObjectAssociation> fields = getAssociations(Contributed.EXCLUDED, noop);
423        for (int i = 0; i < fields.size(); i++) {
424            final ObjectAssociation field = fields.get(i);
425            final List<Facet> facets = field.getFacets(ImperativeFacet.FILTER);
426            for (final Facet facet : facets) {
427                final ImperativeFacet imperativeFacet = ImperativeFacet.Util.getImperativeFacet(facet);
428                for (final Method imperativeFacetMethod : imperativeFacet.getMethods()) {
429                    membersByMethod.put(imperativeFacetMethod, field);
430                }
431            }
432        }
433    }
434
435    private void catalogueActions(final Map<Method, ObjectMember> membersByMethod) {
436        final List<ObjectAction> userActions = getObjectActions(Contributed.INCLUDED);
437        for (int i = 0; i < userActions.size(); i++) {
438            final ObjectAction userAction = userActions.get(i);
439            final List<Facet> facets = userAction.getFacets(ImperativeFacet.FILTER);
440            for (final Facet facet : facets) {
441                final ImperativeFacet imperativeFacet = ImperativeFacet.Util.getImperativeFacet(facet);
442                for (final Method imperativeFacetMethod : imperativeFacet.getMethods()) {
443                    membersByMethod.put(imperativeFacetMethod, userAction);
444                }
445            }
446        }
447    }
448
449    // //////////////////////////////////////////////////////////////////////
450    // Debug, toString
451    // //////////////////////////////////////////////////////////////////////
452
453    @Override
454    public void debugData(final DebugBuilder debug) {
455        debug.blankLine();
456        debug.appendln("Title", getFacet(TitleFacet.class));
457        final IconFacet iconFacet = getFacet(IconFacet.class);
458        if (iconFacet != null) {
459            debug.appendln("Icon", iconFacet);
460        }
461        debug.unindent();
462    }
463
464    @Override
465    public String debugTitle() {
466        return "NO Member Specification";
467    }
468
469    @Override
470    public String toString() {
471        final ToString str = new ToString(this);
472        str.append("class", getFullIdentifier());
473        str.append("type", (isParentedOrFreeCollection() ? "Collection" : "Object"));
474        str.append("persistable", persistability());
475        str.append("superclass", superclass() == null ? "Object" : superclass().getFullIdentifier());
476        return str.toString();
477    }
478
479    // //////////////////////////////////////////////////////////////////
480    // Dependencies (from constructor)
481    // //////////////////////////////////////////////////////////////////
482
483    protected AdapterManager getAdapterMap() {
484        return createObjectContext.getAdapterManager();
485    }
486
487    protected ServicesInjector getDependencyInjector() {
488        return createObjectContext.getDependencyInjector();
489    }
490
491    private ClassSubstitutor getClassSubstitutor() {
492        return introspectionContext.getClassSubstitutor();
493    }
494
495
496}