/** * 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; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import com.google.common.base.Function; 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.AppManifest; import org.apache.isis.applib.annotation.DomainService; import org.apache.isis.applib.annotation.NatureOfService; import org.apache.isis.applib.annotation.Programmatic; import org.apache.isis.core.commons.components.ApplicationScopedComponent; import org.apache.isis.core.commons.ensure.Assert; import org.apache.isis.core.commons.exceptions.IsisException; import org.apache.isis.core.commons.lang.ClassUtil; import org.apache.isis.core.metamodel.facetapi.Facet; import org.apache.isis.core.metamodel.facets.FacetFactory; import org.apache.isis.core.metamodel.facets.object.autocomplete.AutoCompleteFacet; import org.apache.isis.core.metamodel.facets.object.objectspecid.ObjectSpecIdFacet; import org.apache.isis.core.metamodel.layoutmetadata.LayoutMetadataReader; import org.apache.isis.core.metamodel.progmodel.ProgrammingModel; import org.apache.isis.core.metamodel.services.ServicesInjector; import org.apache.isis.core.metamodel.spec.FreeStandingList; import org.apache.isis.core.metamodel.spec.ObjectSpecId; import org.apache.isis.core.metamodel.spec.ObjectSpecification; 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.FacetedMethodsBuilderContext; import org.apache.isis.core.metamodel.specloader.specimpl.ObjectSpecificationAbstract; import org.apache.isis.core.metamodel.specloader.specimpl.dflt.ObjectSpecificationDefault; import org.apache.isis.core.metamodel.specloader.specimpl.standalonelist.ObjectSpecificationOnStandaloneList; import org.apache.isis.core.metamodel.specloader.validator.MetaModelValidator; import org.apache.isis.core.metamodel.specloader.validator.ValidationFailures; import org.apache.isis.progmodels.dflt.ProgrammingModelFacetsJava5; /** * Builds the meta-model. * * <p> * The implementation provides for a degree of pluggability: * <ul> * <li>The most important plug-in point is {@link ProgrammingModel} that * specifies the set of {@link Facet} that make up programming model. If not * specified then defaults to {@link ProgrammingModelFacetsJava5} (which should * be used as a starting point for your own customizations). * <li>The only mandatory plug-in point is {@link ClassSubstitutor}, which * allows the class to be loaded to be substituted if required. This is used in * conjunction with some <tt>PersistenceMechanism</tt>s that do class * enhancement. * </ul> * </p> * * <p> * Implementing class is added to {@link ServicesInjector} as an (internal) domain service; all public methods * must be annotated using {@link Programmatic}. * </p> * */ public class SpecificationLoader implements ApplicationScopedComponent { private final static Logger LOG = LoggerFactory.getLogger(SpecificationLoader.class); //region > constructor, fields private final ClassSubstitutor classSubstitutor = new ClassSubstitutor(); private final ProgrammingModel programmingModel; private final FacetProcessor facetProcessor; private final ServicesInjector servicesInjector; private final MetaModelValidator metaModelValidator; private final SpecificationCacheDefault cache = new SpecificationCacheDefault(); private final List<LayoutMetadataReader> layoutMetadataReaders; public SpecificationLoader( final ProgrammingModel programmingModel, final MetaModelValidator metaModelValidator, final List<LayoutMetadataReader> layoutMetadataReaders, final ServicesInjector servicesInjector) { this.servicesInjector = servicesInjector; this.programmingModel = programmingModel; this.metaModelValidator = metaModelValidator; this.facetProcessor = new FacetProcessor(programmingModel); this.layoutMetadataReaders = layoutMetadataReaders; } @Override protected void finalize() throws Throwable { super.finalize(); LOG.info("finalizing reflector factory " + this); } //endregion //region > init private boolean initialized = false; /** * Initializes and wires up, and primes the cache based on any service * classes (provided by the {@link ServicesInjector}). */ @Programmatic public void init() { if (LOG.isDebugEnabled()) { LOG.debug("initialising {}", this); } // wire subcomponents into each other facetProcessor.setServicesInjector(servicesInjector); for (final LayoutMetadataReader layoutMetadataReader : layoutMetadataReaders) { servicesInjector.injectInto(layoutMetadataReader); } // initialize subcomponents programmingModel.init(); facetProcessor.init(); metaModelValidator.init(this); loadSpecificationsForServices(); loadSpecificationsForMixins(); cacheBySpecId(); initialized = true; } private void loadSpecificationsForServices() { for (final Class<?> serviceClass : allServiceClasses()) { final DomainService domainService = serviceClass.getAnnotation(DomainService.class); final NatureOfService nature = domainService != null ? domainService.nature() : NatureOfService.DOMAIN; // will 'markAsService' internalLoadSpecification(serviceClass, nature); } } private void loadSpecificationsForMixins() { final Set<Class<?>> mixinTypes = AppManifest.Registry.instance().getMixinTypes(); if(mixinTypes == null) { return; } for (final Class<?> mixinType : mixinTypes) { internalLoadSpecification(mixinType); } } private void cacheBySpecId() { final Map<ObjectSpecId, ObjectSpecification> specById = Maps.newHashMap(); for (final ObjectSpecification objSpec : allSpecifications()) { final ObjectSpecId objectSpecId = objSpec.getSpecId(); if (objectSpecId == null) { continue; } specById.put(objectSpecId, objSpec); } cache.setCacheBySpecId(specById); } @Programmatic public boolean isInitialized() { return initialized; } //endregion //region > shutdown @Programmatic public void shutdown() { LOG.info("shutting down " + this); initialized = false; cache.clear(); } //endregion //region > invalidateCache @Programmatic public void invalidateCache(final Class<?> cls) { if(!cache.isInitialized()) { // could be called by JRebel plugin, before we are up-and-running // just ignore. return; } final Class<?> substitutedType = classSubstitutor.getClass(cls); if(substitutedType.isAnonymousClass()) { // JRebel plugin might call us... just ignore 'em. return; } ObjectSpecification spec = loadSpecification(substitutedType); while(spec != null) { final Class<?> type = spec.getCorrespondingClass(); cache.remove(type.getName()); if(spec.containsDoOpFacet(ObjectSpecIdFacet.class)) { // umm. Some specs do not have an ObjectSpecIdFacet... recache(spec); } spec = spec.superclass(); } } private void recache(final ObjectSpecification newSpec) { cache.recache(newSpec); } //endregion //region > validation private ValidationFailures validationFailures; @Programmatic public void validateAndAssert() { ValidationFailures validationFailures = validate(); validationFailures.assertNone(); cacheBySpecId(); } @Programmatic public ValidationFailures validate() { if(validationFailures == null) { validationFailures = new ValidationFailures(); metaModelValidator.validate(validationFailures); } return validationFailures; } //endregion //region > loadSpecification, loadSpecifications /** * Return the specification for the specified class of object. * * <p> * It is possible for this method to return <tt>null</tt>, for example if * the configured {@link org.apache.isis.core.metamodel.specloader.classsubstitutor.ClassSubstitutor} * has filtered out the class. */ @Programmatic public ObjectSpecification loadSpecification(final String className) { assert className != null; try { final Class<?> cls = loadBuiltIn(className); return internalLoadSpecification(cls); } catch (final ClassNotFoundException e) { final ObjectSpecification spec = cache.get(className); if (spec == null) { throw new IsisException("No such class available: " + className); } return spec; } } /** * @see #loadSpecification(String) */ @Programmatic public ObjectSpecification loadSpecification(final Class<?> type) { final ObjectSpecification spec = internalLoadSpecification(type); if(spec == null) { return null; } if(cache.isInitialized()) { // umm. It turns out that anonymous inner classes (eg org.estatio.dom.WithTitleGetter$ToString$1) // don't have an ObjectSpecId; hence the guard. if(spec.containsDoOpFacet(ObjectSpecIdFacet.class)) { ObjectSpecId specId = spec.getSpecId(); if (cache.getByObjectType(specId) == null) { cache.recache(spec); } } } return spec; } private ObjectSpecification internalLoadSpecification(final Class<?> type) { // superclasses tend to be loaded via this method, implicitly. // what can happen is that a subclass domain service, eg a fake one such as FakeLocationLookupService // can be registered first prior to the "real" implementation. As belt-n-braces, if that superclass is // annotated using @DomainService, then we ensure its own spec is created correctly as a service spec. final DomainService domainServiceIfAny = type.getAnnotation(DomainService.class); final NatureOfService natureOfServiceIfAny = domainServiceIfAny != null ? domainServiceIfAny.nature() : null; return internalLoadSpecification(type, natureOfServiceIfAny); } private ObjectSpecification internalLoadSpecification(final Class<?> type, final NatureOfService nature) { final Class<?> substitutedType = classSubstitutor.getClass(type); return substitutedType != null ? loadSpecificationForSubstitutedClass(substitutedType, nature) : null; } private ObjectSpecification loadSpecificationForSubstitutedClass(final Class<?> type, final NatureOfService nature) { Assert.assertNotNull(type); final String typeName = type.getName(); final ObjectSpecification spec = cache.get(typeName); if (spec != null) { return spec; } return loadSpecificationForSubstitutedClassSynchronized(type, nature); } private synchronized ObjectSpecification loadSpecificationForSubstitutedClassSynchronized( final Class<?> type, final NatureOfService natureOfService) { final String typeName = type.getName(); final ObjectSpecification spec = cache.get(typeName); if (spec != null) { // because caller isn't synchronized. return spec; } final ObjectSpecification specification = createSpecification(type, natureOfService); if (specification == null) { throw new IsisException("Failed to create specification for class " + typeName); } // put into the cache prior to introspecting, to prevent // infinite loops cache.cache(typeName, specification); introspectIfRequired(specification); return specification; } /** * Loads the specifications of the specified types except the one specified * (to prevent an infinite loop). */ @Programmatic public boolean loadSpecifications(final List<Class<?>> typesToLoad, final Class<?> typeToIgnore) { boolean anyLoadedAsNull = false; for (final Class<?> typeToLoad : typesToLoad) { if (typeToLoad != typeToIgnore) { final ObjectSpecification noSpec = internalLoadSpecification(typeToLoad); final boolean loadedAsNull = (noSpec == null); anyLoadedAsNull = loadedAsNull || anyLoadedAsNull; } } return anyLoadedAsNull; } /** * Loads the specifications of the specified types. */ @Programmatic public boolean loadSpecifications(final List<Class<?>> typesToLoad) { return loadSpecifications(typesToLoad, null); } /** * Creates the appropriate type of {@link ObjectSpecification}. */ private ObjectSpecification createSpecification( final Class<?> cls, final NatureOfService natureOfServiceIfAny) { // ... and create the specs if (FreeStandingList.class.isAssignableFrom(cls)) { return new ObjectSpecificationOnStandaloneList(servicesInjector, facetProcessor); } else { final FacetedMethodsBuilderContext facetedMethodsBuilderContext = new FacetedMethodsBuilderContext( this, facetProcessor, layoutMetadataReaders); return new ObjectSpecificationDefault(cls, facetedMethodsBuilderContext, servicesInjector, facetProcessor, natureOfServiceIfAny); } } private Class<?> loadBuiltIn(final String className) throws ClassNotFoundException { final Class<?> builtIn = ClassUtil.getBuiltIn(className); if (builtIn != null) { return builtIn; } return Class.forName(className); } /** * Typically does not need to be called, but is available for {@link FacetFactory}s to force * early introspection of referenced specs in certain circumstances. * * <p> * Originally introduced to support {@link AutoCompleteFacet}. */ private ObjectSpecification introspectIfRequired(final ObjectSpecification spec) { final ObjectSpecificationAbstract specSpi = (ObjectSpecificationAbstract)spec; final ObjectSpecificationAbstract.IntrospectionState introspectionState = specSpi.getIntrospectionState(); // REVIEW: can't remember why this is done in multiple passes, could it be simplified? if (introspectionState == ObjectSpecificationAbstract.IntrospectionState.NOT_INTROSPECTED) { specSpi.setIntrospectionState(ObjectSpecificationAbstract.IntrospectionState.BEING_INTROSPECTED); introspect(specSpi); } else if (introspectionState == ObjectSpecificationAbstract.IntrospectionState.BEING_INTROSPECTED) { introspect(specSpi); } else if (introspectionState == ObjectSpecificationAbstract.IntrospectionState.INTROSPECTED) { // nothing to do } return spec; } private void introspect(final ObjectSpecificationAbstract specSpi) { specSpi.introspectTypeHierarchyAndMembers(); specSpi.updateFromFacetValues(); specSpi.setIntrospectionState(ObjectSpecificationAbstract.IntrospectionState.INTROSPECTED); } //endregion //region > allSpecifications /** * Return all the loaded specifications. */ @Programmatic public Collection<ObjectSpecification> allSpecifications() { return cache.allSpecifications(); } //endregion //region > getServiceClasses, isServiceClass @Programmatic public List<Class<?>> allServiceClasses() { List<Class<?>> serviceClasses = Lists .transform(this.servicesInjector.getRegisteredServices(), new Function<Object, Class<?>>(){ public Class<?> apply(Object o) { return o.getClass(); } }); // take a copy, to allow eg I18nFacetFactory to add in default implementations of missing services. return Collections.unmodifiableList(Lists.newArrayList(serviceClasses)); } @Programmatic public boolean isServiceClass(Class<?> cls) { return this.servicesInjector.isRegisteredService(cls); } //endregion //region > loaded /** * Whether this class has been loaded. */ @Programmatic public boolean loaded(final Class<?> cls) { return loaded(cls.getName()); } /** * @see #loaded(Class). */ @Programmatic public boolean loaded(final String fullyQualifiedClassName) { return cache.get(fullyQualifiedClassName) != null; } //endregion //region > lookupBySpecId @Programmatic public ObjectSpecification lookupBySpecId(ObjectSpecId objectSpecId) { final ObjectSpecification objectSpecification = cache.getByObjectType(objectSpecId); if(objectSpecification == null) { // fallback return loadSpecification(objectSpecId.asString()); } return objectSpecification; } //endregion }