/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.isis.core.metamodel.specloader.specimpl; import java.util.List; import com.google.common.collect.Lists; import org.apache.isis.applib.Identifier; import org.apache.isis.applib.filter.Filter; import org.apache.isis.applib.query.Query; import org.apache.isis.applib.query.QueryFindAllInstances; import org.apache.isis.core.commons.lang.ClassExtensions; import org.apache.isis.core.commons.lang.ListExtensions; import org.apache.isis.core.commons.lang.StringExtensions; import org.apache.isis.core.metamodel.adapter.MutableProposedHolder; import org.apache.isis.core.metamodel.adapter.ObjectAdapter; import org.apache.isis.core.metamodel.adapter.mgr.AdapterManager; import org.apache.isis.core.metamodel.consent.Allow; import org.apache.isis.core.metamodel.consent.Consent; import org.apache.isis.core.metamodel.consent.InteractionInitiatedBy; import org.apache.isis.core.metamodel.consent.InteractionResultSet; import org.apache.isis.core.metamodel.facetapi.Facet; import org.apache.isis.core.metamodel.facetapi.FacetHolder; import org.apache.isis.core.metamodel.facetapi.FeatureType; import org.apache.isis.core.metamodel.facetapi.MultiTypedFacet; import org.apache.isis.core.metamodel.facets.TypedHolder; import org.apache.isis.core.metamodel.facets.all.describedas.DescribedAsFacet; import org.apache.isis.core.metamodel.facets.all.named.NamedFacet; import org.apache.isis.core.metamodel.facets.object.choices.ChoicesFacetFromBoundedAbstract; import org.apache.isis.core.metamodel.facets.objectvalue.mandatory.MandatoryFacet; import org.apache.isis.core.metamodel.facets.param.autocomplete.ActionParameterAutoCompleteFacet; import org.apache.isis.core.metamodel.facets.param.autocomplete.MinLengthUtil; import org.apache.isis.core.metamodel.facets.param.choices.ActionParameterChoicesFacet; import org.apache.isis.core.metamodel.facets.param.defaults.ActionParameterDefaultsFacet; import org.apache.isis.core.metamodel.interactions.ActionArgValidityContext; import org.apache.isis.core.metamodel.interactions.InteractionUtils; import org.apache.isis.core.metamodel.interactions.ValidityContext; import org.apache.isis.core.metamodel.services.persistsession.PersistenceSessionServiceInternal; import org.apache.isis.core.metamodel.spec.DomainModelException; import org.apache.isis.core.metamodel.spec.Instance; import org.apache.isis.core.metamodel.spec.ObjectSpecification; import org.apache.isis.core.metamodel.spec.feature.ObjectAction; import org.apache.isis.core.metamodel.spec.feature.ObjectActionParameter; import org.apache.isis.core.metamodel.specloader.SpecificationLoader; public abstract class ObjectActionParameterAbstract implements ObjectActionParameter { private final FeatureType featureType; private final int number; private final ObjectActionDefault parentAction; private final TypedHolder peer; protected ObjectActionParameterAbstract( final FeatureType featureType, final int number, final ObjectActionDefault objectAction, final TypedHolder peer) { this.featureType = featureType; this.number = number; this.parentAction = objectAction; this.peer = peer; } @Override public FeatureType getFeatureType() { return featureType; } /** * Gets the proposed value of the {@link Instance} (downcast as a * {@link MutableProposedHolder}, wrapping the proposed value into a * {@link ObjectAdapter}. */ @Override public ObjectAdapter get(final ObjectAdapter owner, final InteractionInitiatedBy interactionInitiatedBy) { final MutableProposedHolder proposedHolder = getProposedHolder(owner); final Object proposed = proposedHolder.getProposed(); return getAdapterMap().adapterFor(proposed); } protected MutableProposedHolder getProposedHolder(final ObjectAdapter owner) { if (!(owner instanceof MutableProposedHolder)) { throw new IllegalArgumentException("Instance should implement MutableProposedHolder"); } return (MutableProposedHolder) owner; } /** * Parameter number, 0-based. */ @Override public int getNumber() { return number; } @Override public ObjectAction getAction() { return parentAction; } /** * NOT API, but exposed for the benefit of {@link ObjectActionParameterContributee} * and {@link ObjectActionParameterMixedIn}. */ TypedHolder getPeer() { return peer; } @Override public ObjectSpecification getSpecification() { return ObjectMemberAbstract.getSpecification(getSpecificationLoader(), peer.getType()); } @Override public Identifier getIdentifier() { return parentAction.getIdentifier(); } @Override public String getId() { final NamedFacet facet = getFacet(NamedFacet.class); if (facet != null && facet.value() != null) { return StringExtensions.asCamelLowerFirst(facet.value()); } final String name = getSpecification().getSingularName(); final List<ObjectActionParameter> parameters = this.getAction().getParameters(new Filter<ObjectActionParameter>() { @Override public boolean accept(final ObjectActionParameter t) { return equalsShortIdentifier(t.getSpecification(), getSpecification()); } protected boolean equalsShortIdentifier(final ObjectSpecification spec1, final ObjectSpecification spec2) { return spec1.getShortIdentifier().toLowerCase().equals(spec2.getShortIdentifier().toLowerCase()); } }); if (parameters.size() == 1) { return StringExtensions.asCamelLowerFirst(name); } final int indexOf = parameters.indexOf(this); return StringExtensions.asCamelLowerFirst(name + (indexOf + 1)); } @Override public String getName() { final NamedFacet facet = getFacet(NamedFacet.class); if (facet != null && facet.value() != null) { return facet.value(); } final String name = getSpecification().getSingularName(); final List<ObjectActionParameter> parameters = getAction().getParameters(new Filter<ObjectActionParameter>() { @Override public boolean accept(final ObjectActionParameter t) { return equalsShortIdentifier(t.getSpecification(), getSpecification()); } protected boolean equalsShortIdentifier(final ObjectSpecification spec1, final ObjectSpecification spec2) { return spec1.getShortIdentifier().toLowerCase().equals(spec2.getShortIdentifier().toLowerCase()); } }); if (parameters.size() == 1) { return name; } final int indexOf = parameters.indexOf(this); return name + " " + (indexOf + 1); } @Override public String getDescription() { final DescribedAsFacet facet = getFacet(DescribedAsFacet.class); final String description = facet.value(); return description == null ? "" : description; } @Override public boolean isOptional() { final MandatoryFacet facet = getFacet(MandatoryFacet.class); return facet.isInvertedSemantics(); } public Consent isUsable() { return Allow.DEFAULT; } //region > FacetHolder protected FacetHolder getFacetHolder() { return peer; } @Override public boolean containsFacet(final Class<? extends Facet> facetType) { final FacetHolder facetHolder = getFacetHolder(); return facetHolder != null && facetHolder.containsFacet(facetType); } @Override public boolean containsDoOpFacet(final Class<? extends Facet> facetType) { final FacetHolder facetHolder = getFacetHolder(); return facetHolder != null && facetHolder.containsDoOpFacet(facetType); } @Override public boolean containsDoOpNotDerivedFacet(final Class<? extends Facet> facetType) { final FacetHolder facetHolder = getFacetHolder(); return facetHolder != null && facetHolder.containsDoOpNotDerivedFacet(facetType); } @Override public <T extends Facet> T getFacet(final Class<T> cls) { final FacetHolder facetHolder = getFacetHolder(); return facetHolder != null ? facetHolder.getFacet(cls) : null; } @SuppressWarnings("unchecked") @Override public Class<? extends Facet>[] getFacetTypes() { final FacetHolder facetHolder = getFacetHolder(); return facetHolder != null ? facetHolder.getFacetTypes() : new Class[] {}; } @Override public List<Facet> getFacets(final Filter<Facet> filter) { final FacetHolder facetHolder = getFacetHolder(); return facetHolder != null ? facetHolder.getFacets(filter) : Lists.<Facet> newArrayList(); } @Override public void addFacet(final Facet facet) { final FacetHolder facetHolder = getFacetHolder(); if (facetHolder != null) { facetHolder.addFacet(facet); } } @Override public void addFacet(final MultiTypedFacet facet) { final FacetHolder facetHolder = getFacetHolder(); if (facetHolder != null) { facetHolder.addFacet(facet); } } @Override public void removeFacet(final Facet facet) { final FacetHolder facetHolder = getFacetHolder(); if (facetHolder != null) { facetHolder.removeFacet(facet); } } @Override public void removeFacet(final Class<? extends Facet> facetType) { final FacetHolder facetHolder = getFacetHolder(); if (facetHolder != null) { facetHolder.removeFacet(facetType); } } //endregion //region > AutoComplete @Override public boolean hasAutoComplete() { final ActionParameterAutoCompleteFacet facet = getFacet(ActionParameterAutoCompleteFacet.class); return facet != null; } @Override public ObjectAdapter[] getAutoComplete( final ObjectAdapter adapter, final String searchArg, final InteractionInitiatedBy interactionInitiatedBy) { final List<ObjectAdapter> adapters = Lists.newArrayList(); final ActionParameterAutoCompleteFacet facet = getFacet(ActionParameterAutoCompleteFacet.class); if (facet != null) { final Object[] choices = facet.autoComplete(adapter, searchArg, interactionInitiatedBy); checkChoicesOrAutoCompleteType(getSpecificationLoader(), choices, getSpecification()); for (final Object choice : choices) { adapters.add(getAdapterMap().adapterFor(choice)); } } /* // now incorporated into above choices processing (BoundedFacet is no more) if (adapters.size() == 0 && ChoicesFacetUtils.hasChoices(getSpecification())) { addAllInstancesForType(adapters); } */ return adapters.toArray(new ObjectAdapter[0]); } @Override public int getAutoCompleteMinLength() { final ActionParameterAutoCompleteFacet facet = getFacet(ActionParameterAutoCompleteFacet.class); return facet != null? facet.getMinLength(): MinLengthUtil.MIN_LENGTH_DEFAULT; } //endregion //region > Choices @Override public boolean hasChoices() { final ActionParameterChoicesFacet choicesFacet = getFacet(ActionParameterChoicesFacet.class); return choicesFacet != null; } @Override public ObjectAdapter[] getChoices( final ObjectAdapter adapter, final ObjectAdapter[] argumentsIfAvailable, final InteractionInitiatedBy interactionInitiatedBy) { final List<ObjectAdapter> argListIfAvailable = ListExtensions.mutableCopy(argumentsIfAvailable); final ObjectAdapter target = targetForDefaultOrChoices(adapter); final List<ObjectAdapter> args = argsForDefaultOrChoices(adapter, argListIfAvailable); return findChoices(target, args, interactionInitiatedBy); } private ObjectAdapter[] findChoices( final ObjectAdapter target, final List<ObjectAdapter> args, final InteractionInitiatedBy interactionInitiatedBy) { final List<ObjectAdapter> adapters = Lists.newArrayList(); final ActionParameterChoicesFacet facet = getFacet(ActionParameterChoicesFacet.class); if (facet != null) { final Object[] choices = facet.getChoices(target, args, interactionInitiatedBy); checkChoicesOrAutoCompleteType(getSpecificationLoader(), choices, getSpecification()); for (final Object choice : choices) { ObjectAdapter adapter = choice != null? getAdapterMap().adapterFor(choice) : null; adapters.add(adapter); } } // now incorporated into above choices processing (BoundedFacet is no more) /* if (adapters.size() == 0 && BoundedFacetUtils.isBoundedSet(getSpecification())) { addAllInstancesForType(adapters); } */ return adapters.toArray(new ObjectAdapter[adapters.size()]); } //endregion //region > Defaults @Override public ObjectAdapter getDefault(final ObjectAdapter adapter) { final ObjectAdapter target = targetForDefaultOrChoices(adapter); final List<ObjectAdapter> args = argsForDefaultOrChoices(adapter, null); return findDefault(target, args); } private ObjectAdapter findDefault( final ObjectAdapter target, final List<ObjectAdapter> args) { final ActionParameterDefaultsFacet defaultsFacet = getFacet(ActionParameterDefaultsFacet.class); if (defaultsFacet != null) { final Object dflt = defaultsFacet.getDefault(target, args); if (dflt == null) { // it's possible that even though there is a default facet, when // invoked it is unable to return a default. return null; } return getAdapterMap().adapterFor(dflt); } return null; } /** * Hook method; {@link ObjectActionParameterContributee contributed action parameter}s override. */ protected ObjectAdapter targetForDefaultOrChoices(final ObjectAdapter adapter) { return adapter; } /** * Hook method; {@link ObjectActionParameterContributee contributed action parameter}s override. */ protected List<ObjectAdapter> argsForDefaultOrChoices( final ObjectAdapter adapter, final List<ObjectAdapter> argumentsIfAvailable) { return argumentsIfAvailable; } // helpers static void checkChoicesOrAutoCompleteType( final SpecificationLoader specificationLookup, final Object[] objects, final ObjectSpecification paramSpec) { for (final Object object : objects) { if(object == null) { continue; } // check type, but wrap first // (eg we treat int.class and java.lang.Integer.class as compatible with each other) final Class<? extends Object> choiceClass = object.getClass(); final Class<?> paramClass = paramSpec.getCorrespondingClass(); final Class<? extends Object> choiceWrappedClass = ClassExtensions.asWrappedIfNecessary(choiceClass); final Class<? extends Object> paramWrappedClass = ClassExtensions.asWrappedIfNecessary(paramClass); final ObjectSpecification choiceWrappedSpec = specificationLookup.loadSpecification(choiceWrappedClass); final ObjectSpecification paramWrappedSpec = specificationLookup.loadSpecification(paramWrappedClass); // TODO: should implement this instead as a MetaModelValidator if (!choiceWrappedSpec.isOfType(paramWrappedSpec)) { throw new DomainModelException(String.format( "Type incompatible with parameter type; expected %s, but was %s", paramSpec.getFullIdentifier(), choiceClass.getName())); } } } /** * unused - incorporated into the {@link ChoicesFacetFromBoundedAbstract} */ @SuppressWarnings("unused") private <T> void addAllInstancesForType(final List<ObjectAdapter> adapters) { final Query<T> query = new QueryFindAllInstances<T>(getSpecification().getFullIdentifier()); final List<ObjectAdapter> allInstancesAdapter = getObjectPersistor().allMatchingQuery(query); for (final ObjectAdapter choiceAdapter : allInstancesAdapter) { adapters.add(choiceAdapter); } } //endregion //region > Validation @Override public ActionArgValidityContext createProposedArgumentInteractionContext( final ObjectAdapter objectAdapter, final ObjectAdapter[] proposedArguments, final int position, final InteractionInitiatedBy interactionInitiatedBy) { return new ActionArgValidityContext( objectAdapter, parentAction, getIdentifier(), proposedArguments, position, interactionInitiatedBy); } @Override public String isValid( final ObjectAdapter objectAdapter, final Object proposedValue, final InteractionInitiatedBy interactionInitiatedBy) { ObjectAdapter proposedValueAdapter = null; ObjectSpecification proposedValueSpec; if(proposedValue != null) { proposedValueAdapter = getAdapterMap().adapterFor(proposedValue); if(proposedValueAdapter == null) { return null; } proposedValueSpec = proposedValueAdapter.getSpecification(); if(!proposedValueSpec.isOfType(proposedValueSpec)) { return null; } } final ObjectAdapter[] argumentAdapters = arguments(proposedValueAdapter); final ValidityContext<?> ic = createProposedArgumentInteractionContext( objectAdapter, argumentAdapters, getNumber(), interactionInitiatedBy ); final InteractionResultSet buf = new InteractionResultSet(); InteractionUtils.isValidResultSet(this, ic, buf); if (buf.isVetoed()) { return buf.getInteractionResult().getReason(); } return null; } /** * TODO: this is not ideal, because we can only populate the array for * single argument, rather than the entire argument set. Instead, we ought * to do this in two passes, one to build up the argument set as a single * unit, and then validate each in turn. */ private ObjectAdapter[] arguments(final ObjectAdapter proposedValue) { final int parameterCount = getAction().getParameterCount(); final ObjectAdapter[] arguments = new ObjectAdapter[parameterCount]; arguments[getNumber()] = proposedValue; return arguments; } //endregion //region > Dependencies (from parent) protected SpecificationLoader getSpecificationLoader() { return parentAction.getSpecificationLoader(); } protected AdapterManager getAdapterMap() { return parentAction.getPersistenceSessionService(); } protected PersistenceSessionServiceInternal getObjectPersistor() { return parentAction.getPersistenceSessionService(); } //endregion }