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.List;
023
024import com.google.common.collect.Lists;
025
026import org.apache.isis.applib.Identifier;
027import org.apache.isis.applib.filter.Filter;
028import org.apache.isis.applib.profiles.Localization;
029import org.apache.isis.applib.query.Query;
030import org.apache.isis.applib.query.QueryFindAllInstances;
031import org.apache.isis.core.commons.authentication.AuthenticationSession;
032import org.apache.isis.core.commons.authentication.AuthenticationSessionProvider;
033import org.apache.isis.core.commons.lang.ClassExtensions;
034import org.apache.isis.core.commons.lang.ListExtensions;
035import org.apache.isis.core.commons.lang.StringExtensions;
036import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
037import org.apache.isis.core.metamodel.adapter.QuerySubmitter;
038import org.apache.isis.core.metamodel.adapter.mgr.AdapterManager;
039import org.apache.isis.core.metamodel.consent.Allow;
040import org.apache.isis.core.metamodel.consent.Consent;
041import org.apache.isis.core.metamodel.consent.InteractionInvocationMethod;
042import org.apache.isis.core.metamodel.consent.InteractionResultSet;
043import org.apache.isis.core.metamodel.deployment.DeploymentCategory;
044import org.apache.isis.core.metamodel.facetapi.Facet;
045import org.apache.isis.core.metamodel.facetapi.MultiTypedFacet;
046import org.apache.isis.core.metamodel.facets.TypedHolder;
047import org.apache.isis.core.metamodel.facets.describedas.DescribedAsFacet;
048import org.apache.isis.core.metamodel.facets.mandatory.MandatoryFacet;
049import org.apache.isis.core.metamodel.facets.named.NamedFacet;
050import org.apache.isis.core.metamodel.facets.param.autocomplete.ActionParameterAutoCompleteFacet;
051import org.apache.isis.core.metamodel.facets.param.choices.ActionParameterChoicesFacet;
052import org.apache.isis.core.metamodel.facets.param.defaults.ActionParameterDefaultsFacet;
053import org.apache.isis.core.metamodel.interactions.ActionArgumentContext;
054import org.apache.isis.core.metamodel.interactions.InteractionUtils;
055import org.apache.isis.core.metamodel.interactions.ValidityContext;
056import org.apache.isis.core.metamodel.spec.DomainModelException;
057import org.apache.isis.core.metamodel.spec.ObjectSpecification;
058import org.apache.isis.core.metamodel.spec.SpecificationLoader;
059import org.apache.isis.core.metamodel.spec.feature.ObjectAction;
060import org.apache.isis.core.metamodel.spec.feature.ObjectActionParameter;
061import org.apache.isis.core.progmodel.facets.object.bounded.ChoicesFacetFromBoundedAbstract;
062import org.apache.isis.core.progmodel.facets.param.autocomplete.MinLengthUtil;
063
064public abstract class ObjectActionParameterAbstract implements ObjectActionParameter {
065
066    private final int number;
067    private final ObjectActionImpl parentAction;
068    private final TypedHolder peer;
069
070    protected ObjectActionParameterAbstract(final int number, final ObjectActionImpl objectAction, final TypedHolder peer) {
071        this.number = number;
072        this.parentAction = objectAction;
073        this.peer = peer;
074    }
075
076    /**
077     * Subclasses should override either {@link #isObject()} or
078     * {@link #isCollection()}.
079     */
080    @Override
081    public boolean isObject() {
082        return false;
083    }
084
085    /**
086     * Subclasses should override either {@link #isObject()} or
087     * {@link #isCollection()}.
088     */
089    @Override
090    public boolean isCollection() {
091        return false;
092    }
093
094    /**
095     * Parameter number, 0-based.
096     */
097    @Override
098    public int getNumber() {
099        return number;
100    }
101
102    @Override
103    public ObjectAction getAction() {
104        return parentAction;
105    }
106
107    /**
108     * NOT API, but exposed for the benefit of {@link ObjectActionParameterContributee}.
109     */
110    public TypedHolder getPeer() {
111        return peer;
112    }
113
114    @Override
115    public ObjectSpecification getSpecification() {
116        return ObjectMemberAbstract.getSpecification(getSpecificationLookup(), peer.getType());
117    }
118
119    @Override
120    public Identifier getIdentifier() {
121        return parentAction.getIdentifier();
122    }
123
124    @Override
125    public String getId() {
126        final NamedFacet facet = getFacet(NamedFacet.class);
127        if (facet != null && facet.value() != null) {
128            return StringExtensions.asCamelLowerFirst(facet.value());
129        }
130        final String name = getSpecification().getSingularName();
131        final List<ObjectActionParameter> parameters = this.getAction().getParameters(new Filter<ObjectActionParameter>() {
132
133            @Override
134            public boolean accept(final ObjectActionParameter t) {
135                return equalsShortIdentifier(t.getSpecification(), getSpecification());
136            }
137
138            protected boolean equalsShortIdentifier(final ObjectSpecification spec1, final ObjectSpecification spec2) {
139                return spec1.getShortIdentifier().toLowerCase().equals(spec2.getShortIdentifier().toLowerCase());
140            }
141        });
142        if (parameters.size() == 1) {
143            return StringExtensions.asCamelLowerFirst(name);
144        }
145        final int indexOf = parameters.indexOf(this);
146        return StringExtensions.asCamelLowerFirst(name + (indexOf + 1));
147    }
148
149    @Override
150    public String getName() {
151        final NamedFacet facet = getFacet(NamedFacet.class);
152        if (facet != null && facet.value() != null) {
153            return facet.value();
154        }
155        final String name = getSpecification().getSingularName();
156        final List<ObjectActionParameter> parameters = getAction().getParameters(new Filter<ObjectActionParameter>() {
157
158            @Override
159            public boolean accept(final ObjectActionParameter t) {
160                return equalsShortIdentifier(t.getSpecification(), getSpecification());
161            }
162
163            protected boolean equalsShortIdentifier(final ObjectSpecification spec1, final ObjectSpecification spec2) {
164                return spec1.getShortIdentifier().toLowerCase().equals(spec2.getShortIdentifier().toLowerCase());
165            }
166        });
167        if (parameters.size() == 1) {
168            return name;
169        }
170        final int indexOf = parameters.indexOf(this);
171        return name + " " + (indexOf + 1);
172    }
173
174    @Override
175    public String getDescription() {
176        final DescribedAsFacet facet = getFacet(DescribedAsFacet.class);
177        final String description = facet.value();
178        return description == null ? "" : description;
179    }
180
181    @Override
182    public boolean isOptional() {
183        final MandatoryFacet facet = getFacet(MandatoryFacet.class);
184        return facet.isInvertedSemantics();
185    }
186
187    public Consent isUsable() {
188        return Allow.DEFAULT;
189    }
190
191    // //////////////////////////////////////////////////////////
192    // FacetHolder
193    // //////////////////////////////////////////////////////////
194
195    @Override
196    public boolean containsFacet(final Class<? extends Facet> facetType) {
197        return peer != null ? peer.containsFacet(facetType) : false;
198    }
199
200    @Override
201    public boolean containsDoOpFacet(final Class<? extends Facet> facetType) {
202        return peer == null ? false : peer.containsDoOpFacet(facetType);
203    }
204
205    @Override
206    public <T extends Facet> T getFacet(final Class<T> cls) {
207        return peer != null ? peer.getFacet(cls) : null;
208    }
209
210    @SuppressWarnings("unchecked")
211    @Override
212    public Class<? extends Facet>[] getFacetTypes() {
213        return peer != null ? peer.getFacetTypes() : new Class[] {};
214    }
215
216    @Override
217    public List<Facet> getFacets(final Filter<Facet> filter) {
218        return peer != null ? peer.getFacets(filter) : Lists.<Facet> newArrayList();
219    }
220
221    @Override
222    public void addFacet(final Facet facet) {
223        if (peer != null) {
224            peer.addFacet(facet);
225        }
226    }
227
228    @Override
229    public void addFacet(final MultiTypedFacet facet) {
230        if (peer != null) {
231            peer.addFacet(facet);
232        }
233    }
234
235    @Override
236    public void removeFacet(final Facet facet) {
237        if (peer != null) {
238            peer.removeFacet(facet);
239        }
240    }
241
242    @Override
243    public void removeFacet(final Class<? extends Facet> facetType) {
244        if (peer != null) {
245            peer.removeFacet(facetType);
246        }
247    }
248
249
250    // /////////////////////////////////////////////////////////////
251    // AutoComplete
252    // /////////////////////////////////////////////////////////////
253
254    @Override
255    public boolean hasAutoComplete() {
256        final ActionParameterAutoCompleteFacet facet = getFacet(ActionParameterAutoCompleteFacet.class);
257        return facet != null;
258    }
259
260    @Override
261    public ObjectAdapter[] getAutoComplete(ObjectAdapter adapter, String searchArg) {
262        final List<ObjectAdapter> adapters = Lists.newArrayList();
263        final ActionParameterAutoCompleteFacet facet = getFacet(ActionParameterAutoCompleteFacet.class);
264
265        if (facet != null) {
266            final Object[] choices = facet.autoComplete(adapter, searchArg);
267            checkChoicesOrAutoCompleteType(getSpecificationLookup(), choices, getSpecification());
268            for (final Object choice : choices) {
269                adapters.add(getAdapterMap().adapterFor(choice));
270            }
271        }
272        /* // now incorporated into above choices processing (BoundedFacet is no more)
273        if (adapters.size() == 0 && ChoicesFacetUtils.hasChoices(getSpecification())) {
274            addAllInstancesForType(adapters);
275        }
276        */
277        return adapters.toArray(new ObjectAdapter[0]);
278    }
279
280    @Override
281    public int getAutoCompleteMinLength() {
282        final ActionParameterAutoCompleteFacet facet = getFacet(ActionParameterAutoCompleteFacet.class);
283        return facet != null? facet.getMinLength(): MinLengthUtil.MIN_LENGTH_DEFAULT;
284    }
285
286
287    // /////////////////////////////////////////////////////////////
288    // Choices
289    // /////////////////////////////////////////////////////////////
290
291    @Override
292    public boolean hasChoices() {
293        final ActionParameterChoicesFacet choicesFacet = getFacet(ActionParameterChoicesFacet.class);
294        return choicesFacet != null;
295    }
296
297    @Override
298    public ObjectAdapter[] getChoices(final ObjectAdapter adapter, final ObjectAdapter[] argumentsIfAvailable) {
299        final List<ObjectAdapter> argListIfAvailable = ListExtensions.mutableCopy(argumentsIfAvailable);
300        
301        final ObjectAdapter target = targetForDefaultOrChoices(adapter, argListIfAvailable);
302        final List<ObjectAdapter> args = argsForDefaultOrChoices(adapter, argListIfAvailable);
303        
304        return findChoices(target, args);
305    }
306
307    private ObjectAdapter[] findChoices(final ObjectAdapter target, final List<ObjectAdapter> args) {
308        final List<ObjectAdapter> adapters = Lists.newArrayList();
309        final ActionParameterChoicesFacet facet = getFacet(ActionParameterChoicesFacet.class);
310
311        if (facet != null) {
312            final Object[] choices = facet.getChoices(target, args);
313            checkChoicesOrAutoCompleteType(getSpecificationLookup(), choices, getSpecification());
314            for (final Object choice : choices) {
315                ObjectAdapter adapter = choice != null? getAdapterMap().adapterFor(choice) : null;
316                adapters.add(adapter);
317            }
318        }
319        // now incorporated into above choices processing (BoundedFacet is no more)
320        /* 
321           if (adapters.size() == 0 && BoundedFacetUtils.isBoundedSet(getSpecification())) {
322            addAllInstancesForType(adapters);
323        }
324        */
325        return adapters.toArray(new ObjectAdapter[0]);
326    }
327    
328    // /////////////////////////////////////////////////////////////
329    // Defaults
330    // /////////////////////////////////////////////////////////////
331
332    @Override
333    public ObjectAdapter getDefault(final ObjectAdapter adapter) {
334        
335        final ObjectAdapter target = targetForDefaultOrChoices(adapter, null);
336        final List<ObjectAdapter> args = argsForDefaultOrChoices(adapter, null);
337        
338        return findDefault(target, args);
339    }
340
341    private ObjectAdapter findDefault(
342            final ObjectAdapter target, 
343            final List<ObjectAdapter> args) {
344        final ActionParameterDefaultsFacet defaultsFacet = getFacet(ActionParameterDefaultsFacet.class);
345        if (defaultsFacet != null) {
346            final Object dflt = defaultsFacet.getDefault(target, args);
347            if (dflt == null) {
348                // it's possible that even though there is a default facet, when
349                // invoked it is unable to return a default.
350                return null;
351            }
352            return getAdapterMap().adapterFor(dflt);
353        }
354        return null;
355    }
356
357    /**
358     * Hook method; {@link ObjectActionParameterContributee contributed action parameter}s override.
359     */
360    protected ObjectAdapter targetForDefaultOrChoices(ObjectAdapter adapter, final List<ObjectAdapter> argumentsIfAvailable) {
361        return adapter;
362    }
363
364    /**
365     * Hook method; {@link ObjectActionParameterContributee contributed action parameter}s override.
366     */
367    protected List<ObjectAdapter> argsForDefaultOrChoices(final ObjectAdapter adapter, final List<ObjectAdapter> argumentsIfAvailable) {
368        return argumentsIfAvailable;
369    }
370
371    
372    // helpers
373
374    static void checkChoicesOrAutoCompleteType(final SpecificationLoader specificationLookup, final Object[] objects, final ObjectSpecification paramSpec) {
375        for (final Object object : objects) {
376
377            if(object == null) {
378                continue;
379            }
380            
381            // check type, but wrap first 
382            // (eg we treat int.class and java.lang.Integer.class as compatible with each other)
383            final Class<? extends Object> choiceClass = object.getClass();
384            final Class<?> paramClass = paramSpec.getCorrespondingClass();
385            
386            final Class<? extends Object> choiceWrappedClass = ClassExtensions.asWrappedIfNecessary(choiceClass);
387            final Class<? extends Object> paramWrappedClass = ClassExtensions.asWrappedIfNecessary(paramClass);
388            
389            final ObjectSpecification choiceWrappedSpec = specificationLookup.loadSpecification(choiceWrappedClass);
390            final ObjectSpecification paramWrappedSpec = specificationLookup.loadSpecification(paramWrappedClass);
391            
392            if (!choiceWrappedSpec.isOfType(paramWrappedSpec)) {
393                throw new DomainModelException("Type incompatible with parameter type; expected " + paramSpec.getFullIdentifier() + ", but was " + choiceClass.getName());
394            }
395        }
396    }
397
398    /**
399     * unused - incorporated into the {@link ChoicesFacetFromBoundedAbstract}
400     */
401    @SuppressWarnings("unused")
402    private <T> void addAllInstancesForType(final List<ObjectAdapter> adapters) {
403        final Query<T> query = new QueryFindAllInstances<T>(getSpecification().getFullIdentifier());
404        final List<ObjectAdapter> allInstancesAdapter = getQuerySubmitter().allMatchingQuery(query);
405        for (final ObjectAdapter choiceAdapter : allInstancesAdapter) {
406            adapters.add(choiceAdapter);
407        }
408    }
409
410    
411    // /////////////////////////////////////////////////////////////
412    // Validation
413    // /////////////////////////////////////////////////////////////
414
415    @Override
416    public ActionArgumentContext createProposedArgumentInteractionContext(final AuthenticationSession session, final InteractionInvocationMethod invocationMethod, final ObjectAdapter targetObject, final ObjectAdapter[] proposedArguments, final int position) {
417        return new ActionArgumentContext(getDeploymentCategory(), getAuthenticationSession(), invocationMethod, targetObject, getIdentifier(), proposedArguments, position);
418    }
419
420    @Override
421    public String isValid(final ObjectAdapter adapter, final Object proposedValue, final Localization localization) {
422        
423        ObjectAdapter proposedValueAdapter = null;
424        ObjectSpecification proposedValueSpec;
425        if(proposedValue != null) {
426            proposedValueAdapter = getAdapterMap().adapterFor(proposedValue);
427            proposedValueSpec = proposedValueAdapter.getSpecification();
428            if(!proposedValueSpec.isOfType(proposedValueSpec)) {
429                proposedValueAdapter = doCoerceProposedValue(adapter, proposedValue, localization);
430            }
431            
432            // check has been coerced into correct type; otherwise give up
433            if(proposedValueAdapter == null) {
434                return null;
435            }
436            proposedValueSpec = proposedValueAdapter.getSpecification();
437            if(!proposedValueSpec.isOfType(proposedValueSpec)) {
438                return null;
439            }
440        }
441        
442
443        final ValidityContext<?> ic = createProposedArgumentInteractionContext(getAuthenticationSession(), InteractionInvocationMethod.BY_USER, adapter, arguments(proposedValueAdapter), getNumber());
444
445        final InteractionResultSet buf = new InteractionResultSet();
446        InteractionUtils.isValidResultSet(this, ic, buf);
447        if (buf.isVetoed()) {
448            return buf.getInteractionResult().getReason();
449        }
450        return null;
451
452    }
453
454    /**
455     * Optional hook for parsing.
456     */
457    protected ObjectAdapter doCoerceProposedValue(ObjectAdapter adapter, Object proposedValue, final Localization localization) {
458        return null;
459    }
460
461    /**
462     * TODO: this is not ideal, because we can only populate the array for
463     * single argument, rather than the entire argument set. Instead, we ought
464     * to do this in two passes, one to build up the argument set as a single
465     * unit, and then validate each in turn.
466     */
467    private ObjectAdapter[] arguments(final ObjectAdapter proposedValue) {
468        final int parameterCount = getAction().getParameterCount();
469        final ObjectAdapter[] arguments = new ObjectAdapter[parameterCount];
470        arguments[getNumber()] = proposedValue;
471        return arguments;
472    }
473
474    // /////////////////////////////////////////////////////////////
475    // Dependencies (from parent)
476    // /////////////////////////////////////////////////////////////
477
478    private DeploymentCategory getDeploymentCategory() {
479        return parentAction.getDeploymentCategory();
480    }
481
482    protected SpecificationLoader getSpecificationLookup() {
483        return parentAction.getSpecificationLookup();
484    }
485
486    protected AuthenticationSessionProvider getAuthenticationSessionProvider() {
487        return parentAction.getAuthenticationSessionProvider();
488    }
489
490    protected AdapterManager getAdapterMap() {
491        return parentAction.getAdapterManager();
492    }
493
494    protected QuerySubmitter getQuerySubmitter() {
495        return parentAction.getQuerySubmitter();
496    }
497
498    protected AuthenticationSession getAuthenticationSession() {
499        return getAuthenticationSessionProvider().getAuthenticationSession();
500    }
501
502}