/*
* 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
}