/*
* 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.dflt;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.isis.applib.annotation.NatureOfService;
import org.apache.isis.applib.filter.Filter;
import org.apache.isis.applib.filter.Filters;
import org.apache.isis.core.commons.lang.StringExtensions;
import org.apache.isis.core.commons.util.ToString;
import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
import org.apache.isis.core.metamodel.facetapi.Facet;
import org.apache.isis.core.metamodel.facetapi.FacetHolder;
import org.apache.isis.core.metamodel.facets.FacetedMethod;
import org.apache.isis.core.metamodel.facets.ImperativeFacet;
import org.apache.isis.core.metamodel.facets.all.i18n.NamedFacetTranslated;
import org.apache.isis.core.metamodel.facets.all.i18n.PluralFacetTranslated;
import org.apache.isis.core.metamodel.facets.all.named.NamedFacet;
import org.apache.isis.core.metamodel.facets.all.named.NamedFacetInferred;
import org.apache.isis.core.metamodel.facets.object.mixin.MixinFacet;
import org.apache.isis.core.metamodel.facets.object.plural.PluralFacet;
import org.apache.isis.core.metamodel.facets.object.plural.inferred.PluralFacetInferred;
import org.apache.isis.core.metamodel.facets.object.value.ValueFacet;
import org.apache.isis.core.metamodel.facets.object.viewmodel.ViewModelFacet;
import org.apache.isis.core.metamodel.facets.object.wizard.WizardFacet;
import org.apache.isis.core.metamodel.services.ServicesInjector;
import org.apache.isis.core.metamodel.spec.ActionType;
import org.apache.isis.core.metamodel.spec.ObjectSpecification;
import org.apache.isis.core.metamodel.spec.feature.Contributed;
import org.apache.isis.core.metamodel.spec.feature.ObjectAction;
import org.apache.isis.core.metamodel.spec.feature.ObjectAssociation;
import org.apache.isis.core.metamodel.spec.feature.ObjectMember;
import org.apache.isis.core.metamodel.specloader.classsubstitutor.ClassSubstitutor;
import org.apache.isis.core.metamodel.specloader.facetprocessor.FacetProcessor;
import org.apache.isis.core.metamodel.specloader.specimpl.FacetedMethodsBuilder;
import org.apache.isis.core.metamodel.specloader.specimpl.FacetedMethodsBuilderContext;
import org.apache.isis.core.metamodel.specloader.specimpl.ObjectActionDefault;
import org.apache.isis.core.metamodel.specloader.specimpl.ObjectSpecificationAbstract;
import org.apache.isis.core.metamodel.specloader.specimpl.OneToManyAssociationDefault;
import org.apache.isis.core.metamodel.specloader.specimpl.OneToOneAssociationDefault;
public class ObjectSpecificationDefault extends ObjectSpecificationAbstract implements FacetHolder {
private final static Logger LOG = LoggerFactory.getLogger(ObjectSpecificationDefault.class);
private static final ClassSubstitutor classSubstitutor = new ClassSubstitutor();
private static String determineShortName(final Class<?> introspectedClass) {
final String name = introspectedClass.getName();
return name.substring(name.lastIndexOf('.') + 1);
}
//region > constructor, fields
/**
* Lazily built by {@link #getMember(Method)}.
*/
private Map<Method, ObjectMember> membersByMethod = null;
private final FacetedMethodsBuilder facetedMethodsBuilder;
private final boolean isService;
public ObjectSpecificationDefault(
final Class<?> correspondingClass,
final FacetedMethodsBuilderContext facetedMethodsBuilderContext,
final ServicesInjector servicesInjector,
final FacetProcessor facetProcessor,
final NatureOfService natureOfServiceIfAny) {
super(correspondingClass, determineShortName(correspondingClass),
servicesInjector, facetProcessor);
this.isService = natureOfServiceIfAny != null;
this.facetedMethodsBuilder = new FacetedMethodsBuilder(this, facetedMethodsBuilderContext);
}
//endregion
//region > introspectTypeHierarchyAndMembers
@Override
public void introspectTypeHierarchyAndMembers() {
metadataProperties = null;
if(isNotIntrospected()) {
metadataProperties = facetedMethodsBuilder.introspectClass();
}
// name
if(isNotIntrospected()) {
addNamedFacetAndPluralFacetIfRequired();
}
// go no further if a value
if(this.containsFacet(ValueFacet.class)) {
if (LOG.isDebugEnabled()) {
LOG.debug("skipping full introspection for value type " + getFullIdentifier());
}
return;
}
// superclass
if(isNotIntrospected()) {
final Class<?> superclass = getCorrespondingClass().getSuperclass();
updateSuperclass(superclass);
}
// walk superinterfaces
//
// REVIEW: the processing here isn't quite the same as with
// superclasses, in that with superclasses the superclass adds this type as its
// subclass, whereas here this type defines itself as the subtype.
//
// it'd be nice to push the responsibility for adding subclasses to
// the interface type... needs some tests around it, though, before
// making that refactoring.
//
final Class<?>[] interfaceTypes = getCorrespondingClass().getInterfaces();
final List<ObjectSpecification> interfaceSpecList = Lists.newArrayList();
for (final Class<?> interfaceType : interfaceTypes) {
final Class<?> substitutedInterfaceType = classSubstitutor.getClass(interfaceType);
if (substitutedInterfaceType != null) {
final ObjectSpecification interfaceSpec = getSpecificationLoader().loadSpecification(substitutedInterfaceType);
interfaceSpecList.add(interfaceSpec);
}
}
if(isNotIntrospected()) {
updateAsSubclassTo(interfaceSpecList);
}
if(isNotIntrospected()) {
updateInterfaces(interfaceSpecList);
}
// associations and actions
if(isNotIntrospected()) {
final List<ObjectAssociation> associations = createAssociations(metadataProperties);
sortAndUpdateAssociations(associations);
}
if(isNotIntrospected()) {
final List<ObjectAction> actions = createActions(metadataProperties);
sortCacheAndUpdateActions(actions);
}
if(isNotIntrospected()) {
facetedMethodsBuilder.introspectClassPostProcessing(metadataProperties);
}
if(isNotIntrospected()) {
updateFromFacetValues();
}
}
private void addNamedFacetAndPluralFacetIfRequired() {
NamedFacet namedFacet = getFacet(NamedFacet.class);
if (namedFacet == null) {
namedFacet = new NamedFacetInferred(StringExtensions.asNaturalName2(getShortIdentifier()), this);
addFacet(namedFacet);
}
PluralFacet pluralFacet = getFacet(PluralFacet.class);
if (pluralFacet == null) {
if(namedFacet instanceof NamedFacetTranslated) {
final NamedFacetTranslated facet = (NamedFacetTranslated) namedFacet;
pluralFacet = new PluralFacetTranslated(facet, this);
} else {
pluralFacet = new PluralFacetInferred(StringExtensions.asPluralName(namedFacet.value()), this);
}
addFacet(pluralFacet);
}
}
//endregion
//region > create associations and actions
private List<ObjectAssociation> createAssociations(Properties properties) {
final List<FacetedMethod> associationFacetedMethods = facetedMethodsBuilder.getAssociationFacetedMethods(properties);
final List<ObjectAssociation> associations = Lists.newArrayList();
for (FacetedMethod facetedMethod : associationFacetedMethods) {
final ObjectAssociation association = createAssociation(facetedMethod);
if(association != null) {
associations.add(association);
}
}
return associations;
}
private ObjectAssociation createAssociation(final FacetedMethod facetMethod) {
if (facetMethod.getFeatureType().isCollection()) {
return new OneToManyAssociationDefault(facetMethod, servicesInjector);
} else if (facetMethod.getFeatureType().isProperty()) {
return new OneToOneAssociationDefault(facetMethod, servicesInjector);
} else {
return null;
}
}
private List<ObjectAction> createActions(Properties metadataProperties) {
final List<FacetedMethod> actionFacetedMethods = facetedMethodsBuilder.getActionFacetedMethods(metadataProperties);
final List<ObjectAction> actions = Lists.newArrayList();
for (FacetedMethod facetedMethod : actionFacetedMethods) {
final ObjectAction action = createAction(facetedMethod);
if(action != null) {
actions.add(action);
}
}
return actions;
}
private ObjectAction createAction(final FacetedMethod facetedMethod) {
if (facetedMethod.getFeatureType().isAction()) {
return new ObjectActionDefault(facetedMethod, servicesInjector);
} else {
return null;
}
}
//endregion
//region > isXxx
@Override
public boolean isViewModel() {
return containsFacet(ViewModelFacet.class);
}
@Override
public boolean isViewModelCloneable(ObjectAdapter targetAdapter) {
final ViewModelFacet facet = getFacet(ViewModelFacet.class);
if(facet == null) {
return false;
}
final Object pojo = targetAdapter.getObject();
return facet.isCloneable(pojo);
}
@Override
public boolean isMixin() {
return containsFacet(MixinFacet.class);
}
@Override
public boolean isWizard() {
return containsFacet(WizardFacet.class);
}
@Override
public boolean isService() {
return isService;
}
//endregion
//region > getObjectAction
@Override
public ObjectAction getObjectAction(final ActionType type, final String id, final List<ObjectSpecification> parameters) {
final List<ObjectAction> actions =
getObjectActions(type, Contributed.INCLUDED, Filters.<ObjectAction>any());
return firstAction(actions, id, parameters);
}
@Override
public ObjectAction getObjectAction(final ActionType type, final String id) {
final List<ObjectAction> actions =
getObjectActions(type, Contributed.INCLUDED, Filters.<ObjectAction>any());
return firstAction(actions, id);
}
@Override
public ObjectAction getObjectAction(final String id) {
final List<ObjectAction> actions =
getObjectActions(ActionType.ALL, Contributed.INCLUDED, Filters.<ObjectAction>any());
return firstAction(actions, id);
}
private static ObjectAction firstAction(
final List<ObjectAction> candidateActions,
final String actionName,
final List<ObjectSpecification> parameters) {
outer: for (int i = 0; i < candidateActions.size(); i++) {
final ObjectAction action = candidateActions.get(i);
if (actionName != null && !actionName.equals(action.getId())) {
continue outer;
}
if (action.getParameters().size() != parameters.size()) {
continue outer;
}
for (int j = 0; j < parameters.size(); j++) {
if (!parameters.get(j).isOfType(action.getParameters().get(j).getSpecification())) {
continue outer;
}
}
return action;
}
return null;
}
private static ObjectAction firstAction(
final List<ObjectAction> candidateActions,
final String id) {
if (id == null) {
return null;
}
for (int i = 0; i < candidateActions.size(); i++) {
final ObjectAction action = candidateActions.get(i);
if (id.equals(action.getIdentifier().toNameParmsIdentityString())) {
return action;
}
if (id.equals(action.getIdentifier().toNameIdentityString())) {
return action;
}
continue;
}
return null;
}
//endregion
//region > getMember, catalog... (not API)
public ObjectMember getMember(final Method method) {
if (membersByMethod == null) {
this.membersByMethod = catalogueMembers();
}
return membersByMethod.get(method);
}
private HashMap<Method, ObjectMember> catalogueMembers() {
final HashMap<Method, ObjectMember> membersByMethod = Maps.newHashMap();
cataloguePropertiesAndCollections(membersByMethod);
catalogueActions(membersByMethod);
return membersByMethod;
}
private void cataloguePropertiesAndCollections(final Map<Method, ObjectMember> membersByMethod) {
final Filter<ObjectAssociation> noop = Filters.anyOfType(ObjectAssociation.class);
final List<ObjectAssociation> fields = getAssociations(Contributed.EXCLUDED, noop);
for (int i = 0; i < fields.size(); i++) {
final ObjectAssociation field = fields.get(i);
final List<Facet> facets = field.getFacets(ImperativeFacet.FILTER);
for (final Facet facet : facets) {
final ImperativeFacet imperativeFacet = ImperativeFacet.Util.getImperativeFacet(facet);
for (final Method imperativeFacetMethod : imperativeFacet.getMethods()) {
membersByMethod.put(imperativeFacetMethod, field);
}
}
}
}
private void catalogueActions(final Map<Method, ObjectMember> membersByMethod) {
final List<ObjectAction> userActions = getObjectActions(Contributed.INCLUDED);
for (int i = 0; i < userActions.size(); i++) {
final ObjectAction userAction = userActions.get(i);
final List<Facet> facets = userAction.getFacets(ImperativeFacet.FILTER);
for (final Facet facet : facets) {
final ImperativeFacet imperativeFacet = ImperativeFacet.Util.getImperativeFacet(facet);
for (final Method imperativeFacetMethod : imperativeFacet.getMethods()) {
membersByMethod.put(imperativeFacetMethod, userAction);
}
}
}
}
//endregion
//region > toString
@Override
public String toString() {
final ToString str = new ToString(this);
str.append("class", getFullIdentifier());
str.append("type", (isParentedOrFreeCollection() ? "Collection" : "Object"));
str.append("persistable", persistability());
str.append("superclass", superclass() == null ? "Object" : superclass().getFullIdentifier());
return str.toString();
}
//endregion
}