/* * 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.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.isis.core.commons.exceptions.IsisException; import org.apache.isis.core.commons.lang.ListExtensions; import org.apache.isis.core.commons.lang.MethodUtil; import org.apache.isis.core.commons.util.ToString; import org.apache.isis.core.metamodel.exceptions.MetaModelException; import org.apache.isis.core.metamodel.facetapi.FeatureType; import org.apache.isis.core.metamodel.facetapi.MethodRemover; import org.apache.isis.core.metamodel.facets.FacetFactory; import org.apache.isis.core.metamodel.facets.FacetFactory.ProcessClassContext; import org.apache.isis.core.metamodel.facets.FacetedMethod; import org.apache.isis.core.metamodel.facets.FacetedMethodParameter; import org.apache.isis.core.metamodel.facets.actcoll.typeof.TypeOfFacet; import org.apache.isis.core.metamodel.facets.object.facets.FacetsFacet; import org.apache.isis.core.metamodel.layoutmetadata.LayoutMetadataReader; import org.apache.isis.core.metamodel.layoutmetadata.LayoutMetadataReader.ReaderException; import org.apache.isis.core.metamodel.layoutmetadata.LayoutMetadataReader2; import org.apache.isis.core.metamodel.methodutils.MethodScope; import org.apache.isis.core.metamodel.spec.ObjectSpecification; import org.apache.isis.core.metamodel.specloader.SpecificationLoader; 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.traverser.SpecificationTraverser; public class FacetedMethodsBuilder { private static final Logger LOG = LoggerFactory.getLogger(FacetedMethodsBuilder.class); private static final String GET_PREFIX = "get"; private static final String IS_PREFIX = "is"; private static final class FacetedMethodsMethodRemover implements MethodRemover { private final List<Method> methods; private FacetedMethodsMethodRemover(final List<Method> methods) { this.methods = methods; } @Override public void removeMethod( final MethodScope methodScope, final String methodName, final Class<?> returnType, final Class<?>[] parameterTypes) { MethodUtil.removeMethod(methods, methodScope, methodName, returnType, parameterTypes); } @Override public List<Method> removeMethods( final MethodScope methodScope, final String prefix, final Class<?> returnType, final boolean canBeVoid, final int paramCount) { return MethodUtil.removeMethods(methods, methodScope, prefix, returnType, canBeVoid, paramCount); } @Override public void removeMethod(final Method method) { if (method == null) { return; } for (int i = 0; i < methods.size(); i++) { if (methods.get(i) == null) { continue; } if (methods.get(i).equals(method)) { methods.set(i, null); } } } @Override public void removeMethods(final List<Method> methodsToRemove) { for (int i = 0; i < methods.size(); i++) { if (methods.get(i) == null) { continue; } for (final Method method : methodsToRemove) { if (methods.get(i).equals(method)) { methods.set(i, null); break; } } } } } private final ObjectSpecificationAbstract spec; private final Class<?> introspectedClass; private final List<Method> methods; private List<FacetedMethod> associationFacetMethods; private List<FacetedMethod> actionFacetedMethods; private final FacetedMethodsMethodRemover methodRemover; private final FacetProcessor facetProcessor; private final SpecificationTraverser specificationTraverser = new SpecificationTraverser(); private final ClassSubstitutor classSubstitutor = new ClassSubstitutor(); private final SpecificationLoader specificationLoader; private final List<LayoutMetadataReader> layoutMetadataReaders; private final Map<LayoutMetadataReader, LayoutMetadataReader2.Support> supportByReader; // //////////////////////////////////////////////////////////////////////////// // Constructor & finalize // //////////////////////////////////////////////////////////////////////////// public FacetedMethodsBuilder(final ObjectSpecificationAbstract spec, final FacetedMethodsBuilderContext facetedMethodsBuilderContext) { if (LOG.isDebugEnabled()) { LOG.debug("creating JavaIntrospector for " + spec.getFullIdentifier()); } this.spec = spec; this.introspectedClass = spec.getCorrespondingClass(); this.methods = Arrays.asList(introspectedClass.getMethods()); this.methodRemover = new FacetedMethodsMethodRemover(methods); this.facetProcessor = facetedMethodsBuilderContext.facetProcessor; this.specificationLoader = facetedMethodsBuilderContext.specificationLoader; this.layoutMetadataReaders = facetedMethodsBuilderContext.layoutMetadataReaders; this.supportByReader = Maps.newHashMap(); for (LayoutMetadataReader reader : layoutMetadataReaders) { if(reader instanceof LayoutMetadataReader2) { final LayoutMetadataReader2 reader2 = (LayoutMetadataReader2) reader; final LayoutMetadataReader2.Support support = reader2.support(); supportByReader.put(reader, support); } } } @Override protected void finalize() throws Throwable { super.finalize(); if (LOG.isDebugEnabled()) { LOG.debug("finalizing inspector " + this); } } // //////////////////////////////////////////////////////////////////////////// // Class and stuff immediately derived from class // //////////////////////////////////////////////////////////////////////////// private String getClassName() { return introspectedClass.getName(); } // //////////////////////////////////////////////////////////////////////////// // introspect class // //////////////////////////////////////////////////////////////////////////// public Properties introspectClass() { if (LOG.isInfoEnabled()) { LOG.info("introspecting " + getClassName() + ": class-level details"); } // process facets at object level // this will also remove some methods, such as the superclass methods. final Properties metadataProperties = readMetadataProperties(introspectedClass); getFacetProcessor().process(introspectedClass, metadataProperties, methodRemover, spec); // if this class has additional facets (as per @Facets), then process // them. final FacetsFacet facetsFacet = spec.getFacet(FacetsFacet.class); if (facetsFacet != null) { final Class<? extends FacetFactory>[] facetFactories = facetsFacet.facetFactories(); for (final Class<? extends FacetFactory> facetFactorie : facetFactories) { FacetFactory facetFactory; try { facetFactory = facetFactorie.newInstance(); } catch (final InstantiationException | IllegalAccessException e) { throw new IsisException(e); } getFacetProcessor().injectDependenciesInto(facetFactory); facetFactory.process(new ProcessClassContext(introspectedClass, metadataProperties, methodRemover, spec)); } } return metadataProperties; } private Properties readMetadataProperties(final Class<?> domainClass) { for (final LayoutMetadataReader reader : layoutMetadataReaders) { try { // ignore JDK, Joda and Guava classes if(isPrimitiveOrJdkOrJodaOrGuava(domainClass)) { continue; } // skip class if the reader doesn't support it final LayoutMetadataReader2.Support support = supportByReader.get(reader); if(support != null) { if (!support.interfaces() && domainClass.isInterface()) { continue; } if (!support.anonymous() && domainClass.isAnonymousClass()) { continue; } if (!support.synthetic() && domainClass.isSynthetic()) { continue; } if (!support.array() && domainClass.isArray()) { continue; } if (!support.enums() && domainClass.isEnum()) { continue; } if (!support.applibValueTypes() && domainClass.getName().startsWith("org.apache.isis.applib.value")) { continue; } if(!support.services() && getSpecificationLoader().isServiceClass(domainClass)) { continue; } } Properties properties = reader.asProperties(domainClass); if(properties != null) { return properties; } } catch(final ReaderException ignore) { // ignore... it is now the responsibility of the reader to LOG any exceptions } } return null; } private static boolean isPrimitiveOrJdkOrJodaOrGuava(final Class<?> cls) { if(cls.isPrimitive()) { return true; } final String className = cls.getName(); return className.startsWith("java.") || className.startsWith("javax.") || className.startsWith("org.joda") || className.startsWith("com.google.common"); } // //////////////////////////////////////////////////////////////////////////// // introspect associations // //////////////////////////////////////////////////////////////////////////// /** * Returns a {@link List} of {@link FacetedMethod}s representing object * actions, lazily creating them first if required. */ public List<FacetedMethod> getAssociationFacetedMethods(Properties properties) { if (associationFacetMethods == null) { associationFacetMethods = createAssociationFacetedMethods(properties); } return associationFacetMethods; } private List<FacetedMethod> createAssociationFacetedMethods(Properties properties) { if (LOG.isDebugEnabled()) { LOG.debug("introspecting " + getClassName() + ": properties and collections"); } final Set<Method> associationCandidateMethods = getFacetProcessor().findAssociationCandidateAccessors(methods, new HashSet<Method>()); // Ensure all return types are known final List<Class<?>> typesToLoad = Lists.newArrayList(); for (final Method method : associationCandidateMethods) { specificationTraverser.traverseTypes(method, typesToLoad); } getSpecificationLoader().loadSpecifications(typesToLoad, introspectedClass); // now create FacetedMethods for collections and for properties final List<FacetedMethod> associationFacetedMethods = Lists.newArrayList(); findAndRemoveCollectionAccessorsAndCreateCorrespondingFacetedMethods(associationFacetedMethods, properties); findAndRemovePropertyAccessorsAndCreateCorrespondingFacetedMethods(associationFacetedMethods, properties); return Collections.unmodifiableList(associationFacetedMethods); } private void findAndRemoveCollectionAccessorsAndCreateCorrespondingFacetedMethods(final List<FacetedMethod> associationPeers, Properties properties) { final List<Method> collectionAccessors = Lists.newArrayList(); getFacetProcessor().findAndRemoveCollectionAccessors(methodRemover, collectionAccessors); createCollectionFacetedMethodsFromAccessors(collectionAccessors, associationPeers, properties); } /** * Since the value properties and collections have already been processed, * this will pick up the remaining reference properties. * @param properties TODO */ private void findAndRemovePropertyAccessorsAndCreateCorrespondingFacetedMethods(final List<FacetedMethod> fields, Properties properties) { final List<Method> propertyAccessors = Lists.newArrayList(); getFacetProcessor().findAndRemovePropertyAccessors(methodRemover, propertyAccessors); findAndRemovePrefixedNonVoidMethods(MethodScope.OBJECT, GET_PREFIX, Object.class, 0, propertyAccessors); findAndRemovePrefixedNonVoidMethods(MethodScope.OBJECT, IS_PREFIX, Boolean.class, 0, propertyAccessors); createPropertyFacetedMethodsFromAccessors(propertyAccessors, fields, properties); } private void createCollectionFacetedMethodsFromAccessors(final List<Method> accessorMethods, final List<FacetedMethod> facetMethodsToAppendto, Properties properties) { for (final Method accessorMethod : accessorMethods) { if (LOG.isDebugEnabled()) { LOG.debug(" identified accessor method representing collection: " + accessorMethod); } // create property and add facets final FacetedMethod facetedMethod = FacetedMethod.createForCollection(introspectedClass, accessorMethod); getFacetProcessor().process(introspectedClass, accessorMethod, methodRemover, facetedMethod, FeatureType.COLLECTION, properties); // figure out what the type is Class<?> elementType = Object.class; final TypeOfFacet typeOfFacet = facetedMethod.getFacet(TypeOfFacet.class); if (typeOfFacet != null) { elementType = typeOfFacet.value(); } facetedMethod.setType(elementType); // skip if class substitutor says so. if (classSubstitutor.getClass(elementType) == null) { continue; } facetMethodsToAppendto.add(facetedMethod); } } private void createPropertyFacetedMethodsFromAccessors(final List<Method> accessorMethods, final List<FacetedMethod> facetedMethodsToAppendto, Properties properties) throws MetaModelException { for (final Method accessorMethod : accessorMethods) { LOG.debug(" identified accessor method representing property: " + accessorMethod); final Class<?> returnType = accessorMethod.getReturnType(); // skip if class strategy says so. if (classSubstitutor.getClass(returnType) == null) { continue; } // create a 1:1 association peer final FacetedMethod facetedMethod = FacetedMethod.createForProperty(introspectedClass, accessorMethod); // process facets for the 1:1 association getFacetProcessor().process(introspectedClass, accessorMethod, methodRemover, facetedMethod, FeatureType.PROPERTY, properties); facetedMethodsToAppendto.add(facetedMethod); } } // //////////////////////////////////////////////////////////////////////////// // introspect actions // //////////////////////////////////////////////////////////////////////////// /** * Returns a {@link List} of {@link FacetedMethod}s representing object * actions, lazily creating them first if required. */ public List<FacetedMethod> getActionFacetedMethods(final Properties metadataProperties) { if (actionFacetedMethods == null) { actionFacetedMethods = findActionFacetedMethods(MethodScope.OBJECT, metadataProperties); } return actionFacetedMethods; } private enum RecognisedHelpersStrategy { SKIP, DONT_SKIP; public boolean skip() { return this == SKIP; } } /** * REVIEW: I'm not sure why we do two passes here. * * <p> * Perhaps it's important to skip helpers first. I doubt it, though. */ private List<FacetedMethod> findActionFacetedMethods( final MethodScope methodScope, final Properties metadataProperties) { if (LOG.isDebugEnabled()) { LOG.debug("introspecting " + getClassName() + ": actions"); } final List<FacetedMethod> actionFacetedMethods1 = findActionFacetedMethods(methodScope, RecognisedHelpersStrategy.SKIP, metadataProperties); final List<FacetedMethod> actionFacetedMethods2 = findActionFacetedMethods(methodScope, RecognisedHelpersStrategy.DONT_SKIP, metadataProperties); return ListExtensions.combineWith(actionFacetedMethods1, actionFacetedMethods2); } private List<FacetedMethod> findActionFacetedMethods( final MethodScope methodScope, final RecognisedHelpersStrategy recognisedHelpersStrategy, final Properties metadataProperties) { if (LOG.isDebugEnabled()) { LOG.debug(" looking for action methods"); } final List<FacetedMethod> actionFacetedMethods = Lists.newArrayList(); for (int i = 0; i < methods.size(); i++) { final Method method = methods.get(i); if (method == null) { continue; } final FacetedMethod actionPeer = findActionFacetedMethod(methodScope, recognisedHelpersStrategy, method, metadataProperties); if (actionPeer != null) { methods.set(i, null); actionFacetedMethods.add(actionPeer); } } return actionFacetedMethods; } private FacetedMethod findActionFacetedMethod( final MethodScope methodScope, final RecognisedHelpersStrategy recognisedHelpersStrategy, final Method actionMethod, final Properties metadataProperties) { if (!representsAction(actionMethod, methodScope, recognisedHelpersStrategy)) { return null; } // build action return createActionFacetedMethod(actionMethod, metadataProperties); } private FacetedMethod createActionFacetedMethod( final Method actionMethod, final Properties metadataProperties) { if (!isAllParamTypesValid(actionMethod)) { return null; } final FacetedMethod action = FacetedMethod.createForAction(introspectedClass, actionMethod, getSpecificationLoader()); // process facets on the action & parameters getFacetProcessor().process(introspectedClass, actionMethod, methodRemover, action, FeatureType.ACTION, metadataProperties); final List<FacetedMethodParameter> actionParams = action.getParameters(); for (int j = 0; j < actionParams.size(); j++) { getFacetProcessor().processParams(introspectedClass, actionMethod, j, methodRemover, actionParams.get(j)); } return action; } private boolean isAllParamTypesValid(final Method actionMethod) { for (final Class<?> paramType : actionMethod.getParameterTypes()) { final ObjectSpecification paramSpec = getSpecificationLoader().loadSpecification(paramType); if (paramSpec == null) { return false; } } return true; } private boolean representsAction( final Method actionMethod, final MethodScope methodScope, final RecognisedHelpersStrategy recognisedHelpersStrategy) { if (!MethodUtil.inScope(actionMethod, methodScope)) { return false; } final List<Class<?>> typesToLoad = new ArrayList<Class<?>>(); specificationTraverser.traverseTypes(actionMethod, typesToLoad); final boolean anyLoadedAsNull = getSpecificationLoader().loadSpecifications(typesToLoad); if (anyLoadedAsNull) { return false; } if (!loadParamSpecs(actionMethod)) { return false; } if (getFacetProcessor().recognizes(actionMethod)) { // a bit of a hack if (actionMethod.getName().startsWith("set")) { return false; } if (recognisedHelpersStrategy.skip()) { LOG.debug(" skipping possible helper method {0}", actionMethod); return false; } } LOG.debug(" identified action {0}", actionMethod); return true; } private boolean loadParamSpecs(final Method actionMethod) { final Class<?>[] parameterTypes = actionMethod.getParameterTypes(); return loadParamSpecs(parameterTypes); } private boolean loadParamSpecs(final Class<?>[] parameterTypes) { final int numParameters = parameterTypes.length; for (int j = 0; j < numParameters; j++) { final ObjectSpecification paramSpec = getSpecificationLoader().loadSpecification(parameterTypes[j]); if (paramSpec == null) { return false; } } return true; } // //////////////////////////////////////////////////////////////////////////// // introspect class post processing // //////////////////////////////////////////////////////////////////////////// public void introspectClassPostProcessing(final Properties metadataProperties) { if (LOG.isDebugEnabled()) { LOG.debug("introspecting {0}: class-level post-processing", getClassName()); } getFacetProcessor().processPost(introspectedClass, metadataProperties, methodRemover, spec); } // //////////////////////////////////////////////////////////////////////////// // Helpers for finding and removing methods. // //////////////////////////////////////////////////////////////////////////// /** * As per {@link #findAndRemovePrefixedNonVoidMethods(org.apache.isis.core.metamodel.methodutils.MethodScope, String, Class, int, java.util.List)}, * but appends to provided {@link List} (collecting parameter pattern). */ private void findAndRemovePrefixedNonVoidMethods( final MethodScope methodScope, final String prefix, final Class<?> returnType, final int paramCount, final List<Method> methodListToAppendTo) { final List<Method> matchingMethods = findAndRemovePrefixedMethods(methodScope, prefix, returnType, false, paramCount); methodListToAppendTo.addAll(matchingMethods); } /** * Searches for all methods matching the prefix and returns them, also * removing it from the {@link #methods array of methods} if found. */ private List<Method> findAndRemovePrefixedMethods( final MethodScope methodScope, final String prefix, final Class<?> returnType, final boolean canBeVoid, final int paramCount) { return MethodUtil.removeMethods(methods, methodScope, prefix, returnType, canBeVoid, paramCount); } // //////////////////////////////////////////////////////////////////////////// // toString // //////////////////////////////////////////////////////////////////////////// @Override public String toString() { final ToString str = new ToString(this); str.append("class", getClassName()); return str.toString(); } // //////////////////////////////////////////////////////////////////////////// // Dependencies // //////////////////////////////////////////////////////////////////////////// private SpecificationLoader getSpecificationLoader() { return specificationLoader; } private FacetProcessor getFacetProcessor() { return facetProcessor; } }